@doryski/release 1.0.0 → 1.1.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 +25 -0
- package/dist/{chunk-67J7X3FS.js → chunk-IE5POH6X.js} +176 -43
- package/dist/cli.js +6 -2
- package/dist/index.d.ts +19 -1
- package/dist/index.js +9 -3
- package/package.json +1 -1
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,30 @@ 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
|
+
When neither source yields anything, `CHANGELOG.md` is left untouched.
|
|
75
|
+
|
|
51
76
|
## Programmatic use
|
|
52
77
|
|
|
53
78
|
```ts
|
|
@@ -1,5 +1,146 @@
|
|
|
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 buildChangelog = ({
|
|
111
|
+
content,
|
|
112
|
+
version,
|
|
113
|
+
date,
|
|
114
|
+
commits
|
|
115
|
+
}) => {
|
|
116
|
+
const { head, unreleasedHeading, unreleasedBody, tail } = splitDocument(content);
|
|
117
|
+
const body = unreleasedBody || renderEntries(commits);
|
|
118
|
+
if (!body) return null;
|
|
119
|
+
const released = stripV(version);
|
|
120
|
+
const blocks = [
|
|
121
|
+
head.trimEnd(),
|
|
122
|
+
unreleasedHeading,
|
|
123
|
+
`## [${released}] - ${date}
|
|
124
|
+
|
|
125
|
+
${body}`,
|
|
126
|
+
withLinkRef(tail.trim(), released)
|
|
127
|
+
].filter((block) => Boolean(block));
|
|
128
|
+
return `${blocks.join("\n\n")}
|
|
129
|
+
`;
|
|
130
|
+
};
|
|
131
|
+
var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
132
|
+
var updateChangelogFile = async (file, version, commits) => {
|
|
133
|
+
const content = await readFile(file, "utf8").catch(() => null);
|
|
134
|
+
if (content === null) return false;
|
|
135
|
+
const next = buildChangelog({ content, version, date: today(), commits });
|
|
136
|
+
if (!next || next === content) return false;
|
|
137
|
+
await writeFile(file, next, "utf8");
|
|
138
|
+
return true;
|
|
139
|
+
};
|
|
140
|
+
|
|
1
141
|
// src/release.ts
|
|
2
142
|
import { spawnSync } from "child_process";
|
|
143
|
+
import { existsSync } from "fs";
|
|
3
144
|
import path from "path";
|
|
4
145
|
import { createInterface } from "readline/promises";
|
|
5
146
|
import { stdin, stdout } from "process";
|
|
@@ -61,10 +202,10 @@ var getRemoteActionsUrl = () => {
|
|
|
61
202
|
};
|
|
62
203
|
|
|
63
204
|
// src/pkg.ts
|
|
64
|
-
import { readFile, writeFile } from "fs/promises";
|
|
65
|
-
var readJson = async (file) => JSON.parse(await
|
|
205
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
206
|
+
var readJson = async (file) => JSON.parse(await readFile2(file, "utf8"));
|
|
66
207
|
var writeJson = async (file, data) => {
|
|
67
|
-
await
|
|
208
|
+
await writeFile2(file, `${JSON.stringify(data, null, 2)}
|
|
68
209
|
`, "utf8");
|
|
69
210
|
};
|
|
70
211
|
var updateVersionFile = async (file, newVersion) => {
|
|
@@ -75,39 +216,9 @@ var updateVersionFile = async (file, newVersion) => {
|
|
|
75
216
|
return true;
|
|
76
217
|
};
|
|
77
218
|
|
|
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
219
|
// src/release.ts
|
|
110
220
|
var PACKAGE_JSON = path.join(process.cwd(), "package.json");
|
|
221
|
+
var CHANGELOG_MD = path.join(process.cwd(), "CHANGELOG.md");
|
|
111
222
|
var makeRunStep = (dryRun) => (label, cmd, args) => {
|
|
112
223
|
if (dryRun) {
|
|
113
224
|
console.log(`\u2192 [dry-run] ${label}`);
|
|
@@ -122,7 +233,8 @@ var release = async (options = {}) => {
|
|
|
122
233
|
releaseVersion: options.releaseVersion,
|
|
123
234
|
bump: options.bump,
|
|
124
235
|
yes: options.yes ?? false,
|
|
125
|
-
dryRun: options.dryRun ?? false
|
|
236
|
+
dryRun: options.dryRun ?? false,
|
|
237
|
+
changelog: options.changelog ?? true
|
|
126
238
|
};
|
|
127
239
|
ensureCleanTree();
|
|
128
240
|
const branch = ensureOnDefaultBranch();
|
|
@@ -187,13 +299,21 @@ var release = async (options = {}) => {
|
|
|
187
299
|
rl?.close();
|
|
188
300
|
process.exit(1);
|
|
189
301
|
}
|
|
302
|
+
const writesChangelog = flags.changelog && existsSync(CHANGELOG_MD);
|
|
190
303
|
console.log(`
|
|
191
304
|
This will:`);
|
|
192
305
|
console.log(` 1. Bump package.json ${pkg.version} \u2192 ${bareVersion}`);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
`);
|
|
306
|
+
if (writesChangelog) {
|
|
307
|
+
console.log(` 2. Cut CHANGELOG.md section [${bareVersion}]`);
|
|
308
|
+
}
|
|
309
|
+
console.log(` ${writesChangelog ? 3 : 2}. Commit on ${branch}`);
|
|
310
|
+
console.log(
|
|
311
|
+
` ${writesChangelog ? 4 : 3}. Tag ${normalized} and push ${branch} + tag`
|
|
312
|
+
);
|
|
313
|
+
console.log(
|
|
314
|
+
` ${writesChangelog ? 5 : 4}. GitHub Actions will build, test, and publish to npm
|
|
315
|
+
`
|
|
316
|
+
);
|
|
197
317
|
if (flags.dryRun) console.log("Dry-run mode: no changes will be made.\n");
|
|
198
318
|
const confirmed = await askConfirm();
|
|
199
319
|
rl?.close();
|
|
@@ -204,12 +324,22 @@ This will:`);
|
|
|
204
324
|
const runStep = makeRunStep(flags.dryRun);
|
|
205
325
|
if (flags.dryRun) {
|
|
206
326
|
console.log(`\u2192 [dry-run] would bump package.json to ${bareVersion}`);
|
|
327
|
+
if (writesChangelog) {
|
|
328
|
+
console.log(`\u2192 [dry-run] would cut CHANGELOG.md section [${bareVersion}]`);
|
|
329
|
+
}
|
|
207
330
|
} else {
|
|
208
331
|
const pkgChanged = await updateVersionFile(PACKAGE_JSON, bareVersion);
|
|
209
|
-
if (!pkgChanged)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
332
|
+
if (!pkgChanged) console.warn("\n\u26A0 package.json already at target version.");
|
|
333
|
+
const changelogChanged = writesChangelog && await updateChangelogFile(CHANGELOG_MD, normalized, commitsSinceTag);
|
|
334
|
+
if (writesChangelog && !changelogChanged) {
|
|
335
|
+
console.warn("\u26A0 No changelog entries to cut; leaving CHANGELOG.md alone.");
|
|
336
|
+
}
|
|
337
|
+
const staged = [
|
|
338
|
+
pkgChanged ? "package.json" : null,
|
|
339
|
+
changelogChanged ? "CHANGELOG.md" : null
|
|
340
|
+
].filter((file) => file !== null);
|
|
341
|
+
if (staged.length) {
|
|
342
|
+
runStep(`git add ${staged.join(" ")}`, "git", ["add", ...staged]);
|
|
213
343
|
runStep(`git commit -m "release: ${normalized}"`, "git", [
|
|
214
344
|
"commit",
|
|
215
345
|
"-m",
|
|
@@ -251,5 +381,8 @@ This will:`);
|
|
|
251
381
|
};
|
|
252
382
|
|
|
253
383
|
export {
|
|
384
|
+
parseCommit,
|
|
385
|
+
renderEntries,
|
|
386
|
+
buildChangelog,
|
|
254
387
|
release
|
|
255
388
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
release
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IE5POH6X.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
|
-
|
|
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