From b878f44ae2f45970df6a521fd56f28e3542500c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Oct 2026 20:40:32 +0100 Subject: [PATCH 01/10] fix(action): honour dry-run, Node 22, and caller tokens; refresh action.md Fixes #738 Co-authored-by: Cursor --- README.md | 2 +- action.yml | 10 +++++++--- docs/action.md | 16 +++++++++++----- docs/dart.md | 4 +++- docs/rust.md | 4 +++- packages/publish/src/utils/npm-env.ts | 2 +- scripts/run-action.mjs | 16 +++++++++++++++- test/integration/action-runner.spec.ts | 5 +++++ 8 files changed, 46 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0f9adf3e..f0cb1f3f 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Each step is also a subcommand — `releasekit version`, `releasekit notes`, `re ### GitHub Action -Use ReleaseKit as a composite action with `release` or `preview` modes: +Use ReleaseKit as a composite action (`release`, `preview`, `gate`, standing-PR modes, `backfill`, `refresh-after-release` — see [docs/action.md](./docs/action.md)): ```yaml jobs: diff --git a/action.yml b/action.yml index 05400e99..2cd88a23 100644 --- a/action.yml +++ b/action.yml @@ -65,7 +65,11 @@ inputs: node-version: description: Node.js version required: false - default: '20' + default: '22' + github-token: + description: GitHub token for API calls (defaults to the workflow token) + required: false + default: ${{ github.token }} npm-auth: description: NPM auth method (auto|oidc|token) required: false @@ -335,7 +339,7 @@ runs: INPUT_UPDATE_RELEASES: ${{ inputs.update-releases }} INPUT_ONLY_MISSING: ${{ inputs.only-missing }} INPUT_APPLY: ${{ inputs.apply }} - GITHUB_TOKEN: ${{ github.token }} - NPM_TOKEN: ${{ inputs.npm-token }} + GITHUB_TOKEN: ${{ inputs.github-token }} + NPM_TOKEN: ${{ inputs.npm-token != '' && inputs.npm-token || env.NPM_TOKEN }} VERBOSE: ${{ inputs.verbose }} OLLAMA_API_KEY: ${{ env.OLLAMA_API_KEY }} diff --git a/docs/action.md b/docs/action.md index 850bc07d..9f1ebd66 100644 --- a/docs/action.md +++ b/docs/action.md @@ -12,16 +12,17 @@ uses: goosewobbler/releasekit@v0 ## Modes -- `release` (default): runs the full unified release pipeline. -- `preview`: runs PR preview comment generation. +- `release`: runs the full unified release pipeline. +- `preview` (default): runs PR preview comment generation. - `gate`: evaluates merged-PR labels and emits should-release/bump/scope outputs. - `standing-pr-update`: creates or refreshes the standing release PR. - `standing-pr-publish`: publishes from a merged standing release PR's manifest. Resolution order for which PR: the `pr` input, then the `pull_request` event payload, then the most recently merged standing PR via the GitHub API. Pass `pr` explicitly in dispatch funnels (e.g. `workflow_dispatch` routing for npm OIDC trusted publishing) — re-runs of a stale dispatch can otherwise infer a newer standing PR. - `backfill`: regenerates release notes for already-released versions from git history, writing per-version files and/or updating GitHub release bodies. Dry-run unless `apply: true`. Best driven by `workflow_dispatch` for one-off backfills. +- `refresh-after-release`: refreshes the standing release PR after a direct release on `main`. ## Requirements -- Node.js 20+ must be available in the job. All GitHub-hosted runners include a compatible version by default. If you need a specific version, add `actions/setup-node` before this action. +- Node.js **22.12+** (matches ReleaseKit's `engines`). The action runs `actions/setup-node` with `node-version` (default `22`) unless you skip checkout and provide your own Node setup. - For npm publishing, run `actions/setup-node` with `registry-url: https://registry.npmjs.org` before this action so that `NODE_AUTH_TOKEN` is wired up correctly. ## Required permissions and env @@ -49,10 +50,15 @@ uses: goosewobbler/releasekit@v0 | Input | Default | Description | |---|---|---| -| `mode` | `release` | `release`, `preview`, `gate`, `standing-pr-update`, `standing-pr-publish`, or `backfill` | +| `mode` | `preview` | `release`, `preview`, `gate`, `standing-pr-update`, `standing-pr-publish`, `backfill`, or `refresh-after-release` | | `config` | - | Path to `releasekit.config.json` | | `project-dir` | `.` | Project directory | -| `dry-run` | `false` | Global dry-run toggle | +| `dry-run` | `false` | Dry-run for `release`, `preview`, and `backfill` only; other modes fail fast if set | +| `github-token` | workflow token | Token for GitHub API calls (override with a PAT/App token when rulesets require it) | +| `node-version` | `22` | Node.js version for the action's setup-node step | +| `npm-token` | - | NPM token when not using OIDC; when unset, the job's `NPM_TOKEN` env is preserved | +| `summary` | `true` | Write a job summary for `release` and `gate` | +| `skip-checkout` | `false` | Skip the action's checkout/setup-node/pnpm steps (repo must already be on disk) | | `json` | `false` | JSON output (release mode) | | `verbose` | `false` | Verbose logs | | `quiet` | `false` | Suppress non-error logs | diff --git a/docs/dart.md b/docs/dart.md index eb8b7e91..51822f79 100644 --- a/docs/dart.md +++ b/docs/dart.md @@ -192,7 +192,9 @@ jobs: # its own runtime, and a Dart-only repo has no pnpm-lock.yaml (setting cache: pnpm # would fail with "Dependencies lock file is not found"). - name: Run releasekit - uses: goosewobbler/releasekit@v1 + uses: goosewobbler/releasekit@v0 + with: + mode: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For token auth instead of OIDC, set PUB_TOKEN and drop `id-token: write` above: diff --git a/docs/rust.md b/docs/rust.md index 7c50f95f..82812f6f 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -195,7 +195,9 @@ jobs: # its own runtime, and a Rust-only repo has no pnpm-lock.yaml (setting cache: pnpm # would fail with "Dependencies lock file is not found"). - name: Run releasekit - uses: goosewobbler/releasekit@v1 + uses: goosewobbler/releasekit@v0 + with: + mode: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/packages/publish/src/utils/npm-env.ts b/packages/publish/src/utils/npm-env.ts index 0709a99f..ce6453c0 100644 --- a/packages/publish/src/utils/npm-env.ts +++ b/packages/publish/src/utils/npm-env.ts @@ -43,7 +43,7 @@ export function createNpmSubprocessIsolation(options: { return { env: baseEnv, cleanup: () => {} }; } - const token = process.env.NPM_TOKEN ?? process.env.NODE_AUTH_TOKEN; + const token = process.env.NPM_TOKEN || process.env.NODE_AUTH_TOKEN; const registryHost = (() => { try { return new URL(registryUrl).host; diff --git a/scripts/run-action.mjs b/scripts/run-action.mjs index 7f409cd3..d3914322 100644 --- a/scripts/run-action.mjs +++ b/scripts/run-action.mjs @@ -137,7 +137,9 @@ export function buildBackfillArgs(input) { pushBooleanFlag(args, '--all', input.backfillAll); pushBooleanFlag(args, '--update-releases', input.backfillUpdateReleases); pushBooleanFlag(args, '--only-missing', input.backfillOnlyMissing); - pushBooleanFlag(args, '--apply', input.backfillApply); + if (!normalizeBoolean(input.dryRun)) { + pushBooleanFlag(args, '--apply', input.backfillApply); + } return args; } @@ -224,6 +226,16 @@ function setFailure(errorMessage) { process.exit(1); } +export function assertDryRunSupported(mode, dryRun) { + if (!normalizeBoolean(dryRun)) { + return; + } + const unsupported = new Set(['standing-pr-update', 'standing-pr-publish', 'gate', 'refresh-after-release']); + if (unsupported.has(mode)) { + throw new Error(`dry-run is not supported for mode "${mode}"`); + } +} + export function parseInputs(env = process.env) { const input = { mode: normalizeString(env.INPUT_MODE) ?? 'preview', @@ -280,6 +292,8 @@ export function parseInputs(env = process.env) { throw new Error(`Invalid mode: ${input.mode}. Must be one of: ${validModes.join(', ')}`); } + assertDryRunSupported(input.mode, input.dryRun); + return input; } diff --git a/test/integration/action-runner.spec.ts b/test/integration/action-runner.spec.ts index 235e47b6..9cb879bb 100644 --- a/test/integration/action-runner.spec.ts +++ b/test/integration/action-runner.spec.ts @@ -3,6 +3,7 @@ import os from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { + assertDryRunSupported, buildBackfillArgs, buildGateArgs, buildGateSummary, @@ -28,6 +29,10 @@ describe('action runner', () => { tempDirs.length = 0; }); + it('should reject dry-run for standing-pr-publish', () => { + expect(() => assertDryRunSupported('standing-pr-publish', 'true')).toThrow(/dry-run is not supported/); + }); + it('should build release args from inputs', () => { const args = buildReleaseArgs({ config: 'releasekit.config.json', From 817ab92fb272d6debc2dda25c93633cf45956c1f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Oct 2026 20:56:52 +0100 Subject: [PATCH 02/10] docs(action): clarify skip-checkout skips checkout only Co-authored-by: Cursor --- docs/action.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/action.md b/docs/action.md index 9f1ebd66..cca559d1 100644 --- a/docs/action.md +++ b/docs/action.md @@ -22,7 +22,7 @@ uses: goosewobbler/releasekit@v0 ## Requirements -- Node.js **22.12+** (matches ReleaseKit's `engines`). The action runs `actions/setup-node` with `node-version` (default `22`) unless you skip checkout and provide your own Node setup. +- Node.js **22.12+** (matches ReleaseKit's `engines`). The action always runs its own `actions/setup-node` (default `node-version` `22`) and `pnpm/action-setup` unless you manage Node/pnpm entirely outside the action. - For npm publishing, run `actions/setup-node` with `registry-url: https://registry.npmjs.org` before this action so that `NODE_AUTH_TOKEN` is wired up correctly. ## Required permissions and env @@ -58,7 +58,7 @@ uses: goosewobbler/releasekit@v0 | `node-version` | `22` | Node.js version for the action's setup-node step | | `npm-token` | - | NPM token when not using OIDC; when unset, the job's `NPM_TOKEN` env is preserved | | `summary` | `true` | Write a job summary for `release` and `gate` | -| `skip-checkout` | `false` | Skip the action's checkout/setup-node/pnpm steps (repo must already be on disk) | +| `skip-checkout` | `false` | Skip the action's checkout steps only; setup-node, pnpm, and install still run. The repo must already be on disk (e.g. a prior checkout step). | | `json` | `false` | JSON output (release mode) | | `verbose` | `false` | Verbose logs | | `quiet` | `false` | Suppress non-error logs | From 5dbf7835b9c5c04b54cf679e9b9c581e03d2e4c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Oct 2026 22:28:13 +0100 Subject: [PATCH 03/10] docs(action): finish the #738 action reference; allow dry-run in gate mode Finishes the docs items #738 listed and the PR hadn't covered: - the versioning section is folded into the pinning note, which now says vX.Y.Z tags move and to pin the commit one points to; - inputs stable, scope and ssh-key, `bump: prerelease`, and the gate and standing-PR outputs are documented, as is that release outputs need json; - permissions for gate and the standing-PR modes; - the examples drop the ignored `env: GITHUB_TOKEN`, the preview trigger includes label events, and the dry-run example passes the markdown through env instead of interpolating PR text into shell; - README no longer promises rollout guidance. Gate is read-only, so dry-run is now accepted there instead of failing. Tests drive the dry-run guard through parseInputs, cover backfill dropping --apply on a dry run, and cover an empty NPM_TOKEN falling back to NODE_AUTH_TOKEN. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +- docs/action.md | 76 ++++++++++--------- .../publish/test/unit/utils/npm-env.spec.ts | 15 ++++ scripts/run-action.mjs | 4 +- test/integration/action-runner.spec.ts | 24 +++++- 5 files changed, 81 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index f0cb1f3f..8dac9fdd 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -See [docs/action.md](./docs/action.md) for the `preview` mode, full input/output reference, and rollout guidance. +See [docs/action.md](./docs/action.md) for the `preview` mode, and the full input/output reference. ## Configuration @@ -168,7 +168,7 @@ See the [package docs](#documentation) for the full option reference. - [CLI](./docs/cli.md) — every command and flag for the `releasekit` CLI - [Standing-PR body](./docs/standing-pr-body.md) — the editable regions, what an edit does, what is bot-owned - [Configuration](./docs/configuration.md) — full config reference (all `releasekit.config.json` options) -- [GitHub Action](./docs/action.md) — `goosewobbler/releasekit` action inputs, outputs, and rollout +- [GitHub Action](./docs/action.md) — `goosewobbler/releasekit` action modes, inputs and outputs - [@releasekit/release](./packages/release/README.md) — unified pipeline, CI automation, programmatic API - [@releasekit/version](./packages/version/README.md) — versioning strategies, JSON output - [@releasekit/notes](./packages/notes/README.md) — changelog, release notes, LLM, templates diff --git a/docs/action.md b/docs/action.md index cca559d1..6129c107 100644 --- a/docs/action.md +++ b/docs/action.md @@ -8,7 +8,7 @@ Consumers should pin the major tag: uses: goosewobbler/releasekit@v0 ``` -> **Note:** ReleaseKit is pre-1.0. The floating `v0` tag tracks the latest `0.x` release; a `v1` tag will follow the 1.0 release. For stricter supply-chain hygiene, pin to a specific SHA (`@`) — see GitHub's [security hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions). +> **Note:** ReleaseKit is pre-1.0. The floating `v0` tag tracks the latest `0.x` release; a `v1` tag will follow the 1.0 release. `vX.Y.Z` tags aren't immutable: each is moved onto a commit that adds the built action. For stricter supply-chain hygiene, pin the commit a `vX.Y.Z` tag points to (`@ # vX.Y.Z`) — see GitHub's [security hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions). ## Modes @@ -22,27 +22,35 @@ uses: goosewobbler/releasekit@v0 ## Requirements -- Node.js **22.12+** (matches ReleaseKit's `engines`). The action always runs its own `actions/setup-node` (default `node-version` `22`) and `pnpm/action-setup` unless you manage Node/pnpm entirely outside the action. +- The action checks out the repo (unless `skip-checkout`), then runs its own `pnpm/action-setup` and `actions/setup-node` (`node-version`, default `22`; ReleaseKit needs Node 22.12+). That replaces any Node version an earlier step set up. - For npm publishing, run `actions/setup-node` with `registry-url: https://registry.npmjs.org` before this action so that `NODE_AUTH_TOKEN` is wired up correctly. ## Required permissions and env +The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN`, so a `GITHUB_TOKEN` in the step's `env` is ignored. Pass a PAT or App token as `github-token`. + ### Release mode - Recommended job permissions: - `id-token: write` (OIDC trusted publishing) - `contents: read` (checkout) - - `contents: write` (git push + GitHub Releases) -- Required env: - - `GITHUB_TOKEN` + - `contents: write` (git push & GitHub Releases) ### Preview mode - Recommended job permissions: - `contents: read` (checkout) - `pull-requests: write` (posting/updating PR comments) -- Required env: - - `GITHUB_TOKEN` + +### Gate mode + +- Recommended job permissions: + - `contents: read` + - `pull-requests: read` (reading the merged PR's labels) + +### Standing-PR modes + +`standing-pr-update` and `standing-pr-publish` need the permissions and repo setting in [CI setup](../packages/release/docs/ci-setup.md#setup-requirements). ## Inputs @@ -53,13 +61,13 @@ uses: goosewobbler/releasekit@v0 | `mode` | `preview` | `release`, `preview`, `gate`, `standing-pr-update`, `standing-pr-publish`, `backfill`, or `refresh-after-release` | | `config` | - | Path to `releasekit.config.json` | | `project-dir` | `.` | Project directory | -| `dry-run` | `false` | Dry-run for `release`, `preview`, and `backfill` only; other modes fail fast if set | +| `dry-run` | `false` | Dry-run for `release`, `preview` and `backfill`. `gate` is read-only; other modes fail if it's set | | `github-token` | workflow token | Token for GitHub API calls (override with a PAT/App token when rulesets require it) | | `node-version` | `22` | Node.js version for the action's setup-node step | | `npm-token` | - | NPM token when not using OIDC; when unset, the job's `NPM_TOKEN` env is preserved | | `summary` | `true` | Write a job summary for `release` and `gate` | | `skip-checkout` | `false` | Skip the action's checkout steps only; setup-node, pnpm, and install still run. The repo must already be on disk (e.g. a prior checkout step). | -| `json` | `false` | JSON output (release mode) | +| `json` | `false` | JSON output (release mode). The release outputs stay empty without it | | `verbose` | `false` | Verbose logs | | `quiet` | `false` | Suppress non-error logs | @@ -67,8 +75,11 @@ uses: goosewobbler/releasekit@v0 | Input | Default | Description | |---|---|---| -| `bump` | - | Force bump (`patch|minor|major`) | +| `bump` | - | Force bump (`patch|minor|major|prerelease`) | | `prerelease` | - | Prerelease identifier (e.g. `alpha`, `beta`) | +| `stable` | `false` | Graduate prerelease packages to stable without bumping | +| `scope` | - | Scope name resolved to target packages from `ci.scopeLabels` (also `gate`) | +| `ssh-key` | - | SSH deploy key; the action checks out over SSH so the push uses it (not on dry runs or in `gate`) | | `sync` | `false` | Synchronized versioning | | `target` | - | Comma-separated package targets | | `branch` | - | Override push branch | @@ -83,7 +94,7 @@ uses: goosewobbler/releasekit@v0 | Input | Default | Description | |---|---|---| -| `pr` | - | PR number override (also used by `standing-pr-publish`) | +| `pr` | - | PR number override | | `repo` | - | `owner/repo` override | | `preview-prerelease` | - | Force prerelease preview identifier | | `preview-stable` | `false` | Force stable preview | @@ -110,7 +121,7 @@ uses: goosewobbler/releasekit@v0 | `only-missing` | `false` | With `update-releases`, skip releases already carrying releasekit notes | | `apply` | `false` | Apply changes (default: dry-run preview) | -`update-releases` needs `contents: write` permission and `GITHUB_TOKEN` (for `gh release edit`). Writing per-version files needs `notes.releaseNotes.file.dir` set in your config and, to commit them, a follow-up step. +`update-releases` needs `contents: write` permission (for `gh release edit`). Writing per-version files needs `notes.releaseNotes.file.dir` set in your config and, to commit them, a follow-up step. ## Outputs @@ -118,12 +129,20 @@ uses: goosewobbler/releasekit@v0 |---|---| | `success` | Whether action completed successfully | | `mode` | Executed mode | -| `has-changes` | Whether release detected updates (release mode) | -| `version-output` | `versionOutput` JSON string (release mode) | -| `tags` | Comma-separated tags (release mode) | -| `release-output` | Full release JSON output (release mode) | +| `has-changes` | Whether release detected updates (release mode, needs `json: "true"`) | +| `version-output` | `versionOutput` JSON string (release mode, needs `json: "true"`) | +| `tags` | Comma-separated tags (release mode, needs `json: "true"`) | +| `release-output` | Full release JSON output (release mode, needs `json: "true"`) | | `preview-markdown` | Preview markdown (preview mode + dry-run) | | `preview-posted` | `true` when preview mode posted comment | +| `should-release` | Whether a release should proceed (gate mode) | +| `bump` | Resolved bump type (gate mode) | +| `gate-scope` | Resolved scope name (gate mode) | +| `gate-target` | Resolved target packages (gate mode) | +| `gate-stable` | Whether the release is a stable graduation (gate mode) | +| `standing-pr-action` | `created`, `updated`, `closed` or `noop` (standing-pr-update mode) | +| `standing-pr-number` | Standing PR number (standing-pr-update mode) | +| `standing-pr-url` | Standing PR URL (standing-pr-update mode) | ## Examples @@ -145,17 +164,15 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 + # Only for registry-url: the action sets up its own Node version. - uses: actions/setup-node@v6 with: - node-version: "24" registry-url: https://registry.npmjs.org - id: rk uses: goosewobbler/releasekit@v0 with: mode: release json: "true" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Print tags run: echo "Tags: ${{ steps.rk.outputs.tags }}" ``` @@ -167,6 +184,7 @@ name: Release Preview on: pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] jobs: preview: @@ -181,8 +199,6 @@ jobs: - uses: goosewobbler/releasekit@v0 with: mode: preview - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ### Preview mode dry run (markdown only) @@ -200,11 +216,11 @@ jobs: with: mode: preview preview-dry-run: "true" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: | - echo "${{ steps.rk.outputs.preview-markdown }}" + # Through env, not inline: the markdown carries PR text, which inline would run as shell. + - env: + PREVIEW_MARKDOWN: ${{ steps.rk.outputs.preview-markdown }} + run: echo "$PREVIEW_MARKDOWN" ``` ### Backfill mode (update GitHub release bodies) @@ -238,14 +254,4 @@ jobs: update-releases: "true" only-missing: "true" apply: ${{ inputs.apply }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` - -## Versioning and distribution policy - -- Action is distributed through Git tags: - - immutable tags like `v1.0.0` - - moving major alias like `v1` -- Consumers should reference `@v1`. -- Breaking changes publish under a new major alias (`v2`). diff --git a/packages/publish/test/unit/utils/npm-env.spec.ts b/packages/publish/test/unit/utils/npm-env.spec.ts index 6057069e..dce98171 100644 --- a/packages/publish/test/unit/utils/npm-env.spec.ts +++ b/packages/publish/test/unit/utils/npm-env.spec.ts @@ -63,6 +63,21 @@ describe('npm env isolation', () => { iso.cleanup(); }); + it('should fall back to NODE_AUTH_TOKEN when NPM_TOKEN is empty', () => { + // The action sets NPM_TOKEN to '' when its npm-token input is unset. + process.env.NPM_TOKEN = ''; + process.env.NODE_AUTH_TOKEN = 'node_auth_token'; + + const iso = createNpmSubprocessIsolation({ authMethod: 'token', registryUrl: 'https://registry.npmjs.org' }); + + const npmrcPath = iso.env.NPM_CONFIG_USERCONFIG as string; + createdPaths.push(npmrcPath); + expect(iso.env.NODE_AUTH_TOKEN).toBe('node_auth_token'); + expect(fs.readFileSync(npmrcPath, 'utf-8')).toContain('//registry.npmjs.org/:_authToken=node_auth_token'); + + iso.cleanup(); + }); + it('should write the token-bearing temp .npmrc with 0600 mode', () => { process.env.NPM_TOKEN = 'npm_test_token'; diff --git a/scripts/run-action.mjs b/scripts/run-action.mjs index d3914322..2088cc6f 100644 --- a/scripts/run-action.mjs +++ b/scripts/run-action.mjs @@ -226,11 +226,11 @@ function setFailure(errorMessage) { process.exit(1); } -export function assertDryRunSupported(mode, dryRun) { +function assertDryRunSupported(mode, dryRun) { if (!normalizeBoolean(dryRun)) { return; } - const unsupported = new Set(['standing-pr-update', 'standing-pr-publish', 'gate', 'refresh-after-release']); + const unsupported = new Set(['standing-pr-update', 'standing-pr-publish', 'refresh-after-release']); if (unsupported.has(mode)) { throw new Error(`dry-run is not supported for mode "${mode}"`); } diff --git a/test/integration/action-runner.spec.ts b/test/integration/action-runner.spec.ts index 9cb879bb..ec0775f3 100644 --- a/test/integration/action-runner.spec.ts +++ b/test/integration/action-runner.spec.ts @@ -3,7 +3,6 @@ import os from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { - assertDryRunSupported, buildBackfillArgs, buildGateArgs, buildGateSummary, @@ -29,8 +28,27 @@ describe('action runner', () => { tempDirs.length = 0; }); - it('should reject dry-run for standing-pr-publish', () => { - expect(() => assertDryRunSupported('standing-pr-publish', 'true')).toThrow(/dry-run is not supported/); + it.each(['standing-pr-update', 'standing-pr-publish', 'refresh-after-release'])( + 'should reject dry-run for %s, which would otherwise run for real', + (mode) => { + expect(() => parseInputs({ INPUT_MODE: mode, INPUT_DRY_RUN: 'true' })).toThrow( + `dry-run is not supported for mode "${mode}"`, + ); + }, + ); + + it('should accept dry-run for read-only gate mode', () => { + expect(parseInputs({ INPUT_MODE: 'gate', INPUT_DRY_RUN: 'true' }).mode).toBe('gate'); + }); + + it('should drop --apply from backfill args on a dry run', () => { + const input = parseInputs({ + INPUT_MODE: 'backfill', + INPUT_DRY_RUN: 'true', + INPUT_APPLY: 'true', + INPUT_ALL: 'true', + }); + expect(buildBackfillArgs(input)).not.toContain('--apply'); }); it('should build release args from inputs', () => { From 0d3f376b8823d174831781b8bec5990e01edc34e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Oct 2026 04:15:36 +0100 Subject: [PATCH 04/10] fix(action): default node-version to 24 The action's own setup-node replaces any Node version an earlier step set up, so its default is what the publish runs on. Node 22 bundles npm 10, and npm trusted publishing needs npm 11.5.1 or later, which ships with Node 24. This repo's own release job, the examples and ci-setup already use 24. Callers can still pass node-version: '22'. Co-Authored-By: Claude Opus 5.5 --- action.yml | 2 +- docs/action.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/action.yml b/action.yml index 2cd88a23..8bef8ec3 100644 --- a/action.yml +++ b/action.yml @@ -65,7 +65,7 @@ inputs: node-version: description: Node.js version required: false - default: '22' + default: '24' github-token: description: GitHub token for API calls (defaults to the workflow token) required: false diff --git a/docs/action.md b/docs/action.md index 6129c107..c3372622 100644 --- a/docs/action.md +++ b/docs/action.md @@ -22,7 +22,7 @@ uses: goosewobbler/releasekit@v0 ## Requirements -- The action checks out the repo (unless `skip-checkout`), then runs its own `pnpm/action-setup` and `actions/setup-node` (`node-version`, default `22`; ReleaseKit needs Node 22.12+). That replaces any Node version an earlier step set up. +- The action checks out the repo (unless `skip-checkout`), then runs its own `pnpm/action-setup` and `actions/setup-node` (`node-version`, default `24`, whose npm supports OIDC trusted publishing; ReleaseKit needs Node 22.12+). That replaces any Node version an earlier step set up. - For npm publishing, run `actions/setup-node` with `registry-url: https://registry.npmjs.org` before this action so that `NODE_AUTH_TOKEN` is wired up correctly. ## Required permissions and env @@ -63,7 +63,7 @@ The action passes `github-token` (default: the workflow token) to ReleaseKit as | `project-dir` | `.` | Project directory | | `dry-run` | `false` | Dry-run for `release`, `preview` and `backfill`. `gate` is read-only; other modes fail if it's set | | `github-token` | workflow token | Token for GitHub API calls (override with a PAT/App token when rulesets require it) | -| `node-version` | `22` | Node.js version for the action's setup-node step | +| `node-version` | `24` | Node.js version for the action's setup-node step | | `npm-token` | - | NPM token when not using OIDC; when unset, the job's `NPM_TOKEN` env is preserved | | `summary` | `true` | Write a job summary for `release` and `gate` | | `skip-checkout` | `false` | Skip the action's checkout steps only; setup-node, pnpm, and install still run. The repo must already be on disk (e.g. a prior checkout step). | From a5a45c3fba761879a039ef02033e6f9b12098119 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Nov 2026 00:41:07 +0000 Subject: [PATCH 05/10] fix(action): push with github-token, run without pnpm, and tighten dry-run - The action's checkout persists `github-token`, so git pushes use a PAT or App token passed there. An empty input, such as a secret unset on a fork run, falls back to the workflow token for both the checkout and the API. - A repo without a pnpm `packageManager` (npm, Dart or Rust) gets the action's own pnpm, and setup-node caches pnpm only when the repo has a pnpm-lock.yaml. Before, the action failed in such repos before ReleaseKit ran. - Boolean inputs are read case-insensitively, as action.yml's guards are, so `dry-run: TRUE` can't publish for real. Dry-run is checked against the modes that support it, so a new mode fails closed. A rejected input still writes the `success` and `mode` outputs. - docs: `pull-requests: read` for release mode, escaped pipes in the inputs table, accurate `skip-checkout`, `json` and `github-token` rows, a release example that builds before the action and passes outputs through `env`, and no `GITHUB_TOKEN` step env in the README, Dart, Rust and backfill examples. - Templates and the release README use Node 24. - The empty-NPM_TOKEN fallback is tested through the npm publish stage. --- README.md | 2 -- action.yml | 25 +++++++++++++++--- docs/action.md | 25 +++++++++++------- docs/dart.md | 11 +++----- docs/rust.md | 5 +--- .../test/unit/stages/npm-publish.spec.ts | 16 ++++++++++++ .../publish/test/unit/utils/npm-env.spec.ts | 15 ----------- packages/release/README.md | 2 +- scripts/run-action.mjs | 16 +++++++++--- templates/workflows/backfill.yml | 2 -- templates/workflows/release-preview.yml | 2 +- templates/workflows/standing-pr.yml | 6 ++--- test/integration/action-runner.spec.ts | 26 +++++++++++++++++++ 13 files changed, 101 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 8dac9fdd..3a4a58f6 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,6 @@ jobs: - uses: goosewobbler/releasekit@v0 with: mode: release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` See [docs/action.md](./docs/action.md) for the `preview` mode, and the full input/output reference. diff --git a/action.yml b/action.yml index 8bef8ec3..990ee9c9 100644 --- a/action.yml +++ b/action.yml @@ -67,7 +67,7 @@ inputs: required: false default: '24' github-token: - description: GitHub token for API calls (defaults to the workflow token) + description: GitHub token for API calls and, through the action's checkout, git pushes (defaults to the workflow token) required: false default: ${{ github.token }} npm-auth: @@ -225,15 +225,34 @@ runs: uses: actions/checkout@v7 with: fetch-depth: 0 + # Persisted for git pushes. An empty input (a secret unset on a fork run) falls back to the workflow token. + token: ${{ inputs.github-token != '' && inputs.github-token || github.token }} + + # A repo without a pnpm packageManager (npm, Dart or Rust) gets the action's own pnpm, and only a pnpm + # lockfile can key setup-node's cache. + - name: Resolve pnpm setup + id: pnpm-setup + shell: bash + run: | + if ! grep -qs '"packageManager": *"pnpm@' "$GITHUB_WORKSPACE/package.json"; then + echo "version=$(sed -n 's/.*"packageManager": *"pnpm@\([^"+]*\).*/\1/p' "$ACTION_PATH/package.json")" >> "$GITHUB_OUTPUT" + fi + if [ -f "$GITHUB_WORKSPACE/pnpm-lock.yaml" ]; then + echo "cache=pnpm" >> "$GITHUB_OUTPUT" + fi + env: + ACTION_PATH: ${{ github.action_path }} - name: Setup pnpm uses: pnpm/action-setup@v6 + with: + version: ${{ steps.pnpm-setup.outputs.version }} - name: Setup Node uses: actions/setup-node@v7 with: node-version: ${{ inputs.node-version }} - cache: 'pnpm' + cache: ${{ steps.pnpm-setup.outputs.cache }} - name: Install action runtime deps run: pnpm install --frozen-lockfile @@ -339,7 +358,7 @@ runs: INPUT_UPDATE_RELEASES: ${{ inputs.update-releases }} INPUT_ONLY_MISSING: ${{ inputs.only-missing }} INPUT_APPLY: ${{ inputs.apply }} - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token != '' && inputs.github-token || github.token }} NPM_TOKEN: ${{ inputs.npm-token != '' && inputs.npm-token || env.NPM_TOKEN }} VERBOSE: ${{ inputs.verbose }} OLLAMA_API_KEY: ${{ env.OLLAMA_API_KEY }} diff --git a/docs/action.md b/docs/action.md index c3372622..2577b34c 100644 --- a/docs/action.md +++ b/docs/action.md @@ -22,19 +22,19 @@ uses: goosewobbler/releasekit@v0 ## Requirements -- The action checks out the repo (unless `skip-checkout`), then runs its own `pnpm/action-setup` and `actions/setup-node` (`node-version`, default `24`, whose npm supports OIDC trusted publishing; ReleaseKit needs Node 22.12+). That replaces any Node version an earlier step set up. +- The action checks out the repo (unless `skip-checkout`), then runs its own `pnpm/action-setup` and `actions/setup-node` (`node-version`, default `24`, whose npm supports OIDC trusted publishing; ReleaseKit needs Node 22.12+). That replaces any Node version an earlier step set up. pnpm is the version your `package.json` pins in `packageManager`, else the action's own, so npm, Dart and Rust repos need no pnpm setup. - For npm publishing, run `actions/setup-node` with `registry-url: https://registry.npmjs.org` before this action so that `NODE_AUTH_TOKEN` is wired up correctly. ## Required permissions and env -The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN`, so a `GITHUB_TOKEN` in the step's `env` is ignored. Pass a PAT or App token as `github-token`. +The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN`, so a `GITHUB_TOKEN` in the step's `env` is ignored. Pass a PAT or App token as `github-token`. The action's checkout also persists it for git pushes, unless you set `ssh-key` or `skip-checkout`. ### Release mode - Recommended job permissions: - `id-token: write` (OIDC trusted publishing) - - `contents: read` (checkout) - - `contents: write` (git push & GitHub Releases) + - `contents: write` (checkout, git push & GitHub Releases) + - `pull-requests: read` (merged-PR labels such as bump and scope) ### Preview mode @@ -62,12 +62,12 @@ The action passes `github-token` (default: the workflow token) to ReleaseKit as | `config` | - | Path to `releasekit.config.json` | | `project-dir` | `.` | Project directory | | `dry-run` | `false` | Dry-run for `release`, `preview` and `backfill`. `gate` is read-only; other modes fail if it's set | -| `github-token` | workflow token | Token for GitHub API calls (override with a PAT/App token when rulesets require it) | +| `github-token` | workflow token | Token for GitHub API calls and the action's checkout, which git pushes use (override with a PAT/App token when rulesets require it) | | `node-version` | `24` | Node.js version for the action's setup-node step | | `npm-token` | - | NPM token when not using OIDC; when unset, the job's `NPM_TOKEN` env is preserved | | `summary` | `true` | Write a job summary for `release` and `gate` | -| `skip-checkout` | `false` | Skip the action's checkout steps only; setup-node, pnpm, and install still run. The repo must already be on disk (e.g. a prior checkout step). | -| `json` | `false` | JSON output (release mode). The release outputs stay empty without it | +| `skip-checkout` | `false` | Skip the action's checkout. Check the repo out on a branch, not a detached HEAD, with `fetch-depth: 0`; the action then rebases it onto `origin/`, falling back to `git reset --hard`. Pushes use your checkout's credentials, not `ssh-key` or `github-token`. | +| `json` | `false` | JSON output (release mode). Without it, `has-changes` is always `false` and the other release outputs are empty | | `verbose` | `false` | Verbose logs | | `quiet` | `false` | Suppress non-error logs | @@ -75,7 +75,7 @@ The action passes `github-token` (default: the workflow token) to ReleaseKit as | Input | Default | Description | |---|---|---| -| `bump` | - | Force bump (`patch|minor|major|prerelease`) | +| `bump` | - | Force bump (`patch\|minor\|major\|prerelease`) | | `prerelease` | - | Prerelease identifier (e.g. `alpha`, `beta`) | | `stable` | `false` | Graduate prerelease packages to stable without bumping | | `scope` | - | Scope name resolved to target packages from `ci.scopeLabels` (also `gate`) | @@ -83,7 +83,7 @@ The action passes `github-token` (default: the workflow token) to ReleaseKit as | `sync` | `false` | Synchronized versioning | | `target` | - | Comma-separated package targets | | `branch` | - | Override push branch | -| `npm-auth` | `auto` | `auto|oidc|token` | +| `npm-auth` | `auto` | `auto\|oidc\|token` | | `skip-notes` | `false` | Skip notes stage | | `skip-publish` | `false` | Skip publish stage | | `skip-git` | `false` | Skip git stage | @@ -168,13 +168,18 @@ jobs: - uses: actions/setup-node@v6 with: registry-url: https://registry.npmjs.org + # Install and build here. - id: rk uses: goosewobbler/releasekit@v0 with: mode: release json: "true" + # The action's own checkout would clean away the build. + skip-checkout: "true" - name: Print tags - run: echo "Tags: ${{ steps.rk.outputs.tags }}" + env: + TAGS: ${{ steps.rk.outputs.tags }} + run: echo "Tags: $TAGS" ``` ### Preview mode (post/update PR comment) diff --git a/docs/dart.md b/docs/dart.md index 51822f79..152a4e56 100644 --- a/docs/dart.md +++ b/docs/dart.md @@ -188,17 +188,14 @@ jobs: - uses: dart-lang/setup-dart@v1 - # No pnpm/Node setup needed: the bundled goosewobbler/releasekit action brings - # its own runtime, and a Dart-only repo has no pnpm-lock.yaml (setting cache: pnpm - # would fail with "Dependencies lock file is not found"). + # No pnpm/Node setup needed: the action sets up its own. - name: Run releasekit uses: goosewobbler/releasekit@v0 with: mode: release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # For token auth instead of OIDC, set PUB_TOKEN and drop `id-token: write` above: - # PUB_TOKEN: ${{ secrets.PUB_TOKEN }} + # For token auth instead of OIDC, drop `id-token: write` above and add: + # env: + # PUB_TOKEN: ${{ secrets.PUB_TOKEN }} ``` `fetch-depth: 0` is required so that releasekit can walk the full commit history to determine the version bump. diff --git a/docs/rust.md b/docs/rust.md index 82812f6f..1b4be3ee 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -191,15 +191,12 @@ jobs: - uses: dtolnay/rust-toolchain@stable - # No pnpm/Node setup needed: the bundled goosewobbler/releasekit action brings - # its own runtime, and a Rust-only repo has no pnpm-lock.yaml (setting cache: pnpm - # would fail with "Dependencies lock file is not found"). + # No pnpm/Node setup needed: the action sets up its own. - name: Run releasekit uses: goosewobbler/releasekit@v0 with: mode: release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} ``` diff --git a/packages/publish/test/unit/stages/npm-publish.spec.ts b/packages/publish/test/unit/stages/npm-publish.spec.ts index a9c851a6..b3fbf7a4 100644 --- a/packages/publish/test/unit/stages/npm-publish.spec.ts +++ b/packages/publish/test/unit/stages/npm-publish.spec.ts @@ -117,6 +117,22 @@ describe('npm-publish stage', () => { expect(ctx.output.npm[0]?.success).toBe(true); }); + it('should publish with NODE_AUTH_TOKEN when NPM_TOKEN is empty', async () => { + const { execCommand } = await import('../../../src/utils/exec.js'); + const dir = createTmpDir(); + const pkgDir = path.join(dir, 'packages', 'pkg'); + fs.mkdirSync(pkgDir, { recursive: true }); + fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name: '@test/pkg', version: '1.0.0' })); + + // The action passes an empty NPM_TOKEN when neither its npm-token input nor the job env sets one. + process.env.NPM_TOKEN = ''; + process.env.NODE_AUTH_TOKEN = 'node_auth_token'; + await runNpmPublishStage(createContext(dir)); + + const publishCall = vi.mocked(execCommand).mock.calls[1]; + expect(publishCall?.[2]?.env?.NODE_AUTH_TOKEN).toBe('node_auth_token'); + }); + it('should skip private packages', async () => { const { execCommand } = await import('../../../src/utils/exec.js'); const dir = createTmpDir(); diff --git a/packages/publish/test/unit/utils/npm-env.spec.ts b/packages/publish/test/unit/utils/npm-env.spec.ts index dce98171..6057069e 100644 --- a/packages/publish/test/unit/utils/npm-env.spec.ts +++ b/packages/publish/test/unit/utils/npm-env.spec.ts @@ -63,21 +63,6 @@ describe('npm env isolation', () => { iso.cleanup(); }); - it('should fall back to NODE_AUTH_TOKEN when NPM_TOKEN is empty', () => { - // The action sets NPM_TOKEN to '' when its npm-token input is unset. - process.env.NPM_TOKEN = ''; - process.env.NODE_AUTH_TOKEN = 'node_auth_token'; - - const iso = createNpmSubprocessIsolation({ authMethod: 'token', registryUrl: 'https://registry.npmjs.org' }); - - const npmrcPath = iso.env.NPM_CONFIG_USERCONFIG as string; - createdPaths.push(npmrcPath); - expect(iso.env.NODE_AUTH_TOKEN).toBe('node_auth_token'); - expect(fs.readFileSync(npmrcPath, 'utf-8')).toContain('//registry.npmjs.org/:_authToken=node_auth_token'); - - iso.cleanup(); - }); - it('should write the token-bearing temp .npmrc with 0600 mode', () => { process.env.NPM_TOKEN = 'npm_test_token'; diff --git a/packages/release/README.md b/packages/release/README.md index 185b46fc..93023c26 100644 --- a/packages/release/README.md +++ b/packages/release/README.md @@ -450,7 +450,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '24' - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/scripts/run-action.mjs b/scripts/run-action.mjs index 2088cc6f..330eedea 100644 --- a/scripts/run-action.mjs +++ b/scripts/run-action.mjs @@ -10,7 +10,8 @@ function normalizeBoolean(value, defaultValue = false) { if (value === undefined || value === '') { return defaultValue; } - return value === 'true'; + // Case-insensitive, like action.yml's `inputs.x == 'true'` guards. + return value.toLowerCase() === 'true'; } function normalizeString(value) { @@ -230,8 +231,9 @@ function assertDryRunSupported(mode, dryRun) { if (!normalizeBoolean(dryRun)) { return; } - const unsupported = new Set(['standing-pr-update', 'standing-pr-publish', 'refresh-after-release']); - if (unsupported.has(mode)) { + // An allowlist, so a new mode that drops dry-run fails rather than running for real. + const dryRunModes = new Set(['release', 'preview', 'backfill', 'gate']); + if (!dryRunModes.has(mode)) { throw new Error(`dry-run is not supported for mode "${mode}"`); } } @@ -662,7 +664,13 @@ export function buildGateSummary(_input, parsed, success) { } async function main() { - const input = parseInputs(); + let input; + try { + input = parseInputs(); + } catch (err) { + writeCoreOutputs(normalizeString(process.env.INPUT_MODE) ?? 'preview', false); + setFailure(err instanceof Error ? err.message : String(err)); + } const result = await runAction(input); const success = result.status === 0; const verbose = normalizeBoolean(input.verbose); diff --git a/templates/workflows/backfill.yml b/templates/workflows/backfill.yml index 512c95c2..9336a2bb 100644 --- a/templates/workflows/backfill.yml +++ b/templates/workflows/backfill.yml @@ -38,5 +38,3 @@ jobs: update-releases: 'true' only-missing: 'true' apply: ${{ inputs.apply }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/templates/workflows/release-preview.yml b/templates/workflows/release-preview.yml index bfb30128..863a7097 100644 --- a/templates/workflows/release-preview.yml +++ b/templates/workflows/release-preview.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '24' - name: Install dependencies run: npm ci diff --git a/templates/workflows/standing-pr.yml b/templates/workflows/standing-pr.yml index c6566bfc..d4294d84 100644 --- a/templates/workflows/standing-pr.yml +++ b/templates/workflows/standing-pr.yml @@ -82,7 +82,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '24' registry-url: 'https://registry.npmjs.org' - run: pnpm install --frozen-lockfile @@ -134,7 +134,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '24' registry-url: 'https://registry.npmjs.org' - run: pnpm install --frozen-lockfile @@ -184,7 +184,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '24' registry-url: 'https://registry.npmjs.org' - run: pnpm install --frozen-lockfile diff --git a/test/integration/action-runner.spec.ts b/test/integration/action-runner.spec.ts index ec0775f3..19035f6d 100644 --- a/test/integration/action-runner.spec.ts +++ b/test/integration/action-runner.spec.ts @@ -1,6 +1,8 @@ +import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { afterEach, describe, expect, it } from 'vitest'; import { buildBackfillArgs, @@ -37,6 +39,30 @@ describe('action runner', () => { }, ); + it('should read dry-run case-insensitively, as action.yml does', () => { + expect(() => parseInputs({ INPUT_MODE: 'standing-pr-publish', INPUT_DRY_RUN: 'TRUE' })).toThrow( + 'dry-run is not supported for mode "standing-pr-publish"', + ); + }); + + it('should write the failure outputs when an input is rejected', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rk-action-outputs-')); + tempDirs.push(dir); + const outputFile = path.join(dir, 'github-output'); + const script = fileURLToPath(new URL('../../scripts/run-action.mjs', import.meta.url)); + + const result = spawnSync(process.execPath, [script], { + env: { ...process.env, INPUT_MODE: 'standing-pr-publish', INPUT_DRY_RUN: 'true', GITHUB_OUTPUT: outputFile }, + encoding: 'utf-8', + }); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('dry-run is not supported for mode "standing-pr-publish"'); + const outputs = fs.readFileSync(outputFile, 'utf-8'); + expect(outputs).toMatch(/^mode<<\S+\nstanding-pr-publish\n/m); + expect(outputs).toMatch(/^success<<\S+\nfalse\n/m); + }); + it('should accept dry-run for read-only gate mode', () => { expect(parseInputs({ INPUT_MODE: 'gate', INPUT_DRY_RUN: 'true' }).mode).toBe('gate'); }); From 349bc9f6f77a22717884983ec2ffd185e589f36b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Nov 2026 01:02:12 +0000 Subject: [PATCH 06/10] fix(action): persist github-token only where the action pushes - The checkout persists `github-token` only in release, standing-PR and refresh modes outside a dry run, so a PR preview's later steps can't read a PAT. Other runs persist the workflow token. - The pnpm pin check tolerates whitespace around the colon, so a pinned repo isn't given a second pnpm version. - `GH_TOKEN` is set alongside `GITHUB_TOKEN`, so a job-level GH_TOKEN can't override `github-token` for backfill's gh calls. - Every failure, not just a rejected input, writes the `success` and `mode` outputs. - Simpler token fallbacks, and the built-in GITHUB_ACTION_PATH. - The failure-output test runs with a minimal env and a timeout. - docs: `pull-requests: read` in the release examples, a Node version for the release example's build, a note that PAT pushes start `on: push` workflows, and Node.js 22.12+ in the package READMEs and CI setup. --- README.md | 1 + action.yml | 17 +++++++++-------- docs/action.md | 6 ++++-- packages/notes/README.md | 2 +- packages/publish/README.md | 2 +- packages/release/README.md | 2 +- packages/release/docs/ci-setup.md | 2 +- packages/version/README.md | 2 +- scripts/run-action.mjs | 9 ++------- test/integration/action-runner.spec.ts | 9 ++++++++- 10 files changed, 29 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 3a4a58f6..b7124d2e 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ jobs: permissions: id-token: write contents: write + pull-requests: read runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/action.yml b/action.yml index 990ee9c9..a4383378 100644 --- a/action.yml +++ b/action.yml @@ -225,8 +225,9 @@ runs: uses: actions/checkout@v7 with: fetch-depth: 0 - # Persisted for git pushes. An empty input (a secret unset on a fork run) falls back to the workflow token. - token: ${{ inputs.github-token != '' && inputs.github-token || github.token }} + # Persisted for git pushes, so only a mode that pushes gets github-token: a PR preview's later steps can't + # read a PAT. An empty input (a secret unset on a fork run) falls back to the workflow token. + token: ${{ (inputs.mode == 'release' || inputs.mode == 'standing-pr-update' || inputs.mode == 'standing-pr-publish' || inputs.mode == 'refresh-after-release') && inputs.dry-run != 'true' && (inputs.github-token || github.token) || github.token }} # A repo without a pnpm packageManager (npm, Dart or Rust) gets the action's own pnpm, and only a pnpm # lockfile can key setup-node's cache. @@ -234,14 +235,12 @@ runs: id: pnpm-setup shell: bash run: | - if ! grep -qs '"packageManager": *"pnpm@' "$GITHUB_WORKSPACE/package.json"; then - echo "version=$(sed -n 's/.*"packageManager": *"pnpm@\([^"+]*\).*/\1/p' "$ACTION_PATH/package.json")" >> "$GITHUB_OUTPUT" + if ! grep -Eqs '"packageManager"[[:space:]]*:[[:space:]]*"pnpm@' "$GITHUB_WORKSPACE/package.json"; then + echo "version=$(sed -nE 's/.*"packageManager"[[:space:]]*:[[:space:]]*"pnpm@([^"+]*).*/\1/p' "$GITHUB_ACTION_PATH/package.json")" >> "$GITHUB_OUTPUT" fi if [ -f "$GITHUB_WORKSPACE/pnpm-lock.yaml" ]; then echo "cache=pnpm" >> "$GITHUB_OUTPUT" fi - env: - ACTION_PATH: ${{ github.action_path }} - name: Setup pnpm uses: pnpm/action-setup@v6 @@ -358,7 +357,9 @@ runs: INPUT_UPDATE_RELEASES: ${{ inputs.update-releases }} INPUT_ONLY_MISSING: ${{ inputs.only-missing }} INPUT_APPLY: ${{ inputs.apply }} - GITHUB_TOKEN: ${{ inputs.github-token != '' && inputs.github-token || github.token }} - NPM_TOKEN: ${{ inputs.npm-token != '' && inputs.npm-token || env.NPM_TOKEN }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} + # gh, which backfill uses, prefers GH_TOKEN over GITHUB_TOKEN. + GH_TOKEN: ${{ inputs.github-token || github.token }} + NPM_TOKEN: ${{ inputs.npm-token || env.NPM_TOKEN }} VERBOSE: ${{ inputs.verbose }} OLLAMA_API_KEY: ${{ env.OLLAMA_API_KEY }} diff --git a/docs/action.md b/docs/action.md index 2577b34c..85ed009b 100644 --- a/docs/action.md +++ b/docs/action.md @@ -27,7 +27,7 @@ uses: goosewobbler/releasekit@v0 ## Required permissions and env -The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN`, so a `GITHUB_TOKEN` in the step's `env` is ignored. Pass a PAT or App token as `github-token`. The action's checkout also persists it for git pushes, unless you set `ssh-key` or `skip-checkout`. +The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN`, so a `GITHUB_TOKEN` in the step's `env` is ignored. Pass a PAT or App token as `github-token`. In `release`, the standing-PR modes and `refresh-after-release`, the action's checkout also persists it for git pushes, unless you set `ssh-key` or `skip-checkout`. Unlike the workflow token's, those pushes start `on: push` workflows. ### Release mode @@ -160,13 +160,15 @@ jobs: permissions: id-token: write contents: write + pull-requests: read steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - # Only for registry-url: the action sets up its own Node version. + # Node for the build, and registry-url for npm auth. - uses: actions/setup-node@v6 with: + node-version: 24 registry-url: https://registry.npmjs.org # Install and build here. - id: rk diff --git a/packages/notes/README.md b/packages/notes/README.md index a3bc0d20..a4abf2af 100644 --- a/packages/notes/README.md +++ b/packages/notes/README.md @@ -34,7 +34,7 @@ npm install -g @releasekit/notes pnpm add -g @releasekit/notes ``` -> **Note:** ESM only. Requires Node.js 20+. +> **Note:** ESM only. Requires Node.js 22.12+. ## Quick Start diff --git a/packages/publish/README.md b/packages/publish/README.md index 87e1dc9b..8b7892fe 100644 --- a/packages/publish/README.md +++ b/packages/publish/README.md @@ -34,7 +34,7 @@ npm install -g @releasekit/publish pnpm add -g @releasekit/publish ``` -> **Note:** This package is ESM only and requires Node.js 20+. +> **Note:** This package is ESM only and requires Node.js 22.12+. ## Quick Start diff --git a/packages/release/README.md b/packages/release/README.md index bdf6ab61..f0e2d0c1 100644 --- a/packages/release/README.md +++ b/packages/release/README.md @@ -33,7 +33,7 @@ npm install -g @releasekit/release pnpm add -g @releasekit/release ``` -> **Note:** This package is ESM only and requires Node.js 20+. +> **Note:** This package is ESM only and requires Node.js 22.12+. ## Quick Start diff --git a/packages/release/docs/ci-setup.md b/packages/release/docs/ci-setup.md index 9c2c5b40..5cee4ec4 100644 --- a/packages/release/docs/ci-setup.md +++ b/packages/release/docs/ci-setup.md @@ -8,7 +8,7 @@ All workflows require: - `fetch-depth: 0` on checkout — ReleaseKit reads git history to determine version bumps - A `GITHUB_TOKEN` with `contents: write` permission for tagging and GitHub Releases -- Node.js 20+ +- Node.js 22.12+ --- diff --git a/packages/version/README.md b/packages/version/README.md index 46dfcb50..80242be4 100644 --- a/packages/version/README.md +++ b/packages/version/README.md @@ -31,7 +31,7 @@ npm install -g @releasekit/version pnpm add -g @releasekit/version ``` -> **Note:** This package is ESM only and requires Node.js 20+. +> **Note:** This package is ESM only and requires Node.js 22.12+. ## Quick Start diff --git a/scripts/run-action.mjs b/scripts/run-action.mjs index 330eedea..7fc2ed19 100644 --- a/scripts/run-action.mjs +++ b/scripts/run-action.mjs @@ -664,13 +664,7 @@ export function buildGateSummary(_input, parsed, success) { } async function main() { - let input; - try { - input = parseInputs(); - } catch (err) { - writeCoreOutputs(normalizeString(process.env.INPUT_MODE) ?? 'preview', false); - setFailure(err instanceof Error ? err.message : String(err)); - } + const input = parseInputs(); const result = await runAction(input); const success = result.status === 0; const verbose = normalizeBoolean(input.verbose); @@ -716,6 +710,7 @@ async function main() { if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { main().catch((err) => { + writeCoreOutputs(normalizeString(process.env.INPUT_MODE) ?? 'preview', false); process.stderr.write(`[run-action] uncaught error: ${err instanceof Error ? err.message : String(err)}\n`); process.exit(1); }); diff --git a/test/integration/action-runner.spec.ts b/test/integration/action-runner.spec.ts index 19035f6d..b4d233af 100644 --- a/test/integration/action-runner.spec.ts +++ b/test/integration/action-runner.spec.ts @@ -51,9 +51,16 @@ describe('action runner', () => { const outputFile = path.join(dir, 'github-output'); const script = fileURLToPath(new URL('../../scripts/run-action.mjs', import.meta.url)); + // A minimal env and a timeout, so a broken guard can't reach a real publish. const result = spawnSync(process.execPath, [script], { - env: { ...process.env, INPUT_MODE: 'standing-pr-publish', INPUT_DRY_RUN: 'true', GITHUB_OUTPUT: outputFile }, + env: { + PATH: process.env.PATH, + INPUT_MODE: 'standing-pr-publish', + INPUT_DRY_RUN: 'true', + GITHUB_OUTPUT: outputFile, + }, encoding: 'utf-8', + timeout: 10_000, }); expect(result.status).toBe(1); From e1a5710579baef470e04545d8af381bdc658ec1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Nov 2026 01:20:11 +0000 Subject: [PATCH 07/10] fix(action): reject dry-run in gate mode and recommend App tokens - Gate posts label-notice comments and its CLI can't turn them off, so `dry-run` now fails there like the other modes that can't honour it. - The pnpm pin check reads package.json and honours `devEngines.packageManager`, matching pnpm/action-setup, so a pinned repo keeps its pnpm. - setup-node's automatic npm caching is off, so an npm repo without a root lockfile doesn't fail at setup. - The failure handler prints the error before writing outputs, which can fail too. - docs: recommend a GitHub App token over a PAT, which ReleaseKit's bot-authorship checks reject; `pull-requests: write` for release and gate, which post comments; and `GH_TOKEN` as well as `GITHUB_TOKEN` taken from `github-token`. --- README.md | 2 +- action.yml | 13 +++++++++---- docs/action.md | 14 ++++++++------ docs/dart.md | 2 +- docs/rust.md | 2 +- scripts/run-action.mjs | 8 ++++++-- test/integration/action-runner.spec.ts | 6 +----- 7 files changed, 27 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b7124d2e..99edd16a 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ jobs: permissions: id-token: write contents: write - pull-requests: read + pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/action.yml b/action.yml index a4383378..6a54fa6c 100644 --- a/action.yml +++ b/action.yml @@ -229,14 +229,18 @@ runs: # read a PAT. An empty input (a secret unset on a fork run) falls back to the workflow token. token: ${{ (inputs.mode == 'release' || inputs.mode == 'standing-pr-update' || inputs.mode == 'standing-pr-publish' || inputs.mode == 'refresh-after-release') && inputs.dry-run != 'true' && (inputs.github-token || github.token) || github.token }} - # A repo without a pnpm packageManager (npm, Dart or Rust) gets the action's own pnpm, and only a pnpm - # lockfile can key setup-node's cache. + # A repo that doesn't pin pnpm (npm, Dart or Rust) gets the action's own pnpm, and only a pnpm lockfile can + # key setup-node's cache. The pin check mirrors pnpm/action-setup's: devEngines.packageManager or packageManager. - name: Resolve pnpm setup id: pnpm-setup shell: bash run: | - if ! grep -Eqs '"packageManager"[[:space:]]*:[[:space:]]*"pnpm@' "$GITHUB_WORKSPACE/package.json"; then - echo "version=$(sed -nE 's/.*"packageManager"[[:space:]]*:[[:space:]]*"pnpm@([^"+]*).*/\1/p' "$GITHUB_ACTION_PATH/package.json")" >> "$GITHUB_OUTPUT" + if ! node -e ' + const m = require(process.argv[1]); + const d = m.devEngines?.packageManager; + process.exit(String(m.packageManager).startsWith("pnpm@") || (d?.name === "pnpm" && d.version) ? 0 : 1); + ' "$GITHUB_WORKSPACE/package.json" 2>/dev/null; then + echo "version=$(node -p 'require(process.argv[1]).packageManager.split("@")[1].split("+")[0]' "$GITHUB_ACTION_PATH/package.json")" >> "$GITHUB_OUTPUT" fi if [ -f "$GITHUB_WORKSPACE/pnpm-lock.yaml" ]; then echo "cache=pnpm" >> "$GITHUB_OUTPUT" @@ -252,6 +256,7 @@ runs: with: node-version: ${{ inputs.node-version }} cache: ${{ steps.pnpm-setup.outputs.cache }} + package-manager-cache: false - name: Install action runtime deps run: pnpm install --frozen-lockfile diff --git a/docs/action.md b/docs/action.md index 85ed009b..9b1550a8 100644 --- a/docs/action.md +++ b/docs/action.md @@ -27,14 +27,16 @@ uses: goosewobbler/releasekit@v0 ## Required permissions and env -The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN`, so a `GITHUB_TOKEN` in the step's `env` is ignored. Pass a PAT or App token as `github-token`. In `release`, the standing-PR modes and `refresh-after-release`, the action's checkout also persists it for git pushes, unless you set `ssh-key` or `skip-checkout`. Unlike the workflow token's, those pushes start `on: push` workflows. +The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN` and `GH_TOKEN`, so either in the step's `env` is ignored. In `release`, the standing-PR modes and `refresh-after-release`, the action's checkout also persists it for git pushes, unless you set `ssh-key` or `skip-checkout`. + +To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a GitHub App token. Not a PAT: ReleaseKit recognises its own comments by bot authorship, so with a PAT `standing-pr-publish` refuses the release manifest, marker comments are posted again on every run, and the standing-PR workflow's bot-sender check stops filtering its own edits. ### Release mode - Recommended job permissions: - `id-token: write` (OIDC trusted publishing) - `contents: write` (checkout, git push & GitHub Releases) - - `pull-requests: read` (merged-PR labels such as bump and scope) + - `pull-requests: write` (merged-PR labels, publish-failure reports and feeder-PR previews) ### Preview mode @@ -46,7 +48,7 @@ The action passes `github-token` (default: the workflow token) to ReleaseKit as - Recommended job permissions: - `contents: read` - - `pull-requests: read` (reading the merged PR's labels) + - `pull-requests: write` (reading the merged PR's labels and posting label notices) ### Standing-PR modes @@ -61,8 +63,8 @@ The action passes `github-token` (default: the workflow token) to ReleaseKit as | `mode` | `preview` | `release`, `preview`, `gate`, `standing-pr-update`, `standing-pr-publish`, `backfill`, or `refresh-after-release` | | `config` | - | Path to `releasekit.config.json` | | `project-dir` | `.` | Project directory | -| `dry-run` | `false` | Dry-run for `release`, `preview` and `backfill`. `gate` is read-only; other modes fail if it's set | -| `github-token` | workflow token | Token for GitHub API calls and the action's checkout, which git pushes use (override with a PAT/App token when rulesets require it) | +| `dry-run` | `false` | Dry-run for `release`, `preview` and `backfill`; other modes fail if it's set | +| `github-token` | workflow token | Token for GitHub API calls and the action's checkout, which git pushes use (override with a GitHub App token when rulesets require it) | | `node-version` | `24` | Node.js version for the action's setup-node step | | `npm-token` | - | NPM token when not using OIDC; when unset, the job's `NPM_TOKEN` env is preserved | | `summary` | `true` | Write a job summary for `release` and `gate` | @@ -160,7 +162,7 @@ jobs: permissions: id-token: write contents: write - pull-requests: read + pull-requests: write steps: - uses: actions/checkout@v6 with: diff --git a/docs/dart.md b/docs/dart.md index 8bccc218..dfec3d1b 100644 --- a/docs/dart.md +++ b/docs/dart.md @@ -171,7 +171,7 @@ on: permissions: contents: write - pull-requests: read + pull-requests: write id-token: write # required for pub.dev OIDC automated publishing jobs: diff --git a/docs/rust.md b/docs/rust.md index 8d441b32..b12986a8 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -175,7 +175,7 @@ on: permissions: contents: write - pull-requests: read + pull-requests: write jobs: release: diff --git a/scripts/run-action.mjs b/scripts/run-action.mjs index 7fc2ed19..2273815e 100644 --- a/scripts/run-action.mjs +++ b/scripts/run-action.mjs @@ -232,7 +232,7 @@ function assertDryRunSupported(mode, dryRun) { return; } // An allowlist, so a new mode that drops dry-run fails rather than running for real. - const dryRunModes = new Set(['release', 'preview', 'backfill', 'gate']); + const dryRunModes = new Set(['release', 'preview', 'backfill']); if (!dryRunModes.has(mode)) { throw new Error(`dry-run is not supported for mode "${mode}"`); } @@ -710,8 +710,12 @@ async function main() { if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { main().catch((err) => { - writeCoreOutputs(normalizeString(process.env.INPUT_MODE) ?? 'preview', false); process.stderr.write(`[run-action] uncaught error: ${err instanceof Error ? err.message : String(err)}\n`); + try { + writeCoreOutputs(normalizeString(process.env.INPUT_MODE) ?? 'preview', false); + } catch { + // The error above is the one that matters. + } process.exit(1); }); } diff --git a/test/integration/action-runner.spec.ts b/test/integration/action-runner.spec.ts index b4d233af..f3706f4d 100644 --- a/test/integration/action-runner.spec.ts +++ b/test/integration/action-runner.spec.ts @@ -30,7 +30,7 @@ describe('action runner', () => { tempDirs.length = 0; }); - it.each(['standing-pr-update', 'standing-pr-publish', 'refresh-after-release'])( + it.each(['gate', 'standing-pr-update', 'standing-pr-publish', 'refresh-after-release'])( 'should reject dry-run for %s, which would otherwise run for real', (mode) => { expect(() => parseInputs({ INPUT_MODE: mode, INPUT_DRY_RUN: 'true' })).toThrow( @@ -70,10 +70,6 @@ describe('action runner', () => { expect(outputs).toMatch(/^success<<\S+\nfalse\n/m); }); - it('should accept dry-run for read-only gate mode', () => { - expect(parseInputs({ INPUT_MODE: 'gate', INPUT_DRY_RUN: 'true' }).mode).toBe('gate'); - }); - it('should drop --apply from backfill args on a dry run', () => { const input = parseInputs({ INPUT_MODE: 'backfill', From c5a60f2cee5d3ead76ce2e742988cf657f6273cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Nov 2026 01:41:56 +0000 Subject: [PATCH 08/10] fix(action): decide push access once, and set up pnpm without Node - A first step works out whether the run pushes (release, standing-PR or refresh mode, outside a dry run). Both checkouts use it, so the SSH deploy key, like `github-token`, is persisted only for a run that pushes, and the push-mode list lives in one place. - The pnpm step reads the action's own pnpm version with sed, and checks the repo's pin with Node only when the job has Node, falling back to a packageManager grep, so a Rust or Dart container job can set up pnpm. - Boolean inputs accept non-strings again. - docs: devEngines pins, `github-token` through `git.httpsTokenEnv` with skip-checkout, and the `dry-run`, `npm-token` and `ssh-key` input descriptions. - Tests: the empty-NPM_TOKEN test checks the .npmrc npm reads, and the backfill dry-run test goes through runAction. --- action.yml | 53 +++++++++++++------ docs/action.md | 6 +-- .../test/unit/stages/npm-publish.spec.ts | 9 ++++ scripts/run-action.mjs | 2 +- test/integration/action-runner.spec.ts | 12 ++++- 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/action.yml b/action.yml index 6a54fa6c..534defc1 100644 --- a/action.yml +++ b/action.yml @@ -19,7 +19,7 @@ inputs: required: false default: . dry-run: - description: Dry-run mode + description: Dry-run for release, preview and backfill; other modes fail if it's set required: false default: 'false' json: @@ -75,7 +75,7 @@ inputs: required: false default: auto npm-token: - description: NPM token for publishing (or use NPM_TOKEN input) + description: NPM token for publishing without OIDC (defaults to the job's NPM_TOKEN env) required: false skip-notes: description: Skip changelog generation @@ -98,7 +98,7 @@ inputs: required: false default: 'false' ssh-key: - description: SSH deploy key for git push (enables SSH checkout when not dry-run) + description: SSH deploy key for git push (the checkout uses it in modes that push, outside a dry run) required: false reconcile: @@ -213,34 +213,57 @@ outputs: runs: using: composite steps: + # Push credentials (the SSH key, or github-token in place of the workflow token) are persisted only for a run + # that pushes, so a PR preview's later steps can't read them. + - name: Resolve push access + id: access + shell: bash + run: | + dry_run=$(printf '%s' "$INPUT_DRY_RUN" | tr '[:upper:]' '[:lower:]') + case "$INPUT_MODE" in + release | standing-pr-update | standing-pr-publish | refresh-after-release) pushes=true ;; + *) pushes=false ;; + esac + [ "$dry_run" = true ] && pushes=false + echo "pushes=$pushes" >> "$GITHUB_OUTPUT" + env: + INPUT_MODE: ${{ inputs.mode }} + INPUT_DRY_RUN: ${{ inputs.dry-run }} + - name: Checkout (SSH) - if: ${{ inputs.skip-checkout != 'true' && inputs.ssh-key != '' && inputs.dry-run != 'true' && inputs.mode != 'gate' }} + if: ${{ inputs.skip-checkout != 'true' && inputs.ssh-key != '' && steps.access.outputs.pushes == 'true' }} uses: actions/checkout@v7 with: ssh-key: ${{ inputs.ssh-key }} fetch-depth: 0 - name: Checkout (read-only) - if: ${{ inputs.skip-checkout != 'true' && (inputs.ssh-key == '' || inputs.dry-run == 'true' || inputs.mode == 'gate') }} + if: ${{ inputs.skip-checkout != 'true' && (inputs.ssh-key == '' || steps.access.outputs.pushes != 'true') }} uses: actions/checkout@v7 with: fetch-depth: 0 - # Persisted for git pushes, so only a mode that pushes gets github-token: a PR preview's later steps can't - # read a PAT. An empty input (a secret unset on a fork run) falls back to the workflow token. - token: ${{ (inputs.mode == 'release' || inputs.mode == 'standing-pr-update' || inputs.mode == 'standing-pr-publish' || inputs.mode == 'refresh-after-release') && inputs.dry-run != 'true' && (inputs.github-token || github.token) || github.token }} + # An empty input (a secret unset on a fork run) falls back to the workflow token. + token: ${{ steps.access.outputs.pushes == 'true' && (inputs.github-token || github.token) || github.token }} # A repo that doesn't pin pnpm (npm, Dart or Rust) gets the action's own pnpm, and only a pnpm lockfile can - # key setup-node's cache. The pin check mirrors pnpm/action-setup's: devEngines.packageManager or packageManager. + # key setup-node's cache. The pin check mirrors pnpm/action-setup's (devEngines.packageManager or + # packageManager), and falls back to a packageManager grep when the job has no Node yet. - name: Resolve pnpm setup id: pnpm-setup shell: bash run: | - if ! node -e ' - const m = require(process.argv[1]); - const d = m.devEngines?.packageManager; - process.exit(String(m.packageManager).startsWith("pnpm@") || (d?.name === "pnpm" && d.version) ? 0 : 1); - ' "$GITHUB_WORKSPACE/package.json" 2>/dev/null; then - echo "version=$(node -p 'require(process.argv[1]).packageManager.split("@")[1].split("+")[0]' "$GITHUB_ACTION_PATH/package.json")" >> "$GITHUB_OUTPUT" + manifest="$GITHUB_WORKSPACE/package.json" + if command -v node >/dev/null; then + node -e ' + const m = require(process.argv[1]); + const d = m.devEngines?.packageManager; + process.exit(String(m.packageManager).startsWith("pnpm@") || (d?.name === "pnpm" && d.version) ? 0 : 1); + ' "$manifest" 2>/dev/null && pinned=true + else + grep -Eqs '"packageManager"[[:space:]]*:[[:space:]]*"pnpm@' "$manifest" && pinned=true + fi + if [ "$pinned" != true ]; then + echo "version=$(sed -nE 's/.*"packageManager"[[:space:]]*:[[:space:]]*"pnpm@([^"+]*).*/\1/p' "$GITHUB_ACTION_PATH/package.json")" >> "$GITHUB_OUTPUT" fi if [ -f "$GITHUB_WORKSPACE/pnpm-lock.yaml" ]; then echo "cache=pnpm" >> "$GITHUB_OUTPUT" diff --git a/docs/action.md b/docs/action.md index 9b1550a8..18f2029c 100644 --- a/docs/action.md +++ b/docs/action.md @@ -22,7 +22,7 @@ uses: goosewobbler/releasekit@v0 ## Requirements -- The action checks out the repo (unless `skip-checkout`), then runs its own `pnpm/action-setup` and `actions/setup-node` (`node-version`, default `24`, whose npm supports OIDC trusted publishing; ReleaseKit needs Node 22.12+). That replaces any Node version an earlier step set up. pnpm is the version your `package.json` pins in `packageManager`, else the action's own, so npm, Dart and Rust repos need no pnpm setup. +- The action checks out the repo (unless `skip-checkout`), then runs its own `pnpm/action-setup` and `actions/setup-node` (`node-version`, default `24`, whose npm supports OIDC trusted publishing; ReleaseKit needs Node 22.12+). That replaces any Node version an earlier step set up. pnpm is the version your `package.json` pins in `devEngines.packageManager` or `packageManager`, else the action's own, so npm, Dart and Rust repos need no pnpm setup. - For npm publishing, run `actions/setup-node` with `registry-url: https://registry.npmjs.org` before this action so that `NODE_AUTH_TOKEN` is wired up correctly. ## Required permissions and env @@ -68,7 +68,7 @@ To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a | `node-version` | `24` | Node.js version for the action's setup-node step | | `npm-token` | - | NPM token when not using OIDC; when unset, the job's `NPM_TOKEN` env is preserved | | `summary` | `true` | Write a job summary for `release` and `gate` | -| `skip-checkout` | `false` | Skip the action's checkout. Check the repo out on a branch, not a detached HEAD, with `fetch-depth: 0`; the action then rebases it onto `origin/`, falling back to `git reset --hard`. Pushes use your checkout's credentials, not `ssh-key` or `github-token`. | +| `skip-checkout` | `false` | Skip the action's checkout. Check the repo out on a branch, not a detached HEAD, with `fetch-depth: 0`; the action then rebases it onto `origin/`, falling back to `git reset --hard`. Pushes use your checkout's credentials, not `ssh-key`, and `github-token` only through `git.httpsTokenEnv`. | | `json` | `false` | JSON output (release mode). Without it, `has-changes` is always `false` and the other release outputs are empty | | `verbose` | `false` | Verbose logs | | `quiet` | `false` | Suppress non-error logs | @@ -81,7 +81,7 @@ To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a | `prerelease` | - | Prerelease identifier (e.g. `alpha`, `beta`) | | `stable` | `false` | Graduate prerelease packages to stable without bumping | | `scope` | - | Scope name resolved to target packages from `ci.scopeLabels` (also `gate`) | -| `ssh-key` | - | SSH deploy key; the action checks out over SSH so the push uses it (not on dry runs or in `gate`) | +| `ssh-key` | - | SSH deploy key; in the modes that push, outside a dry run, the action checks out over SSH so the push uses it | | `sync` | `false` | Synchronized versioning | | `target` | - | Comma-separated package targets | | `branch` | - | Override push branch | diff --git a/packages/publish/test/unit/stages/npm-publish.spec.ts b/packages/publish/test/unit/stages/npm-publish.spec.ts index b3fbf7a4..557e01dc 100644 --- a/packages/publish/test/unit/stages/npm-publish.spec.ts +++ b/packages/publish/test/unit/stages/npm-publish.spec.ts @@ -127,10 +127,19 @@ describe('npm-publish stage', () => { // The action passes an empty NPM_TOKEN when neither its npm-token input nor the job env sets one. process.env.NPM_TOKEN = ''; process.env.NODE_AUTH_TOKEN = 'node_auth_token'; + // The isolated .npmrc is what npm reads, and it's removed once the stage ends. + let npmrc = ''; + vi.mocked(execCommand).mockImplementation(async (_cmd, args, options) => { + if ((args as string[]).includes('publish')) { + npmrc = fs.readFileSync(options?.env?.NPM_CONFIG_USERCONFIG as string, 'utf-8'); + } + return { stdout: '', stderr: '', exitCode: 0 }; + }); await runNpmPublishStage(createContext(dir)); const publishCall = vi.mocked(execCommand).mock.calls[1]; expect(publishCall?.[2]?.env?.NODE_AUTH_TOKEN).toBe('node_auth_token'); + expect(npmrc).toContain('//registry.npmjs.org/:_authToken=node_auth_token'); }); it('should skip private packages', async () => { diff --git a/scripts/run-action.mjs b/scripts/run-action.mjs index 2273815e..a205ba45 100644 --- a/scripts/run-action.mjs +++ b/scripts/run-action.mjs @@ -11,7 +11,7 @@ function normalizeBoolean(value, defaultValue = false) { return defaultValue; } // Case-insensitive, like action.yml's `inputs.x == 'true'` guards. - return value.toLowerCase() === 'true'; + return String(value).toLowerCase() === 'true'; } function normalizeString(value) { diff --git a/test/integration/action-runner.spec.ts b/test/integration/action-runner.spec.ts index f3706f4d..fa186580 100644 --- a/test/integration/action-runner.spec.ts +++ b/test/integration/action-runner.spec.ts @@ -70,14 +70,22 @@ describe('action runner', () => { expect(outputs).toMatch(/^success<<\S+\nfalse\n/m); }); - it('should drop --apply from backfill args on a dry run', () => { + it('should run backfill without --apply on a dry run', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'releasekit-action-runner-test-')); + tempDirs.push(tempDir); + const cliPath = path.join(tempDir, 'fake-cli.mjs'); + fs.writeFileSync(cliPath, "console.log('ok')\n", 'utf-8'); const input = parseInputs({ INPUT_MODE: 'backfill', INPUT_DRY_RUN: 'true', INPUT_APPLY: 'true', INPUT_ALL: 'true', }); - expect(buildBackfillArgs(input)).not.toContain('--apply'); + + const result = await runAction(input, { cliPath }); + + expect(result.args).toContain('--all'); + expect(result.args).not.toContain('--apply'); }); it('should build release args from inputs', () => { From 5a4e46217bdda2a77549a69b847104c483696806 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Nov 2026 02:02:20 +0000 Subject: [PATCH 09/10] fix(action): let an applied backfill push, and pass NPM_TOKEN only when set - Backfill with `apply` counts as a run that pushes, so the follow-up commit step the docs describe gets the SSH key or `github-token` again. - run-action sets `NPM_TOKEN` only from a non-empty `npm-token` input, so an unset input leaves the job's NPM_TOKEN alone instead of blanking it. - Shorter checkout token expression, the checkout step renamed "Checkout (HTTPS)", and `pinned` initialised in the pnpm step. - docs: App tokens expire after an hour, and refresh-after-release shares the standing-PR permissions. --- action.yml | 17 +++++++++------- docs/action.md | 10 +++++----- scripts/run-action.mjs | 5 +++++ test/integration/action-runner.spec.ts | 27 ++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/action.yml b/action.yml index 534defc1..f5b921fa 100644 --- a/action.yml +++ b/action.yml @@ -98,7 +98,7 @@ inputs: required: false default: 'false' ssh-key: - description: SSH deploy key for git push (the checkout uses it in modes that push, outside a dry run) + description: SSH deploy key for git push (the checkout uses it in a run that pushes) required: false reconcile: @@ -214,21 +214,24 @@ runs: using: composite steps: # Push credentials (the SSH key, or github-token in place of the workflow token) are persisted only for a run - # that pushes, so a PR preview's later steps can't read them. + # that pushes, so a PR preview's later steps can't read them. Backfill doesn't push, but an applied run's files + # are committed by a follow-up step. - name: Resolve push access id: access shell: bash run: | - dry_run=$(printf '%s' "$INPUT_DRY_RUN" | tr '[:upper:]' '[:lower:]') + lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; } case "$INPUT_MODE" in release | standing-pr-update | standing-pr-publish | refresh-after-release) pushes=true ;; + backfill) [ "$(lower "$INPUT_APPLY")" = true ] && pushes=true || pushes=false ;; *) pushes=false ;; esac - [ "$dry_run" = true ] && pushes=false + [ "$(lower "$INPUT_DRY_RUN")" = true ] && pushes=false echo "pushes=$pushes" >> "$GITHUB_OUTPUT" env: INPUT_MODE: ${{ inputs.mode }} INPUT_DRY_RUN: ${{ inputs.dry-run }} + INPUT_APPLY: ${{ inputs.apply }} - name: Checkout (SSH) if: ${{ inputs.skip-checkout != 'true' && inputs.ssh-key != '' && steps.access.outputs.pushes == 'true' }} @@ -237,13 +240,13 @@ runs: ssh-key: ${{ inputs.ssh-key }} fetch-depth: 0 - - name: Checkout (read-only) + - name: Checkout (HTTPS) if: ${{ inputs.skip-checkout != 'true' && (inputs.ssh-key == '' || steps.access.outputs.pushes != 'true') }} uses: actions/checkout@v7 with: fetch-depth: 0 # An empty input (a secret unset on a fork run) falls back to the workflow token. - token: ${{ steps.access.outputs.pushes == 'true' && (inputs.github-token || github.token) || github.token }} + token: ${{ steps.access.outputs.pushes == 'true' && inputs.github-token || github.token }} # A repo that doesn't pin pnpm (npm, Dart or Rust) gets the action's own pnpm, and only a pnpm lockfile can # key setup-node's cache. The pin check mirrors pnpm/action-setup's (devEngines.packageManager or @@ -253,6 +256,7 @@ runs: shell: bash run: | manifest="$GITHUB_WORKSPACE/package.json" + pinned=false if command -v node >/dev/null; then node -e ' const m = require(process.argv[1]); @@ -388,6 +392,5 @@ runs: GITHUB_TOKEN: ${{ inputs.github-token || github.token }} # gh, which backfill uses, prefers GH_TOKEN over GITHUB_TOKEN. GH_TOKEN: ${{ inputs.github-token || github.token }} - NPM_TOKEN: ${{ inputs.npm-token || env.NPM_TOKEN }} VERBOSE: ${{ inputs.verbose }} OLLAMA_API_KEY: ${{ env.OLLAMA_API_KEY }} diff --git a/docs/action.md b/docs/action.md index 18f2029c..8427f5d6 100644 --- a/docs/action.md +++ b/docs/action.md @@ -27,9 +27,9 @@ uses: goosewobbler/releasekit@v0 ## Required permissions and env -The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN` and `GH_TOKEN`, so either in the step's `env` is ignored. In `release`, the standing-PR modes and `refresh-after-release`, the action's checkout also persists it for git pushes, unless you set `ssh-key` or `skip-checkout`. +The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN` and `GH_TOKEN`, so either in the step's `env` is ignored. In a run that pushes (`release`, the standing-PR modes, `refresh-after-release`, or `backfill` with `apply`, for its follow-up commit; never a dry run), the action's checkout also persists it for git pushes, unless you set `ssh-key` or `skip-checkout`. -To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a GitHub App token. Not a PAT: ReleaseKit recognises its own comments by bot authorship, so with a PAT `standing-pr-publish` refuses the release manifest, marker comments are posted again on every run, and the standing-PR workflow's bot-sender check stops filtering its own edits. +To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a GitHub App token. It expires after an hour, so a release that publishes for longer can fail at its final push. Not a PAT: ReleaseKit recognises its own comments by bot authorship, so with a PAT `standing-pr-publish` refuses the release manifest, marker comments are posted again on every run, and the standing-PR workflow's bot-sender check stops filtering its own edits. ### Release mode @@ -50,9 +50,9 @@ To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a - `contents: read` - `pull-requests: write` (reading the merged PR's labels and posting label notices) -### Standing-PR modes +### Standing-PR and refresh modes -`standing-pr-update` and `standing-pr-publish` need the permissions and repo setting in [CI setup](../packages/release/docs/ci-setup.md#setup-requirements). +`standing-pr-update`, `standing-pr-publish` and `refresh-after-release` need the permissions and repo setting in [CI setup](../packages/release/docs/ci-setup.md#setup-requirements). ## Inputs @@ -81,7 +81,7 @@ To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a | `prerelease` | - | Prerelease identifier (e.g. `alpha`, `beta`) | | `stable` | `false` | Graduate prerelease packages to stable without bumping | | `scope` | - | Scope name resolved to target packages from `ci.scopeLabels` (also `gate`) | -| `ssh-key` | - | SSH deploy key; in the modes that push, outside a dry run, the action checks out over SSH so the push uses it | +| `ssh-key` | - | SSH deploy key; in a run that pushes, the action checks out over SSH so the push uses it | | `sync` | `false` | Synchronized versioning | | `target` | - | Comma-separated package targets | | `branch` | - | Override push branch | diff --git a/scripts/run-action.mjs b/scripts/run-action.mjs index a205ba45..dc29b915 100644 --- a/scripts/run-action.mjs +++ b/scripts/run-action.mjs @@ -257,6 +257,7 @@ export function parseInputs(env = process.env) { scope: normalizeString(env.INPUT_SCOPE), branch: normalizeString(env.INPUT_BRANCH), npmAuth: normalizeString(env.INPUT_NPM_AUTH) ?? 'auto', + npmToken: normalizeString(env.INPUT_NPM_TOKEN), skipNotes: env.INPUT_SKIP_NOTES, skipPublish: env.INPUT_SKIP_PUBLISH, skipGit: env.INPUT_SKIP_GIT, @@ -427,6 +428,10 @@ export async function runAction(input, options = {}) { for (const k of Object.keys(spawnEnv).filter((k) => k.startsWith('INPUT_'))) { delete spawnEnv[k]; } + // Only when given, so an unset input keeps the job's NPM_TOKEN rather than blanking it. + if (input.npmToken) { + spawnEnv.NPM_TOKEN = input.npmToken; + } // Not spawnSync: it buffers output until the child exits, hiding where long-running steps // (publish, verify) are in CI logs. diff --git a/test/integration/action-runner.spec.ts b/test/integration/action-runner.spec.ts index fa186580..f1a52986 100644 --- a/test/integration/action-runner.spec.ts +++ b/test/integration/action-runner.spec.ts @@ -70,6 +70,33 @@ describe('action runner', () => { expect(outputs).toMatch(/^success<<\S+\nfalse\n/m); }); + it.each([ + ['the npm-token input', { npmToken: 'input_token' }, 'job_token', 'input_token'], + ["the job's NPM_TOKEN when the input is unset", {}, 'job_token', 'job_token'], + ['no NPM_TOKEN when neither is set', {}, undefined, 'unset'], + ])('should pass the CLI %s', async (_label, input, jobToken, expected) => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'releasekit-action-runner-test-')); + tempDirs.push(tempDir); + const cliPath = path.join(tempDir, 'fake-cli.mjs'); + fs.writeFileSync(cliPath, "console.log(process.env.NPM_TOKEN ?? 'unset')\n", 'utf-8'); + const original = process.env.NPM_TOKEN; + if (jobToken === undefined) { + delete process.env.NPM_TOKEN; + } else { + process.env.NPM_TOKEN = jobToken; + } + try { + const result = await runAction({ mode: 'preview', projectDir: '.', ...input }, { cliPath }); + expect(result.stdout.trim()).toBe(expected); + } finally { + if (original === undefined) { + delete process.env.NPM_TOKEN; + } else { + process.env.NPM_TOKEN = original; + } + } + }); + it('should run backfill without --apply on a dry run', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'releasekit-action-runner-test-')); tempDirs.push(tempDir); From 9f9cacdfa82df2a006062783d6f1f1393a0ee89c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Nov 2026 10:06:24 +0000 Subject: [PATCH 10/10] docs(action): tighten the comments and docs this PR adds - Fix the empty-NPM_TOKEN test comment: the action no longer passes one. - Shorter push-access and pnpm-setup comments in action.yml, with the cache note moved onto the lockfile check. - docs/action.md: split the Requirements bullet into Node and pnpm, move the list of pushing runs out of a parenthetical, and shorten the PAT sentence and the github-token and skip-checkout rows. - Plainer preview and release mode descriptions, and a shorter README action intro. --- README.md | 4 ++-- action.yml | 13 ++++++------- docs/action.md | 15 ++++++++------- .../publish/test/unit/stages/npm-publish.spec.ts | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 99edd16a..35f1d4ad 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Each step is also a subcommand — `releasekit version`, `releasekit notes`, `re ### GitHub Action -Use ReleaseKit as a composite action (`release`, `preview`, `gate`, standing-PR modes, `backfill`, `refresh-after-release` — see [docs/action.md](./docs/action.md)): +Use ReleaseKit as a composite action: ```yaml jobs: @@ -131,7 +131,7 @@ jobs: mode: release ``` -See [docs/action.md](./docs/action.md) for the `preview` mode, and the full input/output reference. +See [docs/action.md](./docs/action.md) for every mode and the full input/output reference. ## Configuration diff --git a/action.yml b/action.yml index f5b921fa..67ad6f99 100644 --- a/action.yml +++ b/action.yml @@ -67,7 +67,7 @@ inputs: required: false default: '24' github-token: - description: GitHub token for API calls and, through the action's checkout, git pushes (defaults to the workflow token) + description: GitHub token for API calls and git pushes (defaults to the workflow token) required: false default: ${{ github.token }} npm-auth: @@ -213,9 +213,8 @@ outputs: runs: using: composite steps: - # Push credentials (the SSH key, or github-token in place of the workflow token) are persisted only for a run - # that pushes, so a PR preview's later steps can't read them. Backfill doesn't push, but an applied run's files - # are committed by a follow-up step. + # Push credentials (the SSH key or github-token) are persisted only for a run that pushes, so a PR preview's + # later steps can't read them. An applied backfill counts: a follow-up step commits its files. - name: Resolve push access id: access shell: bash @@ -248,9 +247,8 @@ runs: # An empty input (a secret unset on a fork run) falls back to the workflow token. token: ${{ steps.access.outputs.pushes == 'true' && inputs.github-token || github.token }} - # A repo that doesn't pin pnpm (npm, Dart or Rust) gets the action's own pnpm, and only a pnpm lockfile can - # key setup-node's cache. The pin check mirrors pnpm/action-setup's (devEngines.packageManager or - # packageManager), and falls back to a packageManager grep when the job has no Node yet. + # A repo that doesn't pin pnpm (npm, Dart or Rust) gets the action's own. The pin check follows + # pnpm/action-setup's, falling back to a packageManager grep when the job has no Node yet. - name: Resolve pnpm setup id: pnpm-setup shell: bash @@ -269,6 +267,7 @@ runs: if [ "$pinned" != true ]; then echo "version=$(sed -nE 's/.*"packageManager"[[:space:]]*:[[:space:]]*"pnpm@([^"+]*).*/\1/p' "$GITHUB_ACTION_PATH/package.json")" >> "$GITHUB_OUTPUT" fi + # Only a pnpm lockfile can key setup-node's cache. if [ -f "$GITHUB_WORKSPACE/pnpm-lock.yaml" ]; then echo "cache=pnpm" >> "$GITHUB_OUTPUT" fi diff --git a/docs/action.md b/docs/action.md index 8427f5d6..26fbaaca 100644 --- a/docs/action.md +++ b/docs/action.md @@ -12,8 +12,8 @@ uses: goosewobbler/releasekit@v0 ## Modes -- `release`: runs the full unified release pipeline. -- `preview` (default): runs PR preview comment generation. +- `release`: runs the release pipeline (version, notes, publish). +- `preview` (default): posts or updates the PR preview comment. - `gate`: evaluates merged-PR labels and emits should-release/bump/scope outputs. - `standing-pr-update`: creates or refreshes the standing release PR. - `standing-pr-publish`: publishes from a merged standing release PR's manifest. Resolution order for which PR: the `pr` input, then the `pull_request` event payload, then the most recently merged standing PR via the GitHub API. Pass `pr` explicitly in dispatch funnels (e.g. `workflow_dispatch` routing for npm OIDC trusted publishing) — re-runs of a stale dispatch can otherwise infer a newer standing PR. @@ -22,14 +22,15 @@ uses: goosewobbler/releasekit@v0 ## Requirements -- The action checks out the repo (unless `skip-checkout`), then runs its own `pnpm/action-setup` and `actions/setup-node` (`node-version`, default `24`, whose npm supports OIDC trusted publishing; ReleaseKit needs Node 22.12+). That replaces any Node version an earlier step set up. pnpm is the version your `package.json` pins in `devEngines.packageManager` or `packageManager`, else the action's own, so npm, Dart and Rust repos need no pnpm setup. +- The action checks out the repo (unless `skip-checkout`), then sets up its own Node with `actions/setup-node`: `node-version`, default `24`, whose npm supports OIDC trusted publishing (ReleaseKit needs Node 22.12+). This replaces any Node version an earlier step set up. +- It sets up pnpm with `pnpm/action-setup`, at the version your `package.json` pins in `devEngines.packageManager` or `packageManager`, else its own, so npm, Dart and Rust repos need no pnpm setup. - For npm publishing, run `actions/setup-node` with `registry-url: https://registry.npmjs.org` before this action so that `NODE_AUTH_TOKEN` is wired up correctly. ## Required permissions and env -The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN` and `GH_TOKEN`, so either in the step's `env` is ignored. In a run that pushes (`release`, the standing-PR modes, `refresh-after-release`, or `backfill` with `apply`, for its follow-up commit; never a dry run), the action's checkout also persists it for git pushes, unless you set `ssh-key` or `skip-checkout`. +The action passes `github-token` (default: the workflow token) to ReleaseKit as `GITHUB_TOKEN` and `GH_TOKEN`, so either in the step's `env` is ignored. In a run that pushes, the action's checkout also persists it for git pushes, unless you set `ssh-key` or `skip-checkout`. Runs that push are `release`, the standing-PR modes, `refresh-after-release` and an applied `backfill`, never a dry run. -To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a GitHub App token. It expires after an hour, so a release that publishes for longer can fail at its final push. Not a PAT: ReleaseKit recognises its own comments by bot authorship, so with a PAT `standing-pr-publish` refuses the release manifest, marker comments are posted again on every run, and the standing-PR workflow's bot-sender check stops filtering its own edits. +To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a GitHub App token. It expires after an hour, so a release that publishes for longer can fail at its final push. Not a PAT: ReleaseKit recognises its own comments by bot authorship, so with a PAT `standing-pr-publish` refuses the release manifest, comments are re-posted on every run, and the standing-PR workflow can retrigger itself. ### Release mode @@ -64,11 +65,11 @@ To bypass a ruleset, or to make those pushes start `on: push` workflows, pass a | `config` | - | Path to `releasekit.config.json` | | `project-dir` | `.` | Project directory | | `dry-run` | `false` | Dry-run for `release`, `preview` and `backfill`; other modes fail if it's set | -| `github-token` | workflow token | Token for GitHub API calls and the action's checkout, which git pushes use (override with a GitHub App token when rulesets require it) | +| `github-token` | workflow token | Token for GitHub API calls and git pushes; use a GitHub App token to bypass rulesets | | `node-version` | `24` | Node.js version for the action's setup-node step | | `npm-token` | - | NPM token when not using OIDC; when unset, the job's `NPM_TOKEN` env is preserved | | `summary` | `true` | Write a job summary for `release` and `gate` | -| `skip-checkout` | `false` | Skip the action's checkout. Check the repo out on a branch, not a detached HEAD, with `fetch-depth: 0`; the action then rebases it onto `origin/`, falling back to `git reset --hard`. Pushes use your checkout's credentials, not `ssh-key`, and `github-token` only through `git.httpsTokenEnv`. | +| `skip-checkout` | `false` | Skip the action's checkout. Check out a branch (not a detached HEAD) with `fetch-depth: 0`; the action rebases it onto `origin/`, or resets to it if the rebase fails. Pushes then use your checkout's credentials. | | `json` | `false` | JSON output (release mode). Without it, `has-changes` is always `false` and the other release outputs are empty | | `verbose` | `false` | Verbose logs | | `quiet` | `false` | Suppress non-error logs | diff --git a/packages/publish/test/unit/stages/npm-publish.spec.ts b/packages/publish/test/unit/stages/npm-publish.spec.ts index 557e01dc..eee6dcd7 100644 --- a/packages/publish/test/unit/stages/npm-publish.spec.ts +++ b/packages/publish/test/unit/stages/npm-publish.spec.ts @@ -124,7 +124,7 @@ describe('npm-publish stage', () => { fs.mkdirSync(pkgDir, { recursive: true }); fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name: '@test/pkg', version: '1.0.0' })); - // The action passes an empty NPM_TOKEN when neither its npm-token input nor the job env sets one. + // An unset secret in `env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }}` arrives as ''. process.env.NPM_TOKEN = ''; process.env.NODE_AUTH_TOKEN = 'node_auth_token'; // The isolated .npmrc is what npm reads, and it's removed once the stage ends.