name: Release on: push: branches: [main] workflow_dispatch: # Publishing authenticates with a long-lived **NPM_TOKEN** (decision C1): an npm # automation / granular-access token with publish rights to the `@graphorin` # scope, injected as `NPM_TOKEN` on the publish step only (CI-10) so the # workspace build/test steps never see it. Sigstore provenance is still attached # via OIDC (`id-token: write` below + `.npmrc` provenance=true + per-package # `publishConfig.provenance`); provenance uses OIDC regardless of the publish # auth method. # # The publish step is gated on the `RELEASE_ENABLED` **repository variable** so # an accidental main-branch push never publishes before the registry side is # configured. Maintainer provisioning order: # # 1. Create an npm automation token with publish rights to `@graphorin/*` # (and `graphorin`); add it as the repo secret `NPM_TOKEN`. Ensure the # `@graphorin` scope exists and the token can publish each (still- # unpublished) package name. # 2. Rehearse with a `workflow_dispatch` run (the dry-run step below fires # whenever RELEASE_ENABLED is not yet 'true') to confirm auth + the set. # 3. Set the repository variable `RELEASE_ENABLED=true`. From then on # `changesets/action` publishes via NPM_TOKEN on a push to main. permissions: contents: write pull-requests: write id-token: write # Sigstore provenance attestation (works alongside NPM_TOKEN auth). concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: release: name: Version & publish via Changesets runs-on: ubuntu-latest # CI-15: a `workflow_dispatch` can be triggered from any branch that carries # this file. Gate the job on `main` so a dispatch from a feature branch is # skipped (it would otherwise version/publish branch content). Mirrors the # `docs.yml` deploy gate. if: github.ref == 'refs/heads/main' steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 0 # changesets/action pushes the "Version Packages" branch + tags, so it # needs the checkout to retain the GITHUB_TOKEN git credentials (CI-6). persist-credentials: true - name: Enable Corepack run: corepack enable - name: Setup Node.js 22 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: '22' cache: pnpm # No `registry-url:`; `changesets/action` writes its own publish # `.npmrc` from the `NPM_TOKEN` env below; a setup-node registry-url # would add a second, empty-token `.npmrc` that shadows it. - name: Install dependencies run: pnpm install --frozen-lockfile - name: MVP readiness gate run: pnpm run mvp-readiness - name: Dry-run publish (releases not enabled yet) if: ${{ vars.RELEASE_ENABLED != 'true' }} run: | echo "::warning title=Release skipped::Repository variable RELEASE_ENABLED is not 'true'; running in dry-run mode (configure NPM_TOKEN + the @graphorin scope first)." pnpm -r publish --dry-run --no-git-checks - name: Create Release PR or Publish id: changesets if: ${{ vars.RELEASE_ENABLED == 'true' }} uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 with: publish: pnpm run release version: pnpm run version commit: 'chore(release): version packages' title: 'chore(release): version packages' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # `changesets/action` reads the env var named exactly `NPM_TOKEN` to # write its publish `.npmrc`; scoped to THIS step only (CI-10) so the # workspace build/test steps never see it. (Passing it as # NODE_AUTH_TOKEN makes the action miss it and fall back to OIDC.) NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: 'true' # C4 / Wave-4 acceptance gate: after a real publish, verify the release # actually carries valid Sigstore provenance + npm registry signatures. # Runs in a throwaway project (NOT the workspace, where `@graphorin/*` are # symlinked and would bypass registry verification) against the base leaf # package `@graphorin/core` (zero internal deps), so it installs # cleanly even when a release is partial/interrupted, and lockstep # versioning means it is always part of the published set. `npm audit # signatures` exits non-zero if any signature or attestation fails to # verify, so a bad provenance chain fails the run. No NPM_TOKEN here: # signature verification is a public-registry read, so the publish token # stays scoped to the step above (CI-10). - name: Post-publish provenance smoke (C4) if: ${{ vars.RELEASE_ENABLED == 'true' && steps.changesets.outputs.published == 'true' }} run: | version="$(node -p "require('./packages/core/package.json').version")" echo "Verifying provenance + registry signatures for @graphorin/core@${version}" workdir="$(mktemp -d)" cd "$workdir" npm init -y >/dev/null 2>&1 # Absorb brief registry propagation lag right after publish. for attempt in 1 2 3; do if npm install "@graphorin/core@${version}" --no-audit --no-fund; then break fi echo "::warning::install attempt ${attempt} failed (registry propagation?); retrying in 15s" sleep 15 done npm audit signatures