calver-bump 0.1.8 → 1.0.0

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 CHANGED
@@ -20,8 +20,7 @@ Example:
20
20
  2. Updates `package-lock.json` or `npm-shrinkwrap.json` when present.
21
21
  3. Warns when `pnpm-lock.yaml` or `yarn.lock` is present, because those lockfiles do not store the root package version consistently.
22
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.
23
+ 5. Prints the git commands needed to commit, tag, and push the reviewed release.
25
24
 
26
25
  ## Usage
27
26
 
@@ -29,6 +28,8 @@ Example:
29
28
  npx calver-bump
30
29
  ```
31
30
 
31
+ By default, `calver-bump` updates files only. Review `CHANGELOG.md`, then run the printed git commands when the release notes are correct.
32
+
32
33
  Preview the planned release without writing files:
33
34
 
34
35
  ```bash
@@ -107,7 +108,19 @@ Update only `CHANGELOG.md`:
107
108
  npx calver-bump --changelog-only
108
109
  ```
109
110
 
110
- Update files without creating a release commit or tag:
111
+ Create a release commit after updating files:
112
+
113
+ ```bash
114
+ npx calver-bump --commit
115
+ ```
116
+
117
+ Create a release commit and annotated tag after updating files:
118
+
119
+ ```bash
120
+ npx calver-bump --tag
121
+ ```
122
+
123
+ Explicitly update files without creating a release commit or tag:
111
124
 
112
125
  ```bash
113
126
  npx calver-bump --skip-commit
@@ -152,6 +165,10 @@ The package includes a manual GitHub Actions publish workflow. Run the `Publish`
152
165
  - Changelog entries fall back to commit hash links for GitHub and GitLab-style remotes when no pull/merge request reference is found.
153
166
  - 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.
154
167
  - Later releases prepend only commits since the previous nearest reachable tag.
168
+ - Plain `calver-bump` does not commit or tag by default; it prints the follow-up `git add`, `git commit`, `git tag`, and `git push --follow-tags` commands.
169
+ - Use `--commit` to create only the release commit.
170
+ - Use `--tag` to create the release commit and annotated tag.
171
+ - Use `--push` to create the release commit, create the annotated tag, and push both.
155
172
  - Release tags are annotated so `git push --follow-tags <remote> <branch>` pushes them.
156
173
  - The working tree must be clean before creating a real release.
157
174
  - Existing release tags are rejected before files are written.
@@ -10,6 +10,7 @@ const execFile = promisify(execFileCallback);
10
10
  const args = process.argv.slice(2);
