From acfacd109e9e09fc3b24ed53cd7e1280e163af35 Mon Sep 17 00:00:00 2001 From: dangreen Date: Sat, 5 Sep 2026 19:03:53 +0400 Subject: [PATCH] docs: add the trusted publishing recipe to the docs and the skills A new GitHub Action page describes publishing to npm without a token, as set up in nano_kit, Argue, and nanoviews: `id-token: write` on the publishing jobs, no `registry-url` and no `npm-token`, Node.js 24 for npm 11.5.1+, the trusted publisher registered per package and workflow file on npmjs.com (or with `npm trust github`), the snapshot job folded into `release.yml` behind a `snapshot` dispatch input because the registration is bound to the workflow file, automatic provenance, and the ENEEDAUTH causes. The release automation, snapshot, inputs, and agent skills pages link to it. The setup skill offers the token-or-trusted-publishing choice, detects it from existing workflows, generates the corresponding release job and the folded snapshot layout, and puts the registration into the final checklist; the simple-release-action skill recognizes the layout, dispatches snapshots through `release.yml`, and diagnoses ENEEDAUTH. --- skills/setup-simple-release-action/SKILL.md | 48 +++++- skills/simple-release-action/SKILL.md | 14 +- .../docs/getting-started/agent-skills.mdx | 4 +- .../src/content/docs/github-action/index.mdx | 2 +- .../docs/github-action/release-automation.mdx | 2 + .../docs/github-action/snapshot-release.mdx | 2 + .../docs/github-action/trusted-publishing.mdx | 148 ++++++++++++++++++ 7 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 website/src/content/docs/github-action/trusted-publishing.mdx diff --git a/skills/setup-simple-release-action/SKILL.md b/skills/setup-simple-release-action/SKILL.md index 3ae8b41..8cde5fd 100644 --- a/skills/setup-simple-release-action/SKILL.md +++ b/skills/setup-simple-release-action/SKILL.md @@ -48,7 +48,9 @@ By default set up only the **main flow** (release pull request → release on me 2. **Snapshot release** — publish a temporary timestamped prerelease from any branch under its own npm dist-tag, without committing anything. 3. **Maintenance branches** — when a release crosses a major boundary, create a branch for the previous major so fixes for it keep releasing under their own dist-tag. -If the user has already said which add-ons they want — including "just the default" — do not ask. Otherwise ask once, in a single concise question listing the three add-ons with one-line explanations, with "none" as the default. +Projects publishing to the npm registry have one more choice: authenticate with an **`NPM_TOKEN` secret** (the default, works for any package) or with **trusted publishing** (no secret — OIDC with automatic provenance; see the npm authentication point in the toolchain section for its prerequisites). + +If the user has already said which add-ons they want — including "just the default" — do not ask. Otherwise ask once, in a single concise question listing the three add-ons with one-line explanations, with "none" as the default, plus the token-or-trusted-publishing choice when the project publishes to npm. The user may also name the project type or addon explicitly (for example "this is a pnpm monorepo with independent versions"). An explicit statement always wins over detection. @@ -105,6 +107,7 @@ Fill the workflow from the repository, not from assumptions: - **Node.js version** — in priority order: `.nvmrc` or `.node-version` file (then prefer `node-version-file` over a hardcoded `node-version` in `actions/setup-node`), the minimum major satisfying `engines.node`, `volta.node`, existing CI workflows. Fall back to the current LTS major. - **Package manager version** — if `package.json` has a `packageManager` field, use `pnpm/action-setup` **without** a `version` input — it reads the field. Otherwise infer the major from `pnpm-lock.yaml`'s `lockfileVersion` (`'6.0'` → pnpm 8, `'9.0'` → pnpm 9 or 10 — prefer the latest and tell the user what you assumed). npm needs no setup beyond `actions/setup-node`. - **Registry** — default `https://registry.npmjs.org`. If `publishConfig.registry` or `.npmrc` points to GitHub Packages (`npm.pkg.github.com`): set that `registry-url` plus `scope: '@owner'` in `actions/setup-node`, pass `npm-token: ${{ secrets.GITHUB_TOKEN }}`, and add `packages: write` to the release job permissions. GitHub Packages requires the package scope to match the repository owner — flag a mismatch instead of publishing into the void. +- **npm authentication** — an `NPM_TOKEN` secret by default. Trusted publishing when the user chose it, or when the repository's existing workflows already publish that way (`id-token: write` on the publishing job and no npm token). Its prerequisites, per the npm docs: the public npm registry (not GitHub Packages), GitHub-hosted runners, npm CLI 11.5.1 or later — use `node-version: 24` in the publishing jobs, Node.js 22 ships npm 10 — and packages that already exist on npm, because the trusted publisher is registered in the package settings. Check every published package with `npm view version`; a 404 means its first version has to be published with a token or by hand, tell the user. Documentation: . - **Existing workflows** — reuse the action versions and conventions the repository already has where they don't conflict with this setup. ## Generate the Config @@ -237,6 +240,7 @@ Variations: - **npm project** — drop the `Install pnpm` step, use `cache: 'npm'` and `run: npm ci`. - **Node.js GitHub Action project** — keep the package manager and Node.js setup and the install step in the `release` job (the configured `build` command needs them), but drop `registry-url` and `npm-token`: publishing pushes git refs, not registry packages. - **No publishing** (`publish.skip` in the config) — drop `registry-url` and `npm-token` from the `release` job. +- **Trusted publishing** — in the `release` job add `id-token: write` to `permissions`, drop `registry-url` from `actions/setup-node` and `npm-token` from the action step, and use `node-version: 24`; the config needs nothing. The snapshot flow then lives in the same workflow file — see the snapshot add-on. - **GitHub Packages** — see the registry point in the toolchain section. ### Manual release add-on @@ -337,6 +341,40 @@ jobs: The package manager, Node.js, and registry setup follows the same detection as the release job. Snapshots publish to a registry — skip this add-on for project types that do not (a Node.js GitHub Action). +With trusted publishing the trusted publisher is registered per workflow file, so put the snapshot flow into `release.yml` instead of a second workflow: a `snapshot` input on the `workflow_dispatch` trigger, a guard on the `check` job so a snapshot dispatch does not run the pull request flow, and a `snapshot` job with the same `id-token: write` permission: + +```yaml +on: + workflow_dispatch: + inputs: + # ...the manual release inputs, if any + snapshot: + description: Snapshot tag — publish a snapshot under this npm dist-tag instead of a release (leave other inputs empty) + type: string + # ...the issue_comment and push triggers stay as they are +jobs: + check: + if: inputs.snapshot == '' + # ...the rest of the check job stays as it is + snapshot: + runs-on: ubuntu-latest + name: Snapshot + if: inputs.snapshot != '' + permissions: + contents: read + id-token: write + steps: + # ...the same checkout, package manager, Node.js 24, and install steps as the release job, without registry-url + - name: Publish snapshot + uses: TrigenSoftware/simple-release-action@v2 + with: + workflow: snapshot + github-token: ${{ secrets.GITHUB_TOKEN }} + bump-snapshot: ${{ inputs.snapshot }} +``` + +On push and comment events `inputs.snapshot` is empty, so `check` runs as usual and `snapshot` is skipped. This layout needs the `workflow_dispatch` trigger even without the manual release add-on. + ## Validate - The config must be valid JSON and the workflows valid YAML — parse them (for example with `node -e` or a YAML-aware tool). Run `actionlint` on the workflows if it is available. @@ -353,7 +391,13 @@ Finish with a checklist of the steps that cannot be automated: -F can_approve_pull_request_reviews=true ``` -2. **`NPM_TOKEN` secret** — required when publishing to the npm registry: an npm automation (granular) token with publish permission for the packages. Not needed for GitHub Packages (the workflow token is used) or for non-publishing projects. +2. **npm authentication** — with the token flow, an `NPM_TOKEN` secret: an npm automation (granular) token with publish permission for the packages. With trusted publishing, no secret; instead register the trusted publisher for every published package on npmjs.com — package **Settings → Trusted Publisher → GitHub Actions**: the organization or user, the repository, the workflow filename `release.yml`, and optionally an environment — or with the npm CLI (11.15 or later, two-factor authentication required): + + ```bash + npm trust github --repository / --file release.yml --allow-publish + ``` + + A package that was never published has no settings to register on: publish its first version with a token first. Neither is needed for GitHub Packages (the workflow token is used) or for non-publishing projects. 3. **Squash-merge release pull requests** — the release is recognized by the `chore(release): ...` commit title on the branch head. A regular merge commit hides it and the release job will not run. Recommend enabling squash merging for the repository. 4. **The first release** — after the setup is merged, the next push of a releasable commit (or the setup push itself) opens a release pull request. When no release tags exist yet, the version is taken from the manifest as is and the changelog covers the whole history. 5. **Reshaping a pending release** — a comment on the release pull request starting with `!simple-release/set-options` followed by a JSON code block (for example `{"bump": {"as": "major"}}`) rebuilds it with those options. A `!simple-release/set-preamble` comment (optionally followed by a full package name to target one package in a monorepo) inserts the markdown after it into the changelog. Both are documented in the cheatsheet included in every release pull request body. diff --git a/skills/simple-release-action/SKILL.md b/skills/simple-release-action/SKILL.md index 6e7eb5e..e53fc67 100644 --- a/skills/simple-release-action/SKILL.md +++ b/skills/simple-release-action/SKILL.md @@ -57,8 +57,8 @@ From the release workflow — usually `.github/workflows/release.yml` — note: - `on.push.branches`: the flow runs only for pushes to these branches. - A `workflow_dispatch` trigger with `version`, `as`, `prerelease` (and `by-project`) inputs forwarded to the `bump-*` action inputs: the **manual release** add-on. - A `branch` input on the action steps: the release branch name. It defaults to `simple-release`; the `gh pr list --head` commands below use it. -- A separate workflow running `workflow: snapshot` — usually `.github/workflows/snapshot.yml`: the **snapshot** add-on. -- Whether the release job publishes to a registry (`registry-url`, `npm-token`). Node.js GitHub Action projects publish built git refs instead (the `latest` and `v{major}` branches and the `v{version}` tag), and projects with `publish.skip` only tag and create the GitHub release. +- A job running `workflow: snapshot` — a separate `.github/workflows/snapshot.yml`, or a `snapshot` job in `release.yml` guarded by a `snapshot` dispatch input: the **snapshot** add-on. +- How the release job publishes: `npm-token` and `registry-url` mean the token flow; `id-token: write` in the job permissions and no token mean [trusted publishing](https://simple-release.js.org/github-action/trusted-publishing/), where the trusted publisher on npmjs.com is bound to the workflow file — that is why the snapshot job then usually sits in `release.yml`. Node.js GitHub Action projects publish built git refs instead (the `latest` and `v{major}` branches and the `v{version}` tag), and projects with `publish.skip` only tag and create the GitHub release. A single job running `workflow: full` (the default) behaves like the three-job layout with `check`, `pull-request`, and `release` — the action picks the flow from the event, so everything below applies to both. @@ -235,7 +235,7 @@ Notes: ## Snapshot from Any Branch -Requires the snapshot workflow. A snapshot publishes the current state of a branch as a timestamped prerelease under its own npm dist-tag — nothing is committed, tagged, or written to the changelog. Not applicable to projects that publish nothing (Node.js GitHub Action projects, `publish.skip`). +Requires the snapshot add-on. A snapshot publishes the current state of a branch as a timestamped prerelease under its own npm dist-tag — nothing is committed, tagged, or written to the changelog. Not applicable to projects that publish nothing (Node.js GitHub Action projects, `publish.skip`). ```bash git push -u origin my-feature @@ -245,6 +245,13 @@ gh run watch npm view dist-tags ``` +When the snapshot job lives in `release.yml` (the trusted publishing layout), dispatch that workflow with its `snapshot` input instead — the `check` job and the release flow are skipped for such a run: + +```bash +gh workflow run release.yml --ref my-feature -f snapshot=canary +gh run list --workflow release.yml --event workflow_dispatch --limit 1 +``` + The branch must exist on the remote — `--ref` names the branch to check out and to take the workflow file from. The `tag` input is both the prerelease identifier and the dist-tag: a repository released as `1.1.0` with a `feat` on the branch publishes `1.2.0-canary.20260707111020`, installable with `npm i @canary`. Without new commits the version falls back to a patch bump, so a snapshot never collides with a real release. In a monorepo every package is snapshotted in one run, each from its own version. ## Maintenance Branches @@ -294,6 +301,7 @@ gh api repos/{owner}/{repo}/actions/permissions/workflow | | The run happened but the options were not applied: invalid JSON, no `json` fence, or the author is not an owner, member, or collaborator | Fix and post again; with `releaser.verbose` the log says "Failed to parse parameters comment" for invalid JSON | | | The comment was edited — edits do not trigger runs | Post a new comment, or delete and re-post | | `gh workflow run` fails | "Workflow does not have 'workflow_dispatch' trigger", or unexpected inputs | The manual release or snapshot add-on is not set up — offer the setup skill | +| Publish fails with `ENEEDAUTH` or "Unable to authenticate" | Trusted publishing: the workflow filename or repository does not match the trusted publisher registered on npmjs.com, the job lacks `id-token: write`, npm is older than 11.5.1 (Node.js below 24), or the package was never published, so no publisher could be registered. In the token flow: `NPM_TOKEN` missing or expired | Compare the registration with the workflow file name and repository, check the job permissions and Node.js version; publish a brand-new package's first version with a token; rotate the secret in the token flow | | Prerelease produced no pull request | An older action version: `as: prerelease` with an identifier on a stable version used to yield nothing | Update simple-release-action, or use the identifier alone or with `as` set to `major`, `minor`, or `patch` | | Renovate or Dependabot commits do not release a monorepo package | The `deps` scope is not a package name | `bump.extraScopes: ["deps"]` in the config | | Fixed monorepo: unchanged packages kept their version | By design — only changed packages are bumped | `bump.force: true` in the config | diff --git a/website/src/content/docs/getting-started/agent-skills.mdx b/website/src/content/docs/getting-started/agent-skills.mdx index 839102c..27dcaa9 100644 --- a/website/src/content/docs/getting-started/agent-skills.mdx +++ b/website/src/content/docs/getting-started/agent-skills.mdx @@ -14,9 +14,9 @@ The repository ships **universal agent skills** — instructions that teach an A The [`setup-simple-release-action`](https://github.com/TrigenSoftware/simple-release/tree/main/skills/setup-simple-release-action) skill sets up the [GitHub Action](/github-action/) in a repository end to end. Rather than pasting a template, the skill guides the agent to: - **Detect the project type from the repository** — lock files, workspace manifests, `action.yml` — and describe it in the [config](/getting-started/configuration/) with a version-pinned addon [query](/getting-started/configuration/#project-query). You can also just tell the agent what to use. -- **Fill the workflow from your toolchain** — the Node.js version, the package manager and its version, the default branch, and the npm registry are read from the repository, not assumed. +- **Fill the workflow from your toolchain** — the Node.js version, the package manager and its version, the default branch, and the npm registry are read from the repository, not assumed; the release job publishes with an npm token or through [trusted publishing](/github-action/trusted-publishing/), as you choose. - **Add the optional flows on request** — [manual release](/github-action/manual-release/), [snapshot release](/github-action/snapshot-release/), and [maintenance branches](/github-action/maintenance-branches/). By default only the main [release automation](/github-action/release-automation/) flow is set up; if you have not said what you need, the agent asks once. -- **Finish with a checklist** of the settings only you can change — allowing GitHub Actions to create pull requests, the `NPM_TOKEN` secret, squash merging for release pull requests. +- **Finish with a checklist** of the settings only you can change — allowing GitHub Actions to create pull requests, the `NPM_TOKEN` secret or the trusted publisher registration, squash merging for release pull requests. Install the skill into your project with either package runner: diff --git a/website/src/content/docs/github-action/index.mdx b/website/src/content/docs/github-action/index.mdx index f1b98fa..0971872 100644 --- a/website/src/content/docs/github-action/index.mdx +++ b/website/src/content/docs/github-action/index.mdx @@ -33,7 +33,7 @@ The `workflow` input selects what the action does in the current job: | --- | --- | | `workflow` | Workflow to run. Defaults to `full`. | | `github-token` | GitHub token to authenticate with the GitHub API. Required. | -| `npm-token` | npm token for publishing. Passed to the `NODE_AUTH_TOKEN` environment variable. | +| `npm-token` | npm token for publishing. Passed to the `NODE_AUTH_TOKEN` environment variable. Not needed with [trusted publishing](/github-action/trusted-publishing/). | | `publish-token` | Generic token for the config file. Passed to the `PUBLISH_TOKEN` environment variable. | | `branch` | Branch to store release changes and create the pull request from. Defaults to `simple-release`. | | `bump-version` | Force a specific version. | diff --git a/website/src/content/docs/github-action/release-automation.mdx b/website/src/content/docs/github-action/release-automation.mdx index fec445e..1137b8f 100644 --- a/website/src/content/docs/github-action/release-automation.mdx +++ b/website/src/content/docs/github-action/release-automation.mdx @@ -105,6 +105,8 @@ The main flow keeps a **release pull request** up to date and releases it on mer The three jobs map to the action [workflows](/github-action/#workflows): `check` decides what should run for the current event, `pull-request` maintains the release pull request, and `release` runs on the merged release commit — it creates the tags, publishes the packages, and creates the GitHub release. + The `release` job publishes with the `NPM_TOKEN` secret — an npm automation token with publish permission for the packages. To publish without any secret, switch the job to [trusted publishing](/github-action/trusted-publishing/). + 3. Allow the action to create pull requests: in the repository **Settings → Actions → General**, enable **Allow GitHub Actions to create and approve pull requests**. Without it the `pull-request` job fails with a "GitHub Actions is not permitted to create or approve pull requests" error. 4. Push a `feat: ...` or `fix: ...` commit to `main` — the release pull request appears. Merge it with **squash** to release. diff --git a/website/src/content/docs/github-action/snapshot-release.mdx b/website/src/content/docs/github-action/snapshot-release.mdx index 6ba3c8f..aab1797 100644 --- a/website/src/content/docs/github-action/snapshot-release.mdx +++ b/website/src/content/docs/github-action/snapshot-release.mdx @@ -61,6 +61,8 @@ jobs: The `tag` input doubles as the prerelease identifier and the npm dist-tag the snapshot is published under. +With [trusted publishing](/github-action/trusted-publishing/#snapshots) the snapshot job moves into the release workflow instead, because the trusted publisher is registered per workflow file. + ## Running it Go to **Actions → Snapshot → Run workflow**, pick the branch and the tag, and run — or from the CLI: diff --git a/website/src/content/docs/github-action/trusted-publishing.mdx b/website/src/content/docs/github-action/trusted-publishing.mdx new file mode 100644 index 0000000..360e311 --- /dev/null +++ b/website/src/content/docs/github-action/trusted-publishing.mdx @@ -0,0 +1,148 @@ +--- +title: Trusted Publishing +description: Publish to npm from simple-release-action without a token — OIDC trusted publishing with automatic provenance, registered per workflow file on npmjs.com. +sidebar: + order: 6 +--- + +import { Aside, Steps } from '@astrojs/starlight/components' + +[Trusted publishing](https://docs.npmjs.com/trusted-publishers) lets the release job authenticate with the npm registry through GitHub's OIDC token instead of a long-lived `NPM_TOKEN` secret: npm exchanges the workflow's identity — the repository and the workflow file — for a short-lived publish token, and provenance attestations are generated on the way. The action needs nothing for it. `npm publish` detects the OIDC environment by itself, and `pnpm publish` runs npm's publish under the hood, so the switch is a matter of job permissions and a registration on npmjs.com. + +## Requirements + +- The public npm registry and GitHub-hosted runners — npm does not support trusted publishing from self-hosted runners, and GitHub Packages authenticates with the workflow token anyway. +- npm CLI 11.5.1 or later in the publishing jobs. Node.js 24 ships it (from 24.5.0); Node.js 22 ships npm 10, so use `node-version: 24` in the `release` and snapshot jobs even when the project itself targets an older Node.js. +- A package that already exists on npm. The trusted publisher is configured in the package settings, so the first version of a new package has to be published with a token or from a maintainer's machine; trusted publishing takes over from the second one. +- One registration per package and per workflow file. A monorepo needs a registration for every published package. + +## Setup + + + +1. Register the trusted publisher for each package on npmjs.com — package **Settings → Trusted Publisher → GitHub Actions**: the organization or user, the repository name, the workflow filename `release.yml`, and optionally a GitHub environment. The same from the npm CLI (11.15 or later, two-factor authentication required): + + ```sh frame="none" + npm trust github your-package --repository your-org/your-repo --file release.yml --allow-publish + ``` + + Nothing is validated at this point — a typo in the repository or the filename shows up only as an authentication error at publish time. + +2. Change the `release` job of the [release workflow](/github-action/release-automation/): grant it the `id-token: write` permission, drop `registry-url` from `actions/setup-node` and `npm-token` from the action step — there is no token to pass — and use Node.js 24: + + ```yaml title=".github/workflows/release.yml" {8,20-21,25} + jobs: + # ...the check and pull-request jobs stay as they are + release: + runs-on: ubuntu-latest + name: Release + needs: check + if: needs.check.outputs.workflow == 'release' + permissions: + contents: write + id-token: write + steps: + - name: Checkout the repository + uses: actions/checkout@v7 + - name: Install pnpm + uses: pnpm/action-setup@v6 + with: + version: 11 + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: 'pnpm' + - name: Install dependencies + run: pnpm install + - name: Release + uses: TrigenSoftware/simple-release-action@v2 + with: + workflow: release + github-token: ${{ secrets.GITHUB_TOKEN }} + ``` + +3. Squash-merge the next release pull request. The publish step of the release job now reports the OIDC publish and the provenance statement: + + ```text frame="none" + Publishing with command: pnpm publish --access public --recursive --no-git-checks + npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access + npm notice publish Signed provenance statement with source and build information from GitHub Actions + npm notice publish Provenance statement published to transparency log: https://search.sigstore.dev/?logIndex=... + ``` + + + + + +## Snapshots + +The registration is bound to the workflow file, so the [snapshot flow](/github-action/snapshot-release/) is better kept in `release.yml` than in a workflow of its own: a `snapshot` input on the `workflow_dispatch` trigger routes a run to a snapshot job, while pushes and comments keep running the regular flow because the input is empty for them. + +```yaml title=".github/workflows/release.yml" {4-7,15,26,29,45} +name: Release +on: + workflow_dispatch: + inputs: + snapshot: + description: Snapshot tag — publish a snapshot under this npm dist-tag instead of a release (leave other inputs empty) + type: string + issue_comment: + types: [created, deleted] + push: + branches: + - main +jobs: + check: + if: inputs.snapshot == '' + # ...the rest of the check job stays as it is + pull-request: + needs: check + if: needs.check.outputs.workflow == 'pull-request' + # ... + release: + needs: check + if: needs.check.outputs.workflow == 'release' + # ... + snapshot: + runs-on: ubuntu-latest + name: Snapshot + if: inputs.snapshot != '' + permissions: + contents: read + id-token: write + steps: + - name: Checkout the repository + uses: actions/checkout@v7 + - name: Install pnpm + uses: pnpm/action-setup@v6 + with: + version: 11 + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: 'pnpm' + - name: Install dependencies + run: pnpm install + - name: Publish snapshot + uses: TrigenSoftware/simple-release-action@v2 + with: + workflow: snapshot + github-token: ${{ secrets.GITHUB_TOKEN }} + bump-snapshot: ${{ inputs.snapshot }} +``` + +The `snapshot` input can sit next to the [manual release](/github-action/manual-release/) inputs in the same trigger. Dispatch it with the branch to snapshot: + +```sh frame="none" +gh workflow run release.yml --ref my-feature-branch -f snapshot=canary +``` + +The `check`, `pull-request`, and `release` jobs are skipped for such a run, and the snapshot job publishes `1.2.0-canary.20260707111020` under the `canary` dist-tag with the same OIDC authentication. A separate `snapshot.yml` works too if it is registered as another trusted publisher — npm allows up to ten per package — but that doubles the registrations in a monorepo. + +## Provenance + +Publishing through trusted publishing from a public repository attaches a provenance attestation to every version: a signed statement of the commit and the workflow run it was built from, shown on npmjs.com and verifiable with `npm audit signatures`. Nothing has to be enabled for it, and private repositories simply publish without it. To opt out, set `publishConfig.provenance` to `false` in the package manifest.