calver-bump 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calver-bump",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Release CLI for internal applications using readable CalVer versions.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -152,7 +152,8 @@ async function releaseNotes(cwd, options = {}) {
152
152
  const compareUrlBuilder = remoteUrl ? buildCompareUrlBuilder(remoteUrl) : null;
153
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
- const conventionalCommits = commits
155
+ const conventionalCommits = dedupeConventionalChanges(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 ?? ''))
@@ -160,7 +161,7 @@ async function releaseNotes(cwd, options = {}) {
160
161
  ...commit,
161
162
  request: requestForCommit(commit, requestUrlBuilder),
162
163
  url: commitUrlBuilder ? commitUrlBuilder(commit.hash) : null,
163
- }));
164
+ })));
164
165
  const requests = uniqueRequests(commits.map((commit) => requestForCommit(commit, requestUrlBuilder)).filter(Boolean));
165
166
  return {
166
167
  previousTag: latestTag,
@@ -186,6 +187,31 @@ function isConventionalCommit(subject) {
186
187
  return /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: .+/.test(subject);
187
188
  }
188
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
+
200
+ function dedupeConventionalChanges(changes) {
201
+ const deduped = [];
202
+ for (const change of changes) {
203
+ const existingIndex = deduped.findIndex((candidate) => candidate.subject === change.subject);
204
+ if (existingIndex < 0) {
205
+ deduped.push(change);
206
+ continue;
207
+ }
208
+ if (!deduped[existingIndex].request && change.request) {
209
+ deduped[existingIndex] = change;
210
+ }
211
+ }
212
+ return deduped;
213
+ }
214
+
189
215
  function formatReleaseNotes(changes) {
190
216
  if (changes.length === 1 && changes[0] === 'No conventional commits in this release.') {
191
217
  return `- ${changes[0]}`;
@@ -317,13 +343,12 @@ function requestForCommit(commit, requestUrlBuilder) {
317
343
  }
318
344
 
319
345
  function requestTitleForCommit(commit) {
320
- if (isConventionalCommit(commit.subject)) {
321
- return commit.subject;
346
+ const conventionalSubject = conventionalSubjectForCommit(commit);
347
+ if (conventionalSubject) {
348
+ return conventionalSubject;
322
349
  }
323
350
 
324
- return (commit.body ?? '')
325
- .split('\n')
326
- .map((line) => line.trim())
351
+ return commitLines(commit)
327
352
  .find((line) => line && !parseRequestReference(line) && !/^Merge\b/i.test(line)) ?? null;
328
353
  }
329
354
 
@@ -269,6 +269,8 @@ test('runRelease links changelog entries to GitLab merge requests and dedupes th
269
269
  execFileSync('git', [
270
270
  'commit',
271
271
  '-m',
272
+ 'Merge branch feature/review into main',
273
+ '-m',
272
274
  'fix(review): block rule-listed reviewers',
273
275
  '-m',
274
276
  'See merge request platform/demo-app!77',
@@ -301,6 +303,40 @@ test('runRelease links changelog entries to GitLab merge requests and dedupes th
301
303
  );
302
304
  });
303
305
 
306
+ test('runRelease prefers merge request entries over duplicate commit hash entries', async () => {
307
+ const repo = await makeRepo();
308
+ execFileSync('git', ['remote', 'add', 'origin', 'git@gitlab.internal.example.com:platform/demo-app.git'], { cwd: repo });
309
+ execFileSync('git', ['tag', 'v1.0.0'], { cwd: repo });
310
+ await writeFile(path.join(repo, 'raw-fix.txt'), 'raw\n');
311
+ execFileSync('git', ['add', 'raw-fix.txt'], { cwd: repo });
312
+ execFileSync('git', ['commit', '-m', 'fix(review): block rule-listed reviewers'], { cwd: repo });
313
+ const rawHash = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim();
314
+ await writeFile(path.join(repo, 'merge-fix.txt'), 'merge\n');
315
+ execFileSync('git', ['add', 'merge-fix.txt'], { cwd: repo });
316
+ execFileSync('git', [
317
+ 'commit',
318
+ '-m',
319
+ 'Merge branch feature/review into main',
320
+ '-m',
321
+ 'fix(review): block rule-listed reviewers',
322
+ '-m',
323
+ 'See merge request platform/demo-app!77',
324
+ ], { cwd: repo });
325
+
326
+ await runRelease({
327
+ cwd: repo,
328
+ date: new Date('2026-05-29T12:00:00-07:00'),
329
+ });
330
+
331
+ const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
332
+ const fixes = changelog.match(/### Fixes\n\n(?<body>[\s\S]*?)(?:\n\n###|\n?$)/)?.groups.body ?? '';
333
+ assert.match(
334
+ fixes,
335
+ /- fix\(review\): block rule-listed reviewers \(\[!77\]\(https:\/\/gitlab\.internal\.example\.com\/platform\/demo-app\/-\/merge_requests\/77\)\)/,
336
+ );
337
+ assert.doesNotMatch(fixes, new RegExp(rawHash.slice(0, 7)));
338
+ });
339
+
304
340
  test('runRelease prepends only commits since the previous CalVer tag on later releases', async () => {
305
341
  const repo = await makeRepo();
306
342
  await runRelease({