Skip to content

Commit

Permalink
chore: add automated release script (#417)
Browse files Browse the repository at this point in the history
This branch adds a new `release.sh` script in `bin/` that can be used to
publish releases of console crates by running a single command. This
makes our release process require less effort, and also ensures that all
necessary steps are always performed when releasing a crate.
  • Loading branch information
hawkw authored May 9, 2023
1 parent eeca672 commit 1a94c4b
Show file tree
Hide file tree
Showing 2 changed files with 307 additions and 0 deletions.
42 changes: 42 additions & 0 deletions bin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# tokio-console dev scripts

This directory contains shell scripts useful for Tokio Console development.
Currently, all the scripts in this directory are related to publishing releases.

- `release.sh`: Releases a new version of a Tokio Console crate. This includes
updating the crate's version in its `Cargo.toml`, updating the changelog,
running pre-release tests, creating a release tag, and publishing the crate to
crates.io.

Invoked with the name of the crate to release, and the version of the new
release. For example:

```console
$ bin/release.sh tokio-console 0.1.9
```

The script will validate whether a new release can be published prior to
updating the changelog and crate version. Then, the script will display the
git diff for the generated release commit, and prompt the user to confirm
that it is correct prior to publishing the release.

Releases should be published on the `main` branch. Note that this script
requires that the user is authenticated to publish releases of the crate in
question to crates.io.

- `update-changelog.sh`: Updates the generated `CHANGELOG.md` for a given crate
and version, without committing the changelog update or publishing a tag.

Invoked with the path to the crate to generate change notes for, and the name
that will be used for the new release's Git tag. For example:

```console
$ bin/update-changelog.sh tokio-console tokio-console-v0.1.9
```

The `release.sh` script will run this script automatically as part of the
release process. However, it can also be invoked separately to just update the
changelog.

- `_util.sh`: Contains utilities used by other shell scripts. This script is not
run directly.
265 changes: 265 additions & 0 deletions bin/release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
#!/usr/bin/env bash
usage="Releases a tokio-console crate.
USAGE:
$(basename "$0") [FLAGS] <CRATE> <VERSION>
FLAGS:
-h, --help Show this help text and exit.
-v, --verbose Enable verbose output.
-d, --dry-run Do not change any files or commit a git tag."

set -euo pipefail

bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
rootdir=$( cd "$bindir"/.. && pwd )

# shellcheck source=_util.sh
. "$bindir"/_util.sh

cd "$rootdir"

verify() {
status "Verifying" "if $crate v$version can be released"

local branch
branch=$(git rev-parse --abbrev-ref HEAD)
if [[ "$branch" != "main" ]]; then
err "you are not on the 'main' branch"
if ! confirm " are you sure you want to release from the '$branch' branch?"; then
echo "okay, exiting"
exit 1
fi
fi

if ! cargo --list | grep -q "hack"; then
err "missing cargo-hack executable"
if confirm " install it?"; then
cargo install cargo-hack
else
echo "okay, exiting"
exit 1
fi
fi

status "Checking" "if $crate builds across feature combinations"

local cargo_hack=(cargo hack -p "$crate" --feature-powerset --no-dev-deps)

if [[ "$verbose" ]]; then
cargo_hack+=("$verbose" check)
else
cargo_hack+=(check --quiet)
fi

"${cargo_hack[@]}"
local cargo_hack_status="$?"

if [[ "$cargo_hack_status" != "0" ]] ; then
err "$crate did not build with all feature combinations (cargo hack exited with $cargo_hack_status)!"
exit 1
fi


if git tag -l | grep -Fxq "$tag" ; then
err "git tag \`$tag\` already exists"
exit 1
fi
}

update_version() {
# check the current version of the crate
local curr_version
curr_version=$(cargo pkgid -p "$crate" | sed -n 's/.*#\(.*\)/\1/p')
if [[ "$curr_version" == "$version" ]]; then
err "crate $crate is already at version $version!"
if ! confirm " are you sure you want to release $version?"; then
echo "okay, exiting"
exit 0
fi
else
status "Updating" "$crate from $curr_version to $version"
sed -i \
"/\[package\]/,/\[.*dependencies\]/{s/^version = \"$curr_version\"/version = \"$version\"/}" \
"$cargo_toml"
fi
}

publish() {
status "Publishing" "$crate v$version"
cd "$path"
local cargo_package=(cargo package)
local cargo_publish=(cargo publish)

if [[ "$verbose" ]]; then
cargo_package+=("$verbose")
cargo_publish+=("$verbose")
fi

if [[ "$dry_run" ]]; then
cargo_publish+=("$dry_run")
fi

"${cargo_package[@]}"
"${cargo_publish[@]}"

status "Tagging" "$tag"
local git_tag=(git tag "$tag")
local git_push_tags=(git push --tags)
if [[ "$dry_run" ]]; then
echo "# " "${git_tag[@]}"
echo "# " "${git_push_tags[@]}"
else
"${git_tag[@]}"
"${git_push_tags[@]}"
fi
}

update_changelog() {
# shellcheck source=update-changelog
. "$bindir"/update-changelog.sh
changelog_status="$?"

if [[ $changelog_status -ne 0 ]]; then
err "failed to update changelog"
exit "$changelog_status"
fi
}


verbose=''
dry_run=''

for arg in "$@"
do
case "$arg" in
-h|--help)
echo "$usage"
exit 0
;;
-v|--verbose)
verbose="--verbose"
;;
-d|--dry-run)
dry_run="--dry-run"
;;
-*)
err "unknown flag $arg"
echo "$usage"
exit 1
;;
*) # crate or version
if [[ -z "${crate+crate}" ]]; then
crate="$arg"
elif [[ -z "${version+version}" ]]; then
version="$arg"
else
err "unknown positional argument \"$arg\""
echo "$usage"
exit 1
fi
;;
esac
done

