calver-bump 0.1.7 → 0.1.8

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/src/git.js ADDED
@@ -0,0 +1,82 @@
1
+ import { execFile as execFileCallback } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const execFile = promisify(execFileCallback);
5
+
6
+ export async function git(cwd, args) {
7
+ return execFile('git', args, { cwd, encoding: 'utf8' });
8
+ }
9
+
10
+ export async function gitLines(cwd, args) {
11
+ const { stdout } = await git(cwd, args);
12
+ return stdout.split('\n').map((line) => line.trim()).filter(Boolean);
13
+ }
14
+
15
+ export async function gitCommits(cwd, range) {
16
+ const { stdout } = await git(cwd, ['log', '--pretty=format:%H%x00%s%x00%B%x1e', ...range]);
17
+ return stdout
18
+ .split('\x1e')
19
+ .filter(Boolean)
20
+ .map((record) => {
21
+ const [hash, subject, body = ''] = record.replace(/^\n+|\n+$/g, '').split('\0');
22
+ return { hash, subject, body };
23
+ });
24
+ }
25
+
26
+ export async function getRemoteUrl(cwd, remote) {
27
+ try {
28
+ const { stdout } = await git(cwd, ['remote', 'get-url', remote]);
29
+ return stdout.trim();
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export async function fetchTags(cwd, remote) {
36
+ try {
37
+ await git(cwd, ['remote', 'get-url', remote]);
38
+ await git(cwd, ['fetch', '--tags', remote]);
39
+ } catch {
40
+ // Local/offline repos can still release using tags already present.
41
+ }
42
+ }
43
+
44
+ export async function currentBranch(cwd) {
45
+ try {
46
+ const { stdout } = await git(cwd, ['branch', '--show-current']);
47
+ return stdout.trim() || 'HEAD';
48
+ } catch {
49
+ return 'HEAD';
50
+ }
51
+ }
52
+
53
+ export async function assertCleanWorktree(cwd) {
54
+ const status = await gitLines(cwd, ['status', '--porcelain']);
55
+ if (status.length > 0) {
56
+ throw new Error('Working tree is not clean. Commit or stash changes before releasing.');
57
+ }
58
+ }
59
+
60
+ export async function tagExists(cwd, tag) {
61
+ try {
62
+ await git(cwd, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}^{}`]);
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ export async function assertTagAvailable(cwd, tag) {
70
+ if (await tagExists(cwd, tag)) {
71
+ throw new Error(`Git tag ${tag} already exists. Choose another date/format or delete the existing tag before releasing.`);
72
+ }
73
+ }
74
+
75
+ export async function latestReachableTag(cwd) {
76
+ try {
77
+ const { stdout } = await git(cwd, ['describe', '--tags', '--abbrev=0']);
78
+ return stdout.trim() || null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
package/src/index.js CHANGED
@@ -1,11 +1,14 @@
1
- import { execFile as execFileCallback } from 'node:child_process';
2
- import { access, readFile, writeFile } from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { promisify } from 'node:util';
5
-
6
1
  import { nextCalVer } from './calver.js';
7
-
8
- const execFile = promisify(execFileCallback);
2
+ import { formatFullChangelog, formatReleaseHeading, formatReleaseNotes, releaseNotes } from './changelog.js';
3
+ import {
4
+ readChangelog,
5
+ releaseFiles,
6
+ releaseWarnings,
7
+ updatePackageLock,
8
+ updatePackageVersion,
9
+ writeChangelog,
10
+ } from './files.js';
11
+ import { assertCleanWorktree, assertTagAvailable, currentBranch, git, gitLines } from './git.js';
9
12
 
10
13
  export async function planRelease(options = {}) {
11
14
  const cwd = options.cwd ?? process.cwd();
@@ -16,13 +19,34 @@ export async function planRelease(options = {}) {
16
19
  format: options.format ?? 'short',
17
20
  });
18
21
  const tag = `${options.tagPrefix ?? ''}${version}`;
22
+ const notes = options.versionOnly
23
+ ? null
24
+ : await releaseNotes(cwd, { ...options, tag, existingChangelog: await readChangelog(cwd) });
25
+ const files = await releaseFiles(cwd, {
26
+ version: !options.changelogOnly,
27
+ changelog: !options.versionOnly,
28
+ });
29
+ const warnings = await releaseWarnings(cwd, {
30
+ updatesVersion: !options.changelogOnly,
31
+ });
19
32
 
20
33
  return {
21
34
  version,
22
35
  tag,
23
36
  branch: await currentBranch(cwd),
24
37
  remote: options.remote ?? 'origin',
25
- actions: releaseActions({ version, tag, skipCommit: options.skipCommit }),
38
+ previousTag: notes?.previousTag ?? null,
39
+ range: notes?.range ?? null,
40
+ files,
41
+ warnings,
42
+ actions: releaseActions({
43
+ version,
44
+ tag,
45
+ skipCommit: options.skipCommit || options.versionOnly || options.changelogOnly,
46
+ versionOnly: options.versionOnly,
47
+ changelogOnly: options.changelogOnly,
48
+ files,
49
+ }),
26
50
  };
27
51
  }
28
52
 
@@ -35,11 +59,19 @@ export async function runRelease(options = {}) {
35
59
  }
36
60
 
37
61
  await assertCleanWorktree(cwd);
38
- await updatePackageVersion(cwd, plan.version);
39
- await updatePackageLock(cwd, plan.version);
40
- await prependChangelog(cwd, plan.version, options.date ?? new Date(), options);
62
+ if (!options.skipCommit && !options.versionOnly && !options.changelogOnly) {
63
+ await assertTagAvailable(cwd, plan.tag);
64
+ }
41
65
 
42
- if (options.skipCommit) {
66
+ if (!options.changelogOnly) {
67
+ await updatePackageVersion(cwd, plan.version);
68
+ await updatePackageLock(cwd, plan.version);
69
+ }
70
+ if (!options.versionOnly) {
71
+ await prependChangelog(cwd, plan.version, options);
72
+ }
73
+
74
+ if (options.skipCommit || options.versionOnly || options.changelogOnly) {
43
75
  return { ...plan, tag: null };
44
76
  }
45
77
 
@@ -48,18 +80,24 @@ export async function runRelease(options = {}) {
48
80
  try {
49
81
  await git(cwd, ['tag', '-a', plan.tag, '-m', `Release ${plan.version}`]);
50
82
  } catch (error) {
51
- await git(cwd, ['reset', '--hard', 'HEAD~1']);
52
- throw new Error(`Failed to create git tag ${plan.tag}; rolled back release commit. ${error.message}`);
83
+ await git(cwd, ['reset', '--soft', 'HEAD~1']);
84
+ throw new Error(`Failed to create git tag ${plan.tag}; release commit was undone and file changes were left in the working tree. ${error.message}`);
53
85
  }
54
86
 
55
87
  return plan;
56
88
  }
57
89
 
58
- function releaseActions({ version, tag, skipCommit = false }) {
59
- const actions = [
60
- `update package.json version to ${version}`,
61
- `prepend CHANGELOG.md entry for ${version}`,
62
- ];
90
+ function releaseActions({ version, tag, skipCommit = false, versionOnly = false, changelogOnly = false, files = [] }) {
91
+ const actions = [];
92
+ if (!changelogOnly) {
93
+ actions.push(`update package.json version to ${version}`);
94
+ if (files.some((file) => ['package-lock.json', 'npm-shrinkwrap.json'].includes(file))) {
95
+ actions.push('update npm lockfile version metadata');
96
+ }
97
+ }
98
+ if (!versionOnly) {
99
+ actions.push(`prepend CHANGELOG.md entry for ${version}`);
100
+ }
63
101
  if (!skipCommit) {
64
102
  actions.push(`create git commit chore(release): ${version}`);
65
103
  actions.push(`create git tag ${tag}`);
@@ -67,398 +105,25 @@ function releaseActions({ version, tag, skipCommit = false }) {
67
105
  return actions;
68
106
  }
69
107
 
70
- async function updatePackageVersion(cwd, version) {
71
- const packagePath = path.join(cwd, 'package.json');
72
- const pkg = JSON.parse(await readFile(packagePath, 'utf8'));
73
- pkg.version = version;
74
- await writeFile(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
75
- }
76
-
77
- async function updatePackageLock(cwd, version) {
78
- for (const fileName of ['package-lock.json', 'npm-shrinkwrap.json']) {
79
- const filePath = path.join(cwd, fileName);
80
- let lock;
81
- try {
82
- lock = JSON.parse(await readFile(filePath, 'utf8'));
83
- } catch (error) {
84
- if (error.code === 'ENOENT') continue;
85
- throw error;
86
- }
87
-
88
- if (typeof lock.version === 'string') {
89
- lock.version = version;
90
- }
91
- if (lock.packages?.[''] && typeof lock.packages[''].version === 'string') {
92
- lock.packages[''].version = version;
93
- }
94
-
95
- await writeFile(filePath, `${JSON.stringify(lock, null, 2)}\n`);
96
- }
97
- }
98
-
99
- async function releaseFiles(cwd) {
100
- const candidates = ['package.json', 'package-lock.json', 'npm-shrinkwrap.json', 'CHANGELOG.md'];
101
- const files = [];
102
- for (const candidate of candidates) {
103
- if (await fileExists(path.join(cwd, candidate))) {
104
- files.push(candidate);
105
- }
106
- }
107
- return files;
108
- }
109
-
110
- async function fileExists(filePath) {
111
- try {
112
- await access(filePath);
113
- return true;
114
- } catch {
115
- return false;
116
- }
117
- }
118
-
119
- async function prependChangelog(cwd, version, date, options = {}) {
120
- const changelogPath = path.join(cwd, 'CHANGELOG.md');
121
- let existing = '';
108
+ async function prependChangelog(cwd, version, options = {}) {
109
+ const existing = await readChangelog(cwd);
122
110
 
123
- try {
124
- existing = await readFile(changelogPath, 'utf8');
125
- } catch (error) {
126
- if (error.code !== 'ENOENT') throw error;
127
- }
128
-
129
- const notes = await releaseNotes(cwd, { ...options, existingChangelog: existing });
111
+ const notes = await releaseNotes(cwd, {
112
+ ...options,
113
+ existingChangelog: existing,
114
+ tag: `${options.tagPrefix ?? ''}${version}`,
115
+ });
130
116
  const heading = formatReleaseHeading({
131
117
  version,
132
118
  previousTag: notes.previousTag,
133
- tag: `${options.tagPrefix ?? ''}${version}`,
119
+ tag: options.tagPrefix ? `${options.tagPrefix}${version}` : version,
134
120
  compareUrlBuilder: notes.compareUrlBuilder,
135
121
  });
136
- const entry = `${heading}\n\n${formatReleaseNotes(notes.changes)}${formatFullChangelog(notes.requests)}\n`;
122
+ const entry = `${heading}\n\n${formatReleaseNotes(notes.changes, options.changelogSections)}${formatFullChangelog(notes.requests)}\n`;
137
123
 
138
124
  const body = existing.trim().startsWith('# Changelog')
139
125
  ? existing.replace(/^# Changelog\s*/, `# Changelog\n\n${entry}\n`)
140
126
  : `# Changelog\n\n${entry}\n${existing}`;
141
127
 
142
- await writeFile(changelogPath, body);
143
- }
144
-
145
- async function releaseNotes(cwd, options = {}) {
146
- await fetchTags(cwd, options.remote ?? 'origin');
147
- const latestTag = await latestReleaseTag(cwd, options.existingChangelog ?? '');
148
- const range = latestTag ? [`${latestTag}..HEAD`] : [];
149
- const commits = await gitCommits(cwd, range);
150
- const remoteUrl = await getRemoteUrl(cwd, options.remote ?? 'origin');
151
- const commitUrlBuilder = remoteUrl ? buildCommitUrlBuilder(remoteUrl) : null;
152
- const compareUrlBuilder = remoteUrl ? buildCompareUrlBuilder(remoteUrl) : null;
153
- const requestUrlBuilder = remoteUrl ? buildRequestUrlBuilder(remoteUrl) : null;
154
- const allowedTypes = options.types ?? ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'];
155
- const conventionalCommits = dedupeConventionalChanges(commits
156
- .map((commit) => ({ ...commit, subject: conventionalSubjectForCommit(commit) ?? commit.subject }))
157
- .filter((commit) => isConventionalCommit(commit.subject))
158
- .filter((commit) => allowedTypes.includes(conventionalType(commit.subject)))
159
- .filter((commit) => !isCommitInChangelog(commit, options.existingChangelog ?? ''))
160
- .map((commit) => ({
161
- ...commit,
162
- request: requestForCommit(commit, requestUrlBuilder),
163
- url: commitUrlBuilder ? commitUrlBuilder(commit.hash) : null,
164
- })));
165
- const requests = uniqueRequests(commits.map((commit) => requestForCommit(commit, requestUrlBuilder)).filter(Boolean));
166
- return {
167
- previousTag: latestTag,
168
- compareUrlBuilder,
169
- changes: conventionalCommits.length > 0 ? conventionalCommits : ['No conventional commits in this release.'],
170
- requests,
171
- };
172
- }
173
-
174
- function formatReleaseHeading({ version, previousTag, tag, compareUrlBuilder }) {
175
- const label = previousTag && compareUrlBuilder
176
- ? `[${version}](${compareUrlBuilder(previousTag, tag)})`
177
- : version;
178
- return `## ${label}`;
179
- }
180
-
181
- function isCommitInChangelog(commit, changelog) {
182
- if (!changelog) return false;
183
- return changelog.includes(commit.hash) || changelog.includes(commit.hash.slice(0, 7));
184
- }
185
-
186
- function isConventionalCommit(subject) {
187
- return /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: .+/.test(subject);
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
-
215
- function formatReleaseNotes(changes) {
216
- if (changes.length === 1 && changes[0] === 'No conventional commits in this release.') {
217
- return `- ${changes[0]}`;
218
- }
219
-
220
- const features = changes.filter((change) => conventionalType(change.subject) === 'feat');
221
- const fixes = changes.filter((change) => conventionalType(change.subject) === 'fix');
222
- const other = changes.filter((change) => !['feat', 'fix'].includes(conventionalType(change.subject)));
223
- const sections = [
224
- ['Features', features],
225
- ['Fixes', fixes],
226
- ['Other Changes', other],
227
- ];
228
-
229
- return sections
230
- .filter(([, entries]) => entries.length > 0)
231
- .map(([heading, entries]) => `### ${heading}\n\n${entries.map((entry) => `- ${formatCommitEntry(entry)}`).join('\n')}`)
232
- .join('\n\n');
233
- }
234
-
235
- function formatFullChangelog(requests) {
236
- if (requests.length === 0) {
237
- return '';
238
- }
239
- return `\n\n### Full Changelog\n\n${requests.map((request) => `- ${formatRequestEntry(request)}`).join('\n')}`;
240
- }
241
-
242
- function conventionalType(subject) {
243
- return /^(?<type>[a-z]+)(\([^)]+\))?!?: .+/.exec(subject)?.groups.type;
244
- }
245
-
246
- function formatCommitEntry(commit) {
247
- const shortHash = commit.hash.slice(0, 7);
248
- const suffix = commit.request
249
- ? ` (${formatRequestLink(commit.request)})`
250
- : commit.url ? ` ([${shortHash}](${commit.url}))` : ` (${shortHash})`;
251
- return `${commit.subject}${suffix}`;
252
- }
253
-
254
- function formatRequestLink(request) {
255
- return request.url ? `[${request.label}](${request.url})` : request.label;
256
- }
257
-
258
- function formatRequestEntry(request) {
259
- return request.title ? `${formatRequestLink(request)} ${request.title}` : formatRequestLink(request);
260
- }
261
-
262
- async function gitCommits(cwd, range) {
263
- const { stdout } = await git(cwd, ['log', '--pretty=format:%H%x00%s%x00%B%x1e', ...range]);
264
- return stdout
265
- .split('\x1e')
266
- .filter(Boolean)
267
- .map((record) => {
268
- const [hash, subject, body = ''] = record.replace(/^\n+|\n+$/g, '').split('\0');
269
- return { hash, subject, body };
270
- });
271
- }
272
-
273
- async function getRemoteUrl(cwd, remote) {
274
- try {
275
- const { stdout } = await git(cwd, ['remote', 'get-url', remote]);
276
- return stdout.trim();
277
- } catch {
278
- return null;
279
- }
280
- }
281
-
282
- function buildCommitUrlBuilder(remote) {
283
- const parsed = parseGitRemote(remote);
284
- if (!parsed) {
285
- return null;
286
- }
287
-
288
- const baseUrl = `https://${parsed.host}/${parsed.repo}`;
289
- if (parsed.host === 'github.com') {
290
- return (hash) => `${baseUrl}/commit/${hash}`;
291
- }
292
- if (parsed.host.includes('gitlab')) {
293
- return (hash) => `${baseUrl}/-/commit/${hash}`;
294
- }
295
-
296
- return null;
297
- }
298
-
299
- function buildCompareUrlBuilder(remote) {
300
- const parsed = parseGitRemote(remote);
301
- if (!parsed) {
302
- return null;
303
- }
304
-
305
- const baseUrl = `https://${parsed.host}/${parsed.repo}`;
306
- if (parsed.host === 'github.com') {
307
- return (from, to) => `${baseUrl}/compare/${from}...${to}`;
308
- }
309
- if (parsed.host.includes('gitlab')) {
310
- return (from, to) => `${baseUrl}/-/compare/${from}...${to}`;
311
- }
312
-
313
- return null;
314
- }
315
-
316
- function buildRequestUrlBuilder(remote) {
317
- const parsed = parseGitRemote(remote);
318
- if (!parsed) {
319
- return null;
320
- }
321
-
322
- const baseUrl = `https://${parsed.host}/${parsed.repo}`;
323
- if (parsed.host === 'github.com') {
324
- return (request) => request.provider === 'github' ? `${baseUrl}/pull/${request.number}` : null;
325
- }
326
- if (parsed.host.includes('gitlab')) {
327
- return (request) => request.provider === 'gitlab' ? `${baseUrl}/-/merge_requests/${request.number}` : null;
328
- }
329
-
330
- return null;
331
- }
332
-
333
- function requestForCommit(commit, requestUrlBuilder) {
334
- const request = parseRequestReference(`${commit.subject}\n${commit.body ?? ''}`);
335
- if (!request) {
336
- return null;
337
- }
338
- return {
339
- ...request,
340
- title: requestTitleForCommit(commit),
341
- url: requestUrlBuilder ? requestUrlBuilder(request) : null,
342
- };
343
- }
344
-
345
- function requestTitleForCommit(commit) {
346
- const conventionalSubject = conventionalSubjectForCommit(commit);
347
- if (conventionalSubject) {
348
- return conventionalSubject;
349
- }
350
-
351
- return commitLines(commit)
352
- .find((line) => line && !parseRequestReference(line) && !/^Merge\b/i.test(line)) ?? null;
353
- }
354
-
355
- function parseRequestReference(message) {
356
- const gitlabMerge = /(?:^|\s)(?:See merge request\s+\S+!|!)(?<number>\d+)(?=\D|$)/i.exec(message);
357
- if (gitlabMerge) {
358
- return { provider: 'gitlab', number: gitlabMerge.groups.number, label: `!${gitlabMerge.groups.number}` };
359
- }
360
-
361
- const githubPull = /(?:Merge pull request\s+#|#)(?<number>\d+)(?=\D|$)/i.exec(message);
362
- if (githubPull) {
363
- return { provider: 'github', number: githubPull.groups.number, label: `#${githubPull.groups.number}` };
364
- }
365
-
366
- return null;
367
- }
368
-
369
- function uniqueRequests(requests) {
370
- const seen = new Set();
371
- const unique = [];
372
- for (const request of requests) {
373
- const key = `${request.provider}:${request.number}`;
374
- if (seen.has(key)) {
375
- const existing = unique.find((candidate) => `${candidate.provider}:${candidate.number}` === key);
376
- if (existing && !existing.title && request.title) {
377
- existing.title = request.title;
378
- }
379
- continue;
380
- }
381
- seen.add(key);
382
- unique.push(request);
383
- }
384
- return unique;
385
- }
386
-
387
- function parseGitRemote(remote) {
388
- const sshMatch = /^git@(?<host>[^:]+):(?<repo>.+?)(?:\.git)?$/.exec(remote);
389
- if (sshMatch) {
390
- return sshMatch.groups;
391
- }
392
-
393
- const httpsMatch = /^https:\/\/(?<host>[^/]+)\/(?<repo>.+?)(?:\.git)?$/.exec(remote);
394
- if (httpsMatch) {
395
- return httpsMatch.groups;
396
- }
397
-
398
- return null;
399
- }
400
-
401
- async function latestReleaseTag(cwd, changelog) {
402
- const changelogTag = latestChangelogCompareTarget(changelog);
403
- if (changelogTag && await tagExists(cwd, changelogTag)) {
404
- return changelogTag;
405
- }
406
- return latestReachableTag(cwd);
407
- }
408
-
409
- function latestChangelogCompareTarget(changelog) {
410
- const match = /^## \[[^\]]+\]\([^)]*\/(?:-\/)?compare\/[^)]*?\.{3}(?<tag>[^)\s]+)\)/m.exec(changelog);
411
- return match?.groups.tag ?? null;
412
- }
413
-
414
- async function tagExists(cwd, tag) {
415
- try {
416
- await git(cwd, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}^{}`]);
417
- return true;
418
- } catch {
419
- return false;
420
- }
421
- }
422
-
423
- async function latestReachableTag(cwd) {
424
- try {
425
- const { stdout } = await git(cwd, ['describe', '--tags', '--abbrev=0']);
426
- return stdout.trim() || null;
427
- } catch {
428
- return null;
429
- }
430
- }
431
-
432
- async function fetchTags(cwd, remote) {
433
- try {
434
- await git(cwd, ['remote', 'get-url', remote]);
435
- await git(cwd, ['fetch', '--tags', remote]);
436
- } catch {
437
- // Local/offline repos can still release using tags already present.
438
- }
439
- }
440
-
441
- async function currentBranch(cwd) {
442
- try {
443
- const { stdout } = await git(cwd, ['branch', '--show-current']);
444
- return stdout.trim() || 'HEAD';
445
- } catch {
446
- return 'HEAD';
447
- }
448
- }
449
-
450
- async function assertCleanWorktree(cwd) {
451
- const status = await gitLines(cwd, ['status', '--porcelain']);
452
- if (status.length > 0) {
453
- throw new Error('Working tree is not clean. Commit or stash changes before releasing.');
454
- }
455
- }
456
-
457
- async function gitLines(cwd, args) {
458
- const { stdout } = await git(cwd, args);
459
- return stdout.split('\n').map((line) => line.trim()).filter(Boolean);
460
- }
461
-
462
- async function git(cwd, args) {
463
- return execFile('git', args, { cwd, encoding: 'utf8' });
128
+ await writeChangelog(cwd, body);
464
129
  }
@@ -1,69 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { test } from 'node:test';
3
-
4
- import { nextCalVer } from '../src/calver.js';
5
-
6
- test('nextCalVer defaults to readable YY.MMDD format for the first release of the day', () => {
7
- const version = nextCalVer({
8
- date: new Date('2026-05-29T12:00:00-07:00'),
9
- existingTags: [],
10
- });
11
-
12
- assert.equal(version, '26.0529');
13
- });
14
-
15
- test('nextCalVer increments the sequence for existing tags on the same day', () => {
16
- const version = nextCalVer({
17
- date: new Date('2026-05-29T12:00:00-07:00'),
18
- existingTags: ['26.0528.7', '26.0529', 'v26.0529.2'],
19
- });
20
-
21
- assert.equal(version, '26.0529.3');
22
- });
23
-
24
- test('nextCalVer emits .1 for the second release of the day', () => {
25
- const version = nextCalVer({
26
- date: new Date('2026-05-29T12:00:00-07:00'),
27
- existingTags: ['26.0529'],
28
- });
29
-
30
- assert.equal(version, '26.0529.1');
31
- });
32
-
33
- test('nextCalVer can emit compact YYMMDD without sequence for the first release of the day', () => {
34
- const version = nextCalVer({
35
- date: new Date('2026-05-29T12:00:00-07:00'),
36
- format: 'compact',
37
- });
38
-
39
- assert.equal(version, '260529');
40
- });
41
-
42
- test('nextCalVer can emit compact YYMMDD.N after the first release of the day', () => {
43
- const version = nextCalVer({
44
- date: new Date('2026-05-29T12:00:00-07:00'),
45
- existingTags: ['260529'],
46
- format: 'compact',
47
- });
48
-
49
- assert.equal(version, '260529.1');
50
- });
51
-
52
- test('nextCalVer can emit long YYYY.MM.DD without sequence for the first release of the day', () => {
53
- const version = nextCalVer({
54
- date: new Date('2026-05-29T12:00:00-07:00'),
55
- format: 'long',
56
- });
57
-
58
- assert.equal(version, '2026.05.29');
59
- });
60
-
61
- test('nextCalVer can emit long YYYY.MM.DD.N after the first release of the day', () => {
62
- const version = nextCalVer({
63
- date: new Date('2026-05-29T12:00:00-07:00'),
64
- existingTags: ['2026.05.29'],
65
- format: 'long',
66
- });
67
-
68
- assert.equal(version, '2026.05.29.1');
69
- });