Repository navigation
Release #39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # One-dispatch release orchestrator: version gate, build matrix, tag, and | |
| # GitHub release live here; each registry publish lives in its own | |
| # independently-runnable workflow (publish-cargo.yml, publish-npm.yml). | |
| # The fan-out jobs below dispatch each at the release tag via `gh workflow run` | |
| # (scripts/dispatch-publish.sh) and watch the dispatched run to completion, | |
| # so this run's job graph still reflects every registry's real outcome and | |
| # "Re-run failed jobs" re-dispatches exactly the failed legs. A failed leg | |
| # can also be retried without this run at all: fix the cause and dispatch | |
| # that registry's workflow manually with the release version — no rebuild | |
| # happens either way. The npm leg takes the prebuilt binaries from | |
| # the GitHub release's assets (verified against SHA256SUMS, pinned by digest | |
| # on the fan-out path), which is why it waits on `github-release` below. | |
| # | |
| # Why dispatch instead of `uses:` (reusable workflows) — two GitHub/registry | |
| # facts, verified 2026-08-21 against the registries' docs and source: | |
| # 1. npm and crates.io OIDC trusted publishers match the top-level | |
| # workflow filename (`workflow_ref` claim). npm additionally | |
| # allows only ONE trusted publisher per package, so a leg that is | |
| # sometimes `uses:`-called (top-level = release.yml) and sometimes | |
| # dispatched (top-level = its own file) can never be authorized for | |
| # both paths. Dispatching makes every leg ALWAYS the top-level workflow | |
| # of its own run, so both claims name the leg's file and one publisher | |
| # registration per package covers everything, on every registry. | |
| # 2. GitHub suppresses events caused by this workflow's own GITHUB_TOKEN — | |
| # a `release: published` (or `push: tags:`) trigger in another file | |
| # would never fire — but workflow_dispatch (and repository_dispatch) events | |
| # are documented exceptions, so `gh workflow run` with GITHUB_TOKEN | |
| # works without a PAT/GitHub App token. | |
| # | |
| # Registry credentials: | |
| # - crates.io: OIDC trusted publishing (rust-lang/crates-io-auth-action); | |
| # no long-lived secret, no environment. | |
| # - npm: OIDC via `npm stage publish`; staged versions require | |
| # manual 2FA approval (see the npm run's step summary). | |
| # Every registry's trusted publisher is keyed on the repo + the publish | |
| # workflow's own filename (see each publish-*.yml header), NOT release.yml. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| dry-run: | |
| description: 'Dry run (build only, skip publish)' | |
| type: boolean | |
| default: false | |
| permissions: {} | |
| jobs: | |
| version: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| version: ${{ steps.read.outputs.VERSION }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Read version from Cargo.toml | |
| id: read | |
| run: | | |
| VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "Release version: $VERSION" | |
| - name: Release-readiness gate | |
| # scripts/release-lint.sh is the single source of truth for the | |
| # version chores, shared with CI's release-readiness job (which runs | |
| # it on the PR that bumps the version, so failures surface at PR | |
| # time, not here). Checks: version coherence (the offline stamp, | |
| # scripts/release.py stamp, is a no-op), CHANGELOG has a non-empty | |
| # section for this version, and — via --tag-check, asked of the | |
| # remote since this checkout is shallow and tagless — the tag doesn't | |
| # exist at a different commit (a tag already at $GITHUB_SHA is a | |
| # retry of a previous run and passes). --stable-only: this legacy | |
| # pipeline publishes as GitHub Latest / npm latest, so it must never | |
| # see an X.Y.Z-rc.N version; rcs ship only through the release train | |
| # (docs/release-train/DESIGN.md), which replaces this workflow. | |
| run: bash scripts/release-lint.sh --stable-only --tag-check | |
| build: | |
| needs: version | |
| strategy: | |
| matrix: | |
| include: | |
| - target: aarch64-apple-darwin | |
| runner: macos-14 | |
| archive: tar.gz | |
| build-tool: cargo | |
| - target: x86_64-apple-darwin | |
| runner: macos-14 | |
| archive: tar.gz | |
| build-tool: cargo | |
| - target: x86_64-unknown-linux-gnu | |
| runner: ubuntu-latest | |
| archive: tar.gz | |
| build-tool: cross | |
| - target: x86_64-unknown-linux-musl | |
| runner: ubuntu-latest | |
| archive: tar.gz | |
| build-tool: cross | |
| - target: aarch64-unknown-linux-gnu | |
| runner: ubuntu-latest | |
| archive: tar.gz | |
| build-tool: cross | |
| - target: aarch64-unknown-linux-musl | |
| runner: ubuntu-latest | |
| archive: tar.gz | |
| build-tool: cross | |
| - target: x86_64-pc-windows-msvc | |
| runner: windows-latest | |
| archive: zip | |
| build-tool: cargo | |
| - target: i686-pc-windows-msvc | |
| runner: windows-latest | |
| archive: zip | |
| build-tool: cargo | |
| - target: aarch64-pc-windows-msvc | |
| runner: windows-latest | |
| archive: zip | |
| build-tool: cargo | |
| - target: aarch64-linux-android | |
| runner: ubuntu-latest | |
| archive: tar.gz | |
| build-tool: cross | |
| - target: arm-unknown-linux-gnueabihf | |
| runner: ubuntu-latest | |
| archive: tar.gz | |
| build-tool: cross | |
| - target: arm-unknown-linux-musleabihf | |
| runner: ubuntu-latest | |
| archive: tar.gz | |
| build-tool: cross | |
| - target: i686-unknown-linux-gnu | |
| runner: ubuntu-latest | |
| archive: tar.gz | |
| build-tool: cross | |
| - target: i686-unknown-linux-musl | |
| runner: ubuntu-latest | |
| archive: tar.gz | |
| build-tool: cross | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Install Rust | |
| # rustup is pre-installed on GitHub-hosted runners. `rustup show` | |
| # reads rust-toolchain.toml in the repo root, then installs the | |
| # pinned channel + listed components if missing. The dtolnay action | |
| # cannot auto-detect the channel when pinned by SHA (it normally | |
| # parses it from the ref name), so we go through rustup directly. | |
| run: | | |
| rustup show | |
| rustup target add ${{ matrix.target }} | |
| - name: Install cross | |
| if: matrix.build-tool == 'cross' | |
| run: cargo install --locked --version =0.2.5 cross | |
| - name: Build (cargo) | |
| if: matrix.build-tool == 'cargo' | |
| run: cargo build --release -p socket-patch-cli --target ${{ matrix.target }} | |
| - name: Build (cross) | |
| if: matrix.build-tool == 'cross' | |
| run: cross build --release -p socket-patch-cli --target ${{ matrix.target }} | |
| - name: Package (unix) | |
| if: matrix.archive == 'tar.gz' | |
| run: | | |
| cd target/${{ matrix.target }}/release | |
| tar czf ../../../socket-patch-${{ matrix.target }}.tar.gz socket-patch | |
| cd ../../.. | |
| - name: Package (windows) | |
| if: matrix.archive == 'zip' | |
| shell: pwsh | |
| run: | | |
| Compress-Archive -Path "target/${{ matrix.target }}/release/socket-patch.exe" -DestinationPath "socket-patch-${{ matrix.target }}.zip" | |
| - name: Upload artifact (tar.gz) | |
| if: matrix.archive == 'tar.gz' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: socket-patch-${{ matrix.target }} | |
| path: socket-patch-${{ matrix.target }}.tar.gz | |
| - name: Upload artifact (zip) | |
| if: matrix.archive == 'zip' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: socket-patch-${{ matrix.target }} | |
| path: socket-patch-${{ matrix.target }}.zip | |
| tag: | |
| needs: [version, build] | |
| if: ${{ !inputs.dry-run }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| # Intentionally persists credentials: the tag push below authenticates | |
| # with this workflow's GITHUB_TOKEN. Note that tags (and releases) | |
| # created with GITHUB_TOKEN do NOT trigger other workflows — GitHub | |
| # suppresses events caused by that token — which is why the publish | |
| # legs hang off explicit workflow_dispatch fan-out (the documented | |
| # suppression exception; see this file's header) rather than a | |
| # `push: tags:` or `release: published` trigger. | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Create and push tag | |
| env: | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: | | |
| TAG="v${VERSION}" | |
| git tag "$TAG" | |
| # Pushing a tag that already exists at the same commit is a no-op | |
| # success, so a re-run after a mid-release failure passes here. | |
| git push origin "$TAG" | |
| github-release: | |
| needs: [version, build, tag] | |
| if: ${{ !inputs.dry-run }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| outputs: | |
| # sha256 of the SHA256SUMS file uploaded below. The npm fan-out | |
| # passes it to the publish leg, which re-downloads the assets from the | |
| # release: pinning the sums file by digest binds what it publishes | |
| # to exactly what THIS run built, restoring the same-run-artifact | |
| # provenance the pre-split inline jobs had (release assets are mutable; | |
| # anyone with contents:write could clobber them between jobs). | |
| sums-digest: ${{ steps.sums.outputs.digest }} | |
| steps: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Generate SHA256SUMS | |
| id: sums | |
| run: | | |
| cd artifacts | |
| # Hash every release artifact (tar.gz + zip) so install.sh can verify | |
| # the binary before extraction. Sorted output keeps the file stable. | |
| sha256sum -- *.tar.gz *.zip 2>/dev/null | sort > SHA256SUMS | |
| cat SHA256SUMS | |
| echo "digest=$(sha256sum SHA256SUMS | awk '{print $1}')" >> "$GITHUB_OUTPUT" | |
| - name: Create or update GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: | | |
| TAG="v${VERSION}" | |
| # Idempotent for "Re-run failed jobs": if a previous attempt already | |
| # created the release, refresh its assets instead of hard-failing. | |
| if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| echo "Release $TAG already exists; re-uploading assets with --clobber." | |
| gh release upload "$TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --clobber \ | |
| artifacts/* | |
| else | |
| gh release create "$TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --generate-notes \ | |
| artifacts/* | |
| fi | |
| # ── Publish fan-out ──────────────────────────────────────────────────────── | |
| # Each job dispatches one registry's publish workflow at the release tag | |
| # and watches the dispatched run to completion (scripts/dispatch-publish.sh), | |
| # failing iff that run fails — see this file's header for why the legs run | |
| # as separate workflow_dispatch runs. The step summary links each leg's | |
| # run. timeout-minutes bounds the watch; a timed-out watch does NOT cancel | |
| # the leg, which finishes (and stays retryable) on its own. If a watcher | |
| # fails while its leg is still alive (watch timeout, correlation miss), | |
| # re-running it dispatches a second run of the same leg — safe, because | |
| # each publish workflow serializes same-version runs with a concurrency | |
| # group, so the duplicate queues behind the live run and then no-ops on | |
| # the already-published probes. | |
| cargo-publish: | |
| needs: [version, build, tag] | |
| if: ${{ !inputs.dry-run }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| permissions: | |
| contents: read | |
| actions: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Dispatch and watch Publish crates.io | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: bash scripts/dispatch-publish.sh publish-cargo.yml "$VERSION" | |
| # npm takes the prebuilt binaries from the GitHub release's assets, | |
| # so it waits on `github-release`. | |
| npm-publish: | |
| needs: [version, github-release] | |
| if: ${{ !inputs.dry-run }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| permissions: | |
| contents: read | |
| actions: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Dispatch and watch Publish npm | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.version.outputs.version }} | |
| SUMS_DIGEST: ${{ needs.github-release.outputs.sums-digest }} | |
| run: | | |
| # Fail closed: an empty digest would silently downgrade the leg's | |
| # asset check from provenance-pinned to integrity-only. | |
| if [ -z "$SUMS_DIGEST" ]; then | |
| echo "::error::github-release produced no SHA256SUMS digest; refusing to dispatch an unpinned publish" | |
| exit 1 | |
| fi | |
| bash scripts/dispatch-publish.sh publish-npm.yml "$VERSION" "sums-digest=${SUMS_DIGEST}" |