@releasekit/release 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # @releasekit/release
2
2
 
3
- Unified release pipeline: version, changelog, and publish in a single command.
3
+ [![@releasekit/release](https://img.shields.io/badge/@releasekit-release-9feaf9?labelColor=1a1a1a&style=plastic)](https://www.npmjs.com/package/@releasekit/release)
4
+ [![Version](https://img.shields.io/npm/v/@releasekit/release?color=28a745&labelColor=1a1a1a)](https://www.npmjs.com/package/@releasekit/release)
5
+ [![Downloads](https://img.shields.io/npm/dw/@releasekit/release?color=6f42c1&labelColor=1a1a1a)](https://www.npmjs.com/package/@releasekit/release)
6
+
7
+ **Unified release pipeline: version, changelog, and publish in a single command.**
4
8
 
5
9
  ## Features
6
10
 
@@ -71,6 +75,18 @@ If no releasable changes are found after step 1, the command exits with code 0 a
71
75
  | `-q, --quiet` | Suppress non-error output | `false` |
72
76
  | `--project-dir <path>` | Project directory | cwd |
73
77
 
78
+ ### `releasekit init`
79
+
80
+ Create a default `releasekit.config.json` in the current directory.
81
+
82
+ ```bash
83
+ releasekit init [--force]
84
+ ```
85
+
86
+ Detects monorepo layout and sets `changelog.mode` accordingly. Adds `access: "public"` only for scoped packages (`@scope/name`), which npm defaults to restricted.
87
+
88
+ Use `--force` to overwrite an existing config file.
89
+
74
90
  ### `releasekit preview`
75
91
 
76
92
  Posts a release preview comment on a pull request showing what would be released if merged.
@@ -275,6 +291,11 @@ jobs:
275
291
 
276
292
  A template is also available at [`templates/workflows/release-preview.yml`](../../templates/workflows/release-preview.yml).
277
293
 
294
+ ## Documentation
295
+
296
+ **Getting Started**
297
+ - [CI Setup](./docs/ci-setup.md) — GitHub Actions workflows (push, label, OIDC, PR preview, prerelease)
298
+
278
299
  ## License
279
300
 
280
301
  MIT
@@ -5,7 +5,7 @@ import {
5
5
  runRelease,
6
6
  success,
7
7
  warn
8
- } from "./chunk-I5ABZWVU.js";
8
+ } from "./chunk-YZHGXRG6.js";
9
9
 
10
10
  // src/preview-context.ts
11
11
  import * as fs from "fs";
@@ -98,15 +98,15 @@ var TYPE_LABELS = {
98
98
  function getNoChangesMessage(strategy) {
99
99
  switch (strategy) {
100
100
  case "manual":
101
- return "> No releasable changes detected. Run the release workflow manually if a release is needed.";
101
+ return "Run the release workflow manually if a release is needed.";
102
102
  case "direct":
103
- return "> No releasable changes detected. Merging this PR will not trigger a release.";
103
+ return "Merging this PR will not trigger a release.";
104
104
  case "standing-pr":
105
- return "> No releasable changes detected. Merging this PR will not affect the release PR.";
105
+ return "Merging this PR will not affect the release PR.";
106
106
  case "scheduled":
107
- return "> No releasable changes detected. These changes will not be included in the next scheduled release.";
107
+ return "These changes will not be included in the next scheduled release.";
108
108
  default:
109
- return "> No releasable changes detected.";
109
+ return "";
110
110
  }
111
111
  }
112
112
  function getIntroMessage(strategy, standingPrNumber) {
@@ -125,20 +125,20 @@ function getLabelBanner(labelContext) {
125
125
  if (!labelContext) return [];
126
126
  if (labelContext.trigger === "commit") {
127
127
  if (labelContext.skip) {
128
- return ["> [!WARNING]", "> This PR is marked to skip release.", ""];
128
+ return ["> **Warning:** This PR is marked to skip release.", ""];
129
129
  }
130
130
  if (labelContext.bumpLabel === "major") {
131
- return ["> [!IMPORTANT]", "> This PR is labeled for a **major** release.", ""];
131
+ return ["> **Important:** This PR is labeled for a **major** release.", ""];
132
132
  }
133
133
  }
134
134
  if (labelContext.trigger === "label") {
135
135
  if (labelContext.noBumpLabel) {
136
136
  const labels = labelContext.labels;
137
137
  const labelExamples = labels ? `\`${labels.patch}\`, \`${labels.minor}\`, or \`${labels.major}\`` : "a release label (e.g., `release:patch`, `release:minor`, `release:major`)";
138
- return ["> [!NOTE]", `> No release label detected. Add ${labelExamples} to trigger a release.`, ""];
138
+ return ["> No release label detected.", `> **Note:** Add ${labelExamples} to trigger a release.`, ""];
139
139
  }
140
140
  if (labelContext.bumpLabel) {
141
- return ["> [!NOTE]", `> This PR is labeled for a **${labelContext.bumpLabel}** release.`, ""];
141
+ return [`> This PR is labeled for a **${labelContext.bumpLabel}** release.`, ""];
142
142
  }
143
143
  }
144
144
  return [];
@@ -152,7 +152,7 @@ function formatPreviewComment(result, options) {
152
152
  lines.push("<details>", "<summary><b>Release Preview</b> \u2014 no release</summary>", "");
153
153
  lines.push(...banner);
154
154
  if (!labelContext?.noBumpLabel) {
155
- lines.push("> [!NOTE]", getNoChangesMessage(strategy));
155
+ lines.push(`> **Note:** No releasable changes detected. ${getNoChangesMessage(strategy)}`);
156
156
  }
157
157
  lines.push("", "---", FOOTER, "</details>");
158
158
  return lines.join("\n");
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  EXIT_CODES,
3
3
  runRelease
4
- } from "./chunk-I5ABZWVU.js";
4
+ } from "./chunk-YZHGXRG6.js";
5
5
 
6
6
  // src/release-command.ts
7
- import { Command } from "commander";
7
+ import { Command, Option } from "commander";
8
8
  function createReleaseCommand() {
9
- return new Command("release").description("Run the full release pipeline").option("-c, --config <path>", "Path to config file").option("-d, --dry-run", "Preview all steps without side effects", false).option("-b, --bump <type>", "Force bump type (patch|minor|major)").option("-p, --prerelease [identifier]", "Create prerelease version").option("-s, --sync", "Use synchronized versioning across all packages", false).option("-t, --target <packages>", "Target specific packages (comma-separated)").option("--branch <name>", "Override the git branch used for push").option("--skip-notes", "Skip changelog generation", false).option("--skip-publish", "Skip registry publishing and git operations", false).option("--skip-git", "Skip git commit/tag/push", false).option("--skip-github-release", "Skip GitHub release creation", false).option("--skip-verification", "Skip post-publish verification", false).option("-j, --json", "Output results as JSON", false).option("-v, --verbose", "Verbose logging", false).option("-q, --quiet", "Suppress non-error output", false).option("--project-dir <path>", "Project directory", process.cwd()).action(async (opts) => {
9
+ return new Command("release").description("Run the full release pipeline").option("-c, --config <path>", "Path to config file").option("-d, --dry-run", "Preview all steps without side effects", false).option("-b, --bump <type>", "Force bump type (patch|minor|major)").option("-p, --prerelease [identifier]", "Create prerelease version").option("-s, --sync", "Use synchronized versioning across all packages", false).option("-t, --target <packages>", "Target specific packages (comma-separated)").option("--branch <name>", "Override the git branch used for push").addOption(new Option("--npm-auth <method>", "NPM auth method").choices(["auto", "oidc", "token"]).default("auto")).option("--skip-notes", "Skip changelog generation", false).option("--skip-publish", "Skip registry publishing and git operations", false).option("--skip-git", "Skip git commit/tag/push", false).option("--skip-github-release", "Skip GitHub release creation", false).option("--skip-verification", "Skip post-publish verification", false).option("-j, --json", "Output results as JSON", false).option("-v, --verbose", "Verbose logging", false).option("-q, --quiet", "Suppress non-error output", false).option("--project-dir <path>", "Project directory", process.cwd()).action(async (opts) => {
10
10
  const options = {
11
11
  config: opts.config,
12
12
  dryRun: opts.dryRun,
@@ -15,6 +15,7 @@ function createReleaseCommand() {
15
15
  sync: opts.sync,
16
16
  target: opts.target,
17
17
  branch: opts.branch,
18
+ npmAuth: opts.npmAuth,
18
19
  skipNotes: opts.skipNotes,
19
20
  skipPublish: opts.skipPublish,
20
21
  skipGit: opts.skipGit,
@@ -208,14 +208,14 @@ var GitHubReleaseConfigSchema = z.object({
208
208
  perPackage: z.boolean().default(true),
209
209
  prerelease: z.union([z.literal("auto"), z.boolean()]).default("auto"),
210
210
  /**
211
- * Controls how release notes are sourced for GitHub releases.
212
- * - 'auto': Use RELEASE_NOTES.md if it exists, then per-package changelog
213
- * data from the version output, then GitHub's auto-generated notes.
214
- * - 'github': Always use GitHub's auto-generated notes.
215
- * - 'none': No notes body.
216
- * - Any other string: Treated as a file path to read notes from.
211
+ * Controls the source for the GitHub release body.
212
+ * - 'auto': Use release notes if enabled, else changelog, else GitHub auto-generated.
213
+ * - 'releaseNotes': Use LLM-generated release notes (requires notes.releaseNotes.enabled: true).
214
+ * - 'changelog': Use formatted changelog entries.
215
+ * - 'generated': Use GitHub's auto-generated notes.
216
+ * - 'none': No body.
217
217
  */
218
- releaseNotes: z.union([z.literal("auto"), z.literal("github"), z.literal("none"), z.string()]).default("auto")
218
+ body: z.enum(["auto", "releaseNotes", "changelog", "generated", "none"]).default("auto")
219
219
  });
220
220
  var VerifyRegistryConfigSchema = z.object({
221
221
  enabled: z.boolean().default(true),
@@ -259,7 +259,7 @@ var PublishConfigSchema = z.object({
259
259
  draft: true,
260
260
  perPackage: true,
261
261
  prerelease: "auto",
262
- releaseNotes: "auto"
262
+ body: "auto"
263
263
  }),
264
264
  verify: VerifyConfigSchema.default({
265
265
  npm: {
@@ -280,10 +280,10 @@ var TemplateConfigSchema = z.object({
280
280
  path: z.string().optional(),
281
281
  engine: z.enum(["handlebars", "liquid", "ejs"]).optional()
282
282
  });
283
- var OutputConfigSchema = z.object({
284
- format: z.enum(["markdown", "github-release", "json"]),
283
+ var LocationModeSchema = z.enum(["root", "packages", "both"]);
284
+ var ChangelogConfigSchema = z.object({
285
+ mode: LocationModeSchema.optional(),
285
286
  file: z.string().optional(),
286
- options: z.record(z.string(), z.unknown()).optional(),
287
287
  templates: TemplateConfigSchema.optional()
288
288
  });
289
289
  var LLMOptionsSchema = z.object({
@@ -343,17 +343,20 @@ var LLMConfigSchema = z.object({
343
343
  scopes: ScopeConfigSchema.optional(),
344
344
  prompts: LLMPromptsConfigSchema.optional()
345
345
  });
346
+ var ReleaseNotesConfigSchema = z.object({
347
+ mode: LocationModeSchema.optional(),
348
+ file: z.string().optional(),
349
+ templates: TemplateConfigSchema.optional(),
350
+ llm: LLMConfigSchema.optional()
351
+ });
346
352
  var NotesInputConfigSchema = z.object({
347
353
  source: z.string().optional(),
348
354
  file: z.string().optional()
349
355
  });
350
356
  var NotesConfigSchema = z.object({
351
- input: NotesInputConfigSchema.optional(),
352
- output: z.array(OutputConfigSchema).default([{ format: "markdown", file: "CHANGELOG.md" }]),
353
- monorepo: MonorepoConfigSchema.optional(),
354
- templates: TemplateConfigSchema.optional(),
355
- llm: LLMConfigSchema.optional(),
356
- updateStrategy: z.enum(["prepend", "regenerate"]).default("prepend")
357
+ changelog: z.union([z.literal(false), ChangelogConfigSchema]).optional(),
358
+ releaseNotes: z.union([z.literal(false), ReleaseNotesConfigSchema]).optional(),
359
+ updateStrategy: z.enum(["prepend", "regenerate"]).optional()
357
360
  });
358
361
  var CILabelsConfigSchema = z.object({
359
362
  stable: z.string().default("release:stable"),
@@ -554,11 +557,13 @@ async function runRelease(inputOptions) {
554
557
  }
555
558
  let notesGenerated = false;
556
559
  let packageNotes;
560
+ let releaseNotes;
557
561
  let notesFiles = [];
558
562
  if (!options.skipNotes) {
559
563
  info("Generating release notes...");
560
564
  const notesResult = await runNotesStep(versionOutput, options);
561
565
  packageNotes = notesResult.packageNotes;
566
+ releaseNotes = notesResult.releaseNotes;
562
567
  notesFiles = notesResult.files;
563
568
  notesGenerated = true;
564
569
  success("Release notes generated");
@@ -566,10 +571,10 @@ async function runRelease(inputOptions) {
566
571
  let publishOutput;
567
572
  if (!options.skipPublish) {
568
573
  info("Publishing...");
569
- publishOutput = await runPublishStep(versionOutput, options, packageNotes, notesFiles);
574
+ publishOutput = await runPublishStep(versionOutput, options, releaseNotes ?? packageNotes, notesFiles);
570
575
  success("Publish complete");
571
576
  }
572
- return { versionOutput, notesGenerated, packageNotes, publishOutput };
577
+ return { versionOutput, notesGenerated, packageNotes, releaseNotes, publishOutput };
573
578
  }
574
579
  async function runVersionStep(options) {
575
580
  const { loadConfig: loadConfig2, VersionEngine, enableJsonOutput, getJsonData } = await import("@releasekit/version");
@@ -605,14 +610,11 @@ async function runVersionStep(options) {
605
610
  return getJsonData();
606
611
  }
607
612
  async function runNotesStep(versionOutput, options) {
608
- const { parseVersionOutput, runPipeline, loadConfig: loadConfig2, getDefaultConfig } = await import("@releasekit/notes");
613
+ const { parseVersionOutput, runPipeline, loadConfig: loadConfig2 } = await import("@releasekit/notes");
609
614
  const config = loadConfig2(options.projectDir, options.config);
610
- if (config.output.length === 0) {
611
- config.output = getDefaultConfig().output;
612
- }
613
615
  const input = parseVersionOutput(JSON.stringify(versionOutput));
614
616
  const result = await runPipeline(input, config, options.dryRun);
615
- return { packageNotes: result.packageNotes, files: result.files };
617
+ return { packageNotes: result.packageNotes, releaseNotes: result.releaseNotes, files: result.files };
616
618
  }
617
619
  async function runPublishStep(versionOutput, options, releaseNotes, additionalFiles) {
618
620
  const { runPipeline, loadConfig: loadConfig2 } = await import("@releasekit/publish");
@@ -623,7 +625,7 @@ async function runPublishStep(versionOutput, options, releaseNotes, additionalFi
623
625
  const publishOptions = {
624
626
  dryRun: options.dryRun,
625
627
  registry: "all",
626
- npmAuth: "auto",
628
+ npmAuth: options.npmAuth ?? "auto",
627
629
  skipGit: options.skipGit,
628
630
  skipPublish: false,
629
631
  skipGithubRelease: options.skipGithubRelease,
@@ -638,6 +640,7 @@ async function runPublishStep(versionOutput, options, releaseNotes, additionalFi
638
640
 
639
641
  export {
640
642
  readPackageVersion,
643
+ error,
641
644
  warn,
642
645
  info,
643
646
  success,
package/dist/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createReleaseCommand
4
- } from "./chunk-EDF3BZFF.js";
4
+ } from "./chunk-WNKYXS62.js";
5
5
  import {
6
6
  runPreview
7
- } from "./chunk-J5NT6OX2.js";
7
+ } from "./chunk-I5CGC253.js";
8
8
  import {
9
9
  EXIT_CODES,
10
10
  readPackageVersion
11
- } from "./chunk-I5ABZWVU.js";
11
+ } from "./chunk-YZHGXRG6.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { realpathSync } from "fs";
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createReleaseCommand
4
- } from "./chunk-EDF3BZFF.js";
4
+ } from "./chunk-WNKYXS62.js";
5
5
  import {
6
- readPackageVersion
7
- } from "./chunk-I5ABZWVU.js";
6
+ EXIT_CODES,
7
+ error,
8
+ info,
9
+ readPackageVersion,
10
+ success
11
+ } from "./chunk-YZHGXRG6.js";
8
12
 
9
13
  // src/dispatcher.ts
10
14
  import { realpathSync } from "fs";
@@ -12,10 +16,62 @@ import { fileURLToPath } from "url";
12
16
  import { createNotesCommand } from "@releasekit/notes/cli";
13
17
  import { createPublishCommand } from "@releasekit/publish/cli";
14
18
  import { createVersionCommand } from "@releasekit/version/cli";
19
+ import { Command as Command2 } from "commander";
20
+
21
+ // src/init-command.ts
22
+ import * as fs from "fs";
23
+ import { detectMonorepo } from "@releasekit/notes";
15
24
  import { Command } from "commander";
25
+ function createInitCommand() {
26
+ return new Command("init").description("Create a default releasekit.config.json").option("-f, --force", "Overwrite existing config").action((options) => {
27
+ const configPath = "releasekit.config.json";
28
+ if (fs.existsSync(configPath) && !options.force) {
29
+ error(`Config file already exists at ${configPath}. Use --force to overwrite.`);
30
+ process.exit(EXIT_CODES.GENERAL_ERROR);
31
+ } else {
32
+ let changelogMode;
33
+ try {
34
+ const detected = detectMonorepo(process.cwd());
35
+ changelogMode = detected.isMonorepo ? "packages" : "root";
36
+ info(
37
+ detected.isMonorepo ? "Monorepo detected \u2014 using mode: packages" : "Single-package repo detected \u2014 using mode: root"
38
+ );
39
+ } catch {
40
+ changelogMode = "root";
41
+ info("Could not detect project type \u2014 using mode: root");
42
+ }
43
+ let packageName;
44
+ try {
45
+ const pkg = JSON.parse(fs.readFileSync("package.json", "utf-8"));
46
+ packageName = pkg.name;
47
+ } catch {
48
+ }
49
+ const isScoped = packageName?.startsWith("@") ?? false;
50
+ const defaultConfig = {
51
+ $schema: "https://goosewobbler.github.io/releasekit/schema.json",
52
+ notes: {
53
+ changelog: {
54
+ mode: changelogMode
55
+ }
56
+ },
57
+ publish: {
58
+ npm: {
59
+ enabled: true,
60
+ ...isScoped ? { access: "public" } : {}
61
+ }
62
+ }
63
+ };
64
+ fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), "utf-8");
65
+ success(`Created ${configPath}`);
66
+ }
67
+ });
68
+ }
69
+
70
+ // src/dispatcher.ts
16
71
  function createDispatcherProgram() {
17
- const program = new Command().name("releasekit").description("Unified release pipeline: version, changelog, and publish").version(readPackageVersion(import.meta.url));
72
+ const program = new Command2().name("releasekit").description("Unified release pipeline: version, changelog, and publish").version(readPackageVersion(import.meta.url));
18
73
  program.addCommand(createReleaseCommand(), { isDefault: true });
74
+ program.addCommand(createInitCommand());
19
75
  program.addCommand(createVersionCommand());
20
76
  program.addCommand(createNotesCommand());
21
77
  program.addCommand(createPublishCommand());
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  runPreview
3
- } from "./chunk-J5NT6OX2.js";
3
+ } from "./chunk-I5CGC253.js";
4
4
  import {
5
5
  runRelease
6
- } from "./chunk-I5ABZWVU.js";
6
+ } from "./chunk-YZHGXRG6.js";
7
7
  export {
8
8
  runPreview,
9
9
  runRelease
@@ -0,0 +1,266 @@
1
+ # CI Setup
2
+
3
+ This guide covers common GitHub Actions patterns for automating releases with `@releasekit/release`.
4
+
5
+ ## Prerequisites
6
+
7
+ All workflows require:
8
+
9
+ - `fetch-depth: 0` on checkout — ReleaseKit reads git history to determine version bumps
10
+ - A `GITHUB_TOKEN` with `contents: write` permission for tagging and GitHub Releases
11
+ - Node.js 20+
12
+
13
+ ---
14
+
15
+ ## Minimal Setup (push to main)
16
+
17
+ Trigger a release on every push to `main`. If there are no releasable commits, the command exits cleanly with code 0 and does nothing.
18
+
19
+ ```yaml
20
+ # .github/workflows/release.yml
21
+ name: Release
22
+
23
+ on:
24
+ push:
25
+ branches: [main]
26
+
27
+ permissions:
28
+ contents: write
29
+ id-token: write # for npm OIDC trusted publishing
30
+
31
+ jobs:
32
+ release:
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ with:
37
+ fetch-depth: 0
38
+
39
+ - uses: actions/setup-node@v4
40
+ with:
41
+ node-version: '20'
42
+ registry-url: 'https://registry.npmjs.org'
43
+
44
+ - run: npm ci
45
+
46
+ - run: npx releasekit release
47
+ env:
48
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
+ # For OIDC trusted publishing (recommended) — no NPM_TOKEN needed.
50
+ # For token-based publishing: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Label-Based Trigger
56
+
57
+ Only release when a PR is merged with a release label. Conventional commits determine the changelog entries; the label controls whether and at what level to bump.
58
+
59
+ **Required labels** (customisable in config):
60
+
61
+ | Label | Effect |
62
+ |-------|--------|
63
+ | `release:patch` | Bump patch version |
64
+ | `release:minor` | Bump minor version |
65
+ | `release:major` | Bump major version |
66
+ | `release:stable` | Graduate a prerelease to stable |
67
+ | `release:prerelease` | Create a prerelease |
68
+ | `release:skip` | Suppress release on this PR |
69
+
70
+ ```yaml
71
+ # .github/workflows/release.yml
72
+ name: Release
73
+
74
+ on:
75
+ push:
76
+ branches: [main]
77
+
78
+ permissions:
79
+ contents: write
80
+ id-token: write
81
+ pull-requests: read
82
+
83
+ jobs:
84
+ release:
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ with:
89
+ fetch-depth: 0
90
+
91
+ - uses: actions/setup-node@v4
92
+ with:
93
+ node-version: '20'
94
+ registry-url: 'https://registry.npmjs.org'
95
+
96
+ - run: npm ci
97
+
98
+ - run: npx releasekit release
99
+ env:
100
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
101
+ ```
102
+
103
+ Configure the trigger in `releasekit.config.json`:
104
+
105
+ ```json
106
+ {
107
+ "ci": {
108
+ "releaseTrigger": "label",
109
+ "labels": {
110
+ "major": "release:major",
111
+ "minor": "release:minor",
112
+ "patch": "release:patch",
113
+ "skip": "release:skip"
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ Without a `release:patch/minor/major` label on the merged PR, no release is triggered. The `labels` block shown above reflects the defaults — omit it if your repository already uses those label names.
120
+
121
+ See [@releasekit/release — CI Configuration](../README.md#ci-configuration) for all `ci.*` options.
122
+
123
+ ---
124
+
125
+ ## PR Preview Comments
126
+
127
+ Post a comment on every PR showing what would be released if merged. Requires `pull-requests: write`.
128
+
129
+ ```yaml
130
+ # .github/workflows/release-preview.yml
131
+ name: Release Preview
132
+
133
+ on:
134
+ pull_request:
135
+ branches: [main]
136
+ types: [opened, synchronize, labeled, unlabeled]
137
+
138
+ concurrency:
139
+ group: release-preview-${{ github.event.pull_request.number }}
140
+ cancel-in-progress: true
141
+
142
+ permissions:
143
+ pull-requests: write
144
+ contents: read
145
+
146
+ jobs:
147
+ preview:
148
+ runs-on: ubuntu-latest
149
+ steps:
150
+ - uses: actions/checkout@v4
151
+ with:
152
+ fetch-depth: 0
153
+
154
+ - uses: actions/setup-node@v4
155
+ with:
156
+ node-version: '20'
157
+
158
+ - run: npm ci
159
+
160
+ - run: npx releasekit preview
161
+ env:
162
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
163
+ ```
164
+
165
+ A ready-to-use template is available at [`templates/workflows/release-preview.yml`](../../../templates/workflows/release-preview.yml).
166
+
167
+ ---
168
+
169
+ ## npm OIDC Trusted Publishing (Recommended)
170
+
171
+ With OIDC, no `NPM_TOKEN` secret is required. The workflow exchanges a GitHub-issued OIDC token for a short-lived npm token at publish time.
172
+
173
+ **Requirements:**
174
+ - npm `>=9.5.0`
175
+ - Each package must have an **Automation policy** configured at npmjs.com (Settings → Automation policies → add a GitHub Actions OIDC publisher for your repo and workflow)
176
+
177
+ **Important:** `actions/setup-node` with `registry-url` writes a project `.npmrc` that injects `_authToken=${NODE_AUTH_TOKEN}`. When `NODE_AUTH_TOKEN` is unset, npm resolves this to an empty token and fails with `ENEEDAUTH` instead of falling through to the OIDC exchange. Delete the file before publishing:
178
+
179
+ ```yaml
180
+ permissions:
181
+ contents: write
182
+ id-token: write # grants the OIDC token
183
+
184
+ steps:
185
+ - uses: actions/setup-node@v4
186
+ with:
187
+ node-version: '20'
188
+ registry-url: 'https://registry.npmjs.org'
189
+
190
+ - run: npx releasekit release
191
+ env:
192
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
193
+ # No NPM_TOKEN needed with OIDC
194
+ ```
195
+
196
+ ReleaseKit detects OIDC availability automatically (`npm-auth: auto`). To force it:
197
+
198
+ ```json
199
+ {
200
+ "publish": {
201
+ "npm": { "auth": "oidc" }
202
+ }
203
+ }
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Prerelease Workflow
209
+
210
+ ```yaml
211
+ # Manual dispatch for prereleases
212
+ on:
213
+ workflow_dispatch:
214
+ inputs:
215
+ prerelease:
216
+ description: 'Prerelease identifier (e.g. beta, rc)'
217
+ required: true
218
+ default: 'beta'
219
+
220
+ jobs:
221
+ prerelease:
222
+ runs-on: ubuntu-latest
223
+ steps:
224
+ - uses: actions/checkout@v4
225
+ with:
226
+ fetch-depth: 0
227
+
228
+ - uses: actions/setup-node@v4
229
+ with:
230
+ node-version: '20'
231
+ registry-url: 'https://registry.npmjs.org'
232
+
233
+ - run: npm ci
234
+
235
+ - run: npx releasekit release --prerelease ${{ inputs.prerelease }}
236
+ env:
237
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Monorepo Targeted Release
243
+
244
+ Release only specific packages:
245
+
246
+ ```bash
247
+ npx releasekit release --target @myorg/core,@myorg/cli
248
+ ```
249
+
250
+ Or version all packages together:
251
+
252
+ ```bash
253
+ npx releasekit release --sync
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Dry Run in CI
259
+
260
+ Useful for verifying pipeline setup before enabling real releases:
261
+
262
+ ```yaml
263
+ - run: npx releasekit release --dry-run --json
264
+ ```
265
+
266
+ `--dry-run` prints what would happen without modifying any files, creating tags, or publishing packages. `--json` emits structured output for inspection.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@releasekit/release",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Unified release pipeline: version, changelog, and publish in a single command",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -37,6 +37,7 @@
37
37
  "license": "MIT",
38
38
  "files": [
39
39
  "dist",
40
+ "docs",
40
41
  "README.md",
41
42
  "LICENSE"
42
43
  ],
@@ -49,9 +50,9 @@
49
50
  "commander": "^14.0.3",
50
51
  "smol-toml": "^1.6.1",
51
52
  "zod": "^4.3.6",
52
- "@releasekit/notes": "0.3.1",
53
- "@releasekit/version": "0.3.1",
54
- "@releasekit/publish": "0.3.1"
53
+ "@releasekit/publish": "0.4.1",
54
+ "@releasekit/version": "0.4.1",
55
+ "@releasekit/notes": "0.4.1"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@biomejs/biome": "^2.4.6",