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