@doryski/release 1.0.0 → 1.1.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
@@ -37,6 +37,7 @@ pnpm release
37
37
  | `-r, --release-version <ver>` | Exact release version (`0.2.0` or `v0.2.0`). Highest precedence. |
38
38
  | `--bump <major\|minor\|patch>` | Force the bump level. Overrides the auto-detected level; ignored when `--release-version` is set. |
39
39
  | `-y, --yes` | Skip the confirmation prompt (required for non-interactive runs). |
40
+ | `--no-changelog` | Skip cutting a `CHANGELOG.md` section for this release. |
40
41
  | `-n, --dry-run` | Preview actions without modifying files, committing, tagging, or pushing. |
41
42
  | `-h, --help` | Show help. |
42
43
 
@@ -48,6 +49,34 @@ The auto-detected bump follows conventional commits since the latest tag:
48
49
  - any `feat:` → **minor**
49
50
  - everything else (`fix`, `chore`, `docs`, …) → **patch**
50
51
 
52
+ ## Changelog
53
+
54
+ If the repo has a `CHANGELOG.md`, the CLI cuts a dated section for the release
55
+ and includes it in the `release: vX.Y.Z` commit, so the file can never drift
56
+ behind the tags. Repos without the file are unaffected; `--no-changelog` opts
57
+ out.
58
+
59
+ The format follows [Keep a Changelog](https://keepachangelog.com/): the new
60
+ section goes directly below `## [Unreleased]`, which is left in place, empty and
61
+ ready for the next cycle.
62
+
63
+ Section content comes from one of two places:
64
+
65
+ - **Hand-written notes win.** Anything under `## [Unreleased]` is promoted
66
+ verbatim into the new version section. Write prose there as you go and the
67
+ release just files it under the right version.
68
+ - **Otherwise it is generated** from the conventional commits since the last
69
+ tag: `feat` → `### Added`, `fix` → `### Fixed`, `perf`/`refactor`/`revert` →
70
+ `### Changed`, and anything marked `!` or carrying a `BREAKING CHANGE:` footer
71
+ → `### Breaking Changes`. Scopes render as a bold prefix. Non-user-facing
72
+ types (`chore`, `docs`, `test`, `ci`, `build`, `style`, `release`) are dropped.
73
+
74
+ Link references are maintained only where the document already uses them: a
75
+ `[X.Y.Z]: …/releases/tag/vX.Y.Z` entry is added for the new version, and an
76
+ `[Unreleased]: …/compare/vX.Y.Z...HEAD` entry is repointed at it.
77
+
78
+ When neither source yields anything, `CHANGELOG.md` is left untouched.
79
+
51
80
  ## Programmatic use
52
81
 
53
82
  ```ts
@@ -1,5 +1,148 @@
1
+ // src/changelog.ts
2
+ import { readFile, writeFile } from "fs/promises";
3
+
4
+ // src/semver.ts
5
+ var parseSemver = (tag) => {
6
+ const match = /^v(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?$/.exec(tag);
7
+ if (!match) return null;
8
+ const [, major, minor, patch, prerelease] = match;
9
+ return {
10
+ major: Number(major),
11
+ minor: Number(minor),
12
+ patch: Number(patch),
13
+ prerelease: prerelease ?? null
14
+ };
15
+ };
16
+ var applyBump = (v, type) => {
17
+ if (type === "major") return `v${v.major + 1}.0.0`;
18
+ if (type === "minor") return `v${v.major}.${v.minor + 1}.0`;
19
+ return `v${v.major}.${v.minor}.${v.patch + 1}`;
20
+ };
21
+ var determineBump = (commits) => {
22
+ let bump = "patch";
23
+ const subjectRe = /^(\w+)(?:\([^)]*\))?(!)?:/;
24
+ for (const msg of commits) {
25
+ const subject = msg.split("\n")[0];
26
+ const match = subjectRe.exec(subject);
27
+ const breaking = match?.[2] === "!" || /^BREAKING CHANGE:/m.test(msg);
28
+ if (breaking) return "major";
29
+ if (match?.[1] === "feat") bump = "minor";
30
+ }
31
+ return bump;
32
+ };
33
+ var stripV = (tag) => tag.startsWith("v") ? tag.slice(1) : tag;
34
+
35
+ // src/changelog.ts
36
+ var SECTION_BY_TYPE = {
37
+ feat: "Added",
38
+ fix: "Fixed",
39
+ perf: "Changed",
40
+ refactor: "Changed",
41
+ revert: "Changed"
42
+ };
43
+ var BREAKING_SECTION = "Breaking Changes";
44
+ var SECTION_ORDER = [
45
+ BREAKING_SECTION,
46
+ "Added",
47
+ "Changed",
48
+ "Fixed"
49
+ ];
50
+ var SUBJECT_RE = /^(\w+)(?:\(([^)]*)\))?(!)?:\s*(.+)$/;
51
+ var isKnownType = (type) => type in SECTION_BY_TYPE;
52
+ var parseCommit = (message) => {
53
+ const [subject = "", ...rest] = message.split("\n");
54
+ const match = SUBJECT_RE.exec(subject.trim());
55
+ if (!match) return null;
56
+ const [, type, scope, bang, description] = match;
57
+ const breaking = bang === "!" || /^BREAKING CHANGE:/m.test(rest.join("\n"));
58
+ if (breaking) return { section: BREAKING_SECTION, scope: scope ?? null, description };
59
+ if (!isKnownType(type)) return null;
60
+ return { section: SECTION_BY_TYPE[type], scope: scope ?? null, description };
61
+ };
62
+ var renderEntry = ({ scope, description }) => scope ? `- **${scope}**: ${description}` : `- ${description}`;
63
+ var renderEntries = (commits) => {
64
+ const entries = commits.map(parseCommit).filter((entry) => entry !== null);
65
+ return SECTION_ORDER.flatMap((section) => {
66
+ const forSection = entries.filter((entry) => entry.section === section);
67
+ if (!forSection.length) return [];
68
+ return [`### ${section}`, "", ...forSection.map(renderEntry), ""];
69
+ }).join("\n").trimEnd();
70
+ };
71
+ var VERSION_HEADING_RE = /^## \[/;
72
+ var UNRELEASED_HEADING_RE = /^## \[unreleased\]/i;
73
+ var splitDocument = (content) => {
74
+ const lines = content.split("\n");
75
+ const headings = lines.flatMap(
76
+ (line, index) => VERSION_HEADING_RE.test(line) ? [index] : []
77
+ );
78
+ const unreleasedAt = headings.find(
79
+ (index) => UNRELEASED_HEADING_RE.test(lines[index])
80
+ );
81
+ if (unreleasedAt === void 0) {
82
+ const at = headings[0] ?? lines.length;
83
+ return {
84
+ head: lines.slice(0, at).join("\n"),
85
+ unreleasedHeading: null,
86
+ unreleasedBody: "",
87
+ tail: lines.slice(at).join("\n")
88
+ };
89
+ }
90
+ const tailAt = headings.find(
91
+ (index) => index > unreleasedAt && !UNRELEASED_HEADING_RE.test(lines[index])
92
+ ) ?? lines.length;
93
+ return {
94
+ head: lines.slice(0, unreleasedAt).join("\n"),
95
+ unreleasedHeading: lines[unreleasedAt],
96
+ unreleasedBody: lines.slice(unreleasedAt + 1, tailAt).join("\n").trim(),
97
+ tail: lines.slice(tailAt).join("\n")
98
+ };
99
+ };
100
+ var LINK_REF_RE = /^\[(\d+\.\d+\.\d+)\]:\s*(\S*\/tag\/)v\d+\.\d+\.\d+\s*$/;
101
+ var withLinkRef = (tail, version) => {
102
+ const lines = tail.split("\n");
103
+ const at = lines.findIndex((line) => LINK_REF_RE.test(line));
104
+ if (at < 0) return tail;
105
+ const [, , base] = LINK_REF_RE.exec(lines[at]) ?? [];
106
+ const ref = `[${version}]: ${base}v${version}`;
107
+ if (lines.some((line) => line.startsWith(`[${version}]:`))) return tail;
108
+ return [...lines.slice(0, at), ref, ...lines.slice(at)].join("\n");
109
+ };
110
+ var UNRELEASED_REF_RE = /^(\[Unreleased\]:\s*\S*\/compare\/)v\d+\.\d+\.\d+(\.{3}HEAD)\s*$/i;
111
+ var withUnreleasedRef = (tail, version) => tail.split("\n").map((line) => line.replace(UNRELEASED_REF_RE, `$1v${version}$2`)).join("\n");
112
+ var buildChangelog = ({
113
+ content,
114
+ version,
115
+ date,
116
+ commits
117
+ }) => {
118
+ const { head, unreleasedHeading, unreleasedBody, tail } = splitDocument(content);
119
+ const body = unreleasedBody || renderEntries(commits);
120
+ if (!body) return null;
121
+ const released = stripV(version);
122
+ const blocks = [
123
+ head.trimEnd(),
124
+ unreleasedHeading,
125
+ `## [${released}] - ${date}
126
+
127
+ ${body}`,
128
+ withUnreleasedRef(withLinkRef(tail.trim(), released), released)
129
+ ].filter((block) => Boolean(block));
130
+ return `${blocks.join("\n\n")}
131
+ `;
132
+ };
133
+ var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
134
+ var updateChangelogFile = async (file, version, commits) => {
135
+ const content = await readFile(file, "utf8").catch(() => null);
136
+ if (content === null) return false;
137
+ const next = buildChangelog({ content, version, date: today(), commits });
138
+ if (!next || next === content) return false;
139
+ await writeFile(file, next, "utf8");
140
+ return true;
141
+ };
142
+
1
143
  // src/release.ts
