From cfd152a3cc071dfe35c6c1880345887c162f476a Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Sat, 12 Sep 2026 13:22:49 -0400 Subject: [PATCH] ci(release): pin publish job to a named environment and tag refs The publish job is the only job that mints a short-lived OIDC identity and exchanges it for a registry publish credential. Declare a named environment on that job so the registry-side trusted publisher and a repository environment protection rule have something to pin to, and add an if: guard so the job runs only from a release tag push, a published release, or a default-branch dispatch (the documented idempotent backfill path) -- never from an arbitrary branch. The existing resolve-ref job validates the TAG (shape, existence, ancestry to origin/main), which governs the code that gets published. It says nothing about the run own ref, which is what lands in the OIDC token ref claim and scopes the run Actions cache. This guard closes that gap. id-token: write stays scoped to this job only; the workflow-level default remains contents: read. Verified locally (org Actions runs are not executing): actionlint exit 0, YAML parses, job graph unchanged, additive diff only. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac8b0da..25895d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -296,8 +296,33 @@ jobs: publish: name: Publish to npm (gated, idempotent) needs: [resolve-ref, secret-scan, verify] + # Ref floor for the one job that mints a publishing credential. + # `resolve-ref` proves the TAG is sdk-v, exists, and is an ancestor + # of origin/main -- so the CODE published is always merged code. It says + # nothing about the RUN's own ref, which is what lands in the OIDC token's + # `ref` claim and scopes this run's Actions cache: a dispatch from an + # arbitrary branch would still reach npm under that branch's identity. + # So: an sdk-v* tag push, a published release, or a dispatch from the + # default branch (the documented backfill path, already ancestry-anchored) + # -- and nothing else. A missing `default_branch` makes `format(...)` yield + # `refs/heads/`, matching no real ref, so the guard fails CLOSED. + if: >- + startsWith(github.ref, 'refs/tags/sdk-v') + || github.event_name == 'release' + || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) runs-on: ubuntu-latest timeout-minutes: 15 + # Named environment on the publish job ONLY. It anchors two things this + # workflow cannot grant itself: the `environment:npm` claim the + # registry-side trusted publisher is pinned to, and an environment + # protection rule (required reviewer + tag-only deployment policy) gating + # the mint. Configuring that environment is an operator act, not this file. + environment: + name: npm + url: https://www.npmjs.com/package/@wave-av/sdk + # `id-token: write` lives here and NOWHERE else; the workflow-level default + # stays `contents: read`. Do not hoist this to the workflow level -- the + # sbom job deliberately holds `contents: write` and never the mint. permissions: id-token: write # mint the OIDC token npm exchanges for a publish credential contents: read