Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
88 changes: 50 additions & 38 deletions docs/action.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,61 +8,78 @@ 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 (`@<commit-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 (`@<commit-sha> # 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

### Core

| 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 |

### Release mode inputs

| 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 |
Expand All @@ -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 |
Expand All @@ -104,20 +121,28 @@ 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

| Output | Description |
|---|---|
| `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

Expand All @@ -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 }}"
```
Expand All @@ -161,6 +184,7 @@ name: Release Preview

on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]

jobs:
preview:
Expand All @@ -175,8 +199,6 @@ jobs:
- uses: goosewobbler/releasekit@v0
with:
mode: preview
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

### Preview mode dry run (markdown only)
Expand All @@ -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)
Expand Down Expand Up @@ -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`).
4 changes: 3 additions & 1 deletion docs/dart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion docs/rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion packages/publish/src/utils/npm-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty-token fallback lacks test coverage

The existing token tests use a nonempty NPM_TOKEN, so they pass with both ?? and || and do not protect this fix. Add a regression test with an empty NPM_TOKEN and a valid NODE_AUTH_TOKEN. Check both the generated .npmrc and the returned NODE_AUTH_TOKEN so this authentication failure cannot return unnoticed.

Fix in Claude Code Fix in Cursor

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5dbf783: added a test with an empty NPM_TOKEN and a set NODE_AUTH_TOKEN, checking both the returned env and the generated .npmrc. It fails with ??.

const registryHost = (() => {
try {
return new URL(registryUrl).host;
Expand Down
15 changes: 15 additions & 0 deletions packages/publish/test/unit/utils/npm-env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
16 changes: 15 additions & 1 deletion scripts/run-action.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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;
}

Expand Down
23 changes: 23 additions & 0 deletions test/integration/action-runner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading