@releasekit/publish 0.3.1 → 0.4.0

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/publish
2
2
 
3
- Publish packages to npm and crates.io with git tagging and GitHub releases.
3
+ [![@releasekit/publish](https://img.shields.io/badge/@releasekit-publish-9feaf9?labelColor=1a1a1a&style=plastic)](https://www.npmjs.com/package/@releasekit/publish)
4
+ [![Version](https://img.shields.io/npm/v/@releasekit/publish?color=28a745&labelColor=1a1a1a)](https://www.npmjs.com/package/@releasekit/publish)
5
+ [![Downloads](https://img.shields.io/npm/dw/@releasekit/publish?color=6f42c1&labelColor=1a1a1a)](https://www.npmjs.com/package/@releasekit/publish)
6
+
7
+ **Publish packages to npm and crates.io with git tagging and GitHub releases.**
4
8
 
5
9
  ## Features
6
10
 
@@ -50,6 +54,8 @@ The publish pipeline runs in order:
50
54
  8. **Git Push** - Push commits and tags to remote
51
55
  9. **GitHub Release** - Create GitHub releases
52
56
 
57
+ The pipeline is **fail-fast**: the first package publish failure throws immediately. Git push and GitHub release are skipped, so the version commit and tag remain local until the issue is fixed and the release is retried.
58
+
53
59
  ## CLI Reference
54
60
 
55
61
  | Flag | Description | Default |
@@ -59,7 +65,7 @@ The publish pipeline runs in order:
59
65
  | `--registry <type>` | Registry to publish to: `npm`, `cargo`, `all` | `all` |
60
66
  | `--npm-auth <method>` | NPM auth method: `oidc`, `token`, `auto` | `auto` |
61
67
  | `--dry-run` | Simulate all operations | `false` |
62
- | `--skip-git` | Skip git commit/tag/push | `false` |
68
+ | `--skip-git` | Skip git commit/tag/push (also skips GitHub release — no tag to release against) | `false` |
63
69
  | `--skip-publish` | Skip registry publishing | `false` |
64
70
  | `--skip-github-release` | Skip GitHub Release creation | `false` |
65
71
  | `--skip-verification` | Skip post-publish verification | `false` |
@@ -126,7 +132,7 @@ Configure via `releasekit.config.json`:
126
132
  "githubRelease": {
127
133
  "enabled": true,
128
134
  "draft": true,
129
- "generateNotes": true
135
+ "body": "auto"
130
136
  },
131
137
  "verify": {
132
138
  "npm": {
@@ -144,6 +150,11 @@ Configure via `releasekit.config.json`:
144
150
 
145
151
  See [@releasekit/config](../config/README.md) for full configuration options.
146
152
 
153
+ ## Documentation
154
+
155
+ **Guides**
156
+ - [GitHub Releases](./docs/github-releases.md) — release body options, LLM prose, draft workflow
157
+
147
158
  ## License
148
159
 
149
160
  MIT
@@ -223,14 +223,14 @@ var GitHubReleaseConfigSchema = z.object({
223
223
  perPackage: z.boolean().default(true),
224
224
  prerelease: z.union([z.literal("auto"), z.boolean()]).default("auto"),
225
225
  /**
226
- * Controls how release notes are sourced for GitHub releases.
227
- * - 'auto': Use RELEASE_NOTES.md if it exists, then per-package changelog
228
- * data from the version output, then GitHub's auto-generated notes.
229
- * - 'github': Always use GitHub's auto-generated notes.
230
- * - 'none': No notes body.
231
- * - Any other string: Treated as a file path to read notes from.
226
+ * Controls the source for the GitHub release body.
227
+ * - 'auto': Use release notes if enabled, else changelog, else GitHub auto-generated.
228
+ * - 'releaseNotes': Use LLM-generated release notes (requires notes.releaseNotes.enabled: true).
229
+ * - 'changelog': Use formatted changelog entries.
230
+ * - 'generated': Use GitHub's auto-generated notes.
231
+ * - 'none': No body.
232
232
  */
233
- releaseNotes: z.union([z.literal("auto"), z.literal("github"), z.literal("none"), z.string()]).default("auto")
233
+ body: z.enum(["auto", "releaseNotes", "changelog", "generated", "none"]).default("auto")
234
234
  });
235
235
  var VerifyRegistryConfigSchema = z.object({
236
236
  enabled: z.boolean().default(true),
@@ -274,7 +274,7 @@ var PublishConfigSchema = z.object({
274
274
  draft: true,
275
275
  perPackage: true,
276
276
  prerelease: "auto",
277
- releaseNotes: "auto"
277
+ body: "auto"
278
278
  }),
279
279
  verify: VerifyConfigSchema.default({
280
280
  npm: {
@@ -295,10 +295,10 @@ var TemplateConfigSchema = z.object({
295
295
  path: z.string().optional(),
296
296
  engine: z.enum(["handlebars", "liquid", "ejs"]).optional()
297
297
  });
298
- var OutputConfigSchema = z.object({
299
- format: z.enum(["markdown", "github-release", "json"]),
298
+ var LocationModeSchema = z.enum(["root", "packages", "both"]);
299
+ var ChangelogConfigSchema = z.object({
300
+ mode: LocationModeSchema.optional(),
300
301
  file: z.string().optional(),
301
- options: z.record(z.string(), z.unknown()).optional(),
302
302
  templates: TemplateConfigSchema.optional()
303
303
  });
304
304
  var LLMOptionsSchema = z.object({
@@ -358,17 +358,20 @@ var LLMConfigSchema = z.object({
358
358
  scopes: ScopeConfigSchema.optional(),
359
359
  prompts: LLMPromptsConfigSchema.optional()
360
360
  });
361
+ var ReleaseNotesConfigSchema = z.object({
362
+ mode: LocationModeSchema.optional(),
363
+ file: z.string().optional(),
364
+ templates: TemplateConfigSchema.optional(),
365
+ llm: LLMConfigSchema.optional()
366
+ });
361
367
  var NotesInputConfigSchema = z.object({
362
368
  source: z.string().optional(),
363
369
  file: z.string().optional()
364
370
  });
365
371
  var NotesConfigSchema = z.object({
366
- input: NotesInputConfigSchema.optional(),
367
- output: z.array(OutputConfigSchema).default([{ format: "markdown", file: "CHANGELOG.md" }]),
368
- monorepo: MonorepoConfigSchema.optional(),
369
- templates: TemplateConfigSchema.optional(),
370
- llm: LLMConfigSchema.optional(),
371
- updateStrategy: z.enum(["prepend", "regenerate"]).default("prepend")
372
+ changelog: z.union([z.literal(false), ChangelogConfigSchema]).optional(),
373
+ releaseNotes: z.union([z.literal(false), ReleaseNotesConfigSchema]).optional(),
374
+ updateStrategy: z.enum(["prepend", "regenerate"]).optional()
372
375
  });
373
376
  var CILabelsConfigSchema = z.object({
374
377
  stable: z.string().default("release:stable"),
@@ -544,7 +547,7 @@ function getDefaultConfig() {
544
547
  draft: true,
545
548
  perPackage: true,
546
549
  prerelease: "auto",
547
- releaseNotes: "auto"
550
+ body: "auto"
548
551
  },
549
552
  verify: {
550
553
  npm: {
@@ -594,7 +597,7 @@ function toPublishConfig(config) {
594
597
  draft: config.githubRelease?.draft ?? defaults.githubRelease.draft,
595
598
  perPackage: config.githubRelease?.perPackage ?? defaults.githubRelease.perPackage,
596
599
  prerelease: config.githubRelease?.prerelease ?? defaults.githubRelease.prerelease,
597
- releaseNotes: config.githubRelease?.releaseNotes ?? defaults.githubRelease.releaseNotes
600
+ body: config.githubRelease?.body ?? defaults.githubRelease.body
598
601
  },
599
602
  verify: {
600
603
  npm: {
@@ -977,11 +980,15 @@ async function runCargoPublishStage(ctx) {
977
980
  if (!dryRun) {
978
981
  success(`Published ${crate.name}@${crate.version} to crates.io`);
979
982
  }
983
+ ctx.output.cargo.push(result);
980
984
  } catch (error) {
981
985
  result.reason = error instanceof Error ? error.message : String(error);
982
- warn(`Failed to publish ${crate.name}: ${result.reason}`);
986
+ ctx.output.cargo.push(result);
987
+ throw createPublishError(
988
+ "CARGO_PUBLISH_ERROR" /* CARGO_PUBLISH_ERROR */,
989
+ `${crate.name}@${crate.version}: ${result.reason}`
990
+ );
983
991
  }
984
- ctx.output.cargo.push(result);
985
992
  }
986
993
  }
987
994
  function findCrates(updates, _cwd) {
@@ -1237,21 +1244,34 @@ async function runGitPushStage(ctx) {
1237
1244
  }
1238
1245
 
1239
1246
  // src/stages/github-release.ts
1240
- import * as fs7 from "fs";
1241
- function resolveNotes(notesSetting, tag, changelogs, pipelineNotes) {
1242
- if (notesSetting === "none") {
1247
+ function resolveNotes(bodySource, tag, changelogs, releaseNotesEnabled, pipelineNotes) {
1248
+ if (bodySource === "none") {
1243
1249
  return { useGithubNotes: false };
1244
1250
  }
1245
- if (notesSetting === "github") {
1251
+ if (bodySource === "generated") {
1246
1252
  return { useGithubNotes: true };
1247
1253
  }
1248
- if (notesSetting !== "auto") {
1249
- const body = readFileIfExists(notesSetting);
1250
- if (body) return { body, useGithubNotes: false };
1251
- debug(`Notes file not found: ${notesSetting}, falling back to GitHub auto-notes`);
1254
+ if (bodySource === "releaseNotes") {
1255
+ if (!releaseNotesEnabled) {
1256
+ warn("releaseNotes is not enabled in notes config but body is set to releaseNotes");
1257
+ return { useGithubNotes: true };
1258
+ }
1259
+ if (pipelineNotes) {
1260
+ const body = findNotesForTag(tag, pipelineNotes);
1261
+ if (body) return { body, useGithubNotes: false };
1262
+ }
1263
+ warn("No release notes found in pipeline output, falling back to GitHub auto-notes");
1264
+ return { useGithubNotes: true };
1265
+ }
1266
+ if (bodySource === "changelog") {
1267
+ const packageBody2 = formatChangelogForTag(tag, changelogs);
1268
+ if (packageBody2) {
1269
+ return { body: packageBody2, useGithubNotes: false };
1270
+ }
1271
+ warn("No changelog found for tag, falling back to GitHub auto-notes");
1252
1272
  return { useGithubNotes: true };
1253
1273
  }
1254
- if (pipelineNotes) {
1274
+ if (releaseNotesEnabled && pipelineNotes) {
1255
1275
  const body = findNotesForTag(tag, pipelineNotes);
1256
1276
  if (body) return { body, useGithubNotes: false };
1257
1277
  }
@@ -1264,6 +1284,15 @@ function resolveNotes(notesSetting, tag, changelogs, pipelineNotes) {
1264
1284
  function isVersionOnlyTag(tag) {
1265
1285
  return /^v?\d+\.\d+\.\d+/.test(tag);
1266
1286
  }
1287
+ function getTitleFromTag(tag) {
1288
+ const atIndex = tag.lastIndexOf("@");
1289
+ if (atIndex === -1) {
1290
+ return tag;
1291
+ }
1292
+ const packageName = tag.slice(0, atIndex);
1293
+ const version = tag.slice(atIndex + 1);
1294
+ return `${packageName} @ ${version}`;
1295
+ }
1267
1296
  function findNotesForTag(tag, notes) {
1268
1297
  for (const [packageName, body] of Object.entries(notes)) {
1269
1298
  if (tag.startsWith(`${packageName}@`) && body.trim()) {
@@ -1274,14 +1303,6 @@ function findNotesForTag(tag, notes) {
1274
1303
  if (entries.length === 1 && isVersionOnlyTag(tag)) return entries[0];
1275
1304
  return void 0;
1276
1305
  }
1277
- function readFileIfExists(filePath) {
1278
- try {
1279
- const content = fs7.readFileSync(filePath, "utf-8").trim();
1280
- return content || void 0;
1281
- } catch {
1282
- return void 0;
1283
- }
1284
- }
1285
1306
  function formatChangelogForTag(tag, changelogs) {
1286
1307
  if (changelogs.length === 0) return void 0;
1287
1308
  const changelog = changelogs.find((c) => tag.startsWith(`${c.packageName}@`));
@@ -1322,16 +1343,19 @@ async function runGithubReleaseStage(ctx) {
1322
1343
  success: false
1323
1344
  };
1324
1345
  const ghArgs = ["release", "create", tag];
1346
+ ghArgs.push("--title", getTitleFromTag(tag));
1325
1347
  if (config.githubRelease.draft) {
1326
1348
  ghArgs.push("--draft");
1327
1349
  }
1328
1350
  if (isPreRel) {
1329
1351
  ghArgs.push("--prerelease");
1330
1352
  }
1353
+ const releaseNotesEnabled = !!(ctx.releaseNotes && Object.keys(ctx.releaseNotes).length > 0);
1331
1354
  const { body, useGithubNotes } = resolveNotes(
1332
- config.githubRelease.releaseNotes,
1355
+ config.githubRelease.body,
1333
1356
  tag,
1334
1357
  ctx.input.changelogs,
1358
+ releaseNotesEnabled,
1335
1359
  ctx.releaseNotes
1336
1360
  );
1337
1361
  if (body) {
@@ -1360,22 +1384,22 @@ async function runGithubReleaseStage(ctx) {
1360
1384
  }
1361
1385
 
1362
1386
  // src/stages/npm-publish.ts
1363
- import * as fs9 from "fs";
1387
+ import * as fs8 from "fs";
1364
1388
  import * as path8 from "path";
1365
1389
 
1366
1390
  // src/utils/npm-env.ts
1367
- import * as fs8 from "fs";
1391
+ import * as fs7 from "fs";
1368
1392
  import * as os2 from "os";
1369
1393
  import * as path7 from "path";
1370
1394
  function writeTempNpmrc(contents) {
1371
- const dir = fs8.mkdtempSync(path7.join(os2.tmpdir(), "releasekit-npmrc-"));
1395
+ const dir = fs7.mkdtempSync(path7.join(os2.tmpdir(), "releasekit-npmrc-"));
1372
1396
  const npmrcPath = path7.join(dir, ".npmrc");
1373
- fs8.writeFileSync(npmrcPath, contents, "utf-8");
1397
+ fs7.writeFileSync(npmrcPath, contents, "utf-8");
1374
1398
  return {
1375
1399
  npmrcPath,
1376
1400
  cleanup: () => {
1377
1401
  try {
1378
- fs8.rmSync(dir, { recursive: true, force: true });
1402
+ fs7.rmSync(dir, { recursive: true, force: true });
1379
1403
  } catch {
1380
1404
  }
1381
1405
  }
@@ -1395,9 +1419,6 @@ function createNpmSubprocessIsolation(options) {
1395
1419
  }
1396
1420
  })();
1397
1421
  const lines = [`registry=${registryUrl}`];
1398
- if (authMethod === "oidc") {
1399
- lines.push("always-auth=false");
1400
- }
1401
1422
  if (authMethod === "token" && token) {
1402
1423
  lines.push(`//${registryHost}/:_authToken=${token}`);
1403
1424
  }
@@ -1413,10 +1434,7 @@ function createNpmSubprocessIsolation(options) {
1413
1434
  npm_config_userconfig: npmrcPath,
1414
1435
  // Auth-specific hardening
1415
1436
  ...isOidc ? {
1416
- // Prevent setup-node's always-auth from forcing token lookups
1417
- NPM_CONFIG_ALWAYS_AUTH: "false",
1418
- npm_config_always_auth: "false",
1419
- // Explicitly prevent token-based publishing from being selected implicitly
1437
+ // Prevent any ambient token from overriding OIDC trusted publishing
1420
1438
  NODE_AUTH_TOKEN: void 0,
1421
1439
  NPM_TOKEN: void 0
1422
1440
  } : {
@@ -1456,7 +1474,7 @@ async function runNpmPublishStage(ctx) {
1456
1474
  };
1457
1475
  const pkgJsonPath = path8.resolve(cwd, update.filePath);
1458
1476
  try {
1459
- const pkgContent = fs9.readFileSync(pkgJsonPath, "utf-8");
1477
+ const pkgContent = fs8.readFileSync(pkgJsonPath, "utf-8");
1460
1478
  const pkgJson = JSON.parse(pkgContent);
1461
1479
  if (pkgJson.private) {
1462
1480
  result.skipped = true;
@@ -1514,11 +1532,15 @@ async function runNpmPublishStage(ctx) {
1514
1532
  if (!dryRun) {
1515
1533
  success(`Published ${update.packageName}@${update.newVersion} to npm`);
1516
1534
  }
1535
+ ctx.output.npm.push(result);
1517
1536
  } catch (error) {
1518
1537
  result.reason = error instanceof Error ? error.message : String(error);
1519
- warn(`Failed to publish ${update.packageName}: ${result.reason}`);
1538
+ ctx.output.npm.push(result);
1539
+ throw createPublishError(
1540
+ "NPM_PUBLISH_ERROR" /* NPM_PUBLISH_ERROR */,
1541
+ `${update.packageName}@${update.newVersion}: ${result.reason}`
1542
+ );
1520
1543
  }
1521
- ctx.output.npm.push(result);
1522
1544
  }
1523
1545
  } finally {
1524
1546
  npmIsolation.cleanup();
@@ -1526,7 +1548,7 @@ async function runNpmPublishStage(ctx) {
1526
1548
  }
1527
1549
 
1528
1550
  // src/stages/prepare.ts
1529
- import * as fs10 from "fs";
1551
+ import * as fs9 from "fs";
1530
1552
  import * as path9 from "path";
1531
1553
  async function runPrepareStage(ctx) {
1532
1554
  const { input, config, cliOptions, cwd } = ctx;
@@ -1536,7 +1558,7 @@ async function runPrepareStage(ctx) {
1536
1558
  for (const file of config.npm.copyFiles) {
1537
1559
  const src = path9.resolve(cwd, file);
1538
1560
  const dest = path9.join(pkgDir, file);
1539
- if (!fs10.existsSync(src)) {
1561
+ if (!fs9.existsSync(src)) {
1540
1562
  debug(`Source file not found, skipping copy: ${src}`);
1541
1563
  continue;
1542
1564
  }
@@ -1549,7 +1571,7 @@ async function runPrepareStage(ctx) {
1549
1571
  continue;
1550
1572
  }
1551
1573
  try {
1552
- fs10.copyFileSync(src, dest);
1574
+ fs9.copyFileSync(src, dest);
1553
1575
  debug(`Copied ${file} \u2192 ${pkgDir}`);
1554
1576
  } catch (error) {
1555
1577
  throw createPublishError(
@@ -1564,7 +1586,7 @@ async function runPrepareStage(ctx) {
1564
1586
  for (const update of input.updates) {
1565
1587
  const pkgDir = path9.dirname(path9.resolve(cwd, update.filePath));
1566
1588
  const cargoPath = path9.join(pkgDir, "Cargo.toml");
1567
- if (!fs10.existsSync(cargoPath)) {
1589
+ if (!fs9.existsSync(cargoPath)) {
1568
1590
  continue;
1569
1591
  }
1570
1592
  if (cliOptions.dryRun) {
@@ -1713,7 +1735,8 @@ async function runPipeline(input, config, options) {
1713
1735
  npm: [],
1714
1736
  cargo: [],
1715
1737
  verification: [],
1716
- githubReleases: []
1738
+ githubReleases: [],
1739
+ publishSucceeded: false
1717
1740
  }
1718
1741
  };
1719
1742
  try {
@@ -1731,14 +1754,15 @@ async function runPipeline(input, config, options) {
1731
1754
  if (options.registry === "all" || options.registry === "cargo") {
1732
1755
  await runCargoPublishStage(ctx);
1733
1756
  }
1757
+ ctx.output.publishSucceeded = ctx.output.npm.every((r) => r.success) && ctx.output.cargo.every((r) => r.success);
1734
1758
  }
1735
1759
  if (!options.skipVerification && !options.skipPublish) {
1736
1760
  await runVerifyStage(ctx);
1737
1761
  }
1738
- if (!options.skipGit) {
1762
+ if (!options.skipGit && (options.skipPublish || ctx.output.publishSucceeded)) {
1739
1763
  await runGitPushStage(ctx);
1740
1764
  }
1741
- if (!options.skipGithubRelease) {
1765
+ if (!options.skipGithubRelease && ctx.output.git.pushed) {
1742
1766
  await runGithubReleaseStage(ctx);
1743
1767
  }
1744
1768
  } catch (error) {
@@ -1750,7 +1774,7 @@ async function runPipeline(input, config, options) {
1750
1774
  }
1751
1775
 
1752
1776
  // src/stages/input.ts
1753
- import * as fs11 from "fs";
1777
+ import * as fs10 from "fs";
1754
1778
  import { z as z3 } from "zod";
1755
1779
  var VersionChangelogEntrySchema = z3.object({
1756
1780
  type: z3.string(),
@@ -1783,7 +1807,7 @@ async function parseInput(inputPath) {
1783
1807
  let raw;
1784
1808
  if (inputPath) {
1785
1809
  try {
1786
- raw = fs11.readFileSync(inputPath, "utf-8");
1810
+ raw = fs10.readFileSync(inputPath, "utf-8");
1787
1811
  } catch {
1788
1812
  throw createPublishError("INPUT_PARSE_ERROR" /* INPUT_PARSE_ERROR */, `Could not read file: ${inputPath}`);
1789
1813
  }
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  runPipeline,
10
10
  setJsonMode,
11
11
  setLogLevel
12
- } from "./chunk-GIMIZS5B.js";
12
+ } from "./chunk-ZMMZ7S6G.js";
13
13
 
14
14
  // src/cli.ts
15
15
  import { realpathSync } from "fs";
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  parseInput,
17
17
  runPipeline,
18
18
  updateCargoVersion
19
- } from "./chunk-GIMIZS5B.js";
19
+ } from "./chunk-ZMMZ7S6G.js";
20
20
  export {
21
21
  BasePublishError,
22
22
  PipelineError,
@@ -0,0 +1,177 @@
1
+ # GitHub Releases
2
+
3
+ `@releasekit/publish` creates a GitHub Release for each published package. This guide covers configuration options and how to use LLM-generated prose as the release body.
4
+
5
+ ## Enabling GitHub Releases
6
+
7
+ GitHub Releases are enabled by default. Requires `GITHUB_TOKEN` with `contents: write` permission.
8
+
9
+ ```json
10
+ {
11
+ "publish": {
12
+ "githubRelease": {
13
+ "enabled": true
14
+ }
15
+ }
16
+ }
17
+ ```
18
+
19
+ To disable:
20
+
21
+ ```json
22
+ {
23
+ "publish": {
24
+ "githubRelease": { "enabled": false }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Release Body (`body`)
32
+
33
+ Controls what appears in the GitHub Release description.
34
+
35
+ | Value | Behaviour |
36
+ |-------|-----------|
37
+ | `"auto"` (default) | Use LLM release notes if available, otherwise changelog entries, otherwise GitHub auto-generated notes |
38
+ | `"releaseNotes"` | Use LLM-generated prose release notes (requires `notes.releaseNotes.llm.tasks.releaseNotes: true`) |
39
+ | `"changelog"` | Use the formatted changelog entries for this version |
40
+ | `"generated"` | GitHub's auto-generated release notes (from merged PRs and commits) |
41
+ | `"none"` | No release body |
42
+
43
+ ```json
44
+ {
45
+ "publish": {
46
+ "githubRelease": {
47
+ "body": "releaseNotes"
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Using LLM-Generated Release Notes
56
+
57
+ To populate the GitHub Release body with LLM-written prose:
58
+
59
+ **1. Enable the `releaseNotes` LLM task in notes config:**
60
+
61
+ ```json
62
+ {
63
+ "notes": {
64
+ "releaseNotes": {
65
+ "llm": {
66
+ "provider": "openai",
67
+ "model": "gpt-4o-mini",
68
+ "tasks": { "releaseNotes": true }
69
+ }
70
+ }
71
+ },
72
+ "publish": {
73
+ "githubRelease": {
74
+ "body": "releaseNotes"
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ The notes step runs first, the LLM generates prose release notes, and the publish step forwards them to the GitHub Release API.
81
+
82
+ If `body` is `"auto"` (default), LLM release notes are used automatically when available — no extra config needed.
83
+
84
+ **2. Optionally write release notes to a file** as well:
85
+
86
+ ```json
87
+ {
88
+ "notes": {
89
+ "releaseNotes": {
90
+ "mode": "root",
91
+ "llm": {
92
+ "provider": "anthropic",
93
+ "model": "claude-haiku-4-5",
94
+ "tasks": { "releaseNotes": true }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ When `mode` is set, a `RELEASE_NOTES.md` file is written in addition to the GitHub Release being populated.
102
+
103
+ ---
104
+
105
+ ## Draft Releases
106
+
107
+ Releases are created as drafts by default, giving you a chance to review before publishing.
108
+
109
+ ```json
110
+ {
111
+ "publish": {
112
+ "githubRelease": {
113
+ "draft": true
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ Set `"draft": false` to publish releases immediately.
120
+
121
+ ---
122
+
123
+ ## Prerelease Marking
124
+
125
+ ```json
126
+ {
127
+ "publish": {
128
+ "githubRelease": {
129
+ "prerelease": "auto"
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ | Value | Behaviour |
136
+ |-------|-----------|
137
+ | `"auto"` (default) | Marked as prerelease when the version contains a prerelease identifier (e.g. `1.0.0-beta.1`) |
138
+ | `true` | Always marked as prerelease |
139
+ | `false` | Never marked as prerelease |
140
+
141
+ ---
142
+
143
+ ## Per-Package Releases
144
+
145
+ In a monorepo, a separate GitHub Release is created for each published package by default.
146
+
147
+ ```json
148
+ {
149
+ "publish": {
150
+ "githubRelease": {
151
+ "perPackage": true
152
+ }
153
+ }
154
+ }
155
+ ```
156
+
157
+ Set `"perPackage": false` to create a single release for the entire repo.
158
+
159
+ ---
160
+
161
+ ## Full Configuration Reference
162
+
163
+ ```json
164
+ {
165
+ "publish": {
166
+ "githubRelease": {
167
+ "enabled": true,
168
+ "draft": true,
169
+ "prerelease": "auto",
170
+ "perPackage": true,
171
+ "body": "auto"
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ See the [@releasekit/publish README](../README.md) for all options.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@releasekit/publish",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Publish packages to npm and crates.io with git tagging and GitHub releases",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -41,6 +41,7 @@
41
41
  "license": "MIT",
42
42
  "files": [
43
43
  "dist",
44
+ "docs",
44
45
  "README.md",
45
46
  "LICENSE"
46
47
  ],