diff --git a/README.md b/README.md index 0f9adf3e..8dac9fdd 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: @@ -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/action.yml b/action.yml index 05400e99..8bef8ec3 100644 --- a/action.yml +++ b/action.yml @@ -65,7 +65,11 @@ inputs: node-version: description: Node.js version required: false - default: '20' + default: '24' + 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..c3372622 100644 --- a/docs/action.md +++ b/docs/action.md @@ -8,40 +8,49 @@ 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 -- `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. +- 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 +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 @@ -49,11 +58,16 @@ 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 | -| `json` | `false` | JSON output (release mode) | +| `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` | `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 | | `verbose` | `false` | Verbose logs | | `quiet` | `false` | Suppress non-error logs | @@ -61,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 | @@ -77,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 | @@ -104,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 @@ -112,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 @@ -139,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 }}" ``` @@ -161,6 +184,7 @@ name: Release Preview on: pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] jobs: preview: @@ -175,8 +199,6 @@ jobs: - uses: goosewobbler/releasekit@v0 with: mode: preview - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ### Preview mode dry run (markdown only) @@ -194,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) @@ -232,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/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/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 7f409cd3..2088cc6f 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); } +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)) { + 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..ec0775f3 100644 --- a/test/integration/action-runner.spec.ts +++ b/test/integration/action-runner.spec.ts @@ -28,6 +28,29 @@ describe('action runner', () => { tempDirs.length = 0; }); + 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', () => { const args = buildReleaseArgs({ config: 'releasekit.config.json',