Two gaps left over from the v6.0.0 release.
The v6.0.0 archives shipped unstripped: 3.63 MB vs 4.53 MB locally, 168
symbols instead of 9483. Setting it on the profile rather than in the
workflow covers every target at once and `cargo install` along with them.
Nothing gated the crates.io publish. A published version can be yanked but
never withdrawn, so the release job now waits for the CI run on the same
commit and refuses to publish unless it is green. release-pr deliberately
does not wait; opening the release PR is harmless.
This is not hypothetical: CI failed on eef7bf7, the v6.0.0 release commit.
It was a flaky e2e timeout rather than a regression - the very next commit
touched no code and passed - but the gate would have stopped that release,
which is the point. The gate reads the newest CI run for the commit, so
re-running a flaky job and then re-running Release clears it.
187 lines
6.8 KiB
YAML
187 lines
6.8 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
branches: [master]
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Existing tag to rebuild binaries for (e.g. v6.0.0). Leave empty for a normal release run.'
|
|
required: false
|
|
type: string
|
|
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
|
|
jobs:
|
|
# CI runs on this same push. Nothing may reach crates.io until it has passed,
|
|
# because a published version cannot be withdrawn, only yanked. Deliberately
|
|
# not a dependency of release-pr: opening the release PR is harmless and
|
|
# should not wait.
|
|
ci-gate:
|
|
name: Wait for CI
|
|
runs-on: ubuntu-latest
|
|
if: github.event_name == 'push'
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
steps:
|
|
- name: Wait for CI to pass on this commit
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
# jq reduces the run to a single word so there is no shell word
|
|
# splitting to get wrong. `last` picks the newest run for the commit,
|
|
# so re-running a flaky CI job flips this to success and re-running
|
|
# the Release workflow then goes through.
|
|
for _ in $(seq 1 90); do
|
|
verdict=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$GITHUB_SHA" \
|
|
--jq '[.workflow_runs[] | select(.path==".github/workflows/ci.yml")]
|
|
| sort_by(.run_number) | last
|
|
| if . == null then "absent"
|
|
elif .status != "completed" then "running"
|
|
else (.conclusion // "unknown") end' \
|
|
2>/dev/null || echo "unreachable")
|
|
case "$verdict" in
|
|
success) echo "CI passed for $GITHUB_SHA"; exit 0 ;;
|
|
absent|running|unreachable) ;;
|
|
*) echo "::error::CI concluded '$verdict' for $GITHUB_SHA - refusing to release"; exit 1 ;;
|
|
esac
|
|
sleep 20
|
|
done
|
|
echo "::error::timed out waiting for CI on $GITHUB_SHA - refusing to release"
|
|
exit 1
|
|
|
|
# Opens (or updates) a "chore: release vX.Y.Z" PR with the version bump and
|
|
# changelog derived from the conventional commits on master. Merging it is the
|
|
# release trigger.
|
|
release-pr:
|
|
name: Release PR
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: release-pr-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
|
|
- uses: release-plz/action@v0.5
|
|
with:
|
|
command: release-pr
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
|
|
# No-op until the release PR lands; then tags, publishes both crates to
|
|
# crates.io in dependency order, and creates the GitHub release.
|
|
release:
|
|
name: Tag and publish
|
|
runs-on: ubuntu-latest
|
|
needs: ci-gate
|
|
# ci-gate is skipped on workflow_dispatch, which would skip this job too
|
|
# without always(). A ci-gate failure leaves both clauses false, so a red
|
|
# CI blocks the publish.
|
|
if: always() && (github.event_name == 'workflow_dispatch' || needs.ci-gate.result == 'success')
|
|
concurrency:
|
|
group: release-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
outputs:
|
|
created: ${{ steps.release-plz.outputs.releases_created }}
|
|
tag: ${{ steps.tag.outputs.tag }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
|
|
- uses: release-plz/action@v0.5
|
|
id: release-plz
|
|
with:
|
|
command: release
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
|
|
# Read the tag out via jq rather than fromJSON: `releases` is an empty
|
|
# string on the runs where nothing was released, and fromJSON chokes on it.
|
|
- name: Resolve tag
|
|
id: tag
|
|
if: steps.release-plz.outputs.releases_created == 'true' || inputs.tag != ''
|
|
env:
|
|
RELEASES: ${{ steps.release-plz.outputs.releases }}
|
|
INPUT_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
tag="${INPUT_TAG:-$(jq -r '.[0].tag' <<< "$RELEASES")}"
|
|
# This value is handed to actions/checkout as `ref:` and INPUT_TAG is
|
|
# attacker-shaped (anyone who can dispatch the workflow types it), so
|
|
# refuse anything that is not a literal version tag.
|
|
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { echo "::error::unexpected tag: $tag"; exit 1; }
|
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
|
|
# Attaches downloadable binaries to the release GitHub just created, or to an
|
|
# existing tag when dispatched manually with `tag` (recovering a target that
|
|
# failed to upload needs a re-run of *this* file, not the old one).
|
|
binaries:
|
|
name: ${{ matrix.target }}
|
|
needs: release
|
|
if: needs.release.outputs.created == 'true' || inputs.tag != ''
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
# One flaky cross-linker must not hold back the five targets that built.
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- target: x86_64-unknown-linux-musl
|
|
os: ubuntu-latest
|
|
- target: aarch64-unknown-linux-musl
|
|
os: ubuntu-24.04-arm
|
|
- target: x86_64-apple-darwin
|
|
os: macos-latest
|
|
- target: aarch64-apple-darwin
|
|
os: macos-latest
|
|
- target: x86_64-pc-windows-msvc
|
|
os: windows-latest
|
|
- target: aarch64-pc-windows-msvc
|
|
os: windows-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ needs.release.outputs.tag }}
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- name: Install musl toolchain
|
|
if: endsWith(matrix.target, '-musl')
|
|
run: sudo apt-get update && sudo apt-get install -y musl-tools
|
|
|
|
# Builds, strips, archives (.tar.gz / .zip on Windows), writes a .sha256
|
|
# and uploads the lot to the release.
|
|
#
|
|
# build-tool is pinned to cargo because the action otherwise reaches for
|
|
# `cross` on any *-musl target. Every entry in the matrix builds on a
|
|
# runner of its own architecture — only the libc differs — so cross adds
|
|
# nothing, and on the native arm64 runner it hard-fails trying to install
|
|
# an x86_64 toolchain.
|
|
- uses: taiki-e/upload-rust-binary-action@v1
|
|
with:
|
|
bin: soulseek-rs
|
|
target: ${{ matrix.target }}
|
|
build-tool: cargo
|
|
archive: $bin-$tag-$target
|
|
include: README.md,LICENSE
|
|
checksum: sha256
|
|
ref: refs/tags/${{ needs.release.outputs.tag }}
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|