calver-bump 0.1.4 → 0.1.6
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 +4 -1
- package/package.json +1 -1
- package/src/index.js +136 -12
- package/test/release.test.js +107 -6
package/README.md
CHANGED
|
@@ -94,11 +94,14 @@ Project defaults can be stored in `.calverbumprc.json`:
|
|
|
94
94
|
- The default `short` format is `YY.MMDD` for the first release of the day, then `YY.MMDD.1`, `YY.MMDD.2`, etc.
|
|
95
95
|
- The optional `compact` format is `YYMMDD` for the first release of the day, then `YYMMDD.1`, `YYMMDD.2`, etc.
|
|
96
96
|
- The optional `long` format is `YYYY.MM.DD` for the first release of the day, then `YYYY.MM.DD.1`, `YYYY.MM.DD.2`, etc.
|
|
97
|
+
- Changelog release headings use the CalVer version only and do not append a separate `YYYY-MM-DD` date.
|
|
97
98
|
- Existing `v`-prefixed tags are considered when calculating the next sequence number.
|
|
98
99
|
- Changelog ranges start from the nearest reachable tag, even when it is not a CalVer tag.
|
|
99
100
|
- Changelog entries include conventional commit subjects only, such as `feat:`, `fix(scope):`, or `chore!:`.
|
|
100
101
|
- Changelog entries are grouped into `Features`, `Fixes`, and `Other Changes`.
|
|
101
|
-
- Changelog entries link to
|
|
102
|
+
- Changelog entries link to GitHub pull requests or GitLab merge requests when the local git message includes references such as `#123`, `Merge pull request #123`, `!123`, or `See merge request group/project!123`.
|
|
103
|
+
- Changelog entries fall back to commit hash links for GitHub and GitLab-style remotes when no pull/merge request reference is found.
|
|
104
|
+
- Release entries include a `Full Changelog` section with a deduped list of pull/merge requests found in the release range, including the local commit title when available.
|
|
102
105
|
- Later releases prepend only commits since the previous nearest reachable tag.
|
|
103
106
|
- Release tags are annotated so `git push --follow-tags <remote> <branch>` pushes them.
|
|
104
107
|
- The working tree must be clean before creating a real release.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { access, readFile, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { nextCalVer } from './calver.js';
|
|
7
7
|
|
|
8
8
|
const execFile = promisify(execFileCallback);
|
|
9
9
|
|
|
@@ -129,12 +129,11 @@ async function prependChangelog(cwd, version, date, options = {}) {
|
|
|
129
129
|
const notes = await releaseNotes(cwd, { ...options, existingChangelog: existing });
|
|
130
130
|
const heading = formatReleaseHeading({
|
|
131
131
|
version,
|
|
132
|
-
date,
|
|
133
132
|
previousTag: notes.previousTag,
|
|
134
133
|
tag: `${options.tagPrefix ?? ''}${version}`,
|
|
135
134
|
compareUrlBuilder: notes.compareUrlBuilder,
|
|
136
135
|
});
|
|
137
|
-
const entry = `${heading}\n\n${formatReleaseNotes(notes.changes)}\n`;
|
|
136
|
+
const entry = `${heading}\n\n${formatReleaseNotes(notes.changes)}${formatFullChangelog(notes.requests)}\n`;
|
|
138
137
|
|
|
139
138
|
const body = existing.trim().startsWith('# Changelog')
|
|
140
139
|
? existing.replace(/^# Changelog\s*/, `# Changelog\n\n${entry}\n`)
|
|
@@ -145,33 +144,38 @@ async function prependChangelog(cwd, version, date, options = {}) {
|
|
|
145
144
|
|
|
146
145
|
async function releaseNotes(cwd, options = {}) {
|
|
147
146
|
await fetchTags(cwd, options.remote ?? 'origin');
|
|
148
|
-
const latestTag = await
|
|
147
|
+
const latestTag = await latestReleaseTag(cwd, options.existingChangelog ?? '');
|
|
149
148
|
const range = latestTag ? [`${latestTag}..HEAD`] : [];
|
|
150
149
|
const commits = await gitCommits(cwd, range);
|
|
151
150
|
const remoteUrl = await getRemoteUrl(cwd, options.remote ?? 'origin');
|
|
152
151
|
const commitUrlBuilder = remoteUrl ? buildCommitUrlBuilder(remoteUrl) : null;
|
|
153
152
|
const compareUrlBuilder = remoteUrl ? buildCompareUrlBuilder(remoteUrl) : null;
|
|
153
|
+
const requestUrlBuilder = remoteUrl ? buildRequestUrlBuilder(remoteUrl) : null;
|
|
154
154
|
const allowedTypes = options.types ?? ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'];
|
|
155
155
|
const conventionalCommits = commits
|
|
156
|
+
.map((commit) => ({ ...commit, subject: conventionalSubjectForCommit(commit) ?? commit.subject }))
|
|
156
157
|
.filter((commit) => isConventionalCommit(commit.subject))
|
|
157
158
|
.filter((commit) => allowedTypes.includes(conventionalType(commit.subject)))
|
|
158
159
|
.filter((commit) => !isCommitInChangelog(commit, options.existingChangelog ?? ''))
|
|
159
160
|
.map((commit) => ({
|
|
160
161
|
...commit,
|
|
162
|
+
request: requestForCommit(commit, requestUrlBuilder),
|
|
161
163
|
url: commitUrlBuilder ? commitUrlBuilder(commit.hash) : null,
|
|
162
164
|
}));
|
|
165
|
+
const requests = uniqueRequests(commits.map((commit) => requestForCommit(commit, requestUrlBuilder)).filter(Boolean));
|
|
163
166
|
return {
|
|
164
167
|
previousTag: latestTag,
|
|
165
168
|
compareUrlBuilder,
|
|
166
169
|
changes: conventionalCommits.length > 0 ? conventionalCommits : ['No conventional commits in this release.'],
|
|
170
|
+
requests,
|
|
167
171
|
};
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
function formatReleaseHeading({ version,
|
|
174
|
+
function formatReleaseHeading({ version, previousTag, tag, compareUrlBuilder }) {
|
|
171
175
|
const label = previousTag && compareUrlBuilder
|
|
172
176
|
? `[${version}](${compareUrlBuilder(previousTag, tag)})`
|
|
173
177
|
: version;
|
|
174
|
-
return `## ${label}
|
|
178
|
+
return `## ${label}`;
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
function isCommitInChangelog(commit, changelog) {
|
|
@@ -183,6 +187,16 @@ function isConventionalCommit(subject) {
|
|
|
183
187
|
return /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: .+/.test(subject);
|
|
184
188
|
}
|
|
185
189
|
|
|
190
|
+
function conventionalSubjectForCommit(commit) {
|
|
191
|
+
return commitLines(commit).find((line) => isConventionalCommit(line)) ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function commitLines(commit) {
|
|
195
|
+
return [commit.subject, ...(commit.body ?? '').split('\n')]
|
|
196
|
+
.map((line) => line.trim())
|
|
197
|
+
.filter(Boolean);
|
|
198
|
+
}
|
|
199
|
+
|
|
186
200
|
function formatReleaseNotes(changes) {
|
|
187
201
|
if (changes.length === 1 && changes[0] === 'No conventional commits in this release.') {
|
|
188
202
|
return `- ${changes[0]}`;
|
|
@@ -203,24 +217,41 @@ function formatReleaseNotes(changes) {
|
|
|
203
217
|
.join('\n\n');
|
|
204
218
|
}
|
|
205
219
|
|
|
220
|
+
function formatFullChangelog(requests) {
|
|
221
|
+
if (requests.length === 0) {
|
|
222
|
+
return '';
|
|
223
|
+
}
|
|
224
|
+
return `\n\n### Full Changelog\n\n${requests.map((request) => `- ${formatRequestEntry(request)}`).join('\n')}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
206
227
|
function conventionalType(subject) {
|
|
207
228
|
return /^(?<type>[a-z]+)(\([^)]+\))?!?: .+/.exec(subject)?.groups.type;
|
|
208
229
|
}
|
|
209
230
|
|
|
210
231
|
function formatCommitEntry(commit) {
|
|
211
232
|
const shortHash = commit.hash.slice(0, 7);
|
|
212
|
-
const suffix = commit.
|
|
233
|
+
const suffix = commit.request
|
|
234
|
+
? ` (${formatRequestLink(commit.request)})`
|
|
235
|
+
: commit.url ? ` ([${shortHash}](${commit.url}))` : ` (${shortHash})`;
|
|
213
236
|
return `${commit.subject}${suffix}`;
|
|
214
237
|
}
|
|
215
238
|
|
|
239
|
+
function formatRequestLink(request) {
|
|
240
|
+
return request.url ? `[${request.label}](${request.url})` : request.label;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatRequestEntry(request) {
|
|
244
|
+
return request.title ? `${formatRequestLink(request)} ${request.title}` : formatRequestLink(request);
|
|
245
|
+
}
|
|
246
|
+
|
|
216
247
|
async function gitCommits(cwd, range) {
|
|
217
|
-
const { stdout } = await git(cwd, ['log', '--pretty=format:%H%x00%s', ...range]);
|
|
248
|
+
const { stdout } = await git(cwd, ['log', '--pretty=format:%H%x00%s%x00%B%x1e', ...range]);
|
|
218
249
|
return stdout
|
|
219
|
-
.split('\
|
|
250
|
+
.split('\x1e')
|
|
220
251
|
.filter(Boolean)
|
|
221
|
-
.map((
|
|
222
|
-
const [hash, subject] =
|
|
223
|
-
return { hash, subject };
|
|
252
|
+
.map((record) => {
|
|
253
|
+
const [hash, subject, body = ''] = record.replace(/^\n+|\n+$/g, '').split('\0');
|
|
254
|
+
return { hash, subject, body };
|
|
224
255
|
});
|
|
225
256
|
}
|
|
226
257
|
|
|
@@ -267,6 +298,77 @@ function buildCompareUrlBuilder(remote) {
|
|
|
267
298
|
return null;
|
|
268
299
|
}
|
|
269
300
|
|
|
301
|
+
function buildRequestUrlBuilder(remote) {
|
|
302
|
+
const parsed = parseGitRemote(remote);
|
|
303
|
+
if (!parsed) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const baseUrl = `https://${parsed.host}/${parsed.repo}`;
|
|
308
|
+
if (parsed.host === 'github.com') {
|
|
309
|
+
return (request) => request.provider === 'github' ? `${baseUrl}/pull/${request.number}` : null;
|
|
310
|
+
}
|
|
311
|
+
if (parsed.host.includes('gitlab')) {
|
|
312
|
+
return (request) => request.provider === 'gitlab' ? `${baseUrl}/-/merge_requests/${request.number}` : null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function requestForCommit(commit, requestUrlBuilder) {
|
|
319
|
+
const request = parseRequestReference(`${commit.subject}\n${commit.body ?? ''}`);
|
|
320
|
+
if (!request) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
...request,
|
|
325
|
+
title: requestTitleForCommit(commit),
|
|
326
|
+
url: requestUrlBuilder ? requestUrlBuilder(request) : null,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function requestTitleForCommit(commit) {
|
|
331
|
+
const conventionalSubject = conventionalSubjectForCommit(commit);
|
|
332
|
+
if (conventionalSubject) {
|
|
333
|
+
return conventionalSubject;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return commitLines(commit)
|
|
337
|
+
.find((line) => line && !parseRequestReference(line) && !/^Merge\b/i.test(line)) ?? null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function parseRequestReference(message) {
|
|
341
|
+
const gitlabMerge = /(?:^|\s)(?:See merge request\s+\S+!|!)(?<number>\d+)(?=\D|$)/i.exec(message);
|
|
342
|
+
if (gitlabMerge) {
|
|
343
|
+
return { provider: 'gitlab', number: gitlabMerge.groups.number, label: `!${gitlabMerge.groups.number}` };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const githubPull = /(?:Merge pull request\s+#|#)(?<number>\d+)(?=\D|$)/i.exec(message);
|
|
347
|
+
if (githubPull) {
|
|
348
|
+
return { provider: 'github', number: githubPull.groups.number, label: `#${githubPull.groups.number}` };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function uniqueRequests(requests) {
|
|
355
|
+
const seen = new Set();
|
|
356
|
+
const unique = [];
|
|
357
|
+
for (const request of requests) {
|
|
358
|
+
const key = `${request.provider}:${request.number}`;
|
|
359
|
+
if (seen.has(key)) {
|
|
360
|
+
const existing = unique.find((candidate) => `${candidate.provider}:${candidate.number}` === key);
|
|
361
|
+
if (existing && !existing.title && request.title) {
|
|
362
|
+
existing.title = request.title;
|
|
363
|
+
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
seen.add(key);
|
|
367
|
+
unique.push(request);
|
|
368
|
+
}
|
|
369
|
+
return unique;
|
|
370
|
+
}
|
|
371
|
+
|
|
270
372
|
function parseGitRemote(remote) {
|
|
271
373
|
const sshMatch = /^git@(?<host>[^:]+):(?<repo>.+?)(?:\.git)?$/.exec(remote);
|
|
272
374
|
if (sshMatch) {
|
|
@@ -281,6 +383,28 @@ function parseGitRemote(remote) {
|
|
|
281
383
|
return null;
|
|
282
384
|
}
|
|
283
385
|
|
|
386
|
+
async function latestReleaseTag(cwd, changelog) {
|
|
387
|
+
const changelogTag = latestChangelogCompareTarget(changelog);
|
|
388
|
+
if (changelogTag && await tagExists(cwd, changelogTag)) {
|
|
389
|
+
return changelogTag;
|
|
390
|
+
}
|
|
391
|
+
return latestReachableTag(cwd);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function latestChangelogCompareTarget(changelog) {
|
|
395
|
+
const match = /^## \[[^\]]+\]\([^)]*\/(?:-\/)?compare\/[^)]*?\.{3}(?<tag>[^)\s]+)\)/m.exec(changelog);
|
|
396
|
+
return match?.groups.tag ?? null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function tagExists(cwd, tag) {
|
|
400
|
+
try {
|
|
401
|
+
await git(cwd, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}^{}`]);
|
|
402
|
+
return true;
|
|
403
|
+
} catch {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
284
408
|
async function latestReachableTag(cwd) {
|
|
285
409
|
try {
|
|
286
410
|
const { stdout } = await git(cwd, ['describe', '--tags', '--abbrev=0']);
|
package/test/release.test.js
CHANGED
|
@@ -64,7 +64,7 @@ test('runRelease updates package.json, prepends changelog, commits, and tags', a
|
|
|
64
64
|
assert.equal(pkg.version, '26.0529');
|
|
65
65
|
|
|
66
66
|
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
67
|
-
assert.match(changelog, /^# Changelog\n\n## 26\.0529
|
|
67
|
+
assert.match(changelog, /^# Changelog\n\n## 26\.0529\n\n### Features\n\n- feat: initial app/);
|
|
68
68
|
|
|
69
69
|
const tag = execFileSync('git', ['tag', '--list', '26.0529'], {
|
|
70
70
|
cwd: repo,
|
|
@@ -180,7 +180,7 @@ test('runRelease groups changelog entries by conventional commit type', async ()
|
|
|
180
180
|
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
181
181
|
assert.match(
|
|
182
182
|
changelog,
|
|
183
|
-
/## 26\.0529
|
|
183
|
+
/## 26\.0529\n\n### Features\n\n- feat: add release grouping \([a-f0-9]{7}\)\n- feat: initial app \([a-f0-9]{7}\)\n\n### Fixes\n\n- fix\(auth\): repair token refresh \([a-f0-9]{7}\)/,
|
|
184
184
|
);
|
|
185
185
|
});
|
|
186
186
|
|
|
@@ -206,7 +206,7 @@ test('runRelease links each changelog entry to its commit on GitHub', async () =
|
|
|
206
206
|
);
|
|
207
207
|
assert.match(
|
|
208
208
|
changelog,
|
|
209
|
-
/^# Changelog\n\n## \[26\.0529\]\(https:\/\/github\.com\/msako\/demo-app\/compare\/v1\.0\.0\.\.\.26\.0529\)
|
|
209
|
+
/^# Changelog\n\n## \[26\.0529\]\(https:\/\/github\.com\/msako\/demo-app\/compare\/v1\.0\.0\.\.\.26\.0529\)/,
|
|
210
210
|
);
|
|
211
211
|
});
|
|
212
212
|
|
|
@@ -232,7 +232,74 @@ test('runRelease links changelog entries for private GitLab-style remotes', asyn
|
|
|
232
232
|
);
|
|
233
233
|
assert.match(
|
|
234
234
|
changelog,
|
|
235
|
-
/^# Changelog\n\n## \[26\.0529\]\(https:\/\/gitlab\.internal\.example\.com\/platform\/demo-app\/-\/compare\/v1\.0\.0\.\.\.26\.0529\)
|
|
235
|
+
/^# Changelog\n\n## \[26\.0529\]\(https:\/\/gitlab\.internal\.example\.com\/platform\/demo-app\/-\/compare\/v1\.0\.0\.\.\.26\.0529\)/,
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('runRelease links changelog entries to GitHub pull requests when commit subjects include PR numbers', async () => {
|
|
240
|
+
const repo = await makeRepo();
|
|
241
|
+
execFileSync('git', ['remote', 'add', 'origin', 'git@github.com:msako/demo-app.git'], { cwd: repo });
|
|
242
|
+
execFileSync('git', ['tag', 'v1.0.0'], { cwd: repo });
|
|
243
|
+
await writeFile(path.join(repo, 'feature.txt'), 'feature\n');
|
|
244
|
+
execFileSync('git', ['add', 'feature.txt'], { cwd: repo });
|
|
245
|
+
execFileSync('git', ['commit', '-m', 'feat: add pull request links (#42)'], { cwd: repo });
|
|
246
|
+
|
|
247
|
+
await runRelease({
|
|
248
|
+
cwd: repo,
|
|
249
|
+
date: new Date('2026-05-29T12:00:00-07:00'),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
253
|
+
assert.match(
|
|
254
|
+
changelog,
|
|
255
|
+
/- feat: add pull request links \(#42\) \(\[#42\]\(https:\/\/github\.com\/msako\/demo-app\/pull\/42\)\)/,
|
|
256
|
+
);
|
|
257
|
+
assert.match(
|
|
258
|
+
changelog,
|
|
259
|
+
/### Full Changelog\n\n- \[#42\]\(https:\/\/github\.com\/msako\/demo-app\/pull\/42\) feat: add pull request links \(#42\)/,
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('runRelease links changelog entries to GitLab merge requests and dedupes the full changelog', async () => {
|
|
264
|
+
const repo = await makeRepo();
|
|
265
|
+
execFileSync('git', ['remote', 'add', 'origin', 'git@gitlab.internal.example.com:platform/demo-app.git'], { cwd: repo });
|
|
266
|
+
execFileSync('git', ['tag', 'v1.0.0'], { cwd: repo });
|
|
267
|
+
await writeFile(path.join(repo, 'fix.txt'), 'fix\n');
|
|
268
|
+
execFileSync('git', ['add', 'fix.txt'], { cwd: repo });
|
|
269
|
+
execFileSync('git', [
|
|
270
|
+
'commit',
|
|
271
|
+
'-m',
|
|
272
|
+
'Merge branch feature/review into main',
|
|
273
|
+
'-m',
|
|
274
|
+
'fix(review): block rule-listed reviewers',
|
|
275
|
+
'-m',
|
|
276
|
+
'See merge request platform/demo-app!77',
|
|
277
|
+
], { cwd: repo });
|
|
278
|
+
await writeFile(path.join(repo, 'merge.txt'), 'merge\n');
|
|
279
|
+
execFileSync('git', ['add', 'merge.txt'], { cwd: repo });
|
|
280
|
+
execFileSync('git', [
|
|
281
|
+
'commit',
|
|
282
|
+
'-m',
|
|
283
|
+
'Merge branch feature/review into main',
|
|
284
|
+
'-m',
|
|
285
|
+
'See merge request platform/demo-app!77',
|
|
286
|
+
], { cwd: repo });
|
|
287
|
+
|
|
288
|
+
await runRelease({
|
|
289
|
+
cwd: repo,
|
|
290
|
+
date: new Date('2026-05-29T12:00:00-07:00'),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
294
|
+
assert.match(
|
|
295
|
+
changelog,
|
|
296
|
+
/- fix\(review\): block rule-listed reviewers \(\[!77\]\(https:\/\/gitlab\.internal\.example\.com\/platform\/demo-app\/-\/merge_requests\/77\)\)/,
|
|
297
|
+
);
|
|
298
|
+
const fullChangelogEntries = changelog.match(/\[!77\]\(https:\/\/gitlab\.internal\.example\.com\/platform\/demo-app\/-\/merge_requests\/77\)/g) ?? [];
|
|
299
|
+
assert.equal(fullChangelogEntries.length, 2);
|
|
300
|
+
assert.match(
|
|
301
|
+
changelog,
|
|
302
|
+
/### Full Changelog\n\n- \[!77\]\(https:\/\/gitlab\.internal\.example\.com\/platform\/demo-app\/-\/merge_requests\/77\) fix\(review\): block rule-listed reviewers/,
|
|
236
303
|
);
|
|
237
304
|
});
|
|
238
305
|
|
|
@@ -252,8 +319,8 @@ test('runRelease prepends only commits since the previous CalVer tag on later re
|
|
|
252
319
|
});
|
|
253
320
|
|
|
254
321
|
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
255
|
-
const latestEntry = changelog.split('## 26.0529
|
|
256
|
-
assert.match(latestEntry, /## 26\.0529\.1
|
|
322
|
+
const latestEntry = changelog.split('\n## 26.0529\n')[0];
|
|
323
|
+
assert.match(latestEntry, /## 26\.0529\.1/);
|
|
257
324
|
assert.match(latestEntry, /- fix: second release only/);
|
|
258
325
|
assert.doesNotMatch(latestEntry, /feat: initial app/);
|
|
259
326
|
});
|
|
@@ -352,6 +419,40 @@ test('runRelease does not duplicate commits already present in an existing chang
|
|
|
352
419
|
assert.doesNotMatch(latestEntry, /feat: already documented/);
|
|
353
420
|
});
|
|
354
421
|
|
|
422
|
+
test('runRelease uses the previous changelog compare target for the new compare link', async () => {
|
|
423
|
+
const repo = await makeRepo();
|
|
424
|
+
execFileSync('git', ['remote', 'add', 'origin', 'git@gitlab.ops:pss/d2pass/d2p_next.git'], { cwd: repo });
|
|
425
|
+
execFileSync('git', ['tag', 'v1.34.0'], { cwd: repo });
|
|
426
|
+
await writeFile(path.join(repo, 'released.txt'), 'released\n');
|
|
427
|
+
execFileSync('git', ['add', 'released.txt'], { cwd: repo });
|
|
428
|
+
execFileSync('git', ['commit', '-m', 'feat: already documented release'], { cwd: repo });
|
|
429
|
+
const releaseHead = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim();
|
|
430
|
+
await writeFile(
|
|
431
|
+
path.join(repo, 'CHANGELOG.md'),
|
|
432
|
+
'# Changelog\n\n## [1.35.0](https://gitlab.ops/pss/d2pass/d2p_next/compare/v1.34.0...v1.35.0) (2026-06-01)\n\n### Features\n\n* **site-map:** already documented release\n',
|
|
433
|
+
);
|
|
434
|
+
await writeFile(path.join(repo, 'unreleased.txt'), 'unreleased\n');
|
|
435
|
+
execFileSync('git', ['add', 'CHANGELOG.md', 'unreleased.txt'], { cwd: repo });
|
|
436
|
+
execFileSync('git', ['commit', '-m', 'fix: new release change'], { cwd: repo });
|
|
437
|
+
execFileSync('git', ['tag', 'v1.35.0', releaseHead], { cwd: repo });
|
|
438
|
+
|
|
439
|
+
await runRelease({
|
|
440
|
+
cwd: repo,
|
|
441
|
+
date: new Date('2026-06-02T12:00:00-07:00'),
|
|
442
|
+
tagPrefix: 'v',
|
|
443
|
+
types: ['feat', 'fix'],
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
447
|
+
assert.match(
|
|
448
|
+
changelog,
|
|
449
|
+
/^# Changelog\n\n## \[26\.0602\]\(https:\/\/gitlab\.ops\/pss\/d2pass\/d2p_next\/-\/compare\/v1\.35\.0\.\.\.v26\.0602\)/,
|
|
450
|
+
);
|
|
451
|
+
const latestEntry = changelog.split('## [1.35.0]')[0];
|
|
452
|
+
assert.match(latestEntry, /- fix: new release change/);
|
|
453
|
+
assert.doesNotMatch(latestEntry, /feat: already documented release/);
|
|
454
|
+
});
|
|
455
|
+
|
|
355
456
|
test('runRelease rolls back its release commit when tag creation fails', async () => {
|
|
356
457
|
const repo = await makeRepo();
|
|
357
458
|
execFileSync('git', ['tag', '26.0529'], { cwd: repo });
|