Skip to content

chore(provenance): rebind after the 0.18.1 version bump #51

chore(provenance): rebind after the 0.18.1 version bump

chore(provenance): rebind after the 0.18.1 version bump #51

Workflow file for this run

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