release-suite 0.1.0 โ†’ 1.0.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.
@@ -1,192 +1,213 @@
1
- #!/usr/bin/env node
2
- import { execSync } from "node:child_process";
3
- import fs from "node:fs";
4
-
5
- function run(cmd) {
6
- return execSync(cmd, { encoding: "utf8" }).trim();
7
- }
8
-
9
- function getAllTags() {
10
- try {
11
- return run("git tag --sort=-creatordate")
12
- .split("\n")
13
- .filter(Boolean);
14
- } catch {
15
- return [];
16
- }
17
- }
18
-
19
- function getCommitsBetween(from, to) {
20
- const range = from ? `${from}..${to}` : to;
21
- try {
22
- return run(`git log ${range} --pretty=format:%H%x1f%s%x1f%b`)
23
- .split("\n")
24
- .filter(Boolean);
25
- } catch {
26
- return [];
27
- }
28
- }
29
-
30
- function parseCommit(line) {
31
- const [hash, subject = "", body = ""] = line.split("\x1f");
32
- return { hash, subject: subject.trim(), body: body.trim() };
33
- }
34
-
35
- function cleanSubject(subject) {
36
- let s = subject.replace(/^(:\S+: )?/, "");
37
- s = s.replace(
38
- /^(feat|fix|refactor|docs|chore|style|test|build|perf|ci|raw|cleanup|remove)(\(.+\))?(!)?:\s*/i,
39
- ""
40
- );
41
- return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
42
- }
43
-
44
- function categorize(commits) {
45
- const buckets = {
46
- breaking: [],
47
- feat: [],
48
- fix: [],
49
- refactor: [],
50
- chore: [],
51
- docs: [],
52
- style: [],
53
- test: [],
54
- build: [],
55
- perf: [],
56
- ci: [],
57
- raw: [],
58
- cleanup: [],
59
- remove: [],
60
- };
61
-
62
- const reType =
63
- /^(:\S+: )?(feat|fix|refactor|docs|chore|style|test|build|perf|ci|raw|cleanup|remove)(\(.+\))?(!)?:/i;
64
-
65
- for (const c of commits) {
66
- const { subject, body } = c;
67
-
68
- if (/BREAKING CHANGE/i.test(body) || /!:/i.test(subject)) {
69
- buckets.breaking.push({ desc: cleanSubject(subject), hash: c.hash });
70
- continue;
71
- }
72
-
73
- const match = subject.match(reType);
74
- const desc = cleanSubject(subject);
75
-
76
- if (!match) {
77
- buckets.chore.push({ desc: desc || subject, hash: c.hash });
78
- continue;
79
- }
80
-
81
- const type = match[2].toLowerCase();
82
- (buckets[type] || buckets.chore).push({ desc, hash: c.hash });
83
- }
84
-
85
- return buckets;
86
- }
87
-
88
- function buildSection(version, buckets) {
89
- const out = [];
90
- out.push(`## ${version}\n`);
91
-
92
- const sections = [
93
- ["breaking", "### ๐Ÿ’ฅ Breaking Changes"],
94
- ["feat", "### โœจ Features"],
95
- ["fix", "### ๐Ÿ› Fixes"],
96
- ["refactor", "### โš™๏ธ Refactor"],
97
- ["chore", "### ๐Ÿ”ง Chore"],
98
- ["docs", "### ๐Ÿ“š Docs"],
99
- ["style", "### ๐ŸŽจ Style"],
100
- ["test", "### ๐Ÿงช Tests"],
101
- ["build", "### ๐Ÿ›  Build"],
102
- ["perf", "### โšก Performance"],
103
- ["ci", "### ๐Ÿ” CI"],
104
- ["raw", "### ๐Ÿ—ƒ Raw"],
105
- ["cleanup", "### ๐Ÿงน Cleanup"],
106
- ["remove", "### ๐Ÿ—‘ Remove"],
107
- ];
108
-
109
- let hasContent = false;
110
-
111
- for (const [key, title] of sections) {
112
- if (buckets[key].length) {
113
- hasContent = true;
114
- out.push(`${title}\n`);
115
- for (const c of buckets[key]) out.push(`- ${c.desc}`);
116
- out.push("");
117
- }
118
- }
119
-
120
- if (!hasContent) out.push("_No changes._\n");
121
-
122
- return out.join("\n");
123
- }
124
-
125
- function changelogHasVersion(file, version) {
126
- if (!fs.existsSync(file)) return false;
127
- const content = fs.readFileSync(file, "utf8");
128
- const safe = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
129
- return new RegExp(`^##\\s+${safe}\\b`, "m").test(content);
130
- }
131
-
132
- function main() {
133
- const isPreview = process.env.PREVIEW_MODE === "true";
134
- const CHANGELOG_FILE = isPreview
135
- ? "CHANGELOG.preview.md"
136
- : "CHANGELOG.md";
137
-
138
- const tags = getAllTags();
139
- const sections = [];
140
-
141
- if (tags.length === 0) {
142
- const pkgVersion = (() => {
143
- try {
144
- const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
145
- return pkg.version || "0.1.0";
146
- } catch {
147
- return "0.1.0";
148
- }
149
- })();
150
-
151
- if (!changelogHasVersion(CHANGELOG_FILE, pkgVersion)) {
152
- const commits = getCommitsBetween(null, "HEAD").map(parseCommit);
153
- const buckets = categorize(commits);
154
- sections.push(buildSection(pkgVersion, buckets));
155
- }
156
- } else {
157
- for (let i = 0; i < tags.length; i++) {
158
- const tag = tags[i];
159
- const previous = tags[i + 1] || null;
160
-
161
- if (changelogHasVersion(CHANGELOG_FILE, tag)) continue;
162
-
163
- const commits = getCommitsBetween(previous, tag).map(parseCommit);
164
- const buckets = categorize(commits);
165
- sections.push(buildSection(tag, buckets));
166
- }
167
- }
168
-
169
- if (!sections.length) {
170
- console.log("โ„น No new versions to add.");
171
- return;
172
- }
173
-
174
- if (isPreview) {
175
- fs.writeFileSync(CHANGELOG_FILE, sections.join("\n"), "utf8");
176
- } else {
177
- const existing = fs.existsSync(CHANGELOG_FILE)
178
- ? "\n" + fs.readFileSync(CHANGELOG_FILE, "utf8")
179
- : "";
180
- fs.writeFileSync(
181
- CHANGELOG_FILE,
182
- sections.join("\n") + existing,
183
- "utf8"
184
- );
185
- }
186
-
187
- console.log(
188
- isPreview ? "CHANGELOG preview generated." : "CHANGELOG updated."
189
- );
190
- }
191
-
192
- main();
1
+ #!/usr/bin/env node
2
+ import { execSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { computeVersion } from "./compute-version.js";
7
+
8
+ function run(cmd, cwd = process.cwd()) {
9
+ return execSync(cmd, { encoding: "utf8", cwd }).trim();
10
+ }
11
+
12
+ function getAllTags(cwd = process.cwd()) {
13
+ const sortStrategies = [
14
+ "-version:refname",
15
+ "-v:refname",
16
+ "-refname",
17
+ "-creatordate",
18
+ ];
19
+
20
+ for (const sort of sortStrategies) {
21
+ try {
22
+ const tags = run(`git tag --sort=${sort}`, cwd)
23
+ .split("\n")
24
+ .map(tag => tag.trim())
25
+ .filter(Boolean);
26
+
27
+ if (tags.length) {
28
+ return tags;
29
+ }
30
+ } catch (err) {
31
+ console.debug(`Sort '${sort}' not supported: ${err.message}`);
32
+ }
33
+ }
34
+
35
+ return [];
36
+ }
37
+
38
+ function getCommitsBetween(from, to, cwd = process.cwd()) {
39
+ const range = from ? `${from}..${to}` : to;
40
+ try {
41
+ return run(`git log ${range} --pretty=format:%H%x1f%s%x1f%b`, cwd)
42
+ .split("\n")
43
+ .filter(Boolean);
44
+ } catch {
45
+ return [];
46
+ }
47
+ }
48
+
49
+ function parseCommit(line) {
50
+ const [hash, subject = "", body = ""] = line.split("\x1f");
51
+ return { hash, subject: subject.trim(), body: body.trim() };
52
+ }
53
+
54
+ function cleanSubject(subject) {
55
+ let s = subject.replace(/^(:\S+: )?/, "");
56
+ s = s.replace(
57
+ /^(feat|fix|refactor|docs|chore|style|test|build|perf|ci|raw|cleanup|remove)(\(.+\))?(!)?:\s*/i,
58
+ ""
59
+ );
60
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
61
+ }
62
+
63
+ function categorize(commits) {
64
+ const buckets = {
65
+ breaking: [],
66
+ feat: [],
67
+ fix: [],
68
+ refactor: [],
69
+ chore: [],
70
+ docs: [],
71
+ style: [],
72
+ test: [],
73
+ build: [],
74
+ perf: [],
75
+ ci: [],
76
+ raw: [],
77
+ cleanup: [],
78
+ remove: [],
79
+ };
80
+
81
+ const reType =
82
+ /^(:\S+: )?(feat|fix|refactor|docs|chore|style|test|build|perf|ci|raw|cleanup|remove)(\(.+\))?(!)?:/i;
83
+
84
+ for (const c of commits) {
85
+ const { subject, body } = c;
86
+
87
+ if (/BREAKING CHANGE/i.test(body) || /!:/i.test(subject)) {
88
+ buckets.breaking.push({ desc: cleanSubject(subject), hash: c.hash });
89
+ continue;
90
+ }
91
+
92
+ const match = subject.match(reType);
93
+ const desc = cleanSubject(subject);
94
+
95
+ if (!match) {
96
+ buckets.chore.push({ desc: desc || subject, hash: c.hash });
97
+ continue;
98
+ }
99
+
100
+ const type = match[2].toLowerCase();
101
+ (buckets[type] || buckets.chore).push({ desc, hash: c.hash });
102
+ }
103
+
104
+ return buckets;
105
+ }
106
+
107
+ function buildSection(version, buckets) {
108
+ const out = [];
109
+ out.push(`## ${version}\n`);
110
+
111
+ const sections = [
112
+ ["breaking", "### ๐Ÿ’ฅ Breaking Changes"],
113
+ ["feat", "### โœจ Features"],
114
+ ["fix", "### ๐Ÿ› Fixes"],
115
+ ["refactor", "### โš™๏ธ Refactor"],
116
+ ["chore", "### ๐Ÿ”ง Chore"],
117
+ ["docs", "### ๐Ÿ“š Docs"],
118
+ ["style", "### ๐ŸŽจ Style"],
119
+ ["test", "### ๐Ÿงช Tests"],
120
+ ["build", "### ๐Ÿ›  Build"],
121
+ ["perf", "### โšก Performance"],
122
+ ["ci", "### ๐Ÿ” CI"],
123
+ ["raw", "### ๐Ÿ—ƒ Raw"],
124
+ ["cleanup", "### ๐Ÿงน Cleanup"],
125
+ ["remove", "### ๐Ÿ—‘ Remove"],
126
+ ];
127
+
128
+ let hasContent = false;
129
+
130
+ for (const [key, title] of sections) {
131
+ if (buckets[key].length) {
132
+ hasContent = true;
133
+ out.push(`${title}\n`);
134
+ for (const c of buckets[key]) out.push(`- ${c.desc}`);
135
+ out.push("");
136
+ }
137
+ }
138
+
139
+ if (!hasContent) out.push("_No changes._\n");
140
+
141
+ return out.join("\n");
142
+ }
143
+
144
+ function changelogHasVersion(file, version, cwd = process.cwd()) {
145
+ const full = path.join(cwd, file);
146
+ if (!fs.existsSync(full)) return false;
147
+ const content = fs.readFileSync(full, "utf8");
148
+ const safe = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
149
+ return new RegExp(`^##\\s+${safe}\\b`, "m").test(content);
150
+ }
151
+
152
+ export function generateChangelog({ isPreview = process.env.PREVIEW_MODE === "true", cwd = process.cwd() } = {}) {
153
+ const CHANGELOG_FILE = isPreview ? "CHANGELOG.preview.md" : "CHANGELOG.md";
154
+
155
+ const tags = getAllTags(cwd);
156
+ const sections = [];
157
+
158
+ const lastTag = tags[0] || null;
159
+ const obj = computeVersion({ cwd }) || "Unreleased";
160
+ const nextVersion = obj.nextVersion || "Unreleased";
161
+
162
+ if (nextVersion === "Unreleased" && isPreview) {
163
+ console.log("โ„น No version bump detected, showing Unreleased section.");
164
+ }
165
+
166
+ // Always generate the upcoming version (preview & release)
167
+ if (!changelogHasVersion(CHANGELOG_FILE, nextVersion, cwd)) {
168
+ const commits = getCommitsBetween(lastTag, "HEAD", cwd).map(parseCommit);
169
+
170
+ if (commits.length) {
171
+ const buckets = categorize(commits);
172
+ sections.push(buildSection(nextVersion, buckets));
173
+ }
174
+ }
175
+
176
+ // keep historical tag-based sections
177
+ for (let i = 0; i < tags.length; i++) {
178
+ const tag = tags[i];
179
+ const previous = tags[i + 1] || null;
180
+
181
+ if (changelogHasVersion(CHANGELOG_FILE, tag, cwd)) continue;
182
+
183
+ const commits = getCommitsBetween(previous, tag, cwd).map(parseCommit);
184
+ if (!commits.length) continue;
185
+
186
+ const buckets = categorize(commits);
187
+ sections.push(buildSection(tag, buckets));
188
+ }
189
+
190
+ if (!sections.length) {
191
+ console.log("โ„น No new versions to add.");
192
+ return;
193
+ }
194
+
195
+ const targetPath = path.join(cwd, CHANGELOG_FILE);
196
+
197
+ if (isPreview) {
198
+ fs.writeFileSync(targetPath, sections.join("\n"), "utf8");
199
+ } else {
200
+ const existing = fs.existsSync(targetPath) ? "\n" + fs.readFileSync(targetPath, "utf8") : "";
201
+ fs.writeFileSync(targetPath, sections.join("\n") + existing, "utf8");
202
+ }
203
+
204
+ console.log(isPreview ? "CHANGELOG preview generated." : "CHANGELOG updated.");
205
+ }
206
+
207
+ function main() {
208
+ const isPreview = process.env.PREVIEW_MODE === "true";
209
+ generateChangelog({ isPreview });
210
+ }
211
+
212
+ const __filename = fileURLToPath(import.meta.url);
213
+ if (process.argv[1] === __filename) main();