@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 +29 -0
- package/dist/{chunk-67J7X3FS.js → chunk-EBHQERFN.js} +178 -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,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
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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-
|
|
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
|
-
|
|
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