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.