calver-bump 0.1.1 → 0.1.2
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 +5 -5
- package/package.json +1 -1
- package/src/calver.js +16 -10
- package/test/calver.test.js +36 -9
- package/test/cli.test.js +5 -5
- package/test/release.test.js +22 -22
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@ Release CLI for applications and internal tools that use readable CalVer version
|
|
|
5
5
|
Default version and tag format:
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
|
-
YY.MMDD
|
|
8
|
+
YY.MMDD
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Example:
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
|
-
26.0529
|
|
14
|
+
26.0529
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## What it does
|
|
@@ -91,9 +91,9 @@ Project defaults can be stored in `.calverbumprc.json`:
|
|
|
91
91
|
|
|
92
92
|
## Notes
|
|
93
93
|
|
|
94
|
-
- The default `short` format is `YY.MMDD.
|
|
95
|
-
- The optional `compact` format is `YYMMDD.
|
|
96
|
-
- The optional `long` format is `YYYY.MM.DD.
|
|
94
|
+
- The default `short` format is `YY.MMDD` for the first release of the day, then `YY.MMDD.1`, `YY.MMDD.2`, etc.
|
|
95
|
+
- The optional `compact` format is `YYMMDD` for the first release of the day, then `YYMMDD.1`, `YYMMDD.2`, etc.
|
|
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
97
|
- Existing `v`-prefixed tags are considered when calculating the next sequence number.
|
|
98
98
|
- Changelog ranges start from the latest reachable tag, even when it is not a CalVer tag.
|
|
99
99
|
- Changelog entries include conventional commit subjects only, such as `feat:`, `fix(scope):`, or `chore!:`.
|
package/package.json
CHANGED
package/src/calver.js
CHANGED
|
@@ -2,14 +2,20 @@ export function nextCalVer({ date = new Date(), existingTags = [], format = 'sho
|
|
|
2
2
|
assertFormat(format);
|
|
3
3
|
const parts = dateParts(date);
|
|
4
4
|
const prefix = calVerPrefix(parts, format);
|
|
5
|
-
const matcher = new RegExp(`^v?${escapeRegExp(prefix)}
|
|
6
|
-
const
|
|
5
|
+
const matcher = new RegExp(`^v?${escapeRegExp(prefix)}(?:\\.(\\d+))?$`);
|
|
6
|
+
const releaseState = existingTags.reduce((state, tag) => {
|
|
7
7
|
const match = matcher.exec(tag.trim());
|
|
8
|
-
if (!match) return
|
|
9
|
-
return
|
|
10
|
-
|
|
8
|
+
if (!match) return state;
|
|
9
|
+
return {
|
|
10
|
+
hasBase: state.hasBase || !match[1],
|
|
11
|
+
highestSequence: match[1] ? Math.max(state.highestSequence, Number(match[1])) : state.highestSequence,
|
|
12
|
+
};
|
|
13
|
+
}, { hasBase: false, highestSequence: 0 });
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
if (!releaseState.hasBase && releaseState.highestSequence === 0) {
|
|
16
|
+
return prefix;
|
|
17
|
+
}
|
|
18
|
+
return `${prefix}.${releaseState.highestSequence + 1}`;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export function assertFormat(format) {
|
|
@@ -19,10 +25,10 @@ export function assertFormat(format) {
|
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export function isCalVerTag(tag) {
|
|
22
|
-
return /^v?\d{2}\.\d{4}
|
|
23
|
-
|| /^v?\d{6}
|
|
24
|
-
|| /^v?\d{4}\.\d{2}\.\d{2}
|
|
25
|
-
|| /^v?\d{8}
|
|
28
|
+
return /^v?\d{2}\.\d{4}(?:\.\d+)?$/.test(tag)
|
|
29
|
+
|| /^v?\d{6}(?:\.\d+)?$/.test(tag)
|
|
30
|
+
|| /^v?\d{4}\.\d{2}\.\d{2}(?:\.\d+)?$/.test(tag)
|
|
31
|
+
|| /^v?\d{8}(?:\.\d+)?$/.test(tag);
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export function isoDate(date = new Date()) {
|
package/test/calver.test.js
CHANGED
|
@@ -3,40 +3,67 @@ import { test } from 'node:test';
|
|
|
3
3
|
|
|
4
4
|
import { nextCalVer } from '../src/calver.js';
|
|
5
5
|
|
|
6
|
-
test('nextCalVer defaults to readable YY.MMDD
|
|
6
|
+
test('nextCalVer defaults to readable YY.MMDD format for the first release of the day', () => {
|
|
7
7
|
const version = nextCalVer({
|
|
8
8
|
date: new Date('2026-05-29T12:00:00-07:00'),
|
|
9
9
|
existingTags: [],
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
assert.equal(version, '26.0529
|
|
12
|
+
assert.equal(version, '26.0529');
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
test('nextCalVer increments the sequence for existing tags on the same day', () => {
|
|
16
16
|
const version = nextCalVer({
|
|
17
17
|
date: new Date('2026-05-29T12:00:00-07:00'),
|
|
18
|
-
existingTags: ['26.0528.7', '26.0529
|
|
18
|
+
existingTags: ['26.0528.7', '26.0529', 'v26.0529.2'],
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
assert.equal(version, '26.0529.3');
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
test('nextCalVer
|
|
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', () => {
|
|
25
43
|
const version = nextCalVer({
|
|
26
44
|
date: new Date('2026-05-29T12:00:00-07:00'),
|
|
27
|
-
existingTags: ['260529
|
|
45
|
+
existingTags: ['260529'],
|
|
28
46
|
format: 'compact',
|
|
29
47
|
});
|
|
30
48
|
|
|
31
|
-
assert.equal(version, '260529.
|
|
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');
|
|
32
59
|
});
|
|
33
60
|
|
|
34
|
-
test('nextCalVer can emit long YYYY.MM.DD.N
|
|
61
|
+
test('nextCalVer can emit long YYYY.MM.DD.N after the first release of the day', () => {
|
|
35
62
|
const version = nextCalVer({
|
|
36
63
|
date: new Date('2026-05-29T12:00:00-07:00'),
|
|
37
|
-
existingTags: ['2026.05.29
|
|
64
|
+
existingTags: ['2026.05.29'],
|
|
38
65
|
format: 'long',
|
|
39
66
|
});
|
|
40
67
|
|
|
41
|
-
assert.equal(version, '2026.05.29.
|
|
68
|
+
assert.equal(version, '2026.05.29.1');
|
|
42
69
|
});
|
package/test/cli.test.js
CHANGED
|
@@ -45,12 +45,12 @@ test('CLI reads .calverbumprc.json defaults', async () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
assert.equal(result.status, 0, result.stderr);
|
|
48
|
-
assert.match(result.stdout, /create git tag v26\.\d{4}
|
|
49
|
-
const tag = execFileSync('git', ['tag', '--list', 'v26
|
|
48
|
+
assert.match(result.stdout, /create git tag v26\.\d{4}/);
|
|
49
|
+
const tag = execFileSync('git', ['tag', '--list', 'v26*'], {
|
|
50
50
|
cwd: repo,
|
|
51
51
|
encoding: 'utf8',
|
|
52
52
|
}).trim();
|
|
53
|
-
assert.match(tag, /^v26\.\d{4}
|
|
53
|
+
assert.match(tag, /^v26\.\d{4}$/);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
test('CLI prints and runs push command when --push is enabled', async () => {
|
|
@@ -66,11 +66,11 @@ test('CLI prints and runs push command when --push is enabled', async () => {
|
|
|
66
66
|
|
|
67
67
|
assert.equal(result.status, 0, result.stderr);
|
|
68
68
|
assert.match(result.stdout, /Running: git push --follow-tags origin main/);
|
|
69
|
-
const remoteTags = execFileSync('git', ['tag', '--list', '26
|
|
69
|
+
const remoteTags = execFileSync('git', ['tag', '--list', '26*'], {
|
|
70
70
|
cwd: remote,
|
|
71
71
|
encoding: 'utf8',
|
|
72
72
|
}).trim();
|
|
73
|
-
assert.match(remoteTags, /^26\.\d{4}
|
|
73
|
+
assert.match(remoteTags, /^26\.\d{4}$/);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
test('CLI supports --remote with --push', async () => {
|
package/test/release.test.js
CHANGED
|
@@ -16,12 +16,12 @@ test('planRelease reports version, changelog, commit, and tag actions without wr
|
|
|
16
16
|
dryRun: true,
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
assert.equal(plan.version, '26.0529
|
|
19
|
+
assert.equal(plan.version, '26.0529');
|
|
20
20
|
assert.deepEqual(plan.actions, [
|
|
21
|
-
'update package.json version to 26.0529
|
|
22
|
-
'prepend CHANGELOG.md entry for 26.0529
|
|
23
|
-
'create git commit chore(release): 26.0529
|
|
24
|
-
'create git tag 26.0529
|
|
21
|
+
'update package.json version to 26.0529',
|
|
22
|
+
'prepend CHANGELOG.md entry for 26.0529',
|
|
23
|
+
'create git commit chore(release): 26.0529',
|
|
24
|
+
'create git tag 26.0529',
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
27
|
const pkg = JSON.parse(await readFile(path.join(repo, 'package.json'), 'utf8'));
|
|
@@ -37,17 +37,17 @@ test('runRelease supports tag prefixes without changing package.json version', a
|
|
|
37
37
|
tagPrefix: 'v',
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
assert.equal(result.version, '26.0529
|
|
41
|
-
assert.equal(result.tag, 'v26.0529
|
|
40
|
+
assert.equal(result.version, '26.0529');
|
|
41
|
+
assert.equal(result.tag, 'v26.0529');
|
|
42
42
|
|
|
43
43
|
const pkg = JSON.parse(await readFile(path.join(repo, 'package.json'), 'utf8'));
|
|
44
|
-
assert.equal(pkg.version, '26.0529
|
|
44
|
+
assert.equal(pkg.version, '26.0529');
|
|
45
45
|
|
|
46
|
-
const tag = execFileSync('git', ['tag', '--list', 'v26.0529
|
|
46
|
+
const tag = execFileSync('git', ['tag', '--list', 'v26.0529'], {
|
|
47
47
|
cwd: repo,
|
|
48
48
|
encoding: 'utf8',
|
|
49
49
|
}).trim();
|
|
50
|
-
assert.equal(tag, 'v26.0529
|
|
50
|
+
assert.equal(tag, 'v26.0529');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
test('runRelease updates package.json, prepends changelog, commits, and tags', async () => {
|
|
@@ -58,25 +58,25 @@ test('runRelease updates package.json, prepends changelog, commits, and tags', a
|
|
|
58
58
|
date: new Date('2026-05-29T12:00:00-07:00'),
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
assert.equal(result.version, '26.0529
|
|
61
|
+
assert.equal(result.version, '26.0529');
|
|
62
62
|
|
|
63
63
|
const pkg = JSON.parse(await readFile(path.join(repo, 'package.json'), 'utf8'));
|
|
64
|
-
assert.equal(pkg.version, '26.0529
|
|
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 - 2026-05-29\n\n### Features\n\n- feat: initial app/);
|
|
68
68
|
|
|
69
|
-
const tag = execFileSync('git', ['tag', '--list', '26.0529
|
|
69
|
+
const tag = execFileSync('git', ['tag', '--list', '26.0529'], {
|
|
70
70
|
cwd: repo,
|
|
71
71
|
encoding: 'utf8',
|
|
72
72
|
}).trim();
|
|
73
|
-
assert.equal(tag, '26.0529
|
|
73
|
+
assert.equal(tag, '26.0529');
|
|
74
74
|
|
|
75
75
|
const subject = execFileSync('git', ['log', '-1', '--pretty=%s'], {
|
|
76
76
|
cwd: repo,
|
|
77
77
|
encoding: 'utf8',
|
|
78
78
|
}).trim();
|
|
79
|
-
assert.equal(subject, 'chore(release): 26.0529
|
|
79
|
+
assert.equal(subject, 'chore(release): 26.0529');
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
test('runRelease returns the current branch for push guidance', async () => {
|
|
@@ -100,8 +100,8 @@ test('runRelease updates package-lock.json when it exists', async () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
const lock = JSON.parse(await readFile(path.join(repo, 'package-lock.json'), 'utf8'));
|
|
103
|
-
assert.equal(lock.version, '26.0529
|
|
104
|
-
assert.equal(lock.packages[''].version, '26.0529
|
|
103
|
+
assert.equal(lock.version, '26.0529');
|
|
104
|
+
assert.equal(lock.packages[''].version, '26.0529');
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
test('runRelease uses the latest reachable tag as the changelog base', async () => {
|
|
@@ -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 - 2026-05-29\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
|
|
|
@@ -242,8 +242,8 @@ test('runRelease prepends only commits since the previous CalVer tag on later re
|
|
|
242
242
|
});
|
|
243
243
|
|
|
244
244
|
const changelog = await readFile(path.join(repo, 'CHANGELOG.md'), 'utf8');
|
|
245
|
-
const latestEntry = changelog.split('## 26.0529
|
|
246
|
-
assert.match(latestEntry, /## 26\.0529\.
|
|
245
|
+
const latestEntry = changelog.split('## 26.0529 - 2026-05-29')[0];
|
|
246
|
+
assert.match(latestEntry, /## 26\.0529\.1 - 2026-05-29/);
|
|
247
247
|
assert.match(latestEntry, /- fix: second release only/);
|
|
248
248
|
assert.doesNotMatch(latestEntry, /feat: initial app/);
|
|
249
249
|
});
|
|
@@ -267,7 +267,7 @@ test('runRelease uses the latest reachable tag as the changelog base even when i
|
|
|
267
267
|
|
|
268
268
|
test('runRelease rolls back its release commit when tag creation fails', async () => {
|
|
269
269
|
const repo = await makeRepo();
|
|
270
|
-
execFileSync('git', ['tag', '26.0529
|
|
270
|
+
execFileSync('git', ['tag', '26.0529'], { cwd: repo });
|
|
271
271
|
const before = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim();
|
|
272
272
|
|
|
273
273
|
await assert.rejects(
|