mirror of
https://github.com/tweag/nickel.git
synced 2024-07-14 15:10:33 +03:00
Update the release script by adding some missing `git add`, which were lost when switching to using Topiary from crates.io. Doing so, we also remove the `--dry-run` step before publishing: the usual workflow is to perform the dry run test, and if it succeeds, immediately do the actual publication. This isn't very useful: if the dry run fails, we could have run the actual publication as well, which would have fail in the same way, so this makes no difference. However, if the dry run succeeds, we recompile each package one more time from scratch, performing the exact same work for publication, which is wasteful. At this point of the release script, we clearly intend to release and we should just use `cargo publish` normally.
485 lines
18 KiB
Bash
Executable File
485 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Nickel release script
|
|
#
|
|
# This script automates part of the process of releasing a new version of
|
|
# Nickel.
|
|
#
|
|
# For requirements, see RELEASING.md.
|
|
#
|
|
# [^tomlq-sed]: tomlq has an --in-place option that would make the update much
|
|
# more pleasant. Unfortunately, tomlq works by transcoding to
|
|
# JSON, passing the JSON to jq, and then transcoding back to
|
|
# TOML, which has the very unpleasant effect of removing all
|
|
# comments and changing entirely the formatting and the layout
|
|
# of the file. Thus, we resort to good old and ugly sed.
|
|
# This is of course less robust. For now, it seems largely sufficient, but it
|
|
# might break in the future if the Cargo.toml files style change.
|
|
|
|
# In some cases it can be useful to leave the workspace in the state it was to
|
|
# finish the release manually. In that case, set this variable to false.
|
|
DO_CLEANUP=true
|
|
# Where to branch off the release. It's `master` most of the time, but for patch
|
|
# releases it's common to not release current master but just cherry pick a few
|
|
# commits on top of the previous release.
|
|
#
|
|
# Ideally that would be specified as an argument. For the time being, having a
|
|
# variable makes it at least a bit more flexible than harcoding master.
|
|
RELEASE_BASE_BRANCH="master"
|
|
|
|
# Perform clean up actions upon unexpected exit.
|
|
cleanup() {
|
|
set +e
|
|
|
|
if [[ $DO_CLEANUP == true ]]; then
|
|
echo "++ Unexpected exit. Cleaning up..."
|
|
|
|
for ((i=${#cleanup_actions[@]}-1; i>=0; i--)); do
|
|
echo "++ Running cleanup action: ${cleanup_actions[$i]}"
|
|
${cleanup_actions[$i]} || true
|
|
done
|
|
else
|
|
echo "++ Unexpected exit. Leaving the workspace in its current state (DO_CLEANUP=$DO_CLEANUP)."
|
|
fi
|
|
|
|
cleanup_actions=()
|
|
exit 1
|
|
}
|
|
|
|
# Take a subdirectory containing a crate of the current workspace as the first
|
|
# argument and a variable name as the second argument. Read the crate version
|
|
# from Cargo.toml and populate the variable with an array of the three version
|
|
# numbers (major, minor, patch).
|
|
read_crate_version() {
|
|
local -n version_array=$2 # use nameref for indirection: this will populate the variable named by the first argument
|
|
local version
|
|
version=$(tomlq -r .package.version "$1/Cargo.toml")
|
|
# Shellcheck isn't able to understand that we're populating the caller's
|
|
# provided variable `version_array` via namerefs and claims it's unused.
|
|
# shellcheck disable=SC2034
|
|
readarray -td'.' version_array <<< "$version"
|
|
}
|
|
|
|
# Take a string message as an argument and ask the user to confirm that the
|
|
# script should proceed with the next actions.
|
|
# If the user doesn't confirm, exit the script.
|
|
confirm_proceed() {
|
|
read -p "$1. Proceed (y/n)?" -n 1 -r
|
|
echo ""
|
|
|
|
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
|
echo "++ Aborting..."
|
|
cleanup
|
|
fi
|
|
}
|
|
|
|
# Take a bash array representing a version (major, minor, patch) or (minor,
|
|
# patch) as an argument and print the corresponding version string
|
|
# ("major.minor.patch") This function uses nameref, so the argument must be a
|
|
# variable name and not a value.
|
|
#
|
|
# For example:
|
|
# ```
|
|
# local version=(1 2 3)
|
|
# print_version_array version
|
|
# ```
|
|
print_version_array() {
|
|
local -n version=$1
|
|
local result
|
|
|
|
result="${version[0]}"
|
|
|
|
for component in "${version[@]:1}"; do
|
|
result="$result.$component"
|
|
done
|
|
|
|
echo "$result"
|
|
}
|
|
|
|
# Go through a Cargo.toml file and bump all dependencies to local crates that are being updated (in practice, the crates populating version_map)
|
|
# Arguments:
|
|
# - $1: the type of Cargo.toml, either "workspace" or "crate"
|
|
# - $2: the path to the Cargo.toml file
|
|
# - $3: the version map, a bash associative array mapping updated crate names to
|
|
# new versions. This argument is taken by nameref, so you must pass the name
|
|
# of an existing variable containing the map, not a value. We do so because
|
|
# passing associative arrays by value is painful in bash.
|
|
#
|
|
# For example:
|
|
# ```
|
|
# local -A version_map
|
|
# version_map["nickel-lang-core"]="1.2.3"
|
|
# update_dependencies "workspace" "./Cargo.toml" version_map
|
|
# ```
|
|
update_dependencies() {
|
|
local path_cargo_toml="$2"
|
|
local -n local_version_map=$3
|
|
|
|
# If we are looking at a crate's Cargo.toml file, the dependencies are
|
|
# located at .dependencies. If we are looking at the workspace's Cargo.toml,
|
|
# the dependencies are located at .workspace.dependencies. This difference
|
|
# is abstracted away in `dependencies_path`
|
|
local dependencies_path
|
|
|
|
if [[ $1 == "workspace" ]]; then
|
|
dependencies_path=".workspace.dependencies"
|
|
elif [[ $1 == "crate" ]]; then
|
|
dependencies_path=".dependencies"
|
|
else
|
|
echo "[Internal error] Invalid argument for update_dependencies(): expected 'crate' or 'workspace', got '$1'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local -a dependencies
|
|
readarray -t dependencies < <(tomlq -r '('$dependencies_path' | keys[])' "$path_cargo_toml")
|
|
|
|
for dependency in "${dependencies[@]}"; do
|
|
# If the dependency is in the version map, it means that we might have
|
|
# bumped it as part of the release process. In that case, we need to
|
|
# update the dependency to the new version.
|
|
if [[ -v local_version_map["$dependency"] ]]; then
|
|
# Cargo dependencies can be either specified as a simple version
|
|
# string, as in
|
|
# `foo_crate = "1.2.3"`
|
|
# or as an object with a `version` field, as in
|
|
# `foo_crate = { version = "1.2.3", features = ["bar"] }`
|
|
# The updated thus depend on the type of the dependency field, which
|
|
# can be determined by tomlq's `type` function
|
|
local dependency_type
|
|
local has_version
|
|
|
|
dependency_type=$(tomlq -r '('$dependencies_path'."'"$dependency"'" | type)' "$path_cargo_toml")
|
|
has_version=$(tomlq -r '('$dependencies_path'."'"$dependency"'" | has("version"))' "$path_cargo_toml")
|
|
|
|
cleanup_actions+=('git restore '"$path_cargo_toml")
|
|
|
|
if [[ $dependency_type == "string" ]]; then
|
|
report_progress "Patching cross-dependency $dependency in $path_cargo_toml to version ${local_version_map[$dependency]}"
|
|
# see [^tomlq-sed]
|
|
sed -i 's/\('"$dependency"'\s*=\s*"\)[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?"/\1'"${local_version_map[$dependency]}"'"/g' "$path_cargo_toml"
|
|
# Most of local crates use the workspace's version by default, i.e.
|
|
# are of the form `foo_crate = { workspace = true }`. In that case,
|
|
# the update is already taken care of when updating the workspace's
|
|
# Cargo.toml, so we don't do anything if the dependency doesn't have
|
|
# a version field
|
|
elif [[ $dependency_type == "object" && $has_version == "true" ]]; then
|
|
report_progress "Patching cross-dependency $dependency in $path_cargo_toml to version ${local_version_map[$dependency]}"
|
|
# see [^tomlq-sed]
|
|
# the regexp below recognizes dependencies of the form
|
|
# `foo_crate = { version = "1.2.3", features = ["bar"], ..etc }`
|
|
# Note that version must come first, which is the case currently
|
|
# throughout the codebase
|
|
sed -i 's/\('"$dependency"'\s*=\s*{\s*version\s*=\s*"\)[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?"/\1'"${local_version_map[$dependency]}"'"/g' "$path_cargo_toml"
|
|
fi
|
|
|
|
git add "$path_cargo_toml"
|
|
cleanup_actions+=('git reset -- '"$path_cargo_toml")
|
|
fi
|
|
done
|
|
}
|
|
|
|
print_usage_and_exit() {
|
|
echo "Usage: $0 <major|minor|patch>" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Report progress to the user with adapted indentation
|
|
report_progress() {
|
|
echo " -- $1"
|
|
}
|
|
|
|
trap cleanup ERR
|
|
set -eEuo pipefail
|
|
|
|
arg="${1:-}"
|
|
|
|
if [[ "$arg" == "" ]]; then
|
|
echo "Missing argument" >&2
|
|
print_usage_and_exit
|
|
elif [[ "$arg" != "major" && "$arg" != "minor" && "$arg" != "patch" ]]; then
|
|
echo "Invalid argument: $arg" >&2
|
|
print_usage_and_exit
|
|
fi
|
|
|
|
# A stack of actions to perform upon unexpected error. Cleanup actions are
|
|
# indeed popped from the top of cleanup_actions (that is, in reverse order, when
|
|
# seen as an array)
|
|
cleanup_actions=()
|
|
|
|
cat <<EOF
|
|
++ Nickel release script
|
|
++
|
|
++ This script will:
|
|
++
|
|
++ - Bump version numbers in Cargo.toml files
|
|
++ - Bump local dependencies to local crates accordingly
|
|
++ - Commit and push the changes on a new release branch
|
|
++ - Make the remote 'stable' branch point to the release branch
|
|
++ - Preprocess and publish relevant local crate to crates.io
|
|
++
|
|
++ Sanity checks (build, test, publish --dry-run etc.) are performed along the
|
|
++ way. In case of failure, this release script will its best to restore things
|
|
++ to the previous state as much as possible
|
|
EOF
|
|
|
|
confirm_proceed "++"
|
|
echo ""
|
|
|
|
# Moving to the root of the git project
|
|
cd "$(git rev-parse --show-toplevel)"
|
|
|
|
# Check that the working directory is clean
|
|
if [[ -n $(git status --untracked-files=no --porcelain) ]]; then
|
|
confirm_proceed "++ [WARNING] Working directory is not clean. The cleanup code of this script might revert some of your uncommited changes"
|
|
fi
|
|
|
|
git switch "$RELEASE_BASE_BRANCH" > /dev/null
|
|
|
|
echo "++ Prepare release branch from '$RELEASE_BASE_BRANCH'"
|
|
|
|
# Directories of subcrates following their own independent versioning
|
|
independent_crates=(core utils lsp/lsp-harness ./wasm-repl)
|
|
# All subcrate directories, including the ones above
|
|
all_crates=("${independent_crates[@]}" cli lsp/nls pyckel)
|
|
|
|
workspace_version=$(tomlq -r .workspace.package.version ./Cargo.toml)
|
|
workspace_version_array=()
|
|
readarray -td'.' workspace_version_array <<< "$workspace_version"
|
|
|
|
# We checked at the beginning of the script that $1 was either "major", "minor"
|
|
# or "patch", so we don't need to handle the cath-all case.
|
|
if [[ $1 == "major" ]]; then
|
|
new_workspace_version=$((workspace_version_array[0] + 1)).0.0
|
|
elif [[ $1 == "minor" ]]; then
|
|
new_workspace_version=${workspace_version_array[0]}.$((workspace_version_array[1] + 1)).0
|
|
elif [[ $1 == "patch" ]]; then
|
|
new_workspace_version=${workspace_version_array[0]}.${workspace_version_array[1]}.$((workspace_version_array[2] + 1))
|
|
fi
|
|
|
|
confirm_proceed " -- Updating to version $new_workspace_version"
|
|
|
|
report_progress "Creating release branch..."
|
|
|
|
release_branch="$new_workspace_version-release"
|
|
|
|
if git rev-parse --verify --quiet "$release_branch" > /dev/null; then
|
|
confirm_proceed " -- [WARNING] The branch '$release_branch' already exists. The script will skip forward to publication to crates.io (but still run checks)."
|
|
git switch "$release_branch"
|
|
|
|
report_progress "Building and running checks..."
|
|
|
|
nix flake check
|
|
else
|
|
git switch --create "$release_branch" > /dev/null
|
|
cleanup_actions+=("git branch -d $release_branch")
|
|
cleanup_actions+=('git switch '"$RELEASE_BASE_BRANCH")
|
|
|
|
report_progress "Bumping workspace version number..."
|
|
|
|
# see [^tomlq-sed]
|
|
sed -i 's/^\(version\s*=\s*"\)[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?"$/\1'"$new_workspace_version"'"/g' ./Cargo.toml
|
|
cleanup_actions+=("git reset -- ./Cargo.toml")
|
|
|
|
git add ./Cargo.toml
|
|
cleanup_actions+=("git restore ./Cargo.toml")
|
|
|
|
report_progress "Bumping other crates version numbers..."
|
|
|
|
for crate in "${independent_crates[@]}"; do
|
|
crate_version_array=()
|
|
read_crate_version "$crate" crate_version_array
|
|
|
|
new_crate_version=${crate_version_array[0]}.$((crate_version_array[1] + 1)).0
|
|
|
|
read -p " -- $crate is currently in version $(print_version_array crate_version_array). Bump to the next version $new_crate_version [if no, you'll have a pause later to manually bump those versions if needed] (y/n) ?" -n 1 -r
|
|
echo ""
|
|
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
# see [^tomlq-sed]
|
|
sed -i 's/^\(version\s*=\s*"\)[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?"$/\1'"$new_crate_version"'"/g' "$crate/Cargo.toml"
|
|
cleanup_actions+=('git restore '"$crate/Cargo.toml")
|
|
|
|
git add "$crate/Cargo.toml"
|
|
cleanup_actions+=('git reset -- '"$crate/Cargo.toml")
|
|
fi
|
|
done
|
|
|
|
read -n 1 -s -r -p " -- Please manually update any crate version not automatically handled by this script so far if you need to, and then press any key to continue"
|
|
echo ""
|
|
|
|
# Because the user might have updated the version numbers manually, we need to
|
|
# parse them again before updating cross-dependencies
|
|
|
|
declare -A version_map
|
|
|
|
for crate in "${all_crates[@]}"; do
|
|
crate_version_array=()
|
|
read_crate_version "$crate" crate_version_array
|
|
crate_name=$(tomlq -r .package.name "$crate/Cargo.toml")
|
|
# Shellcheck isn't able to understand that we're passing `version_map`
|
|
# to `update_dependencies` as a nameref and claims it's unused.
|
|
# shellcheck disable=SC2034
|
|
version_map[$crate_name]=$(print_version_array crate_version_array)
|
|
done
|
|
|
|
report_progress "Updating cross-dependencies..."
|
|
|
|
for crate in "${all_crates[@]}"; do
|
|
update_dependencies "crate" "$crate/Cargo.toml" version_map
|
|
done
|
|
|
|
# Patch workspace dependencies
|
|
update_dependencies "workspace" "./Cargo.toml" version_map
|
|
# We need to update the lockfile here, at least for the dependencies that we
|
|
# might have bumped. We changed ./Cargo.toml but Nix tries to build with
|
|
# --frozen, which will fail if the lockfile is outdated.
|
|
cargo update "${!version_map[@]}" > /dev/null
|
|
cleanup_actions+=("git restore ./Cargo.lock")
|
|
|
|
git add ./Cargo.lock
|
|
cleanup_actions+=("git reset -- ./Cargo.lock")
|
|
|
|
report_progress "Building and running checks..."
|
|
|
|
nix flake check
|
|
|
|
report_progress "Checks run successfully."
|
|
confirm_proceed " -- Please add the release notes to RELEASES.md if not already done and save. Then press 'y'."
|
|
|
|
git add RELEASES.md
|
|
cleanup_actions+=("git reset -- ./RELEASES.md")
|
|
|
|
report_progress "Pushing the release branch..."
|
|
|
|
git commit -m "[release.sh] update to $new_workspace_version"
|
|
git push -u origin "$release_branch"
|
|
|
|
report_progress "Saving current 'stable' branch to 'stable-local-save'..."
|
|
|
|
# Delete the branch if already present, but if not, don't fail
|
|
git branch -D stable-local-save &>/dev/null || true
|
|
git checkout stable
|
|
git branch stable-local-save
|
|
|
|
report_progress "If anything goes wrong from now on, you can restore the previous stable branch by resetting stable to stable-local-save"
|
|
|
|
confirm_proceed " -- Pushing the release branch to 'stable' and making it the new default"
|
|
|
|
git checkout stable
|
|
git reset --hard "$release_branch"
|
|
git push --force-with-lease
|
|
|
|
git checkout "$release_branch"
|
|
|
|
echo "++ Release branch successfully pushed!"
|
|
fi
|
|
|
|
# Reset cleanup actions as creating and pushing the release branch was successful
|
|
cleanup_actions=()
|
|
|
|
cat <<EOF
|
|
++ Release branch successfully pushed!
|
|
++ CAUTION: '$release_branch' won't be cleaned up automatically from there, even
|
|
++ if the publication fails. If you call this script again with an
|
|
++ existing release branch, you'll be asked if you want to resume from
|
|
++ here.
|
|
++
|
|
++ Now preparing the release to crates.io
|
|
EOF
|
|
|
|
crates_to_publish=(core cli lsp/nls)
|
|
|
|
report_progress "Removing 'nickel-lang-utils' and 'lsp-test-harness' from dev-dependencies..."
|
|
|
|
for crate in "${crates_to_publish[@]}"; do
|
|
# Remove `nickel-lang-utils` from `dev-dependencies` of released crates.
|
|
# Indeed, `nickel-lang-utils` is only used for testing or benchmarking and
|
|
# it creates a circular dependency. We just don't publish it and cut it off
|
|
# from dev-dependencies (which aren't required for proper publication on
|
|
# crates.io)
|
|
#
|
|
# see [^tomlq-sed]
|
|
sed -i '/^nickel-lang-utils\.workspace\s*=\s*true\s*$/d' "$crate/Cargo.toml"
|
|
sed -i '/^lsp-harness\.workspace\s*=\s*true\s*$/d' "$crate/Cargo.toml"
|
|
cleanup_actions+=('git restore '"$crate/Cargo.toml")
|
|
done
|
|
|
|
report_progress "Removing 'lsp-harness' from dev-dependencies..."
|
|
|
|
for crate in "${crates_to_publish[@]}"; do
|
|
# Remove `lsp-harness` from `dev-dependencies` of released crates.
|
|
# `lsp-harness` is only used for testing the LSP and isn't published on
|
|
# crates.io, so we cut it off as well
|
|
#
|
|
# see [^tomlq-sed]
|
|
sed -i '/^lsp-harness\.workspace\s*=\s*true$/d' "$crate/Cargo.toml"
|
|
cleanup_actions+=('git restore '"$crate/Cargo.toml")
|
|
done
|
|
|
|
for crate in "${crates_to_publish[@]}"; do
|
|
# Stage the modifications of the previous processing steps
|
|
git add "$crate/Cargo.toml"
|
|
cleanup_actions+=('git reset -- '"$crate/Cargo.toml")
|
|
done
|
|
|
|
# Cargo requires to commit changes, but the last changes are temporary
|
|
# work-arounds for the crates.io release that aren't supposed to stay. we'll
|
|
# reset them later.
|
|
git commit -m "[release.sh][tmp] clean unpublished crates from dev-dependencies"
|
|
cleanup_actions+=("git reset --hard HEAD~")
|
|
|
|
# We have had reproducibility issues before due to the fact that when installing
|
|
# the version of say `nickel-lang-cli` from crates.io, Cargo doesn't pick the
|
|
# current workspace file `Cargo.lock`, but regenerates a fresh one. This can
|
|
# lead to a local installation of `nickel-lang-cli` succeeding, but after
|
|
# publication, the version on crates.io wouldn't install.
|
|
#
|
|
# This is a bit unsatisfactory, but a cheap way to have good faith that the
|
|
# published version correctly builds and install is to temporarily delete
|
|
# `Cargo.lock` and try to install the nickel crates locally
|
|
report_progress "Trying to install 'nickel-lang-cli' and 'nickel-lang-lsp' locally..."
|
|
report_progress "[WARNING] This will override your local Nickel installation"
|
|
|
|
rm -f ./Cargo.lock
|
|
cleanup_actions+=("git restore ./Cargo.lock")
|
|
|
|
cargo install --force --path ./cli
|
|
cargo install --force --path ./lsp/nls
|
|
|
|
git restore ./Cargo.lock
|
|
|
|
report_progress "Successfully installed locally."
|
|
confirm_proceed "Proceed with publication of 'nickel-lang-core' to crates.io ?"
|
|
cargo publish -p nickel-lang-core
|
|
|
|
report_progress "'nickel-lang-core' published successfully"
|
|
confirm_proceed "Proceed with publication of 'nickel-lang-cli' to crates.io ?"
|
|
cargo publish -p nickel-lang-cli
|
|
|
|
report_progress "'nickel-lang-cli' published successfully"
|
|
confirm_proceed "Proceed with publication of 'nickel-lang-lsp' to crates.io ?"
|
|
cargo publish -p nickel-lang-lsp
|
|
|
|
report_progress "'nickel-lang-lsp' published successfully"
|
|
report_progress "Cleaning up..."
|
|
|
|
# Undo the previous commit massaging dependencies, and restore Cargo.lock.
|
|
git reset --hard HEAD~
|
|
|
|
cleanup_actions=()
|
|
|
|
cat <<EOF
|
|
++ SUCCESS
|
|
++
|
|
++ Successfully published to crates.io
|
|
++
|
|
++ Now, you need to:
|
|
++
|
|
++ - Do the GitHub release
|
|
++ - Redeploy the website
|
|
++
|
|
++ Please refer to RELEASING.md for more details
|
|
EOF
|
|
|
|
exit 0
|