calver-bump 0.1.4 → 0.1.5
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 +126 -12
- package/test/release.test.js +105 -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,12 +144,13 @@ 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
156
|
.filter((commit) => isConventionalCommit(commit.subject))
|
|
@@ -158,20 +158,23 @@ async function releaseNotes(cwd, options = {}) {
|
|
|
158
158
|
.filter((commit) => !isCommitInChangelog(commit, options.existingChangelog ?? ''))
|
|
159
159
|
.map((commit) => ({
|
|
160
160
|
...commit,
|
|
161
|
+
request: requestForCommit(commit, requestUrlBuilder),
|
|
161
162
|
url: commitUrlBuilder ? commitUrlBuilder(commit.hash) : null,
|
|
162
163
|
}));
|
|
164
|
+
const requests = uniqueRequests(commits.map((commit) => requestForCommit(commit, requestUrlBuilder)).filter(Boolean));
|
|
163
165
|
return {
|
|
164
166
|
previousTag: latestTag,
|
|
165
167
|
compareUrlBuilder,
|
|
166
168
|
changes: conventionalCommits.length > 0 ? conventionalCommits : ['No conventional commits in this release.'],
|
|
169
|
+
requests,
|
|
167
170
|
};
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
function formatReleaseHeading({ version,
|
|
173
|
+
function formatReleaseHeading({ version, previousTag, tag, compareUrlBuilder }) {
|
|
171
174
|
const label = previousTag && compareUrlBuilder
|
|
172
175
|
? `[${version}](${compareUrlBuilder(previousTag, tag)})`
|
|
173
176
|
: version;
|
|
174
|
-
return `## ${label}
|
|
177
|
+
return `## ${label}`;
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
function isCommitInChangelog(commit, changelog) {
|
|
@@ -203,24 +206,41 @@ function formatReleaseNotes(changes) {
|
|
|
203
206
|
.join('\n\n');
|
|
204
207
|
}
|
|
205
208
|
|
|
209
|
+
function formatFullChangelog(requests) {
|
|
210
|
+
if (requests.length === 0) {
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
return `\n\n### Full Changelog\n\n${requests.map((request) => `- ${formatRequestEntry(request)}`).join('\n')}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
206
216
|
function conventionalType(subject) {
|
|
207
217
|
return /^(?<type>[a-z]+)(\([^)]+\))?!?: .+/.exec(subject)?.groups.type;
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
function formatCommitEntry(commit) {
|
|
211
221
|
const shortHash = commit.hash.slice(0, 7);
|
|
212
|
-
const suffix = commit.
|
|
222
|
+
const suffix = commit.request
|
|
223
|
+
? ` (${formatRequestLink(commit.request)})`
|
|
224
|
+
: commit.url ? ` ([${shortHash}](${commit.url}))` : ` (${shortHash})`;
|
|
213
225
|
return `${commit.subject}${suffix}`;
|
|
214
226
|
}
|
|
215
227
|
|
|
228
|
+
function formatRequestLink(request) {
|
|
229
|
+
return request.url ? `[${request.label}](${request.url})` : request.label;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatRequestEntry(request) {
|
|
233
|
+
return request.title ? `${formatRequestLink(request)} ${request.title}` : formatRequestLink(request);
|
|
234
|
+
}
|
|
235
|
+
|
|
216
236
|
async function gitCommits(cwd, range) {
|
|
217
|
-
const { stdout } = await git(cwd, ['log', '--pretty=format:%H%x00%s', ...range]);
|
|
237
|
+
const { stdout } = await git(cwd, ['log', '--pretty=format:%H%x00%s%x00%B%x1e', ...range]);
|
|
218
238
|
return stdout
|
|
219
|
-
.split('\
|
|
239
|
+
.split('\x1e')
|
|
220
240
|
.filter(Boolean)
|
|
221
|
-
.map((
|
|
222
|
-
const [hash, subject] =
|
|
223
|
-
return { hash, subject };
|
|
241
|
+
.map((record) => {
|
|
242
|
+
const [hash, subject, body = ''] = record.replace(/^\n+|\n+$/g, '').split('\0');
|
|
243
|
+
return { hash, subject, body };
|
|
224
244
|
});
|
|
225
245
|
}
|
|
226
246
|
|
|
@@ -267,6 +287,78 @@ function buildCompareUrlBuilder(remote) {
|
|
|
267
287
|
return null;
|
|
268
288
|
}
|
|
269
289
|
|
|
290
|
+
function buildRequestUrlBuilder(remote) {
|
|
291
|
+
const parsed = parseGitRemote(remote);
|
|
292
|
+
if (!parsed) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const baseUrl = `https://${parsed.host}/${parsed.repo}`;
|
|
297
|
+
if (parsed.host === 'github.com') {
|
|
298
|
+
return (request) => request.provider === 'github' ? `${baseUrl}/pull/${request.number}` : null;
|
|
299
|
+
}
|
|
300
|
+
if (parsed.host.includes('gitlab')) {
|
|
301
|
+
return (request) => request.provider === 'gitlab' ? `${baseUrl}/-/merge_requests/${request.number}` : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function requestForCommit(commit, requestUrlBuilder) {
|
|
308
|
+
const request = parseRequestReference(`${commit.subject}\n${commit.body ?? ''}`);
|
|
309
|
+
if (!request) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
...request,
|
|
314
|
+
title: requestTitleForCommit(commit),
|
|
315
|
+
url: requestUrlBuilder ? requestUrlBuilder(request) : null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function requestTitleForCommit(commit) {
|
|
320
|
+
if (isConventionalCommit(commit.subject)) {
|
|
321
|
+
return commit.subject;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return (commit.body ?? '')
|
|
325
|
+
.split('\n')
|
|
326
|
+
.map((line) => line.trim())
|
|
327
|
+
.find((line) => line && !parseRequestReference(line) && !/^Merge\b/i.test(line)) ?? null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseRequestReference(message) {
|
|
331
|
+
const gitlabMerge = /(?:^|\s)(?:See merge request\s+\S+!|!)(?<number>\d+)(?=\D|$)/i.exec(message);
|
|
332
|
+
if (gitlabMerge) {
|
|
333
|
+
return { provider: 'gitlab', number: gitlabMerge.groups.number, label: `!${gitlabMerge.groups.number}` };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const githubPull = /(?:Merge pull request\s+#|#)(?<number>\d+)(?=\D|$)/i.exec(message);
|
|
337
|
+
if (githubPull) {
|
|
338
|
+
return { provider: 'github', number: githubPull.groups.number, label: `#${githubPull.groups.number}` };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function uniqueRequests(requests) {
|
|
345
|
+
const seen = new Set();
|
|
346
|
+
const unique = [];
|
|
347
|
+
for (const request of requests) {
|
|
348
|
+
const key = `${request.provider}:${request.number}`;
|
|
349
|
+
if (seen.has(key)) {
|
|
350
|
+
const existing = unique.find((candidate) => `${candidate.provider}:${candidate.number}` === key);
|
|
351
|
+
if (existing && !existing.title && request.title) {
|
|
352
|
+
existing.title = request.title;
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
seen.add(key);
|
|
357
|
+
unique.push(request);
|
|
358
|
+
}
|
|
359
|
+
return unique;
|
|
360
|
+
}
|
|
361
|
+
|
|
270
362
|
function parseGitRemote(remote) {
|
|
271
363
|
const sshMatch = /^git@(?<host>[^:]+):(?<repo>.+?)(?:\.git)?$/.exec(remote);
|
|
272
364
|
if (sshMatch) {
|
|
@@ -281,6 +373,28 @@ function parseGitRemote(remote) {
|
|
|
281
373
|
return null;
|
|
282
374
|
}
|
|
283
375
|
|
|
376
|
+
async function latestReleaseTag(cwd, changelog) {
|
|
377
|
+
const changelogTag = latestChangelogCompareTarget(changelog);
|
|
378
|
+
if (changelogTag && await tagExists(cwd, changelogTag)) {
|
|
379
|
+
return changelogTag;
|
|
380
|
+
}
|
|
381
|
+
return latestReachableTag(cwd);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function latestChangelogCompareTarget(changelog) {
|
|
385
|
+
const match = /^## \[[^\]]+\]\([^)]*\/(?:-\/)?compare\/[^)]*?\.{3}(?<tag>[^)\s]+)\)/m.exec(changelog);
|
|
386
|
+
return match?.groups.tag ?? null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function tagExists(cwd, tag) {
|
|
390
|
+
try {
|
|
391
|
+
await git(cwd, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}^{}`]);
|
|
392
|
+
return true;
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
284
398
|
async function latestReachableTag(cwd) {
|
|
285
399
|
try {
|
|
286
400
|
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,72 @@ 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
|
+
'fix(review): block rule-listed reviewers',
|
|
273
|
+
'-m',
|
|
274
|
+
'See merge request platform/demo-app!77',
|
|
275
|
+
], { cwd: repo });
|
|
276
|
+
await writeFile(path.join(repo, 'merge.txt'), 'merge\n');
|
|
277
|
+
execFileSync('git', ['add', 'merge.txt'], { cwd: repo });
|
|
278
|
+
execFileSync('git', [
|
|
279
|
+
'commit',
|
|
280
|
+
'-m',
|
|
281
|
+
'Merge branch feature/review into main',
|
|
282
|
+
'-m',
|
|
283
|
+
'See merge request platform/demo-app!77',
|
|
284
|
+
], { cwd: repo });
|
|
285
|
+
|
|
286
|
+
await runRelease({
|
|
287
|
+
cwd: repo,
|
|
288
|
+
date: new Date('2026-05-29T12:00:00-07:00'),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
292
|
+
assert.match(
|
|
293
|
+
changelog,
|
|
294
|
+
/- fix\(review\): block rule-listed reviewers \(\[!77\]\(https:\/\/gitlab\.internal\.example\.com\/platform\/demo-app\/-\/merge_requests\/77\)\)/,
|
|
295
|
+
);
|
|
296
|
+
const fullChangelogEntries = changelog.match(/\[!77\]\(https:\/\/gitlab\.internal\.example\.com\/platform\/demo-app\/-\/merge_requests\/77\)/g) ?? [];
|
|
297
|
+
assert.equal(fullChangelogEntries.length, 2);
|
|
298
|
+
assert.match(
|
|
299
|
+
changelog,
|
|
300
|
+
/### Full Changelog\n\n- \[!77\]\(https:\/\/gitlab\.internal\.example\.com\/platform\/demo-app\/-\/merge_requests\/77\) fix\(review\): block rule-listed reviewers/,
|
|
236
301
|
);
|
|
237
302
|
});
|
|
238
303
|
|
|
@@ -252,8 +317,8 @@ test('runRelease prepends only commits since the previous CalVer tag on later re
|
|
|
252
317
|
});
|
|
253
318
|
|
|
254
319
|
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
255
|
-
const latestEntry = changelog.split('## 26.0529
|
|
256
|
-
assert.match(latestEntry, /## 26\.0529\.1
|
|
320
|
+
const latestEntry = changelog.split('\n## 26.0529\n')[0];
|
|
321
|
+
assert.match(latestEntry, /## 26\.0529\.1/);
|
|
257
322
|
assert.match(latestEntry, /- fix: second release only/);
|
|
258
323
|
assert.doesNotMatch(latestEntry, /feat: initial app/);
|
|
259
324
|
});
|
|
@@ -352,6 +417,40 @@ test('runRelease does not duplicate commits already present in an existing chang
|
|
|
352
417
|
assert.doesNotMatch(latestEntry, /feat: already documented/);
|
|
353
418
|
});
|
|
354
419
|
|
|
420
|
+
test('runRelease uses the previous changelog compare target for the new compare link', async () => {
|
|
421
|
+
const repo = await makeRepo();
|
|
422
|
+
execFileSync('git', ['remote', 'add', 'origin', 'git@gitlab.ops:pss/d2pass/d2p_next.git'], { cwd: repo });
|
|
423
|
+
execFileSync('git', ['tag', 'v1.34.0'], { cwd: repo });
|
|
424
|
+
await writeFile(path.join(repo, 'released.txt'), 'released\n');
|
|
425
|
+
execFileSync('git', ['add', 'released.txt'], { cwd: repo });
|
|
426
|
+
execFileSync('git', ['commit', '-m', 'feat: already documented release'], { cwd: repo });
|
|
427
|
+
const releaseHead = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim();
|
|
428
|
+
await writeFile(
|
|
429
|
+
path.join(repo, 'CHANGELOG.md'),
|
|
430
|
+
'# 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',
|
|
431
|
+
);
|
|
432
|
+
await writeFile(path.join(repo, 'unreleased.txt'), 'unreleased\n');
|
|
433
|
+
execFileSync('git', ['add', 'CHANGELOG.md', 'unreleased.txt'], { cwd: repo });
|
|
434
|
+
execFileSync('git', ['commit', '-m', 'fix: new release change'], { cwd: repo });
|
|
435
|
+
execFileSync('git', ['tag', 'v1.35.0', releaseHead], { cwd: repo });
|
|
436
|
+
|
|
437
|
+
await runRelease({
|
|
438
|
+
cwd: repo,
|
|
439
|
+
date: new Date('2026-06-02T12:00:00-07:00'),
|
|
440
|
+
tagPrefix: 'v',
|
|
441
|
+
types: ['feat', 'fix'],
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
445
|
+
assert.match(
|
|
446
|
+
changelog,
|
|
447
|
+
/^# Changelog\n\n## \[26\.0602\]\(https:\/\/gitlab\.ops\/pss\/d2pass\/d2p_next\/-\/compare\/v1\.35\.0\.\.\.v26\.0602\)/,
|
|
448
|
+
);
|
|
449
|
+
const latestEntry = changelog.split('## [1.35.0]')[0];
|
|
450
|
+
assert.match(latestEntry, /- fix: new release change/);
|
|
451
|
+
assert.doesNotMatch(latestEntry, /feat: already documented release/);
|
|
452
|
+
});
|
|
453
|
+
|
|
355
454
|
test('runRelease rolls back its release commit when tag creation fails', async () => {
|
|
356
455
|
const repo = await makeRepo();
|
|
357
456
|
execFileSync('git', ['tag', '26.0529'], { cwd: repo });
|