Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -115,19 +124,34 @@ 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
steps:
- 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
Expand Down
24 changes: 22 additions & 2 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
Loading