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.
This commit is contained in:
@@ -18,6 +18,44 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
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
|
# 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
|
# changelog derived from the conventional commits on master. Merging it is the
|
||||||
# release trigger.
|
# release trigger.
|
||||||
@@ -47,6 +85,11 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Tag and publish
|
name: Tag and publish
|
||||||
runs-on: ubuntu-latest
|
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:
|
concurrency:
|
||||||
group: release-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ edition = "2024"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/michel/soulseek-rs"
|
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]
|
[workspace.lints.rust]
|
||||||
unused_lifetimes = "warn"
|
unused_lifetimes = "warn"
|
||||||
unsafe_op_in_unsafe_fn = "warn"
|
unsafe_op_in_unsafe_fn = "warn"
|
||||||
|
|||||||
+10
-1
@@ -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
|
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
|
contains the semver bump in `[workspace.package]`, the refreshed `Cargo.lock`, and the
|
||||||
generated `CHANGELOG.md` entry. Review it like any other PR.
|
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,
|
- 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
|
- publishes `soulseek-rs-lib` then `soulseek-rs` to crates.io (dependency order, waiting
|
||||||
for the index between them),
|
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
|
Actions UI replays the *old* workflow file, so it is no use when the workflow is the thing
|
||||||
that needs fixing.
|
that needs fixing.
|
||||||
- To skip a release for a commit that would otherwise trigger one, use a `chore:` type.
|
- 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
|
## Installing a release
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user