11
11
  const KNOWN_OPTIONS = new Set([
12
12
  '--changelog-only',
13
+ '--commit',
13
14
  '--dry-run',
14
15
  '--format',
15
16
  '--from',
@@ -18,6 +19,7 @@ const KNOWN_OPTIONS = new Set([
18
19
  '--push',
19
20
  '--remote',
20
21
  '--skip-commit',
22
+ '--tag',
21
23
  '--tag-prefix',
22
24
  '--types',
23
25
  '--version',
@@ -49,19 +51,17 @@ try {
49
51
  for (const action of result.actions) {
50
52
  console.log(`- ${action}`);
51
53
  }
52
- if (options.push && !options.dryRun && createsGitObjects(options)) {
54
+ if (options.push && !options.dryRun && createsTag(options)) {
53
55
  const pushArgs = ['push', '--follow-tags', result.remote, result.branch];
54
56
  console.log('');
55
57
  console.log(`Running: git ${pushArgs.join(' ')}`);
56
58
  await execFile('git', pushArgs, { encoding: 'utf8' });
57
- } else if (!options.dryRun && createsGitObjects(options)) {
59
+ } else if (!options.dryRun) {
58
60
  console.log('');
59
61
  console.log('Next steps:');
60
- console.log('1. Review the release commit:');
61
- console.log(' git show --stat HEAD');
62
- console.log('2. Push the release commit and tag:');
63
- console.log(` git push --follow-tags ${result.remote} ${result.branch}`);
64
- console.log('3. Trigger or verify your deployment pipeline.');
62
+ for (const instruction of nextStepInstructions(result, options)) {
63
+ console.log(instruction);
64
+ }
65
65
  }
66
66
  } catch (error) {
67
67
  console.error(error.message);
@@ -73,15 +73,24 @@ async function releaseOptions(args) {
73
73
  const config = await readConfig();
74
74
  const versionOnly = flag(args, '--version-only') ?? config.versionOnly ?? false;
75
75
  const changelogOnly = flag(args, '--changelog-only') ?? config.changelogOnly ?? false;
76
+ const skipCommit = flag(args, '--skip-commit') ?? config.skipCommit ?? false;
77
+ const push = flag(args, '--push') ?? config.push ?? false;
78
+ const tag = push || (flag(args, '--tag') ?? config.tag ?? false);
79
+ const commit = tag || (flag(args, '--commit') ?? config.commit ?? false);
76
80
  if (versionOnly && changelogOnly) {
77
81
  throw new Error('--version-only cannot be combined with --changelog-only.');
78
82
  }
83
+ if (skipCommit && (commit || tag || push)) {
84
+ throw new Error('--skip-commit cannot be combined with --commit, --tag, or --push.');
85
+ }
79
86
  return {
80
87
  ...config,
81
88
  dryRun: flag(args, '--dry-run') ?? config.dryRun ?? false,
82
89
  noFetch: flag(args, '--no-fetch') ?? config.noFetch ?? false,
83
- push: flag(args, '--push') ?? config.push ?? false,
84
- skipCommit: flag(args, '--skip-commit') ?? config.skipCommit ?? false,
90
+ commit,
91
+ tag,
92
+ push,
93
+ skipCommit,
85
94
  versionOnly,
86
95
  changelogOnly,
87
96
  from: value(args, '--from') ?? config.from,
@@ -134,7 +143,7 @@ function rejectUnknownOptions(args) {
134
143
 
135
144
  function printResult(result, options) {
136
145
  console.log(`Release version: ${result.version}`);
137
- console.log(`Git tag: ${createsGitObjects(options) ? result.tag : '(not created)'}`);
146
+ console.log(`Git tag: ${result.createdTag ?? '(not created)'}`);
138
147
  console.log(`Branch: ${result.branch}`);
139
148
  console.log(`Remote: ${result.remote}`);
140
149
  if (!options.versionOnly) {
@@ -151,8 +160,42 @@ function printResult(result, options) {
151
160
  console.log(options.dryRun ? 'Planned actions:' : 'Completed actions:');
152
161
  }
153
162
 
154
- function createsGitObjects(options) {
155
- return !options.skipCommit && !options.versionOnly && !options.changelogOnly;
163
+ function createsCommit(options) {
164
+ return Boolean(options.commit || options.tag || options.push) && !options.skipCommit && !options.versionOnly && !options.changelogOnly;
165
+ }
166
+
167
+ function createsTag(options) {
168
+ return Boolean(options.tag || options.push) && createsCommit(options);
169
+ }
170
+
171
+ function nextStepInstructions(result, options) {
172
+ if (options.versionOnly || options.changelogOnly) {
173
+ return ['Review the generated file changes.'];
174
+ }
175
+ if (!createsCommit(options)) {
176
+ return [
177
+ 'Review CHANGELOG.md, then run:',
178
+ `git add ${result.files.join(' ')}`,
179
+ `git commit -m "chore(release): ${result.version}"`,
180
+ `git tag -a ${result.tag} -m "Release ${result.version}"`,
181
+ `git push --follow-tags ${result.remote} ${result.branch}`,
182
+ ];
183
+ }
184
+ if (!createsTag(options)) {
185
+ return [
186
+ 'Review the release commit, then run:',
187
+ 'git show --stat HEAD',
188
+ `git tag -a ${result.tag} -m "Release ${result.version}"`,
189
+ `git push --follow-tags ${result.remote} ${result.branch}`,
190
+ ];
191
+ }
192
+ return [
193
+ 'Review the release commit:',
194
+ 'git show --stat HEAD',
195
+ 'Push the release commit and tag:',
196
+ `git push --follow-tags ${result.remote} ${result.branch}`,
197
+ 'Trigger or verify your deployment pipeline.',
198
+ ];
156
199
  }
157
200
 
158
201
  function helpText() {
@@ -167,8 +210,10 @@ Options:
167
210
  --no-fetch Use local tags only; do not fetch remote tags.
168
211
  --version-only Update package.json and supported npm lockfiles only.
169
212
  --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.
213
+ --commit Create the release commit after updating files.
214
+ --tag Create the release commit and annotated tag.
215
+ --skip-commit Explicitly update files without creating a release commit or tag.
216
+ --push Create, tag, and push the release.
172
217
  --remote <name> Remote used for fetch, links, and push.
173
218
  --version Print the calver-bump package version.
174
219
  --help Show this help.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calver-bump",
3
- "version": "0.1.8",
3
+ "version": "1.0.0",
4
4
  "description": "Release CLI for internal applications using readable CalVer versions.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/changelog.js CHANGED
@@ -39,10 +39,7 @@ export async function releaseNotes(cwd, options = {}) {
39
39
  }
40
40
 
41
41
  export function formatReleaseHeading({ version, previousTag, tag, compareUrlBuilder }) {
42
- const label = previousTag && compareUrlBuilder
43
- ? `[${version}](${compareUrlBuilder(previousTag, tag)})`
44
- : version;
45
- return `## ${label}`;
42
+ return `## ${version}`;
46
43
  }
47
44
 
48
45
  export function formatReleaseNotes(changes, sectionConfig = {}) {
@@ -72,11 +69,15 @@ export function formatReleaseNotes(changes, sectionConfig = {}) {
72
69
  .join('\n\n');
73
70
  }
74
71
 
75
- export function formatFullChangelog(requests) {
76
- if (requests.length === 0) {
72
+ export function formatFullChangelog(requests, compareUrl = null) {
73
+ const entries = [
74
+ ...requests.map((request) => formatRequestEntry(request)),
75
+ ...(compareUrl ? [`[Compare changes](${compareUrl})`] : []),
76
+ ];
77
+ if (entries.length === 0) {
77
78
  return '';
78
79
  }
79
- return `\n\n### Full Changelog\n\n${requests.map((request) => `- ${formatRequestEntry(request)}`).join('\n')}`;
80
+ return `\n\n### Full Changelog\n\n${entries.map((entry) => `- ${entry}`).join('\n')}`;
80
81
  }
81
82
 
82
83
  async function latestReleaseTag(cwd, changelog) {
@@ -88,7 +89,7 @@ async function latestReleaseTag(cwd, changelog) {
88
89
  }
89
90
 
90
91
  function latestChangelogCompareTarget(changelog) {
91
- const match = /^## \[[^\]]+\]\([^)]*\/(?:-\/)?compare\/[^)]*?\.{3}(?<tag>[^)\s]+)\)/m.exec(changelog);
92
+ const match = /\[[^\]]+\]\([^)]*\/(?:-\/)?compare\/[^)]*?\.{3}(?<tag>[^)\s]+)\)/m.exec(changelog);
92
93
  return match?.groups.tag ?? null;
93
94
  }
94
95
 
package/src/files.js CHANGED
@@ -32,18 +32,18 @@ export async function updatePackageLock(cwd, version) {
32
32
 
33
33
  export async function releaseFiles(cwd, options = {}) {
34
34
  const candidates = [];
35
+ const files = [];
35
36
  if (options.version !== false) {
36
37
  candidates.push('package.json', 'package-lock.json', 'npm-shrinkwrap.json');
37
38
  }
38
- if (options.changelog !== false) {
39
- candidates.push('CHANGELOG.md');
40
- }
41
- const files = [];
42
39
  for (const candidate of candidates) {
43
40
  if (await fileExists(path.join(cwd, candidate))) {
44
41
  files.push(candidate);
45
42
  }
46
43
  }
44
+ if (options.changelog !== false) {
45
+ files.push('CHANGELOG.md');
46
+ }
47
47
  return files;
48
48
  }
49
49
 
package/src/index.js CHANGED
@@ -42,7 +42,8 @@ export async function planRelease(options = {}) {
42
42
  actions: releaseActions({
43
43
  version,
44
44
  tag,
45
- skipCommit: options.skipCommit || options.versionOnly || options.changelogOnly,
45
+ commit: createsCommit(options),
46
+ tagRelease: createsTag(options),
46
47
  versionOnly: options.versionOnly,
47
48
  changelogOnly: options.changelogOnly,
48
49
  files,
@@ -59,7 +60,7 @@ export async function runRelease(options = {}) {
59
60
  }
60
61
 
61
62
  await assertCleanWorktree(cwd);
62
- if (!options.skipCommit && !options.versionOnly && !options.changelogOnly) {
63
+ if (createsTag(options)) {
63
64
  await assertTagAvailable(cwd, plan.tag);
64
65
  }
65
66
 
@@ -71,23 +72,25 @@ export async function runRelease(options = {}) {
71
72
  await prependChangelog(cwd, plan.version, options);
72
73
  }
73
74
 
74
- if (options.skipCommit || options.versionOnly || options.changelogOnly) {
75
- return { ...plan, tag: null };
75
+ if (!createsCommit(options)) {
76
+ return { ...plan, createdTag: null };
76
77
  }
77
78
 
78
79
  await git(cwd, ['add', ...await releaseFiles(cwd)]);
79
80
  await git(cwd, ['commit', '-m', `chore(release): ${plan.version}`]);
80
- try {
81
- await git(cwd, ['tag', '-a', plan.tag, '-m', `Release ${plan.version}`]);
82
- } catch (error) {
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}`);
81
+ if (createsTag(options)) {
82
+ try {
83
+ await git(cwd, ['tag', '-a', plan.tag, '-m', `Release ${plan.version}`]);
84
+ } catch (error) {
85
+ await git(cwd, ['reset', '--soft', 'HEAD~1']);
86
+ 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}`);
87
+ }
85
88
  }
86
89
 
87
- return plan;
90
+ return createsTag(options) ? { ...plan, createdTag: plan.tag } : { ...plan, createdTag: null };
88
91
  }
89
92
 
90
- function releaseActions({ version, tag, skipCommit = false, versionOnly = false, changelogOnly = false, files = [] }) {
93
+ function releaseActions({ version, tag, commit = false, tagRelease = false, versionOnly = false, changelogOnly = false, files = [] }) {
91
94
  const actions = [];
92
95
  if (!changelogOnly) {
93
96
  actions.push(`update package.json version to ${version}`);
@@ -98,13 +101,23 @@ function releaseActions({ version, tag, skipCommit = false, versionOnly = false,
98
101
  if (!versionOnly) {
99
102
  actions.push(`prepend CHANGELOG.md entry for ${version}`);
100
103
  }
101
- if (!skipCommit) {
104
+ if (commit) {
102
105
  actions.push(`create git commit chore(release): ${version}`);
106
+ }
107
+ if (tagRelease) {
103
108
  actions.push(`create git tag ${tag}`);
104
109
  }
105
110
  return actions;
106
111
  }
107
112
 
113
+ function createsCommit(options) {
114
+ return Boolean(options.commit || options.tag || options.push) && !options.skipCommit && !options.versionOnly && !options.changelogOnly;
115
+ }
116
+
117
+ function createsTag(options) {
118
+ return Boolean(options.tag || options.push) && createsCommit(options);
119
+ }
120
+
108
121
  async function prependChangelog(cwd, version, options = {}) {
109
122
  const existing = await readChangelog(cwd);
110
123
 
@@ -119,7 +132,10 @@ async function prependChangelog(cwd, version, options = {}) {
119
132
  tag: options.tagPrefix ? `${options.tagPrefix}${version}` : version,
120
133
  compareUrlBuilder: notes.compareUrlBuilder,
121
134
  });
122
- const entry = `${heading}\n\n${formatReleaseNotes(notes.changes, options.changelogSections)}${formatFullChangelog(notes.requests)}\n`;
135
+ const compareUrl = notes.previousTag && notes.compareUrlBuilder
136
+ ? notes.compareUrlBuilder(notes.previousTag, options.tagPrefix ? `${options.tagPrefix}${version}` : version)
137
+ : null;
138
+ const entry = `${heading}\n\n${formatReleaseNotes(notes.changes, options.changelogSections)}${formatFullChangelog(notes.requests, compareUrl)}\n`;
123
139
 
124
140
  const body = existing.trim().startsWith('# Changelog')
125
141
  ? existing.replace(/^# Changelog\s*/, `# Changelog\n\n${entry}\n`)