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/README.md +56 -6
- package/bin/calver-bump.js +99 -3
- package/package.json +15 -2
- package/src/changelog.js +266 -0
- package/src/files.js +83 -0
- package/src/git.js +82 -0
- package/src/index.js +67 -402
- package/test/calver.test.js +0 -69
- package/test/cli.test.js +0 -109
- package/test/release.test.js +0 -568
package/README.md
CHANGED
|
@@ -18,9 +18,10 @@ Example:
|
|
|
18
18
|
|
|
19
19
|
1. Bumps `package.json` to the next CalVer version.
|
|
20
20
|
2. Updates `package-lock.json` or `npm-shrinkwrap.json` when present.
|
|
21
|
-
3.
|
|
22
|
-
4. Creates a
|
|
23
|
-
5. Creates
|
|
21
|
+
3. Warns when `pnpm-lock.yaml` or `yarn.lock` is present, because those lockfiles do not store the root package version consistently.
|
|
22
|
+
4. Creates or prepends a `CHANGELOG.md` entry from conventional commits since the nearest reachable tag.
|
|
23
|
+
5. Creates a release commit.
|
|
24
|
+
6. Creates an annotated git tag.
|
|
24
25
|
|
|
25
26
|
## Usage
|
|
26
27
|
|
|
@@ -34,6 +35,18 @@ Preview the planned release without writing files:
|
|
|
34
35
|
npx calver-bump --dry-run
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
Print help:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx calver-bump --help
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Print the installed `calver-bump` package version:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx calver-bump --version
|
|
48
|
+
```
|
|
49
|
+
|
|
37
50
|
Use compact CalVer instead:
|
|
38
51
|
|
|
39
52
|
```bash
|
|
@@ -70,6 +83,30 @@ Only include selected conventional commit types:
|
|
|
70
83
|
npx calver-bump --types feat,fix,perf
|
|
71
84
|
```
|
|
72
85
|
|
|
86
|
+
Use an explicit changelog base tag:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx calver-bump --from v1.35.0
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use local tags only without fetching from the configured remote:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx calver-bump --no-fetch
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Update only `package.json` and supported npm lockfiles:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx calver-bump --version-only
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Update only `CHANGELOG.md`:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx calver-bump --changelog-only
|
|
108
|
+
```
|
|
109
|
+
|
|
73
110
|
Update files without creating a release commit or tag:
|
|
74
111
|
|
|
75
112
|
```bash
|
|
@@ -85,10 +122,22 @@ Project defaults can be stored in `.calverbumprc.json`:
|
|
|
85
122
|
"format": "short",
|
|
86
123
|
"tagPrefix": "v",
|
|
87
124
|
"remote": "origin",
|
|
88
|
-
"types": ["feat", "fix", "perf"]
|
|
125
|
+
"types": ["feat", "fix", "perf"],
|
|
126
|
+
"changelogSections": {
|
|
127
|
+
"perf": "Performance",
|
|
128
|
+
"security": "Security"
|
|
129
|
+
}
|
|
89
130
|
}
|
|
90
131
|
```
|
|
91
132
|
|
|
133
|
+
## Runtime support
|
|
134
|
+
|
|
135
|
+
`calver-bump` supports Node.js LTS lines only. The current supported runtime range is Node.js 22 and Node.js 24.
|
|
136
|
+
|
|
137
|
+
## Publishing
|
|
138
|
+
|
|
139
|
+
The package includes a manual GitHub Actions publish workflow. Run the `Publish` workflow from GitHub after the package version has been bumped, tests pass, and the npm automation token is available as `NPM_TOKEN`.
|
|
140
|
+
|
|
92
141
|
## Notes
|
|
93
142
|
|
|
94
143
|
- The default `short` format is `YY.MMDD` for the first release of the day, then `YY.MMDD.1`, `YY.MMDD.2`, etc.
|
|
@@ -98,11 +147,12 @@ Project defaults can be stored in `.calverbumprc.json`:
|
|
|
98
147
|
- Existing `v`-prefixed tags are considered when calculating the next sequence number.
|
|
99
148
|
- Changelog ranges start from the nearest reachable tag, even when it is not a CalVer tag.
|
|
100
149
|
- Changelog entries include conventional commit subjects only, such as `feat:`, `fix(scope):`, or `chore!:`.
|
|
101
|
-
- Changelog entries are grouped into `Features`, `Fixes`, and `Other Changes
|
|
150
|
+
- Changelog entries are grouped into `Features`, `Fixes`, and `Other Changes` by default. Use `changelogSections` to assign additional conventional commit types to named sections.
|
|
102
151
|
- 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
152
|
- Changelog entries fall back to commit hash links for GitHub and GitLab-style remotes when no pull/merge request reference is found.
|
|
104
153
|
- 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.
|
|
105
154
|
- Later releases prepend only commits since the previous nearest reachable tag.
|
|
106
155
|
- Release tags are annotated so `git push --follow-tags <remote> <branch>` pushes them.
|
|
107
156
|
- The working tree must be clean before creating a real release.
|
|
108
|
-
-
|
|
157
|
+
- Existing release tags are rejected before files are written.
|
|
158
|
+
- If tag creation fails after the release commit, the CLI undoes its own commit and leaves the file changes in the working tree for inspection or recovery.
|
package/bin/calver-bump.js
CHANGED
|
@@ -8,21 +8,53 @@ import { assertFormat } from '../src/calver.js';
|
|
|
8
8
|
|
|
9
9
|
const execFile = promisify(execFileCallback);
|
|
10
10
|
const args = process.argv.slice(2);
|
|
11
|
+
const KNOWN_OPTIONS = new Set([
|
|
12
|
+
'--changelog-only',
|
|
13
|
+
'--dry-run',
|
|
14
|
+
'--format',
|
|
15
|
+
'--from',
|
|
16
|
+
'--help',
|
|
17
|
+
'--no-fetch',
|
|
18
|
+
'--push',
|
|
19
|
+
'--remote',
|
|
20
|
+
'--skip-commit',
|
|
21
|
+
'--tag-prefix',
|
|
22
|
+
'--types',
|
|
23
|
+
'--version',
|
|
24
|
+
'--version-only',
|
|
25
|
+
]);
|
|
11
26
|
|
|
12
27
|
try {
|
|
28
|
+
if (flag(args, '--help')) {
|
|
29
|
+
console.log(helpText());
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
if (flag(args, '--version')) {
|
|
33
|
+
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
34
|
+
console.log(pkg.version);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
const options = await releaseOptions(args);
|
|
14
39
|
assertFormat(options.format);
|
|
15
40
|
const result = await runRelease(options);
|
|
16
|
-
|
|
41
|
+
printResult(result, options);
|
|
42
|
+
if (result.warnings.length > 0) {
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log('Warnings:');
|
|
45
|
+
for (const warning of result.warnings) {
|
|
46
|
+
console.log(`- ${warning}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
17
49
|
for (const action of result.actions) {
|
|
18
50
|
console.log(`- ${action}`);
|
|
19
51
|
}
|
|
20
|
-
if (options.push && !options.dryRun) {
|
|
52
|
+
if (options.push && !options.dryRun && createsGitObjects(options)) {
|
|
21
53
|
const pushArgs = ['push', '--follow-tags', result.remote, result.branch];
|
|
22
54
|
console.log('');
|
|
23
55
|
console.log(`Running: git ${pushArgs.join(' ')}`);
|
|
24
56
|
await execFile('git', pushArgs, { encoding: 'utf8' });
|
|
25
|
-
} else if (!options.dryRun &&
|
|
57
|
+
} else if (!options.dryRun && createsGitObjects(options)) {
|
|
26
58
|
console.log('');
|
|
27
59
|
console.log('Next steps:');
|
|
28
60
|
console.log('1. Review the release commit:');
|
|
@@ -37,16 +69,27 @@ try {
|
|
|
37
69
|
}
|
|
38
70
|
|
|
39
71
|
async function releaseOptions(args) {
|
|
72
|
+
rejectUnknownOptions(args);
|
|
40
73
|
const config = await readConfig();
|
|
74
|
+
const versionOnly = flag(args, '--version-only') ?? config.versionOnly ?? false;
|
|
75
|
+
const changelogOnly = flag(args, '--changelog-only') ?? config.changelogOnly ?? false;
|
|
76
|
+
if (versionOnly && changelogOnly) {
|
|
77
|
+
throw new Error('--version-only cannot be combined with --changelog-only.');
|
|
78
|
+
}
|
|
41
79
|
return {
|
|
42
80
|
...config,
|
|
43
81
|
dryRun: flag(args, '--dry-run') ?? config.dryRun ?? false,
|
|
82
|
+
noFetch: flag(args, '--no-fetch') ?? config.noFetch ?? false,
|
|
44
83
|
push: flag(args, '--push') ?? config.push ?? false,
|
|
45
84
|
skipCommit: flag(args, '--skip-commit') ?? config.skipCommit ?? false,
|
|
85
|
+
versionOnly,
|
|
86
|
+
changelogOnly,
|
|
87
|
+
from: value(args, '--from') ?? config.from,
|
|
46
88
|
format: value(args, '--format') ?? config.format ?? 'short',
|
|
47
89
|
remote: value(args, '--remote') ?? config.remote ?? 'origin',
|
|
48
90
|
tagPrefix: value(args, '--tag-prefix') ?? config.tagPrefix ?? '',
|
|
49
91
|
types: parseTypes(value(args, '--types')) ?? config.types,
|
|
92
|
+
changelogSections: config.changelogSections,
|
|
50
93
|
};
|
|
51
94
|
}
|
|
52
95
|
|
|
@@ -80,3 +123,56 @@ function value(args, name) {
|
|
|
80
123
|
function parseTypes(types) {
|
|
81
124
|
return types?.split(',').map((type) => type.trim()).filter(Boolean);
|
|
82
125
|
}
|
|
126
|
+
|
|
127
|
+
function rejectUnknownOptions(args) {
|
|
128
|
+
for (const arg of args) {
|
|
129
|
+
if (arg.startsWith('--') && !KNOWN_OPTIONS.has(arg)) {
|
|
130
|
+
throw new Error(`Unknown option ${arg}. Run calver-bump --help for usage.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function printResult(result, options) {
|
|
136
|
+
console.log(`Release version: ${result.version}`);
|
|
137
|
+
console.log(`Git tag: ${createsGitObjects(options) ? result.tag : '(not created)'}`);
|
|
138
|
+
console.log(`Branch: ${result.branch}`);
|
|
139
|
+
console.log(`Remote: ${result.remote}`);
|
|
140
|
+
if (!options.versionOnly) {
|
|
141
|
+
console.log(`Changelog range: ${result.range}`);
|
|
142
|
+
console.log(`Previous tag: ${result.previousTag ?? '(none)'}`);
|
|
143
|
+
}
|
|
144
|
+
if (result.files.length > 0) {
|
|
145
|
+
console.log(`Files: ${result.files.join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
if (options.noFetch) {
|
|
148
|
+
console.log('Tag fetch: skipped (--no-fetch)');
|
|
149
|
+
}
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(options.dryRun ? 'Planned actions:' : 'Completed actions:');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createsGitObjects(options) {
|
|
155
|
+
return !options.skipCommit && !options.versionOnly && !options.changelogOnly;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function helpText() {
|
|
159
|
+
return `Usage: calver-bump [options]
|
|
160
|
+
|
|
161
|
+
Options:
|
|
162
|
+
--dry-run Preview the release without writing files.
|
|
163
|
+
--format <name> Version format: short, compact, or long.
|
|
164
|
+
--tag-prefix <prefix> Prefix the git tag without changing package.json.
|
|
165
|
+
--types <list> Comma-separated conventional commit types to include.
|
|
166
|
+
--from <tag> Use an explicit changelog base tag.
|
|
167
|
+
--no-fetch Use local tags only; do not fetch remote tags.
|
|
168
|
+
--version-only Update package.json and supported npm lockfiles only.
|
|
169
|
+
--changelog-only Update CHANGELOG.md only.
|
|
170
|
+
--skip-commit Update files without creating a release commit or tag.
|
|
171
|
+
--push Push the release commit and annotated tag.
|
|
172
|
+
--remote <name> Remote used for fetch, links, and push.
|
|
173
|
+
--version Print the calver-bump package version.
|
|
174
|
+
--help Show this help.
|
|
175
|
+
|
|
176
|
+
Configuration:
|
|
177
|
+
Project defaults can be stored in .calverbumprc.json.`;
|
|
178
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "calver-bump",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Release CLI for internal applications using readable CalVer versions.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/msako/calver-bump.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/msako/calver-bump#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/msako/calver-bump/issues"
|
|
12
|
+
},
|
|
5
13
|
"main": "src/index.js",
|
|
6
14
|
"bin": {
|
|
7
15
|
"calver-bump": "bin/calver-bump.js"
|
|
8
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"src",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
9
22
|
"scripts": {
|
|
10
23
|
"test": "node --test"
|
|
11
24
|
},
|
|
@@ -19,6 +32,6 @@
|
|
|
19
32
|
"license": "MIT",
|
|
20
33
|
"type": "module",
|
|
21
34
|
"engines": {
|
|
22
|
-
"node": "
|
|
35
|
+
"node": "^22.0.0 || ^24.0.0"
|
|
23
36
|
}
|
|
24
37
|
}
|
package/src/changelog.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { fetchTags, getRemoteUrl, gitCommits, latestReachableTag, tagExists } from './git.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'];
|
|
4
|
+
const DEFAULT_CHANGELOG_SECTIONS = {
|
|
5
|
+
feat: 'Features',
|
|
6
|
+
fix: 'Fixes',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function releaseNotes(cwd, options = {}) {
|
|
10
|
+
if (!options.noFetch) {
|
|
11
|
+
await fetchTags(cwd, options.remote ?? 'origin');
|
|
12
|
+
}
|
|
13
|
+
const latestTag = options.from ?? await latestReleaseTag(cwd, options.existingChangelog ?? '');
|
|
14
|
+
const range = latestTag ? [`${latestTag}..HEAD`] : [];
|
|
15
|
+
const commits = await gitCommits(cwd, range);
|
|
16
|
+
const remoteUrl = await getRemoteUrl(cwd, options.remote ?? 'origin');
|
|
17
|
+
const commitUrlBuilder = remoteUrl ? buildCommitUrlBuilder(remoteUrl) : null;
|
|
18
|
+
const compareUrlBuilder = remoteUrl ? buildCompareUrlBuilder(remoteUrl) : null;
|
|
19
|
+
const requestUrlBuilder = remoteUrl ? buildRequestUrlBuilder(remoteUrl) : null;
|
|
20
|
+
const allowedTypes = options.types ?? DEFAULT_TYPES;
|
|
21
|
+
const conventionalCommits = dedupeConventionalChanges(commits
|
|
22
|
+
.map((commit) => ({ ...commit, subject: conventionalSubjectForCommit(commit) ?? commit.subject }))
|
|
23
|
+
.filter((commit) => isConventionalCommit(commit.subject))
|
|
24
|
+
.filter((commit) => allowedTypes.includes(conventionalType(commit.subject)))
|
|
25
|
+
.filter((commit) => !isCommitInChangelog(commit, options.existingChangelog ?? ''))
|
|
26
|
+
.map((commit) => ({
|
|
27
|
+
...commit,
|
|
28
|
+
request: requestForCommit(commit, requestUrlBuilder),
|
|
29
|
+
url: commitUrlBuilder ? commitUrlBuilder(commit.hash) : null,
|
|
30
|
+
})));
|
|
31
|
+
const requests = uniqueRequests(commits.map((commit) => requestForCommit(commit, requestUrlBuilder)).filter(Boolean));
|
|
32
|
+
return {
|
|
33
|
+
previousTag: latestTag,
|
|
34
|
+
range: latestTag ? `${latestTag}..HEAD` : 'HEAD',
|
|
35
|
+
compareUrlBuilder,
|
|
36
|
+
changes: conventionalCommits.length > 0 ? conventionalCommits : ['No conventional commits in this release.'],
|
|
37
|
+
requests,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatReleaseHeading({ version, previousTag, tag, compareUrlBuilder }) {
|
|
42
|
+
const label = previousTag && compareUrlBuilder
|
|
43
|
+
? `[${version}](${compareUrlBuilder(previousTag, tag)})`
|
|
44
|
+
: version;
|
|
45
|
+
return `## ${label}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatReleaseNotes(changes, sectionConfig = {}) {
|
|
49
|
+
if (changes.length === 1 && changes[0] === 'No conventional commits in this release.') {
|
|
50
|
+
return `- ${changes[0]}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sectionMap = { ...DEFAULT_CHANGELOG_SECTIONS, ...sectionConfig };
|
|
54
|
+
const grouped = new Map();
|
|
55
|
+
const other = [];
|
|
56
|
+
for (const change of changes) {
|
|
57
|
+
const heading = sectionMap[conventionalType(change.subject)];
|
|
58
|
+
if (!heading) {
|
|
59
|
+
other.push(change);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
grouped.set(heading, [...(grouped.get(heading) ?? []), change]);
|
|
63
|
+
}
|
|
64
|
+
const sections = [...grouped.entries()];
|
|
65
|
+
if (other.length > 0) {
|
|
66
|
+
sections.push(['Other Changes', other]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return sections
|
|
70
|
+
.filter(([, entries]) => entries.length > 0)
|
|
71
|
+
.map(([heading, entries]) => `### ${heading}\n\n${entries.map((entry) => `- ${formatCommitEntry(entry)}`).join('\n')}`)
|
|
72
|
+
.join('\n\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatFullChangelog(requests) {
|
|
76
|
+
if (requests.length === 0) {
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
return `\n\n### Full Changelog\n\n${requests.map((request) => `- ${formatRequestEntry(request)}`).join('\n')}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function latestReleaseTag(cwd, changelog) {
|
|
83
|
+
const changelogTag = latestChangelogCompareTarget(changelog);
|
|
84
|
+
if (changelogTag && await tagExists(cwd, changelogTag)) {
|
|
85
|
+
return changelogTag;
|
|
86
|
+
}
|
|
87
|
+
return latestReachableTag(cwd);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function latestChangelogCompareTarget(changelog) {
|
|
91
|
+
const match = /^## \[[^\]]+\]\([^)]*\/(?:-\/)?compare\/[^)]*?\.{3}(?<tag>[^)\s]+)\)/m.exec(changelog);
|
|
92
|
+
return match?.groups.tag ?? null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isCommitInChangelog(commit, changelog) {
|
|
96
|
+
if (!changelog) return false;
|
|
97
|
+
return changelog.includes(commit.hash) || changelog.includes(commit.hash.slice(0, 7));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isConventionalCommit(subject) {
|
|
101
|
+
return /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: .+/.test(subject);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function conventionalSubjectForCommit(commit) {
|
|
105
|
+
return commitLines(commit).find((line) => isConventionalCommit(line)) ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function commitLines(commit) {
|
|
109
|
+
return [commit.subject, ...(commit.body ?? '').split('\n')]
|
|
110
|
+
.map((line) => line.trim())
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function dedupeConventionalChanges(changes) {
|
|
115
|
+
const deduped = [];
|
|
116
|
+
for (const change of changes) {
|
|
117
|
+
const existingIndex = deduped.findIndex((candidate) => candidate.subject === change.subject);
|
|
118
|
+
if (existingIndex < 0) {
|
|
119
|
+
deduped.push(change);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!deduped[existingIndex].request && change.request) {
|
|
123
|
+
deduped[existingIndex] = change;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return deduped;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function conventionalType(subject) {
|
|
130
|
+
return /^(?<type>[a-z]+)(\([^)]+\))?!?: .+/.exec(subject)?.groups.type;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatCommitEntry(commit) {
|
|
134
|
+
const shortHash = commit.hash.slice(0, 7);
|
|
135
|
+
const suffix = commit.request
|
|
136
|
+
? ` (${formatRequestLink(commit.request)})`
|
|
137
|
+
: commit.url ? ` ([${shortHash}](${commit.url}))` : ` (${shortHash})`;
|
|
138
|
+
return `${commit.subject}${suffix}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatRequestLink(request) {
|
|
142
|
+
return request.url ? `[${request.label}](${request.url})` : request.label;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function formatRequestEntry(request) {
|
|
146
|
+
return request.title ? `${formatRequestLink(request)} ${request.title}` : formatRequestLink(request);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildCommitUrlBuilder(remote) {
|
|
150
|
+
const parsed = parseGitRemote(remote);
|
|
151
|
+
if (!parsed) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const baseUrl = `https://${parsed.host}/${parsed.repo}`;
|
|
156
|
+
if (parsed.host === 'github.com') {
|
|
157
|
+
return (hash) => `${baseUrl}/commit/${hash}`;
|
|
158
|
+
}
|
|
159
|
+
if (parsed.host.includes('gitlab')) {
|
|
160
|
+
return (hash) => `${baseUrl}/-/commit/${hash}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildCompareUrlBuilder(remote) {
|
|
167
|
+
const parsed = parseGitRemote(remote);
|
|
168
|
+
if (!parsed) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const baseUrl = `https://${parsed.host}/${parsed.repo}`;
|
|
173
|
+
if (parsed.host === 'github.com') {
|
|
174
|
+
return (from, to) => `${baseUrl}/compare/${from}...${to}`;
|
|
175
|
+
}
|
|
176
|
+
if (parsed.host.includes('gitlab')) {
|
|
177
|
+
return (from, to) => `${baseUrl}/-/compare/${from}...${to}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildRequestUrlBuilder(remote) {
|
|
184
|
+
const parsed = parseGitRemote(remote);
|
|
185
|
+
if (!parsed) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const baseUrl = `https://${parsed.host}/${parsed.repo}`;
|
|
190
|
+
if (parsed.host === 'github.com') {
|
|
191
|
+
return (request) => request.provider === 'github' ? `${baseUrl}/pull/${request.number}` : null;
|
|
192
|
+
}
|
|
193
|
+
if (parsed.host.includes('gitlab')) {
|
|
194
|
+
return (request) => request.provider === 'gitlab' ? `${baseUrl}/-/merge_requests/${request.number}` : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function requestForCommit(commit, requestUrlBuilder) {
|
|
201
|
+
const request = parseRequestReference(`${commit.subject}\n${commit.body ?? ''}`);
|
|
202
|
+
if (!request) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
...request,
|
|
207
|
+
title: requestTitleForCommit(commit),
|
|
208
|
+
url: requestUrlBuilder ? requestUrlBuilder(request) : null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function requestTitleForCommit(commit) {
|
|
213
|
+
const conventionalSubject = conventionalSubjectForCommit(commit);
|
|
214
|
+
if (conventionalSubject) {
|
|
215
|
+
return conventionalSubject;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return commitLines(commit)
|
|
219
|
+
.find((line) => line && !parseRequestReference(line) && !/^Merge\b/i.test(line)) ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseRequestReference(message) {
|
|
223
|
+
const gitlabMerge = /(?:^|\s)(?:See merge request\s+\S+!|!)(?<number>\d+)(?=\D|$)/i.exec(message);
|
|
224
|
+
if (gitlabMerge) {
|
|
225
|
+
return { provider: 'gitlab', number: gitlabMerge.groups.number, label: `!${gitlabMerge.groups.number}` };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const githubPull = /(?:Merge pull request\s+#|#)(?<number>\d+)(?=\D|$)/i.exec(message);
|
|
229
|
+
if (githubPull) {
|
|
230
|
+
return { provider: 'github', number: githubPull.groups.number, label: `#${githubPull.groups.number}` };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function uniqueRequests(requests) {
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
const unique = [];
|
|
239
|
+
for (const request of requests) {
|
|
240
|
+
const key = `${request.provider}:${request.number}`;
|
|
241
|
+
if (seen.has(key)) {
|
|
242
|
+
const existing = unique.find((candidate) => `${candidate.provider}:${candidate.number}` === key);
|
|
243
|
+
if (existing && !existing.title && request.title) {
|
|
244
|
+
existing.title = request.title;
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
seen.add(key);
|
|
249
|
+
unique.push(request);
|
|
250
|
+
}
|
|
251
|
+
return unique;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseGitRemote(remote) {
|
|
255
|
+
const sshMatch = /^git@(?<host>[^:]+):(?<repo>.+?)(?:\.git)?$/.exec(remote);
|
|
256
|
+
if (sshMatch) {
|
|
257
|
+
return sshMatch.groups;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const httpsMatch = /^https:\/\/(?<host>[^/]+)\/(?<repo>.+?)(?:\.git)?$/.exec(remote);
|
|
261
|
+
if (httpsMatch) {
|
|
262
|
+
return httpsMatch.groups;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
}
|
package/src/files.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function updatePackageVersion(cwd, version) {
|
|
5
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
6
|
+
const pkg = JSON.parse(await readFile(packagePath, 'utf8'));
|
|
7
|
+
pkg.version = version;
|
|
8
|
+
await writeFile(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function updatePackageLock(cwd, version) {
|
|
12
|
+
for (const fileName of ['package-lock.json', 'npm-shrinkwrap.json']) {
|
|
13
|
+
const filePath = path.join(cwd, fileName);
|
|
14
|
+
let lock;
|
|
15
|
+
try {
|
|
16
|
+
lock = JSON.parse(await readFile(filePath, 'utf8'));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error.code === 'ENOENT') continue;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof lock.version === 'string') {
|
|
23
|
+
lock.version = version;
|
|
24
|
+
}
|
|
25
|
+
if (lock.packages?.[''] && typeof lock.packages[''].version === 'string') {
|
|
26
|
+
lock.packages[''].version = version;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await writeFile(filePath, `${JSON.stringify(lock, null, 2)}\n`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function releaseFiles(cwd, options = {}) {
|
|
34
|
+
const candidates = [];
|
|
35
|
+
if (options.version !== false) {
|
|
36
|
+
candidates.push('package.json', 'package-lock.json', 'npm-shrinkwrap.json');
|
|
37
|
+
}
|
|
38
|
+
if (options.changelog !== false) {
|
|
39
|
+
candidates.push('CHANGELOG.md');
|
|
40
|
+
}
|
|
41
|
+
const files = [];
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
if (await fileExists(path.join(cwd, candidate))) {
|
|
44
|
+
files.push(candidate);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return files;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function releaseWarnings(cwd, options = {}) {
|
|
51
|
+
if (!options.updatesVersion) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const warnings = [];
|
|
55
|
+
for (const fileName of ['pnpm-lock.yaml', 'yarn.lock']) {
|
|
56
|
+
if (await fileExists(path.join(cwd, fileName))) {
|
|
57
|
+
warnings.push(`${fileName} detected; calver-bump does not rewrite this lockfile because it does not store the root package version consistently.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return warnings;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function readChangelog(cwd) {
|
|
64
|
+
try {
|
|
65
|
+
return await readFile(path.join(cwd, 'CHANGELOG.md'), 'utf8');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code === 'ENOENT') return '';
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function writeChangelog(cwd, body) {
|
|
73
|
+
await writeFile(path.join(cwd, 'CHANGELOG.md'), body);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fileExists(filePath) {
|
|
77
|
+
try {
|
|
78
|
+
await access(filePath);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|