diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59ab4a0..c6d5d51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,15 @@ on: # Manual re-run for resuming a rate-limited publish; skips crates whose # version is already on the registry. workflow_dispatch: + inputs: + tag: + description: >- + The release tag this run is finishing, e.g. v2.0.0. Required to + move the floating major tag; a resumed publish that left `vN` + behind is a release whose documented ref still points at the + previous version, which is the failure `float-major-tag` exists + to prevent. + required: false permissions: contents: read @@ -115,7 +124,13 @@ jobs: # `v1` here cannot re-enter this workflow. float-major-tag: needs: publish - if: github.event_name == 'push' && github.ref_type == 'tag' + # A tag push carries the tag in `ref_name`; a dispatch has to be told. + # Runs for both, because RELEASING documents resuming a rate-limited + # publish by dispatch — and a resumed release that skipped this job is a + # release whose documented `@vN` still points at the previous version. + if: >- + (github.event_name == 'push' && github.ref_type == 'tag') + || (github.event_name == 'workflow_dispatch' && inputs.tag != '') runs-on: ubuntu-latest permissions: contents: write # moves refs/tags/vN @@ -123,11 +138,20 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Point vN at ${{ github.ref_name }} + - name: Point vN at the release tag env: - TAG: ${{ github.ref_name }} + TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }} run: | set -euo pipefail + # A dispatch can be handed anything, so the tag has to exist and be + # a version before a floating pointer is aimed at it. + git rev-parse -q --verify "refs/tags/${TAG}^{commit}" >/dev/null || { + echo "::error::no such tag: '$TAG'"; exit 1 + } + case "$TAG" in + v[0-9]*.[0-9]*.[0-9]*) ;; + *) echo "::error::'$TAG' is not a release tag (expected vX.Y.Z)"; exit 1 ;; + esac major="v${TAG#v}" major="${major%%.*}" case "$major" in diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 6d92627..ff57f3f 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -48,6 +48,17 @@ git tag vX.Y.Z && git push origin vX.Y.Z Pushing the tag runs `release.yml`, which gates, then publishes each crate in order via Trusted Publishing. +### The floating major tag + +`release.yml` moves `vN` to each release, so `@v2` follows 2.x without a +manual step. It is covered by the `protect-release-tags` ruleset only up to +`refs/tags/v*.*.*` — release tags are immutable, floating pointers are not, +because being moved is the whole of what a floating pointer is for. + +That distinction was learned the hard way: the ruleset originally covered +`refs/tags/v*`, so the job could not create `v2` at all and 2.0.0's floating +tag had to be pushed by hand. + ## After the tag - **The GitHub Release is created by hand**, from the CHANGELOG section: @@ -73,8 +84,17 @@ order via Trusted Publishing. - **Before publish**: fix, delete the tag (`git push --delete origin vX.Y.Z`), re-tag. Nothing was published; the world never saw it. -- **Part-way through the eight crates**: re-run `release.yml` by dispatch. It - skips what is already on the registry. +- **Part-way through the eight crates**: re-run `release.yml` by dispatch, + passing the tag: + + ```sh + gh workflow run release.yml -f tag=vX.Y.Z + ``` + + It skips what is already on the registry, then moves `vN`. The tag is an + input rather than inferred because a dispatch has no tag of its own, and a + resumed release that skipped the floating tag is a release whose documented + `@vN` still points at the previous version. - **After publish**: crates.io is immutable. Ship `X.Y.Z+1`. Yank only if the release is actively harmful — a yanked crate still breaks downstream lockfiles.