Files
soulseek-rs-modded/.github/workflows/release.yml
T
Michel de Graaf 0b332c5170 ci: let the tap update its own formula
The homebrew job needed a cross-repo PAT in HOMEBREW_TAP_TOKEN, which resolved
to an empty string and failed every release with "Input required and not
supplied: token". A tap can write to itself with the GITHUB_TOKEN it already
has, so the formula is now generated by an Update formula workflow in
michel/homebrew-tap that reads the latest release and commits when it changes.

That removes the secret from the release path entirely. Verified by corrupting
the formula and watching the workflow put it back.
2026-07-25 04:00:20 +02:00

188 lines
6.8 KiB
YAML

name: Release
on:
push:
branches: [master]
paths-ignore: ['website/**']
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|cancelled) ;;
*) 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 }}