2
144
  import { spawnSync } from "child_process";
145
+ import { existsSync } from "fs";
3
146
  import path from "path";
4
147
  import { createInterface } from "readline/promises";
5
148
  import { stdin, stdout } from "process";
@@ -61,10 +204,10 @@ var getRemoteActionsUrl = () => {
61
204
  };
62
205
 
63
206
  // src/pkg.ts
64
- import { readFile, writeFile } from "fs/promises";
65
- var readJson = async (file) => JSON.parse(await readFile(file, "utf8"));
207
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
208
+ var readJson = async (file) => JSON.parse(await readFile2(file, "utf8"));
66
209
  var writeJson = async (file, data) => {
67
- await writeFile(file, `${JSON.stringify(data, null, 2)}
210
+ await writeFile2(file, `${JSON.stringify(data, null, 2)}
68
211
  `, "utf8");
69
212
  };
70
213
  var updateVersionFile = async (file, newVersion) => {
@@ -75,39 +218,9 @@ var updateVersionFile = async (file, newVersion) => {
75
218
  return true;
76
219
  };
77
220
 
78
- // src/semver.ts
79
- var parseSemver = (tag) => {
80
- const match = /^v(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?$/.exec(tag);
81
- if (!match) return null;
82
- const [, major, minor, patch, prerelease] = match;
83
- return {
84
- major: Number(major),
85
- minor: Number(minor),
86
- patch: Number(patch),
87
- prerelease: prerelease ?? null
88
- };
89
- };
90
- var applyBump = (v, type) => {
91
- if (type === "major") return `v${v.major + 1}.0.0`;
92
- if (type === "minor") return `v${v.major}.${v.minor + 1}.0`;
93
- return `v${v.major}.${v.minor}.${v.patch + 1}`;
94
- };
95
- var determineBump = (commits) => {
96
- let bump = "patch";
97
- const subjectRe = /^(\w+)(?:\([^)]*\))?(!)?:/;
98
- for (const msg of commits) {
99
- const subject = msg.split("\n")[0];
100
- const match = subjectRe.exec(subject);
101
- const breaking = match?.[2] === "!" || /^BREAKING CHANGE:/m.test(msg);
102
- if (breaking) return "major";
103
- if (match?.[1] === "feat") bump = "minor";
104
- }
105
- return bump;
106
- };
107
- var stripV = (tag) => tag.startsWith("v") ? tag.slice(1) : tag;
108
-
109
221
  // src/release.ts
110
222
  var PACKAGE_JSON = path.join(process.cwd(), "package.json");
223
+ var CHANGELOG_MD = path.join(process.cwd(), "CHANGELOG.md");
111
224
  var makeRunStep = (dryRun) => (label, cmd, args) => {
112
225
  if (dryRun) {
113
226
  console.log(`\u2192 [dry-run] ${label}`);
@@ -122,7 +235,8 @@ var release = async (options = {}) => {
122
235
  releaseVersion: options.releaseVersion,
123
236
  bump: options.bump,
124
237
  yes: options.yes ?? false,
125
- dryRun: options.dryRun ?? false
238
+ dryRun: options.dryRun ?? false,
239
+ changelog: options.changelog ?? true
126
240
  };
127
241
  ensureCleanTree();
128
242
  const branch = ensureOnDefaultBranch();
@@ -187,13 +301,21 @@ var release = async (options = {}) => {
187
301
  rl?.close();
188
302
  process.exit(1);
189
303
  }
304
+ const writesChangelog = flags.changelog && existsSync(CHANGELOG_MD);
190
305
  console.log(`
191
306
  This will:`);
192
307
  console.log(` 1. Bump package.json ${pkg.version} \u2192 ${bareVersion}`);
193
- console.log(` 2. Commit on ${branch}`);
194
- console.log(` 3. Tag ${normalized} and push ${branch} + tag`);
195
- console.log(` 4. GitHub Actions will build, test, and publish to npm
196
- `);
308
+ if (writesChangelog) {
309
+ console.log(` 2. Cut CHANGELOG.md section [${bareVersion}]`);
310
+ }
311
+ console.log(` ${writesChangelog ? 3 : 2}. Commit on ${branch}`);
312
+ console.log(
313
+ ` ${writesChangelog ? 4 : 3}. Tag ${normalized} and push ${branch} + tag`
314
+ );
315
+ console.log(
316
+ ` ${writesChangelog ? 5 : 4}. GitHub Actions will build, test, and publish to npm
317
+ `
318
+ );
197
319
  if (flags.dryRun) console.log("Dry-run mode: no changes will be made.\n");
198
320
  const confirmed = await askConfirm();
199
321
  rl?.close();
@@ -204,12 +326,22 @@ This will:`);
204
326
  const runStep = makeRunStep(flags.dryRun);
205
327
  if (flags.dryRun) {
206
328
  console.log(`\u2192 [dry-run] would bump package.json to ${bareVersion}`);
329
+ if (writesChangelog) {
330
+ console.log(`\u2192 [dry-run] would cut CHANGELOG.md section [${bareVersion}]`);
331
+ }
207
332
  } else {
208
333
  const pkgChanged = await updateVersionFile(PACKAGE_JSON, bareVersion);
209
- if (!pkgChanged) {
210
- console.warn("\n\u26A0 package.json already at target version.");
211
- } else {
212
- runStep(`git add package.json`, "git", ["add", "package.json"]);
334
+ if (!pkgChanged) console.warn("\n\u26A0 package.json already at target version.");
335
+ const changelogChanged = writesChangelog && await updateChangelogFile(CHANGELOG_MD, normalized, commitsSinceTag);
336
+ if (writesChangelog && !changelogChanged) {
337
+ console.warn("\u26A0 No changelog entries to cut; leaving CHANGELOG.md alone.");
338
+ }
339
+ const staged = [
340
+ pkgChanged ? "package.json" : null,
341
+ changelogChanged ? "CHANGELOG.md" : null
342
+ ].filter((file) => file !== null);
343
+ if (staged.length) {
344
+ runStep(`git add ${staged.join(" ")}`, "git", ["add", ...staged]);
213
345
  runStep(`git commit -m "release: ${normalized}"`, "git", [
214
346
  "commit",
215
347
  "-m",
@@ -251,5 +383,8 @@ This will:`);
251
383
  };
252
384
 
253
385
  export {
386
+ parseCommit,
387
+ renderEntries,
388
+ buildChangelog,
254
389
  release
255
390
  };
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  release
4
- } from "./chunk-67J7X3FS.js";
4
+ } from "./chunk-EBHQERFN.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command, InvalidArgumentError } from "commander";
@@ -20,6 +20,9 @@ var program = new Command().name("doryski-release").description("Bump package.js
20
20
  "force the bump level (overrides the auto-detected level; ignored when --release-version is set)",
21
21
  parseBump
22
22
  ).option("-y, --yes", "skip the confirmation prompt", false).option(
23
+ "--no-changelog",
24
+ "skip cutting a CHANGELOG.md section for this release"
25
+ ).option(
23
26
  "-n, --dry-run",
24
27
  "preview the actions without modifying files, committing, tagging, or pushing",
25
28
  false
@@ -30,7 +33,8 @@ release({
30
33
  releaseVersion: flags.releaseVersion,
31
34
  bump: flags.bump,
32
35
  yes: flags.yes,
33
- dryRun: flags.dryRun
36
+ dryRun: flags.dryRun,
37
+ changelog: flags.changelog
34
38
  }).catch((err) => {
35
39
  console.error(err);
36
40
  process.exit(1);
package/dist/index.d.ts CHANGED
@@ -11,7 +11,25 @@ type ReleaseOptions = {
11
11
  bump?: BumpType;
12
12
  yes?: boolean;
13
13
  dryRun?: boolean;
14
+ changelog?: boolean;
14
15
  };
15
16
  declare const release: (options?: ReleaseOptions) => Promise<void>;
16
17
 
17
- export { type BumpType, type ParsedSemver, type ReleaseOptions, release };
18
+ declare const SECTION_ORDER: readonly ["Breaking Changes", "Added", "Changed", "Fixed"];
19
+ type ChangelogSection = (typeof SECTION_ORDER)[number];
20
+ type ChangelogEntry = {
21
+ section: ChangelogSection;
22
+ scope: string | null;
23
+ description: string;
24
+ };
25
+ declare const parseCommit: (message: string) => ChangelogEntry | null;
26
+ declare const renderEntries: (commits: string[]) => string;
27
+ type BuildChangelogInput = {
28
+ content: string;
29
+ version: string;
30
+ date: string;
31
+ commits: string[];
32
+ };
33
+ declare const buildChangelog: ({ content, version, date, commits, }: BuildChangelogInput) => string | null;
34
+
35
+ export { type BuildChangelogInput, type BumpType, type ChangelogEntry, type ChangelogSection, type ParsedSemver, type ReleaseOptions, buildChangelog, parseCommit, release, renderEntries };
package/dist/index.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import {
2
- release
3
- } from "./chunk-67J7X3FS.js";
2
+ buildChangelog,
3
+ parseCommit,
4
+ release,
5
+ renderEntries
6
+ } from "./chunk-EBHQERFN.js";
4
7
  export {
5
- release
8
+ buildChangelog,
9
+ parseCommit,
10
+ release,
11
+ renderEntries
6
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doryski/release",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "A conventional-commit release CLI and reusable GitHub Actions workflow for npm packages",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",