@programinglive/commiter 1.1.0 โ 1.1.3
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/ISSUE_TEMPLATE/bug_report.md +28 -28
- package/.github/ISSUE_TEMPLATE/config.yml +5 -5
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +24 -24
- package/CHANGELOG.md +59 -43
- package/CODE_OF_CONDUCT.md +36 -36
- package/LICENSE +21 -21
- package/PRD.md +86 -0
- package/PUBLISH.md +142 -142
- package/README.md +187 -187
- package/SECURITY.md +30 -30
- package/commitlint.config.cjs +4 -4
- package/docs/prd-fs-f-ok-warning.md +47 -0
- package/docs/release-notes/RELEASE_NOTES.md +52 -0
- package/docs/release-notes/fs-f-ok-warning.md +14 -0
- package/index.js +148 -148
- package/package.json +94 -94
- package/scripts/preload/fs-f-ok.cjs +23 -0
- package/scripts/release.js +127 -4
- package/scripts/update-release-notes.js +182 -0
package/package.json
CHANGED
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@programinglive/commiter",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Commiter keeps repositories release-ready by enforcing conventional commits, generating icon-rich changelog entries, and orchestrating semantic version bumps without manual toil. It bootstraps Husky hooks, commitlint rules, and release scripts that inspect history, detect framework-specific test commands, run them automatically, tag git releases, coordinate npm publishing, surface release metrics, enforce project-specific checks, and give maintainers observability across distributed teams. Plus!",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"commiter": "./index.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "node test
|
|
11
|
-
"prepare": "husky",
|
|
12
|
-
"release": "node scripts/release.js",
|
|
13
|
-
"release:major": "node scripts/release.js major",
|
|
14
|
-
"release:minor": "node scripts/release.js minor",
|
|
15
|
-
"release:patch": "node scripts/release.js patch"
|
|
16
|
-
},
|
|
17
|
-
"keywords": [
|
|
18
|
-
"commit",
|
|
19
|
-
"conventional-commits",
|
|
20
|
-
"standard-version",
|
|
21
|
-
"changelog",
|
|
22
|
-
"versioning",
|
|
23
|
-
"semantic-release",
|
|
24
|
-
"git-hooks",
|
|
25
|
-
"husky",
|
|
26
|
-
"commitlint"
|
|
27
|
-
],
|
|
28
|
-
"author": "Programming Live",
|
|
29
|
-
"license": "MIT",
|
|
30
|
-
"repository": {
|
|
31
|
-
"type": "git",
|
|
32
|
-
"url": "git+https://github.com/programinglive/commiter.git"
|
|
33
|
-
},
|
|
34
|
-
"bugs": {
|
|
35
|
-
"url": "https://github.com/programinglive/commiter/issues"
|
|
36
|
-
},
|
|
37
|
-
"homepage": "https://github.com/programinglive/commiter#readme",
|
|
38
|
-
"type": "commonjs",
|
|
39
|
-
"standard-version": {
|
|
40
|
-
"releaseCommitMessageFormat": "chore(release): {{currentTag}} ๐",
|
|
41
|
-
"types": [
|
|
42
|
-
{
|
|
43
|
-
"type": "feat",
|
|
44
|
-
"section": "โจ Features"
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"type": "fix",
|
|
48
|
-
"section": "๐ Bug Fixes"
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
"type": "perf",
|
|
52
|
-
"section": "โก Performance"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"type": "refactor",
|
|
56
|
-
"section": "โป๏ธ Refactors"
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"type": "docs",
|
|
60
|
-
"section": "๐ Documentation"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"type": "style",
|
|
64
|
-
"section": "๐ Styles"
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
"type": "test",
|
|
68
|
-
"section": "โ
Tests"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"type": "build",
|
|
72
|
-
"section": "๐๏ธ Build System"
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"type": "ci",
|
|
76
|
-
"section": "๐ท Continuous Integration"
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
"type": "chore",
|
|
80
|
-
"section": "๐งน Chores"
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"type": "revert",
|
|
84
|
-
"section": "โช Reverts"
|
|
85
|
-
}
|
|
86
|
-
]
|
|
87
|
-
},
|
|
88
|
-
"devDependencies": {
|
|
89
|
-
"@commitlint/cli": "^20.1.0",
|
|
90
|
-
"@commitlint/config-conventional": "^20.0.0",
|
|
91
|
-
"husky": "^9.1.7",
|
|
92
|
-
"standard-version": "^9.5.0"
|
|
93
|
-
}
|
|
94
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@programinglive/commiter",
|
|
3
|
+
"version": "1.1.3",
|
|
4
|
+
"description": "Commiter keeps repositories release-ready by enforcing conventional commits, generating icon-rich changelog entries, and orchestrating semantic version bumps without manual toil. It bootstraps Husky hooks, commitlint rules, and release scripts that inspect history, detect framework-specific test commands, run them automatically, tag git releases, coordinate npm publishing, surface release metrics, enforce project-specific checks, and give maintainers observability across distributed teams. Plus!",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"commiter": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"prepare": "husky",
|
|
12
|
+
"release": "node scripts/release.js",
|
|
13
|
+
"release:major": "node scripts/release.js major",
|
|
14
|
+
"release:minor": "node scripts/release.js minor",
|
|
15
|
+
"release:patch": "node scripts/release.js patch"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"commit",
|
|
19
|
+
"conventional-commits",
|
|
20
|
+
"standard-version",
|
|
21
|
+
"changelog",
|
|
22
|
+
"versioning",
|
|
23
|
+
"semantic-release",
|
|
24
|
+
"git-hooks",
|
|
25
|
+
"husky",
|
|
26
|
+
"commitlint"
|
|
27
|
+
],
|
|
28
|
+
"author": "Programming Live",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/programinglive/commiter.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/programinglive/commiter/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/programinglive/commiter#readme",
|
|
38
|
+
"type": "commonjs",
|
|
39
|
+
"standard-version": {
|
|
40
|
+
"releaseCommitMessageFormat": "chore(release): {{currentTag}} ๐",
|
|
41
|
+
"types": [
|
|
42
|
+
{
|
|
43
|
+
"type": "feat",
|
|
44
|
+
"section": "โจ Features"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "fix",
|
|
48
|
+
"section": "๐ Bug Fixes"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"type": "perf",
|
|
52
|
+
"section": "โก Performance"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "refactor",
|
|
56
|
+
"section": "โป๏ธ Refactors"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"type": "docs",
|
|
60
|
+
"section": "๐ Documentation"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"type": "style",
|
|
64
|
+
"section": "๐ Styles"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"type": "test",
|
|
68
|
+
"section": "โ
Tests"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"type": "build",
|
|
72
|
+
"section": "๐๏ธ Build System"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"type": "ci",
|
|
76
|
+
"section": "๐ท Continuous Integration"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"type": "chore",
|
|
80
|
+
"section": "๐งน Chores"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"type": "revert",
|
|
84
|
+
"section": "โช Reverts"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@commitlint/cli": "^20.1.0",
|
|
90
|
+
"@commitlint/config-conventional": "^20.0.0",
|
|
91
|
+
"husky": "^9.1.7",
|
|
92
|
+
"standard-version": "^9.5.0"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const Module = require('module');
|
|
3
|
+
|
|
4
|
+
const targetPath = require.resolve('standard-version/lib/lifecycles/changelog');
|
|
5
|
+
|
|
6
|
+
if (!Module._commiterPatchedFsFok) {
|
|
7
|
+
const originalLoader = Module._extensions['.js'];
|
|
8
|
+
|
|
9
|
+
Module._extensions['.js'] = function patchedLoader(module, filename) {
|
|
10
|
+
if (filename === targetPath) {
|
|
11
|
+
let source = fs.readFileSync(filename, 'utf8');
|
|
12
|
+
if (source.includes('fs.F_OK')) {
|
|
13
|
+
source = source.replace(/fs\.F_OK/g, 'fs.constants.F_OK');
|
|
14
|
+
}
|
|
15
|
+
module._compile(source, filename);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return originalLoader(module, filename);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
Module._commiterPatchedFsFok = true;
|
|
23
|
+
}
|
package/scripts/release.js
CHANGED
|
@@ -4,6 +4,13 @@ const { spawnSync } = require('child_process');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
+
const { updateReleaseNotes } = require('./update-release-notes');
|
|
8
|
+
|
|
9
|
+
const fsFokPreloadPath = path.join(__dirname, 'preload', 'fs-f-ok.cjs');
|
|
10
|
+
const FS_FOK_PRELOAD_FLAG = buildPreloadFlag(fsFokPreloadPath);
|
|
11
|
+
|
|
12
|
+
require('./preload/fs-f-ok.cjs');
|
|
13
|
+
|
|
7
14
|
const VALID_RELEASE_TYPES = new Set([
|
|
8
15
|
'major',
|
|
9
16
|
'minor',
|
|
@@ -127,7 +134,13 @@ function runProjectTests({ spawn = spawnSync, env = process.env, cwd = process.c
|
|
|
127
134
|
});
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
function runRelease({
|
|
137
|
+
function runRelease({
|
|
138
|
+
argv = process.argv,
|
|
139
|
+
env = process.env,
|
|
140
|
+
spawn = spawnSync,
|
|
141
|
+
cwd = process.cwd(),
|
|
142
|
+
dependencies = {}
|
|
143
|
+
} = {}) {
|
|
131
144
|
const { releaseType: cliReleaseType, extraArgs } = getCliArguments(argv);
|
|
132
145
|
const inferredReleaseType = cliReleaseType || getNpmRunArgument(env);
|
|
133
146
|
const standardVersionArgs = buildStandardVersionArgs({
|
|
@@ -135,15 +148,89 @@ function runRelease({ argv = process.argv, env = process.env, spawn = spawnSync
|
|
|
135
148
|
extraArgs
|
|
136
149
|
});
|
|
137
150
|
|
|
151
|
+
const checkWorkingTreeClean = dependencies.isWorkingTreeClean || (() => isWorkingTreeClean({ cwd }));
|
|
152
|
+
const workingTreeClean = checkWorkingTreeClean();
|
|
153
|
+
if (!workingTreeClean) {
|
|
154
|
+
throw new Error('Working tree has uncommitted changes. Commit or stash before running release.');
|
|
155
|
+
}
|
|
156
|
+
|
|
138
157
|
const testResult = runProjectTests({ spawn, env });
|
|
139
158
|
if (testResult && typeof testResult.status === 'number' && testResult.status !== 0) {
|
|
140
159
|
return testResult;
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
const standardVersionBin = require.resolve('standard-version/bin/cli.js');
|
|
144
|
-
|
|
145
|
-
|
|
163
|
+
const childEnv = appendPreloadToNodeOptions(env, FS_FOK_PRELOAD_FLAG);
|
|
164
|
+
const releaseResult = spawn(process.execPath, [standardVersionBin, ...standardVersionArgs], {
|
|
165
|
+
stdio: 'inherit',
|
|
166
|
+
env: childEnv,
|
|
167
|
+
cwd
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (releaseResult && typeof releaseResult.status === 'number' && releaseResult.status !== 0) {
|
|
171
|
+
return releaseResult;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const releaseNotesPath = dependencies.releaseNotesPath || path.join('docs', 'release-notes', 'RELEASE_NOTES.md');
|
|
175
|
+
const updateNotes = dependencies.updateReleaseNotes || ((options = {}) => updateReleaseNotes({ rootDir: cwd, ...options }));
|
|
176
|
+
const gitAdd = dependencies.gitAdd || ((files) => spawnSync('git', ['add', ...files], { stdio: 'inherit', cwd }));
|
|
177
|
+
const gitCommitAmend = dependencies.gitCommitAmend || (() => spawnSync('git', ['commit', '--amend', '--no-edit'], { stdio: 'inherit', cwd }));
|
|
178
|
+
const gitTag = dependencies.gitTag || ((tagName, tagMessage) => {
|
|
179
|
+
const args = tagMessage
|
|
180
|
+
? ['tag', '-f', '-a', tagName, '-m', tagMessage]
|
|
181
|
+
: ['tag', '-f', tagName];
|
|
182
|
+
return spawnSync('git', args, { stdio: 'inherit', cwd });
|
|
146
183
|
});
|
|
184
|
+
const getCommitMessage = dependencies.getCommitMessage || (() => spawnSync('git', ['log', '-1', '--pretty=%s'], { cwd, encoding: 'utf8' }));
|
|
185
|
+
const buildTagName = dependencies.buildTagName || ((version) => `v${version}`);
|
|
186
|
+
const loadPackage = dependencies.loadPackageJson || ((dir) => loadPackageJson(dir));
|
|
187
|
+
|
|
188
|
+
let notesUpdated = false;
|
|
189
|
+
try {
|
|
190
|
+
notesUpdated = updateNotes();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.warn(`โ ๏ธ Skipping release notes update: ${error.message}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (notesUpdated) {
|
|
196
|
+
const gitAddResult = gitAdd([releaseNotesPath]);
|
|
197
|
+
if (!gitAddResult || typeof gitAddResult.status !== 'number' || gitAddResult.status !== 0) {
|
|
198
|
+
console.warn('โ ๏ธ Release notes updated but failed to stage changes. Please add them manually.');
|
|
199
|
+
return releaseResult;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const gitCommitResult = gitCommitAmend();
|
|
203
|
+
if (!gitCommitResult || typeof gitCommitResult.status !== 'number' || gitCommitResult.status !== 0) {
|
|
204
|
+
console.warn('โ ๏ธ Release notes updated but failed to amend release commit. Please amend manually.');
|
|
205
|
+
return releaseResult;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const packageJson = loadPackage(cwd);
|
|
209
|
+
const version = packageJson && packageJson.version;
|
|
210
|
+
if (!version) {
|
|
211
|
+
console.warn('โ ๏ธ Cannot retag release: package.json version missing. Please update tag manually.');
|
|
212
|
+
return releaseResult;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const tagName = buildTagName(version);
|
|
216
|
+
let tagMessage;
|
|
217
|
+
try {
|
|
218
|
+
const commitMessageResult = getCommitMessage();
|
|
219
|
+
if (commitMessageResult && typeof commitMessageResult.status === 'number' && commitMessageResult.status === 0) {
|
|
220
|
+
const output = typeof commitMessageResult.stdout === 'string' ? commitMessageResult.stdout : '';
|
|
221
|
+
tagMessage = output.trim();
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.warn(`โ ๏ธ Unable to read release commit message: ${error.message}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const gitTagResult = gitTag(tagName, tagMessage);
|
|
228
|
+
if (!gitTagResult || typeof gitTagResult.status !== 'number' || gitTagResult.status !== 0) {
|
|
229
|
+
console.warn(`โ ๏ธ Failed to retag ${tagName}. Please update the tag manually before pushing.`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return releaseResult;
|
|
147
234
|
}
|
|
148
235
|
|
|
149
236
|
if (require.main === module) {
|
|
@@ -167,5 +254,41 @@ module.exports = {
|
|
|
167
254
|
loadPackageJson,
|
|
168
255
|
detectPackageManager,
|
|
169
256
|
runProjectTests,
|
|
170
|
-
runRelease
|
|
257
|
+
runRelease,
|
|
258
|
+
isWorkingTreeClean
|
|
171
259
|
};
|
|
260
|
+
|
|
261
|
+
function buildPreloadFlag(filePath) {
|
|
262
|
+
const resolved = path.resolve(filePath);
|
|
263
|
+
const escaped = resolved.includes(' ')
|
|
264
|
+
? `"${resolved.replace(/"/g, '\"')}"`
|
|
265
|
+
: resolved;
|
|
266
|
+
return `--require ${escaped}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function appendPreloadToNodeOptions(env, preloadFlag) {
|
|
270
|
+
const nextEnv = { ...env };
|
|
271
|
+
const existing = typeof nextEnv.NODE_OPTIONS === 'string' ? nextEnv.NODE_OPTIONS.trim() : '';
|
|
272
|
+
nextEnv.NODE_OPTIONS = existing ? `${existing} ${preloadFlag}` : preloadFlag;
|
|
273
|
+
return nextEnv;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isWorkingTreeClean({ cwd = process.cwd() } = {}) {
|
|
277
|
+
const result = spawnSync('git', ['status', '--porcelain'], {
|
|
278
|
+
cwd,
|
|
279
|
+
encoding: 'utf8'
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (result.error) {
|
|
283
|
+
throw result.error;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
287
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
|
|
288
|
+
const message = stderr || 'Failed to determine working tree status.';
|
|
289
|
+
throw new Error(message);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const output = typeof result.stdout === 'string' ? result.stdout : '';
|
|
293
|
+
return output.trim().length === 0;
|
|
294
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function updateReleaseNotes({
|
|
7
|
+
rootDir = process.cwd(),
|
|
8
|
+
releaseNotesRelativePath = path.join('docs', 'release-notes', 'RELEASE_NOTES.md'),
|
|
9
|
+
changelogRelativePath = 'CHANGELOG.md',
|
|
10
|
+
packageJsonRelativePath = 'package.json',
|
|
11
|
+
now = () => new Date()
|
|
12
|
+
} = {}) {
|
|
13
|
+
const releaseNotesPath = path.join(rootDir, releaseNotesRelativePath);
|
|
14
|
+
if (!fs.existsSync(releaseNotesPath)) {
|
|
15
|
+
console.log(`โน๏ธ Release notes file not found at ${releaseNotesPath}; skipping update.`);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const packageJsonPath = path.join(rootDir, packageJsonRelativePath);
|
|
20
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
21
|
+
console.log(`โน๏ธ package.json not found at ${packageJsonPath}; skipping release notes update.`);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const changelogPath = path.join(rootDir, changelogRelativePath);
|
|
26
|
+
if (!fs.existsSync(changelogPath)) {
|
|
27
|
+
console.log(`โน๏ธ CHANGELOG not found at ${changelogPath}; skipping release notes update.`);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
32
|
+
const version = packageJson.version;
|
|
33
|
+
if (!version) {
|
|
34
|
+
console.log('โน๏ธ package.json version is missing; skipping release notes update.');
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const releaseNotesContent = fs.readFileSync(releaseNotesPath, 'utf8');
|
|
39
|
+
if (releaseNotesContent.includes(`| ${version} `) || releaseNotesContent.includes(`## ${version}`)) {
|
|
40
|
+
console.log(`โน๏ธ Release notes already contain version ${version}; skipping.`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const changelogContent = fs.readFileSync(changelogPath, 'utf8');
|
|
45
|
+
const changelogInfo = extractChangelogInfo({ changelogContent, version });
|
|
46
|
+
|
|
47
|
+
const releaseDate = changelogInfo.date || formatDate(now());
|
|
48
|
+
const highlight = changelogInfo.highlight || changelogInfo.heading || 'See CHANGELOG for details.';
|
|
49
|
+
const sectionHeading = changelogInfo.heading || '';
|
|
50
|
+
const detailBullets = Array.isArray(changelogInfo.bullets) && changelogInfo.bullets.length > 0
|
|
51
|
+
? changelogInfo.bullets
|
|
52
|
+
: [highlight];
|
|
53
|
+
|
|
54
|
+
const updatedContent = insertReleaseNotesEntry({
|
|
55
|
+
content: releaseNotesContent,
|
|
56
|
+
version,
|
|
57
|
+
releaseDate,
|
|
58
|
+
highlight,
|
|
59
|
+
sectionHeading,
|
|
60
|
+
detailBullets
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
fs.writeFileSync(releaseNotesPath, updatedContent);
|
|
64
|
+
console.log(`โ
Added release notes entry for version ${version}.`);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function insertReleaseNotesEntry({ content, version, releaseDate, highlight, sectionHeading, detailBullets }) {
|
|
69
|
+
const lines = content.split('\n');
|
|
70
|
+
const headerSeparatorIndex = lines.findIndex((line) => line.trim().startsWith('|---------'));
|
|
71
|
+
if (headerSeparatorIndex === -1) {
|
|
72
|
+
throw new Error('Unable to locate release notes table header.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const newRow = `| ${version} | ${releaseDate} | ${escapePipes(highlight)} |`;
|
|
76
|
+
lines.splice(headerSeparatorIndex + 1, 0, newRow);
|
|
77
|
+
|
|
78
|
+
const sectionLines = [];
|
|
79
|
+
sectionLines.push('');
|
|
80
|
+
const trimmedHeading = typeof sectionHeading === 'string' ? sectionHeading.trim() : '';
|
|
81
|
+
const headingSuffix = trimmedHeading ? ` โ ${trimmedHeading}` : '';
|
|
82
|
+
sectionLines.push(`## ${version}${headingSuffix}`);
|
|
83
|
+
sectionLines.push('');
|
|
84
|
+
sectionLines.push(`Released on **${releaseDate}**.`);
|
|
85
|
+
sectionLines.push('');
|
|
86
|
+
for (const bullet of detailBullets) {
|
|
87
|
+
sectionLines.push(`- ${bullet}`);
|
|
88
|
+
}
|
|
89
|
+
sectionLines.push('');
|
|
90
|
+
|
|
91
|
+
const firstSectionIndex = lines.findIndex((line, idx) => idx > headerSeparatorIndex && line.startsWith('## '));
|
|
92
|
+
if (firstSectionIndex === -1) {
|
|
93
|
+
lines.push(...sectionLines);
|
|
94
|
+
} else {
|
|
95
|
+
lines.splice(firstSectionIndex, 0, ...sectionLines);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractChangelogInfo({ changelogContent, version }) {
|
|
102
|
+
const versionHeadingRegex = new RegExp(`^### \\[${escapeRegExp(version)}\\][^\n]*`, 'm');
|
|
103
|
+
const match = changelogContent.match(versionHeadingRegex);
|
|
104
|
+
if (!match) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const headingIndex = match.index;
|
|
109
|
+
const rest = changelogContent.slice(headingIndex + match[0].length);
|
|
110
|
+
const nextVersionRegex = /^### \[[^\n]*\]/m;
|
|
111
|
+
const nextVersionMatch = nextVersionRegex.exec(rest);
|
|
112
|
+
const section = nextVersionMatch ? rest.slice(0, nextVersionMatch.index) : rest;
|
|
113
|
+
|
|
114
|
+
const dateMatch = match[0].match(/\((\d{4}-\d{2}-\d{2})\)/);
|
|
115
|
+
const date = dateMatch ? dateMatch[1] : undefined;
|
|
116
|
+
|
|
117
|
+
const categoryRegex = /^###\s+([^\n]+)$/m;
|
|
118
|
+
const categoryMatch = section.match(categoryRegex);
|
|
119
|
+
let heading;
|
|
120
|
+
if (categoryMatch) {
|
|
121
|
+
heading = sanitizeText(categoryMatch[1]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const bullets = [];
|
|
125
|
+
const bulletRegex = /^\*\s+([^\n]+)$/gm;
|
|
126
|
+
let bulletMatch;
|
|
127
|
+
while ((bulletMatch = bulletRegex.exec(section)) !== null) {
|
|
128
|
+
const sanitized = sanitizeText(bulletMatch[1]);
|
|
129
|
+
if (!bullets.includes(sanitized)) {
|
|
130
|
+
bullets.push(sanitized);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const highlight = bullets.length > 0 ? bullets[0] : undefined;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
date,
|
|
138
|
+
highlight,
|
|
139
|
+
heading,
|
|
140
|
+
bullets
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function sanitizeText(text) {
|
|
145
|
+
return text
|
|
146
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
|
|
147
|
+
.replace(/\s+/g, ' ')
|
|
148
|
+
.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function escapeRegExp(string) {
|
|
152
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function escapePipes(text) {
|
|
156
|
+
return text.replace(/\|/g, '\\|');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatDate(date) {
|
|
160
|
+
const year = date.getFullYear();
|
|
161
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
162
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
163
|
+
return `${year}-${month}-${day}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
updateReleaseNotes,
|
|
168
|
+
insertReleaseNotesEntry,
|
|
169
|
+
extractChangelogInfo,
|
|
170
|
+
sanitizeText,
|
|
171
|
+
escapePipes,
|
|
172
|
+
formatDate
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (require.main === module) {
|
|
176
|
+
try {
|
|
177
|
+
updateReleaseNotes();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`โ Failed to update release notes: ${error.message}`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
}
|