if [[ "$verbose" ]]; then
set -x
fi

if [[ -z "${version+version}" ]]; then
err "no version specified!"
errexit=1
fi

if [[ "${crate+crate}" ]]; then
tag="$crate-v$version"
else
err "no crate specified!"
errexit=1
fi

if [[ "${errexit+errexit}" ]]; then
echo "$usage"
exit 1
fi

path=$(crate_path "$crate")

cargo_toml="${path}/Cargo.toml"
changelog="${path}/CHANGELOG.md"

files=("$cargo_toml" "$changelog")

is_uncommitted=''
for file in "${files[@]}"; do
if ! git diff-index --quiet HEAD -- "$file"; then
err "would overwrite uncommitted changes to $file!"
is_uncommitted=1
fi
done

if [[ "$is_uncommitted" ]]; then
exit 1
fi

verify
update_version
update_changelog

staged="$(git diff-index --cached --name-only HEAD --)"
if [[ "$staged" ]]; then
err "skipping commit, as it would include the following unrelated staged files:"
echo "$staged"
exit 1
fi

status "Ready" "to prepare release commit!"
echo ""

git add "${files[@]}"
git diff --staged

if [[ "$dry_run" ]]; then
git reset HEAD -- "${files[@]}"
git checkout HEAD -- "${files[@]}"
fi

echo ""

if confirm "commit and push?"; then
git_commit=(git commit -sS -m "chore($crate): prepare to release $crate $version")

if [[ "$dry_run" ]]; then

echo ""
echo "# " "${git_commit[@]}"
echo "# " "${git_push[@]}"
else
"${git_commit[@]}"
fi
else
echo "okay, exiting"
exit 1
fi

if confirm "publish the crate?"; then

echo ""
publish
else
echo "okay, exiting"
exit 1
fi

git add "Cargo.lock"
git_push=(git push -u origin)
git_amend=(git commit --amend --reuse-message HEAD)
if [[ "$dry_run" ]]; then
echo ""
echo "# git add Cargo.lock"
echo "# " "${git_amend[@]}"
echo "# " "${git_push[@]}"
else
"${git_amend[@]}"
"${git_push[@]}"
fi

0 comments on commit 1a94c4b

Please sign in to comment.