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.
- package/.github/workflows/create-release-pr.yml +30 -17
- package/.github/workflows/publish-on-merge.yml +26 -9
- package/CHANGELOG.md +38 -5
- package/README.md +195 -183
- package/bin/compute-version.js +173 -132
- package/bin/create-tag.js +26 -14
- package/bin/generate-changelog.js +213 -192
- package/bin/generate-release-notes.js +135 -123
- package/bin/preview.js +47 -47
- package/docs/api.md +28 -0
- package/docs/ci.md +210 -0
- package/docs/compute-version.md +204 -0
- package/eslint.config.js +89 -47
- package/lib/git.js +73 -0
- package/lib/utils.js +45 -0
- package/lib/versioning.js +110 -0
- package/package.json +62 -62
|
@@ -1,192 +1,213 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
raw
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
[
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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();
|