From 298ec5aad542bd86298ff361d364a661d97d64a8 Mon Sep 17 00:00:00 2001 From: Michel de Graaf Date: Fri, 24 Jul 2026 13:25:03 +0200 Subject: [PATCH] build: strip release binaries and gate publishing on CI 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. --- .github/workflows/release.yml | 43 +++++++++++++++++++++++++++++++++++ Cargo.toml | 7 ++++++ RELEASE.md | 11 ++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f74fd1..fbdef1d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,44 @@ 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. @@ -47,6 +85,11 @@ jobs: 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 diff --git a/Cargo.toml b/Cargo.toml index 6c294df..76bd504 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,13 @@ edition = "2024" license = "MIT" repository = "https://github.com/michel/soulseek-rs" +# The v6.0.0 release binaries shipped unstripped: the Linux archives were +# roughly twice the size of the macOS ones purely because of debug symbols. +# Doing it here rather than in the workflow covers every target at once, +# including `cargo install`. +[profile.release] +strip = true + [workspace.lints.rust] unused_lifetimes = "warn" unsafe_op_in_unsafe_fn = "warn" diff --git a/RELEASE.md b/RELEASE.md index 2511caa..5b7ee13 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,7 +11,9 @@ Releases are automated by [release-plz](https://release-plz.dev). You do not run 2. `.github/workflows/release.yml` opens or updates a **`chore: release vX.Y.Z`** PR. It contains the semver bump in `[workspace.package]`, the refreshed `Cargo.lock`, and the generated `CHANGELOG.md` entry. Review it like any other PR. -3. Merging that PR releases: +3. Merging that PR releases, but only once CI has passed on the merge commit — a published + crates.io version can be yanked but never withdrawn, so the `Wait for CI` job blocks the + publish until the CI run for that exact commit is green: - tags `vX.Y.Z` and creates the GitHub release with the changelog as its notes, - publishes `soulseek-rs-lib` then `soulseek-rs` to crates.io (dependency order, waiting for the index between them), @@ -50,6 +52,13 @@ Both crates always share one version, one tag, and one changelog — see `releas Actions UI replays the *old* workflow file, so it is no use when the workflow is the thing that needs fixing. - To skip a release for a commit that would otherwise trigger one, use a `chore:` type. +- **If CI flakes on the release commit the publish is blocked, by design.** The e2e suite + talks to a real soulfind server over the network and does occasionally time out. To + recover: re-run the failed CI job, wait for that run to go green, then re-run the + **Release** workflow. The gate reads the *newest* CI run for the commit, so it will then + pass. Never work around it by publishing by hand. +- Release binaries are stripped via `[profile.release] strip = true` in the workspace + `Cargo.toml`, so `cargo install` benefits too — not just the release archives. ## Installing a release