diff --git a/.github/workflows/crates-yank.yml b/.github/workflows/crates-yank.yml new file mode 100644 index 0000000..ce0a8b1 --- /dev/null +++ b/.github/workflows/crates-yank.yml @@ -0,0 +1,81 @@ +# Yank or un-yank a published crate version. +# +# Exists because the crates.io token lives here as a repository secret and +# nowhere else, which is correct: a publish credential should not sit on a +# laptop. That also means a yank cannot be run locally by anyone who does not +# separately hold a token, so it runs here, with the same secret the release +# workflow uses. +# +# workflow_dispatch only, with no defaults, so it cannot fire by accident. +# Yanking is reversible (`action: unyank`), unlike publishing: a version can +# be yanked and restored, but never reused. +name: crates-yank + +on: + workflow_dispatch: + inputs: + crate: + description: 'Crate name (e.g. bootintel-cli)' + required: true + type: string + version: + description: 'Exact version to act on (e.g. 0.4.1)' + required: true + type: string + action: + description: 'yank removes it from new resolution; unyank restores it' + required: true + type: choice + options: [yank, unyank] + reason: + description: 'Why, for the run log. Not sent to crates.io.' + required: true + type: string + +permissions: + contents: read + +jobs: + yank: + name: ${{ inputs.action }} ${{ inputs.crate }}@${{ inputs.version }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Require the registry token + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then + echo "::error::CARGO_REGISTRY_TOKEN is not set. See docs/releasing.md." + exit 1 + fi + + - name: Record what is being done and why + run: | + echo "action: ${{ inputs.action }}" + echo "crate: ${{ inputs.crate }}" + echo "version: ${{ inputs.version }}" + echo "reason: ${{ inputs.reason }}" + + # Refuse to touch a version that does not exist, so a typo fails here + # rather than producing a confusing registry error. + - name: Confirm the version exists on the registry + run: | + if ! cargo info "${{ inputs.crate }}@${{ inputs.version }}" >/dev/null 2>&1; then + echo "::error::${{ inputs.crate }} ${{ inputs.version }} is not on crates.io." + exit 1 + fi + + - name: ${{ inputs.action }} + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ "${{ inputs.action }}" = "yank" ]; then + cargo yank --version "${{ inputs.version }}" "${{ inputs.crate }}" + else + cargo yank --undo --version "${{ inputs.version }}" "${{ inputs.crate }}" + fi diff --git a/docs/releasing.md b/docs/releasing.md index 4054b3d..4df96c0 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -63,6 +63,19 @@ having no brew line at all. 6. Publish the draft and mark it latest. That creates the tag on the released commit. +## Yanking a published version + +A crates.io version can be yanked, which stops new dependency resolution and +new installs reaching it, and can be restored with unyank. It can never be +reused: the number is spent whatever happens. + +The registry token lives only as a repository secret, which is correct for a +publish credential, so a yank cannot be run from a laptop that does not hold +its own token. Dispatch the `crates-yank` workflow instead. It takes the crate, +the exact version, yank or unyank, and a reason for the run log. It has no +defaults, so it cannot fire by accident, and it refuses a version that is not +on the registry so a typo fails before it reaches crates.io. + ## Publishing to crates.io Order is not optional. `bootintel` declares `bootintel-detectors` with