auspice
Version:
Web app for visualizing pathogen evolution
275 lines (248 loc) • 11.6 kB
YAML
on:
workflow_dispatch:
inputs:
bump:
description: >-
On master branch: major | feat | minor, or "promote" to ship the current prerelease as
stable (e.g. 3.0.0-alpha.2 -> 3.0.0).
Other branches: major | feat | minor to start a new prerelease series, or
"continue" to increment the existing one.
type: choice
options: [feat, major, minor, continue, promote]
default: feat
label:
description: Prerelease label. Must be "none" on master; required elsewhere.
type: choice
options: [none, alpha, beta, rc]
default: none
dry-run:
description: Compute and validate everything, push/publish nothing.
type: boolean
default: false
name: release
# The branch being released is the ref this workflow was dispatched against, i.e.
# github.ref_name, and this file is read from that branch. There is deliberately no
# "branch" input: the workflow and the code it releases are then always the same commit.
# Consequence: release.yaml must exist on any branch you want to release from.
concurrency:
group: release
cancel-in-progress: false
jobs:
prepare-and-tag:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.compute.outputs.version }}
tag: ${{ steps.compute.outputs.tag }}
dist-tag: ${{ steps.compute.outputs.dist-tag }}
steps:
- uses: actions/checkout@v7
with:
# Tags and origin/master are both needed below, by the existing-tag check and
# the drift warning respectively.
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 24
# Note: --ignore-scripts skips an otherwise unnecessary Auspice build. We only need
# node_modules here for semver.
- run: npm ci --loglevel verbose --ignore-scripts
- name: Check the dispatched ref is a releasable branch
run: |
if [[ "$GITHUB_REF_TYPE" != branch ]]; then
echo "This workflow releases a branch, but was dispatched against a $GITHUB_REF_TYPE ($GITHUB_REF_NAME)." >&2
exit 1
fi
if [[ "$GITHUB_REF_NAME" == release ]]; then
echo "The 'release' branch is an output of this workflow, not an input. Dispatch against master instead." >&2
exit 1
fi
- name: Check package.json and src/version.js agree
run: |
package_version="$(node -p 'require("./package.json").version')"
src_version="$(node -p 'require("./src/version.js").version')"
if [[ "$package_version" != "$src_version" ]]; then
echo "package.json version ($package_version) doesn't match src/version.js version ($src_version)." >&2
exit 1
fi
echo "Current version: $package_version"
- name: Check CI passed for this commit
# Explicit `shell: bash` for pipefail, so a failing `gh api` can't be mistaken for
# "no runs found" below.
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
# Note that pushes to branches other than master and release don't trigger ci,
# so there may legitimately be no runs to check (e.g. a long-lived v3 branch
# with no open PR). We only fail on a run we can see which didn't succeed.
conclusions="$(gh api --paginate "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$GITHUB_SHA" \
--jq '.workflow_runs[] | select(.name == "ci") | .conclusion // "in-progress"' \
| sort -u)"
if [[ -z "$conclusions" ]]; then
echo "::warning::No 'ci' workflow run found for $GITHUB_SHA; releasing without a green CI signal."
elif [[ "$conclusions" != success ]]; then
echo "The 'ci' workflow for $GITHUB_SHA concluded: $(echo "$conclusions" | paste -sd' ' -)." >&2
echo "Fix CI (or re-run it) before releasing." >&2
exit 1
else
echo "ci passed for $GITHUB_SHA"
fi
- name: Compute the new version
id: compute
run: |
release="$(node scripts/compute-release-version.js \
--bump="$BUMP" --label="$LABEL" --branch="$GITHUB_REF_NAME" --json)"
echo "$release"
jq -r '
"version=\(.version)",
"tag=\(.tag)",
"dist-tag=\(.distTag)",
"description=\(.description)"
' <<<"$release" >> "$GITHUB_OUTPUT"
env:
BUMP: ${{ inputs.bump }}
LABEL: ${{ inputs.label }}
- name: Check the new version isn't already taken
env:
TAG: ${{ steps.compute.outputs.tag }}
VERSION: ${{ steps.compute.outputs.version }}
run: |
if git rev-parse -q --verify "refs/tags/$TAG" > /dev/null; then
echo "Tag $TAG already exists." >&2
exit 1
fi
if [[ -n "$(npm view "auspice@$VERSION" version 2> /dev/null)" ]]; then
echo "auspice@$VERSION is already published to npm." >&2
exit 1
fi
- name: Warn if the release tooling has drifted from master
run: |
# release.yaml is read from the branch being released. We may make changes to the script
# from time-to-time, so flag this as a warning not as a fatal error.
for file in .github/workflows/release.yaml scripts/compute-release-version.js; do
if ! git diff --quiet origin/master -- "$file"; then
echo "::warning file=$file::$file differs from origin/master; this release is using $GITHUB_REF_NAME's copy."
echo "⚠️ \`$file\` differs from \`origin/master\`." >> "$GITHUB_STEP_SUMMARY"
fi
done
- name: Bump the version
env:
VERSION: ${{ steps.compute.outputs.version }}
run: |
# Updates package.json and package-lock.json; src/version.js is mirrored by hand.
current="$(node -p 'require("./package.json").version')"
npm version "$VERSION" --no-git-tag-version
perl -pi -e "s/version = \"\Q$current\E\";/version = \"$VERSION\";/" src/version.js
if [[ "$(node -p 'require("./src/version.js").version')" != "$VERSION" ]]; then
echo "Failed to rewrite the version in src/version.js." >&2
exit 1
fi
git diff --stat
- name: Add a CHANGELOG heading
if: ${{ github.ref_name == 'master' }}
env:
VERSION: ${{ steps.compute.outputs.version }}
run: |
# Prepend an h2 for this release while preserving the h1. Both the YYYY/MM/DD
# format and the lowercase word "version" are relied upon by
# scripts/extract-release-notes.js, which produces the GitHub release body.
# Prereleases don't touch the CHANGELOG at all.
today="$(date +'%Y/%m/%d')"
echo -e "# Changelog\n\n## version ${VERSION} - ${today}\n\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md
git diff -- CHANGELOG.md
- name: Summarise
env:
VERSION: ${{ steps.compute.outputs.version }}
DESCRIPTION: ${{ steps.compute.outputs.description }}
DIST_TAG: ${{ steps.compute.outputs.dist-tag }}
run: |
{
echo "## Auspice $VERSION"
echo
echo "| | |"
echo "| --- | --- |"
echo "| branch | \`$GITHUB_REF_NAME\` |"
echo "| description | $DESCRIPTION |"
echo "| npm dist-tag | \`$DIST_TAG\` |"
echo "| dry run | ${{ inputs.dry-run }} |"
} >> "$GITHUB_STEP_SUMMARY"
- name: Commit, tag and push
if: ${{ !inputs.dry-run }}
env:
VERSION: ${{ steps.compute.outputs.version }}
TAG: ${{ steps.compute.outputs.tag }}
DESCRIPTION: ${{ steps.compute.outputs.description }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json src/version.js CHANGELOG.md
git commit -m "version bump to $VERSION for release"
git tag -a "$TAG" -m "$DESCRIPTION"
git push origin "HEAD:refs/heads/$GITHUB_REF_NAME"
git push origin "refs/tags/$TAG"
- name: Fast-forward the release branch
# nextstrain/docker-base installs Auspice from this branch rather than from npm,
# so it must stay a fast-forward of master. Prereleases never touch it.
if: ${{ !inputs.dry-run && github.ref_name == 'master' }}
run: git push origin HEAD:refs/heads/release
publish-npm:
needs: [prepare-and-tag]
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v7
with:
# Publish exactly what was tagged. Under a dry run there is no tag, so fall back
# to the dispatched ref (i.e. the branch) and apply the version bump locally.
ref: ${{ inputs.dry-run && github.ref || needs.prepare-and-tag.outputs.tag }}
- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: https://registry.npmjs.org/
- run: node --version
- if: ${{ inputs.dry-run }}
run: npm version "${{ needs.prepare-and-tag.outputs.version }}" --no-git-tag-version
# Don't use `--ignore-scripts` -- the "prepare" life cycle script builds what we publish
- run: npm ci --loglevel verbose
# --tag is always passed explicitly, because `npm publish` would otherwise point
# `latest` at a prerelease and serve it to every plain `npm install auspice`.
- run: npm publish --tag "$DIST_TAG" ${{ inputs.dry-run && '--dry-run' || '' }}
env:
DIST_TAG: ${{ needs.prepare-and-tag.outputs.dist-tag }}
github-release:
needs: [prepare-and-tag, publish-npm]
# Prereleases get no GitHub release; the tag is enough. Their notes would have to be
# generated by GitHub, since prereleases don't touch the CHANGELOG.
if: ${{ !inputs.dry-run && github.ref_name == 'master' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare-and-tag.outputs.tag }}
- name: Create the GitHub release
# Explicit `shell: bash` for pipefail, so a failure to extract the release notes
# doesn't quietly produce an empty release.
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
VERSION: ${{ needs.prepare-and-tag.outputs.version }}
TAG: ${{ needs.prepare-and-tag.outputs.tag }}
run: |
# Takes the topmost CHANGELOG section, i.e. the one prepended above.
node scripts/extract-release-notes.js \
| gh release create "$TAG" -t "Auspice $VERSION" --notes-file -
rebuild-docker-image:
needs: [github-release]
# docker-base builds from the release branch, which only master releases move.
if: ${{ !inputs.dry-run && github.ref_name == 'master' }}
runs-on: ubuntu-latest
steps:
- run: gh workflow run ci.yml --repo nextstrain/docker-base
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_NEXTSTRAIN_BOT_WORKFLOW_DISPATCH }}