ci(scripts): skipped duplicate release CI runs and enabled OIDC npm publishes

- Added a workflow `gate` job that marks `main` pushes with a `v*` tag at `HEAD` as duplicate release runs.
- Conditioned native, lint/test, and install CI jobs on that gate so redundant build and publish work is skipped on duplicate tagged pushes.
- Updated `scripts/ci-release-publish.ts` to publish packed tarballs via `npm publish` after `bun pm pack`, handling already-published versions as a no-op.
This commit is contained in:
can1357
2026-05-30 18:21:57 +02:00
parent e32c639d2e
commit e1a0d235ec
3 changed files with 465 additions and 35 deletions
+56 -7
View File
@@ -18,6 +18,36 @@ concurrency:
cancel-in-progress: true
jobs:
# During a release the version-bump commit and its `v*` tag are pushed
# atomically (`git push --atomic origin main refs/tags/v*` in
# scripts/release.ts), so GitHub fires two `push` events — one for
# `refs/heads/main`, one for the tag — that would each run the full build.
# The tag run is authoritative: it self-contains the native build and runs
# the release/publish jobs (release_binary downloads natives from its own
# run). Detect when this branch-push run is for a commit that already
# carries a release tag and skip the duplicate build here; normal main
# pushes (no `v*` tag at HEAD) and PRs are unaffected.
gate:
runs-on: ubuntu-22.04
outputs:
skip: ${{ steps.check.outputs.skip }}
steps:
- uses: actions/checkout@v4
with:
fetch-tags: true
- name: Detect duplicate release branch-push run
id: check
shell: bash
run: |
skip=false
if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
if git tag --points-at HEAD | grep -qE '^v[0-9]'; then
echo "HEAD carries a release tag; skipping the duplicate branch-push build (the tag run is authoritative)."
skip=true
fi
fi
echo "skip=$skip" >> "$GITHUB_OUTPUT"
# Compute a stable hash of every input that affects the native cdylib output,
# then look for any prior successful main run that already uploaded the
# native artifacts for this hash. Two independent outputs:
@@ -30,6 +60,8 @@ jobs:
# Non-tag native jobs are skipped when their canary hits; the canary
# retention window (see build-native action) is the effective TTL.
rust-hash:
needs: [gate]
if: ${{ needs.gate.outputs.skip != 'true' }}
runs-on: ubuntu-22.04
outputs:
hash: ${{ steps.compute.outputs.hash }}
@@ -118,6 +150,8 @@ jobs:
# Fast lint + type check (no Rust, no native build needed)
check:
needs: [gate]
if: ${{ needs.gate.outputs.skip != 'true' }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
@@ -136,8 +170,8 @@ jobs:
# Linux x64 baseline + modern: required by `test`, so it runs on every PR
# unless rust-hash found a cached run. Tags always rebuild for fresh artifacts.
native_linux:
needs: [rust-hash]
if: ${{ startsWith(github.ref, 'refs/tags/v') || needs.rust-hash.outputs.linux-run-id == '' }}
needs: [gate, rust-hash]
if: ${{ needs.gate.outputs.skip != 'true' && (startsWith(github.ref, 'refs/tags/v') || needs.rust-hash.outputs.linux-run-id == '') }}
runs-on: ubuntu-22.04
strategy:
fail-fast: false
@@ -160,8 +194,8 @@ jobs:
# building the artifacts that ship in release tags. Skipped on main when the
# rust-hash canary already found a recent run with all artifacts intact.
native_release:
needs: [rust-hash]
if: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.rust-hash.outputs.release-run-id == '') }}
needs: [gate, rust-hash]
if: ${{ needs.gate.outputs.skip != 'true' && (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.rust-hash.outputs.release-run-id == '')) }}
strategy:
fail-fast: false
matrix:
@@ -184,8 +218,8 @@ jobs:
test:
runs-on: ubuntu-22.04
needs: [native_linux, rust-hash]
if: ${{ !cancelled() && needs.native_linux.result != 'failure' }}
needs: [gate, native_linux, rust-hash]
if: ${{ !cancelled() && needs.gate.outputs.skip != 'true' && needs.native_linux.result != 'failure' }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
@@ -234,6 +268,8 @@ jobs:
run: bun run ci:test:smoke
install_methods:
needs: [gate]
if: ${{ needs.gate.outputs.skip != 'true' }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
@@ -417,6 +453,13 @@ jobs:
!inputs.skip_npm }}
needs: [release_binary, release_github_verify, rust-hash]
runs-on: ubuntu-22.04
# `id-token: write` lets npm mint the GitHub OIDC token it exchanges for a
# short-lived publish token (trusted publishing + provenance). When a
# package has no matching trusted publisher configured, npm silently falls
# back to NODE_AUTH_TOKEN below — which also covers first-ever publishes.
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
@@ -426,6 +469,9 @@ jobs:
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
# Trusted publishing (OIDC) and auto-provenance need npm >= 11.5.1.
- name: Ensure npm supports OIDC trusted publishing
run: npm install -g npm@latest
- name: Cache bun dependencies
uses: actions/cache@v4
with:
@@ -440,5 +486,8 @@ jobs:
merge-multiple: true
- name: Publish to npm
env:
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
# Fallback auth: setup-node wrote an .npmrc referencing
# NODE_AUTH_TOKEN; npm uses it only when OIDC has no trusted
# publisher for the package (or on a first publish).
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: bun run ci:release:publish