diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml index d5d10b1..9df155b 100644 --- a/.github/workflows/homebrew.yml +++ b/.github/workflows/homebrew.yml @@ -26,20 +26,31 @@ jobs: # from third-party taps with an UntrustedTapError. A plain file edit # sidesteps `brew` entirely and never hits the trust check. - name: Bump tap formula - # Non-blocking: needs a valid HOMEBREW_TAP_TOKEN. If the token is - # missing/expired the bump fails here, but the calling release run - # stays green (the binaries + checksums have already published). Fix - # the token and re-run to bump the tap. - continue-on-error: true + # Pushes with a deploy key that can write to borghei/homebrew-tap and + # nothing else (secret HOMEBREW_TAP_DEPLOY_KEY; public half is a + # read-write deploy key on the tap). No personal access token, so there + # is nothing to expire. A failure here is real and fails the job: the + # binaries have already published, so fix the cause and re-run this + # workflow by hand with the tag (workflow_dispatch). env: TAG: ${{ inputs.tag }} - TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }} run: | set -euo pipefail VERSION="${TAG#v}" echo "Bumping borghei/tap ink formula to ${VERSION}" - git clone "https://x-access-token:${TAP_TOKEN}@github.com/borghei/homebrew-tap.git" tap + if [ -z "$TAP_DEPLOY_KEY" ]; then + echo "::error::HOMEBREW_TAP_DEPLOY_KEY secret is not set; cannot bump the tap." + exit 1 + fi + install -m 700 -d ~/.ssh + printf '%s\n' "$TAP_DEPLOY_KEY" > ~/.ssh/tap_deploy_key + chmod 600 ~/.ssh/tap_deploy_key + ssh-keyscan -t ed25519 github.com >> ~/.ssh/known_hosts 2>/dev/null + export GIT_SSH_COMMAND="ssh -i ~/.ssh/tap_deploy_key -o IdentitiesOnly=yes" + + git clone git@github.com:borghei/homebrew-tap.git tap F=tap/Formula/ink.rb # Bump the version field and the version embedded in every download URL. @@ -59,6 +70,9 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" if git diff --quiet; then + # Still prove the key can write, so a broken key is found now and + # not at the next release. + git push --dry-run origin HEAD echo "Formula already up to date — nothing to push." exit 0 fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3257e6e..0823f68 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -453,36 +453,56 @@ jobs: # Publish the crate so `cargo install ink-md` (and mise's cargo backend) # track releases instead of going stale (issue #4: crates.io sat at 0.2.1 - # while binaries reached 0.6.x). Skips cleanly when the token secret is - # absent; never fails the release — the binaries have already published. + # while binaries reached 0.6.x). A failure here is loud on purpose: the + # binaries have already published, so fix the cause and re-run the job. crates: needs: release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + # Tag pushes publish. A manual run (workflow_dispatch) only rehearses: + # it does the token exchange and a `cargo publish --dry-run`. + if: >- + always() && + ((github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && needs.release.result == 'success') + || github.event_name == 'workflow_dispatch') runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # OIDC token for crates.io Trusted Publishing steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + # Trusted Publishing: crates.io trusts this repository's release.yml + # (configured on the crate), so there is no long-lived token to store + # or rotate. The action swaps the job's OIDC token for a short-lived + # crates.io token and revokes it when the job ends. + - uses: rust-lang/crates-io-auth-action@v1 + id: auth - name: Publish to crates.io - continue-on-error: true env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} run: | - if [ -z "$CARGO_REGISTRY_TOKEN" ]; then - echo "No CRATES_IO_TOKEN secret — skipping crates.io publish." + set -euo pipefail + version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1) + if [ "$GITHUB_EVENT_NAME" != "push" ]; then + echo "Manual run: rehearsing only." + cargo publish --dry-run --locked + exit 0 + fi + # Re-running a release must not fail because the version is already up. + code=$(curl -s -o /dev/null -w '%{http_code}' -A "ink-release (github.com/borghei/ink)" \ + "https://crates.io/api/v1/crates/ink-md/${version}") + if [ "$code" = "200" ]; then + echo "ink-md ${version} is already on crates.io — nothing to publish." exit 0 fi - cargo publish + cargo publish --locked # Bump the Homebrew tap formula in the same run. Chaining via `needs` (rather # than a `release: published` trigger on homebrew.yml) is deliberate: releases # created with the default GITHUB_TOKEN do not emit events that trigger other # workflows, so a `release`-triggered bump would silently never fire. # - # The bump is made non-blocking inside homebrew.yml (its step is - # continue-on-error): a missing/expired HOMEBREW_TAP_TOKEN fails the bump but - # not the release, since the binaries + checksums have already published. - # (`continue-on-error` is not valid on a reusable-workflow-call job, so the - # tolerance lives on the step instead.) + # The bump pushes with a deploy key (secret HOMEBREW_TAP_DEPLOY_KEY) and + # fails loudly if it cannot; re-run homebrew.yml by hand with the tag. homebrew: needs: release if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')