chore(provenance): rebind after the 0.18.1 version bump #51
Workflow file for this run
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 | |
| # Tokenless publishing via OIDC "Trusted Publishing". There are NO secrets to | |
| # store or rotate: each registry trusts THIS workflow directly, and GitHub mints a | |
| # short-lived identity per run. You configure the trusted publisher once on each | |
| # registry's website (see RELEASING.md), then publishing is just a tag. | |
| # | |
| # SAFE BY DEFAULT: the registry jobs are gated on a repository VARIABLE, so with | |
| # nothing configured this workflow publishes only the GitHub binary. Opt a registry | |
| # in by setting the var: | |
| # PUBLISH_CRATES=true · PUBLISH_NPM=true · PUBLISH_PYPI=true | |
| # Then push a tag (e.g. `v0.0.6`) or run the workflow manually. | |
| on: | |
| push: | |
| tags: ["v*"] | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: "Existing vX.Y.Z tag to recover or verify" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write # attach the static binary to the GitHub Release | |
| id-token: write # the OIDC identity used for crates.io trusted publishing | |
| # A release must finish or fail visibly; a newer dispatch must not cancel a | |
| # partially completed multi-registry publish. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| jobs: | |
| validate_tag: | |
| name: provenance-safe release tag | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| outputs: | |
| validated_commit: ${{ steps.verify.outputs.validated_commit }} | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.12.14" | |
| - name: Verify the exact tagged tree and every package version | |
| id: verify | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: python scripts/release.py verify-tag "$RELEASE_TAG" --github-output "$GITHUB_OUTPUT" | |
| binary: | |
| name: static binary (musl) | |
| needs: [validate_tag] | |
| runs-on: ubuntu-latest | |
| # Job-level permissions override the workflow-level block, so contents: write | |
| # must be restated here for the release-attach step. | |
| permissions: | |
| contents: write # attach the static binary to the GitHub Release | |
| id-token: write # OIDC identity for the provenance attestation | |
| attestations: write # store the attestation on the repo | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| ref: ${{ needs.validate_tag.outputs.validated_commit }} | |
| - name: Install Rust + musl target | |
| # Pin to the rust-toolchain.toml channel (1.96.0) so the musl target lands | |
| # on the toolchain cargo actually builds with. With @stable the target is | |
| # added to the stable toolchain, but the build uses 1.96.0 (rust-toolchain.toml) | |
| # and then can't find std/core for the musl target. | |
| uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # 2026-08-27 | |
| with: | |
| toolchain: "1.96.0" | |
| targets: x86_64-unknown-linux-musl | |
| - name: Install musl tools | |
| run: sudo apt-get update && sudo apt-get install -y musl-tools | |
| - name: Build the static release binary | |
| run: | | |
| cargo build --release --locked -p sharpebench --target x86_64-unknown-linux-musl \ | |
| || cargo build --release -p sharpebench --target x86_64-unknown-linux-musl | |
| - name: Checksum | |
| run: | | |
| cd target/x86_64-unknown-linux-musl/release | |
| sha256sum sharpebench > sharpebench-x86_64-linux-musl.sha256 | |
| cp sharpebench sharpebench-x86_64-linux-musl | |
| # SLSA build provenance for the release binary + its checksum, via GitHub's | |
| # native attestation rather than cosign: keyless (no keypair or secret to | |
| # manage or rotate) and verifiable with the GitHub CLI alone: | |
| # gh attestation verify sharpebench-x86_64-linux-musl --repo general-liquidity/sharpebench | |
| # This matches the OIDC-everywhere pattern the registry jobs below already use. | |
| - name: Attest build provenance (SLSA) | |
| uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 | |
| with: | |
| subject-path: | | |
| target/x86_64-unknown-linux-musl/release/sharpebench-x86_64-linux-musl | |
| target/x86_64-unknown-linux-musl/release/sharpebench-x86_64-linux-musl.sha256 | |
| - name: Attach to the release | |
| uses: softprops/action-gh-release@5113cdc90fd4d541c801c55356214017bf5ae34b # v3.0.3 | |
| with: | |
| tag_name: ${{ inputs.release_tag || github.ref_name }} | |
| files: | | |
| target/x86_64-unknown-linux-musl/release/sharpebench-x86_64-linux-musl | |
| target/x86_64-unknown-linux-musl/release/sharpebench-x86_64-linux-musl.sha256 | |
| crates: | |
| name: crates.io (12 sharpebench-* crates) | |
| if: ${{ vars.PUBLISH_CRATES == 'true' }} | |
| needs: [validate_tag] | |
| runs-on: ubuntu-latest | |
| # A dedicated publishing environment (configure protection rules under | |
| # Settings → Environments → crates). Must match the crates.io trusted-publisher | |
| # config registered for EACH of the 12 crates. | |
| environment: crates | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| ref: ${{ needs.validate_tag.outputs.validated_commit }} | |
| - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # 2026-08-27 | |
| with: | |
| toolchain: "1.96.0" | |
| - name: Assert the workspace manifest matches the release tag | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| TAG="$RELEASE_TAG" | |
| TAG="${TAG#v}" | |
| V="$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')" | |
| test "$V" = "$TAG" || { echo "::error::workspace version $V does not match tag $TAG"; exit 1; } | |
| # Exchanges the GitHub OIDC identity for a short-lived crates.io token. | |
| - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1 2026-08-27 | |
| id: auth | |
| # Publish each crate bottom-up. `cargo publish -p` waits for the crate to index | |
| # before returning, so the next crate's deps resolve. We do NOT use `cargo publish | |
| # --workspace`: its publish planner can deadlock part-way ("no packages ready to | |
| # publish but N packages remain… awaiting confirmation"), which left harness + cli | |
| # unpublished on a prior run. The per-crate skip-if-published guard makes a re-run | |
| # idempotent — it finishes only the crates still missing at this version. | |
| - name: Publish the crates to crates.io (dependency order, idempotent) | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} | |
| run: | | |
| UA="sharpebench-release (general-liquidity)" # crates.io rejects requests without a User-Agent | |
| V="$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')" | |
| # leaves first → CLI last. memory depends only on stats and nothing depends on | |
| # memory, so it follows stats. protocol has no internal deps and core depends on it | |
| # (DeclaredMandate), so protocol precedes core; sim/leaderboard/wasm depend on core/protocol/attest; | |
| # harness on sim; sharpebench on all). edge depends on stats and is itself a dep of | |
| # wasm + the CLI, so it MUST come before wasm. arena depends on core/attest/ | |
| # leaderboard/sim and is a dep of the CLI, so it comes after those and before | |
| # sharpebench. publish=false members are omitted. | |
| for c in sharpebench-stats sharpebench-memory sharpebench-edge sharpebench-protocol \ | |
| sharpebench-core sharpebench-attest sharpebench-sim sharpebench-leaderboard \ | |
| sharpebench-wasm sharpebench-harness sharpebench-arena sharpebench; do | |
| if curl -s -A "$UA" "https://crates.io/api/v1/crates/$c/$V" | grep -q "\"num\":\"$V\""; then | |
| echo "✓ $c@$V already on crates.io — skipping" | |
| else | |
| echo "→ publishing $c@$V" | |
| if ! cargo publish -p "$c" --locked; then | |
| echo "::error::publish of $c@$V failed. If crates.io answered 403 'not valid for crate', the crate has no trusted-publisher config: add repository general-liquidity/sharpebench, workflow release.yml, environment crates under https://crates.io/crates/$c/settings/new-trusted-publisher, then re-dispatch this workflow with release_tag=v$V" | |
| exit 1 | |
| fi | |
| fi | |
| done | |
| npm: | |
| name: npm (@general-liquidity/sharpebench + -mcp) | |
| if: ${{ vars.PUBLISH_NPM == 'true' }} | |
| needs: [validate_tag] | |
| runs-on: ubuntu-latest | |
| # Must match the npm trusted-publisher config for both packages. | |
| environment: npm | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| ref: ${{ needs.validate_tag.outputs.validated_commit }} | |
| # The npm package ships the compiled WASM kernel (npm/pkg), so build it first. | |
| - name: Install Rust (pinned) + wasm target | |
| uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # 2026-08-27 | |
| with: | |
| toolchain: "1.96.0" | |
| targets: wasm32-unknown-unknown | |
| - name: Install wasm-pack | |
| uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2 | |
| with: | |
| tool: wasm-pack@0.15.0 | |
| - name: Build the WASM kernel | |
| run: wasm-pack build crates/sharpebench-wasm --target nodejs --out-dir ../../npm/pkg --out-name sharpebench | |
| - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 | |
| with: | |
| node-version: 24.18.0 | |
| registry-url: https://registry.npmjs.org | |
| # npm trusted publishing (OIDC) needs npm >= 11.5; provenance is automatic. | |
| - run: npm install -g npm@12.0.2 | |
| # cargo-release rewrites the npm manifests from Cargo.toml, so they must already | |
| # equal the tag. If they do not, the steps below read a stale version, find it | |
| # already on the registry, and "skip" — a green run that shipped nothing. That is | |
| # exactly how npm stalled at 0.0.10 while the crates went to 0.0.11. | |
| - name: Assert npm manifests match the release tag | |
| if: startsWith(github.ref, 'refs/tags/v') || inputs.release_tag != '' | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| TAG="$RELEASE_TAG" | |
| TAG="${TAG#v}" | |
| fail=0 | |
| for f in package.json mcp/package.json; do | |
| V=$(node -p "require('./$f').version") | |
| if [ "$V" != "$TAG" ]; then | |
| echo "::error file=npm/$f::version $V does not match tag $TAG — the manifest is stale, cut the release with 'cargo release' so pre-release-replacements rewrite it" | |
| fail=1 | |
| fi | |
| done | |
| exit $fail | |
| working-directory: npm | |
| - name: Publish @general-liquidity/sharpebench (skip if version exists) | |
| working-directory: npm | |
| run: | | |
| npm install | |
| npm run build | |
| V=$(node -p "require('./package.json').version") | |
| if npm view "@general-liquidity/sharpebench@$V" version >/dev/null 2>&1; then | |
| echo "@general-liquidity/sharpebench@$V already published — skipping" | |
| else | |
| npm publish --access public | |
| fi | |
| - name: Publish @general-liquidity/sharpebench-mcp (skip if version exists) | |
| working-directory: npm/mcp | |
| run: | | |
| # mcp depends on @general-liquidity/sharpebench at this release. When this run just | |
| # published that dep, the registry read-path lags a few seconds, so `npm install` | |
| # here can fail ETARGET ("No matching version") before it propagates. Wait for it. | |
| # Do not consult the committed lock for this one install: before a version is | |
| # published, that lock cannot carry the registry's final tarball integrity for the | |
| # same version. npm otherwise accepts the fresh metadata but rejects the tarball | |
| # against the pre-release integrity, preventing the MCP package from publishing. | |
| DEPV="$(node -p "require('../package.json').version")" | |
| for i in $(seq 1 30); do | |
| if npm view "@general-liquidity/sharpebench@$DEPV" version >/dev/null 2>&1; then | |
| echo "dep @general-liquidity/sharpebench@$DEPV available"; break | |
| fi | |
| echo "waiting for @general-liquidity/sharpebench@$DEPV to propagate ($i)…"; sleep 5 | |
| done | |
| npm install --package-lock=false | |
| npm run build | |
| V=$(node -p "require('./package.json').version") | |
| if npm view "@general-liquidity/sharpebench-mcp@$V" version >/dev/null 2>&1; then | |
| echo "@general-liquidity/sharpebench-mcp@$V already published — skipping" | |
| else | |
| npm publish --access public | |
| fi | |
| pypi: | |
| name: PyPI (sharpebench wheel) | |
| if: ${{ vars.PUBLISH_PYPI == 'true' }} | |
| needs: [validate_tag] | |
| runs-on: ubuntu-latest | |
| # Must match the PyPI trusted-publisher config for the `sharpebench` project. | |
| environment: pypi | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| ref: ${{ needs.validate_tag.outputs.validated_commit }} | |
| # cargo-release rewrites crates/sharpebench-py/{Cargo.toml,pyproject.toml} from the | |
| # workspace version. If they are stale the wheel below builds the OLD version, PyPI | |
| # already serves it, skip-existing swallows it, and the run goes green having shipped | |
| # nothing. Exactly the npm-stall failure, so assert the same way npm does. | |
| - name: Assert the Python manifests match the release tag | |
| if: startsWith(github.ref, 'refs/tags/v') || inputs.release_tag != '' | |
| working-directory: crates/sharpebench-py | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| TAG="$RELEASE_TAG" | |
| TAG="${TAG#v}" | |
| fail=0 | |
| for f in Cargo.toml pyproject.toml; do | |
| V=$(grep -m1 '^version = ' "$f" | sed 's/.*"\(.*\)".*/\1/') | |
| if [ "$V" != "$TAG" ]; then | |
| echo "::error file=crates/sharpebench-py/$f::version $V does not match tag $TAG - the manifest is stale, cut the release with 'cargo release' so pre-release-replacements rewrite it" | |
| fail=1 | |
| fi | |
| done | |
| exit $fail | |
| - name: Build manylinux wheels | |
| uses: PyO3/maturin-action@f0a989e78b427215a87ce3021f5a90507cae75fb # 2026-08-27 | |
| with: | |
| command: build | |
| args: --release --out dist --interpreter 3.10 3.11 3.12 3.13 --manifest-path crates/sharpebench-py/Cargo.toml | |
| manylinux: "2014" | |
| container: quay.io/pypa/manylinux2014_x86_64@sha256:edb6edbd84c2fa9d40ee83abb160e302ebce82eb93570d43343942a1fb10b962 | |
| maturin-version: v1.15.0 | |
| - name: Build sdist | |
| uses: PyO3/maturin-action@f0a989e78b427215a87ce3021f5a90507cae75fb # 2026-08-27 | |
| with: | |
| command: sdist | |
| args: --out dist --manifest-path crates/sharpebench-py/Cargo.toml | |
| maturin-version: v1.15.0 | |
| # Trusted publishing - configure the PyPI publisher for repo `sharpebench`, | |
| # workflow `release.yml`, environment `pypi`. No token needed. | |
| - name: Publish to PyPI (OIDC) | |
| uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 2026-08-27 | |
| with: | |
| packages-dir: dist | |
| # Idempotent re-runs: a version already on PyPI is skipped, not an error | |
| # (matches the crates + npm skip-if-published guards in this workflow). | |
| skip-existing: true | |
| # Every publish step above is skip-if-present, which makes re-runs idempotent but also | |
| # lets a surface that shipped nothing still report green. This job asks the registries | |
| # directly, so an incomplete release fails the run instead of passing quietly. | |
| verify: | |
| name: verify every registry serves the release | |
| needs: [validate_tag, crates, npm, pypi] | |
| # Recovery runs dispatched with release_tag must verify too; a recovery that | |
| # skips verification reports success without proving the registries serve it. | |
| if: always() && (startsWith(github.ref, 'refs/tags/v') || inputs.release_tag != '') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Fail if validation or a publish job failed | |
| env: | |
| VALIDATE_TAG_RESULT: ${{ needs.validate_tag.result }} | |
| CRATES_RESULT: ${{ needs.crates.result }} | |
| NPM_RESULT: ${{ needs.npm.result }} | |
| PYPI_RESULT: ${{ needs.pypi.result }} | |
| run: | | |
| fail=0 | |
| for job in validate_tag crates npm pypi; do | |
| case "$job" in | |
| validate_tag) result="$VALIDATE_TAG_RESULT" ;; | |
| crates) result="$CRATES_RESULT" ;; | |
| npm) result="$NPM_RESULT" ;; | |
| pypi) result="$PYPI_RESULT" ;; | |
| esac | |
| echo "$job: $result" | |
| if [ "$result" = "failure" ]; then | |
| echo "::error::release job '$job' failed"; fail=1 | |
| fi | |
| done | |
| exit $fail | |
| - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 | |
| with: | |
| node-version: 24.18.0 | |
| - name: Assert crates.io, npm and PyPI all serve the tag version | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| PUBLISH_CRATES: ${{ vars.PUBLISH_CRATES }} | |
| PUBLISH_NPM: ${{ vars.PUBLISH_NPM }} | |
| PUBLISH_PYPI: ${{ vars.PUBLISH_PYPI }} | |
| run: | | |
| TAG="$RELEASE_TAG" | |
| TAG="${TAG#v}" | |
| UA="sharpebench-release (general-liquidity)" # crates.io rejects requests without a User-Agent | |
| fail=0 | |
| if [ "$PUBLISH_CRATES" = "true" ]; then | |
| for c in sharpebench-stats sharpebench-memory sharpebench-edge sharpebench-protocol sharpebench-core \ | |
| sharpebench-attest sharpebench-sim sharpebench-leaderboard sharpebench-wasm \ | |
| sharpebench-harness sharpebench-arena sharpebench; do | |
| if curl -s -A "$UA" "https://crates.io/api/v1/crates/$c/$TAG" | grep -q "\"num\":\"$TAG\""; then | |
| echo "ok crates.io $c@$TAG" | |
| else | |
| echo "::error::crates.io is not serving $c@$TAG"; fail=1 | |
| fi | |
| done | |
| fi | |
| if [ "$PUBLISH_NPM" = "true" ]; then | |
| for p in @general-liquidity/sharpebench @general-liquidity/sharpebench-mcp; do | |
| ok=0 | |
| # the registry read-path lags a few seconds behind a publish | |
| for i in $(seq 1 20); do | |
| if [ "$(npm view "$p@$TAG" version 2>/dev/null)" = "$TAG" ]; then ok=1; break; fi | |
| sleep 5 | |
| done | |
| if [ "$ok" = "1" ]; then | |
| echo "ok npm $p@$TAG" | |
| else | |
| echo "::error::npm is not serving $p@$TAG"; fail=1 | |
| fi | |
| done | |
| fi | |
| if [ "$PUBLISH_PYPI" = "true" ]; then | |
| ok=0 | |
| # the PyPI JSON API lags a few seconds behind a publish | |
| for i in $(seq 1 20); do | |
| if curl -sf "https://pypi.org/pypi/sharpebench/$TAG/json" >/dev/null 2>&1; then ok=1; break; fi | |
| sleep 5 | |
| done | |
| if [ "$ok" = "1" ]; then | |
| echo "ok pypi sharpebench@$TAG" | |
| else | |
| echo "::error::PyPI is not serving sharpebench@$TAG"; fail=1 | |
| fi | |
| fi | |
| exit $fail |