@eclipse-docks/cli 0.7.114 → 0.7.122

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
@@ -9,18 +9,26 @@ npx @eclipse-docks/cli release # bump patch, prompt for re
9
9
  npx @eclipse-docks/cli release minor # bump minor
10
10
  npx @eclipse-docks/cli release major -m "notes" # bump major with inline release notes
11
11
  npx @eclipse-docks/cli release v1.2.3 # explicit version
12
- npx @eclipse-docks/cli release --dry-run # print what would be committed
13
- npx @eclipse-docks/cli release --no-push # commit locally without pushing
12
+ npx @eclipse-docks/cli release --since v1.2.0 # notes from commits since that tag
13
+ npx @eclipse-docks/cli release --dry-run # print what would be tagged
14
+ npx @eclipse-docks/cli release --no-push # create the tag locally without pushing
14
15
  ```
15
16
 
16
17
  ## `release`
17
18
 
18
- Creates an empty commit whose subject is the next version (`vX.Y.Z`) and pushes it.
19
- Pair this with a CI workflow that tags/releases on that commit subject (see
20
- [`.github/workflows/app-release.yml`](../../.github/workflows/app-release.yml) for a
21
- reusable one) — the commit body becomes the release notes.
19
+ Creates an annotated tag whose name is the version (`vX.Y.Z`) and pushes it.
20
+ When pushing (the default), any unpushed commits on the current branch are pushed
21
+ to `origin` first. The tag message body becomes the GitHub Release notes.
22
+
23
+ Wire release in CI with [`.github/workflows/app-release.yml`](../../.github/workflows/app-release.yml):
24
+ downstream apps usually trigger it on `v*` tag push; this monorepo waits for a green
25
+ **CI** run on `main` first (see [`.github/workflows/release.yml`](../../.github/workflows/release.yml)).
26
+
27
+ The last version is the most recent `vX.Y.Z` tag reachable from `HEAD` (via
28
+ `git describe`), not the highest semver tag in the repository. That drives the
29
+ default bump and changelog range.
22
30
 
23
31
  If no release notes are given via `-m`, an end-user-facing summary is generated from the
24
- commits since the last version, grouped under **Features**, **Fixes**, **Docs**, and
25
- **Chore/Refactor**, and offered as the default (press Enter on an empty first line to
26
- accept it).
32
+ commits since that tag (or since `--since vX.Y.Z` when set), grouped under **Features**,
33
+ **Fixes**, **Docs**, and **Chore/Refactor**, and offered as the default (press Enter on
34
+ an empty first line to accept it).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eclipse-docks/cli",
3
- "version": "0.7.114",
3
+ "version": "0.7.122",
4
4
  "description": "Developer CLI for Eclipse Docks apps (release automation and more)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline/promises';
3
3
 
4
+ /** CLI arg / release tag name: exact `vX.Y.Z`. */
4
5
  const VERSION_RE = /^v\d+\.\d+\.\d+$/;
5
6
 
6
7
  function git(args, cwd) {
@@ -16,8 +17,12 @@ function gitOrNull(args, cwd) {
16
17
  return result.status === 0 ? result.stdout.trim() : null;
17
18
  }
18
19
 
20
+ function usage() {
21
+ return 'Usage: docks release [major|minor|patch|vX.Y.Z] [-m "release notes"] [--since vX.Y.Z] [--dry-run] [--no-push]';
22
+ }
23
+
19
24
  function parseArgs(argv) {
20
- const opts = { bump: 'patch', version: '', notes: '', dryRun: false, push: true };
25
+ const opts = { bump: 'patch', version: '', since: '', notes: '', dryRun: false, push: true };
21
26
 
22
27
  for (let i = 0; i < argv.length; i++) {
23
28
  const arg = argv[i];
@@ -25,6 +30,12 @@ function parseArgs(argv) {
25
30
  opts.bump = arg;
26
31
  } else if (VERSION_RE.test(arg)) {
27
32
  opts.version = arg;
33
+ } else if (arg === '--since') {
34
+ const value = argv[++i] ?? '';
35
+ if (!VERSION_RE.test(value)) {
36
+ throw new Error(`--since requires a version like v1.2.3\n${usage()}`);
37
+ }
38
+ opts.since = value;
28
39
  } else if (arg === '-m' || arg === '--message') {
29
40
  opts.notes = argv[++i] ?? '';
30
41
  } else if (arg === '-n' || arg === '--dry-run') {
@@ -34,9 +45,7 @@ function parseArgs(argv) {
34
45
  } else if (arg === '--no-push') {
35
46
  opts.push = false;
36
47
  } else {
37
- throw new Error(
38
- `Unknown argument: ${arg}\nUsage: docks release [major|minor|patch|vX.Y.Z] [-m "release notes"] [--dry-run] [--no-push]`,
39
- );
48
+ throw new Error(`Unknown argument: ${arg}\n${usage()}`);
40
49
  }
41
50
  }
42
51
 
@@ -44,12 +53,57 @@ function parseArgs(argv) {
44
53
  }
45
54
 
46
55
  function findLastVersion(cwd) {
47
- const log = gitOrNull(['log', '--pretty=%s'], cwd);
48
- if (!log) return null;
49
- for (const subject of log.split('\n')) {
50
- if (VERSION_RE.test(subject)) return subject;
56
+ const tag = gitOrNull(['describe', '--tags', '--abbrev=0', '--match', 'v*', 'HEAD'], cwd);
57
+ if (!tag) return null;
58
+ const trimmed = tag.trim();
59
+ return VERSION_RE.test(trimmed) ? trimmed : null;
60
+ }
61
+
62
+ function findVersionCommit(cwd, version) {
63
+ return gitOrNull(['rev-parse', '--verify', `${version}^{commit}`], cwd);
64
+ }
65
+
66
+ function currentBranch(cwd) {
67
+ const branch = gitOrNull(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
68
+ if (!branch || branch === 'HEAD') return null;
69
+ return branch;
70
+ }
71
+
72
+ function upstreamStatus(cwd) {
73
+ const upstream = gitOrNull(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], cwd);
74
+ if (!upstream) return null;
75
+ const counts = git(['rev-list', '--left-right', '--count', `${upstream}...HEAD`], cwd);
76
+ const [behind, ahead] = counts.split('\t').map(Number);
77
+ return { upstream, behind, ahead };
78
+ }
79
+
80
+ function pushBranchIfNeeded(cwd, { dryRun = false } = {}) {
81
+ const branch = currentBranch(cwd);
82
+ if (!branch) {
83
+ throw new Error('Detached HEAD — checkout a branch before releasing.');
84
+ }
85
+
86
+ const status = upstreamStatus(cwd);
87
+ if (!status) {
88
+ throw new Error(`Branch ${branch} has no upstream. Push it first:\n git push -u origin ${branch}`);
89
+ }
90
+
91
+ if (status.behind > 0) {
92
+ throw new Error(
93
+ `Branch ${branch} is ${status.behind} commit(s) behind ${status.upstream}. Pull before releasing.`,
94
+ );
95
+ }
96
+
97
+ if (status.ahead === 0) return branch;
98
+
99
+ if (dryRun) {
100
+ console.log(`[dry run] would push ${status.ahead} commit(s) to origin ${branch}`);
101
+ return branch;
51
102
  }
52
- return null;
103
+
104
+ console.log(`Pushing ${status.ahead} commit(s) to origin ${branch}...`);
105
+ git(['push', 'origin', branch], cwd);
106
+ return branch;
53
107
  }
54
108
 
55
109
  function bumpVersion(last, bump) {
@@ -64,16 +118,18 @@ function bumpVersion(last, bump) {
64
118
  }
65
119
  }
66
120
 
67
- function generateSummary(cwd, last) {
68
- // find the commit whose subject *is* the last version, not a tag (tags may not exist locally)
69
- const lastCommit = last ? gitOrNull(['log', '-E', '--pretty=%H', '-1', `--grep=^${last}$`], cwd) : null;
70
- const range = lastCommit ? `${lastCommit}..HEAD` : 'HEAD';
121
+ function generateSummary(cwd, since) {
122
+ const sinceCommit = since ? findVersionCommit(cwd, since) : null;
123
+ if (since && !sinceCommit) {
124
+ throw new Error(`No tag found for --since ${since}.`);
125
+ }
126
+ const range = sinceCommit ? `${sinceCommit}..HEAD` : 'HEAD';
71
127
 
72
128
  const log = gitOrNull(['log', range, '--no-merges', '--pretty=%s'], cwd) ?? '';
73
129
  const buckets = { features: [], fixes: [], docs: [], chores: [] };
74
130
 
75
131
  for (const subject of log.split('\n')) {
76
- if (!subject || VERSION_RE.test(subject)) continue;
132
+ if (!subject || VERSION_RE.test(subject.trim())) continue;
77
133
 
78
134
  const stripped = subject.includes(': ') ? subject.slice(subject.indexOf(': ') + 2) : subject;
79
135
  const text = stripped.charAt(0).toUpperCase() + stripped.slice(1);
@@ -126,7 +182,7 @@ export async function release(argv, cwd = process.cwd()) {
126
182
  let version = opts.version;
127
183
  if (!version) {
128
184
  version = bumpVersion(last ?? 'v0.0.0', opts.bump);
129
- console.log(`Last version commit: ${last ?? 'v0.0.0'}`);
185
+ console.log(`Last release tag: ${last ?? 'v0.0.0'}`);
130
186
  }
131
187
 
132
188
  if (gitOrNull(['rev-parse', version], cwd)) {
@@ -135,28 +191,39 @@ export async function release(argv, cwd = process.cwd()) {
135
191
 
136
192
  let notes = opts.notes;
137
193
  if (!notes) {
138
- const summary = generateSummary(cwd, last);
194
+ const since = opts.since || last;
195
+ if (since) console.log(`Summarizing commits since ${since}`);
196
+ const summary = generateSummary(cwd, since);
139
197
  notes = await promptNotes(version, summary);
140
198
  }
141
199
 
142
200
  if (opts.dryRun) {
143
- console.log(`\n[dry run] would create empty commit:\n ${version}\n`);
201
+ if (opts.push) pushBranchIfNeeded(cwd, { dryRun: true });
202
+ console.log(`\n[dry run] would create annotated tag ${version} at HEAD`);
144
203
  console.log(notes.replace(/^/gm, ' '));
145
204
  if (opts.push) {
146
- const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
147
- console.log(`[dry run] would push to origin ${branch}`);
205
+ console.log(`[dry run] would push tag to origin ${version}`);
148
206
  }
149
207
  return;
150
208
  }
151
209
 
152
- git(['commit', '--allow-empty', '-m', version, '-m', notes], cwd);
210
+ if (opts.push) {
211
+ pushBranchIfNeeded(cwd);
212
+ }
213
+
214
+ const tagArgs = ['tag', '-a', version, '-m', version];
215
+ if (notes.trim()) {
216
+ tagArgs.push('-m', notes.trimEnd());
217
+ }
218
+ git(tagArgs, cwd);
153
219
 
154
- const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
155
220
  if (opts.push) {
156
- git(['push', 'origin', branch], cwd);
157
- console.log(`\nPushed ${version} to origin/${branch}; the release workflow will take over.`);
221
+ git(['push', 'origin', version], cwd);
222
+ console.log(`\nPushed tag ${version}; the release workflow will take over.`);
158
223
  } else {
159
- console.log(`\nCreated release commit for ${version}. Push it to trigger the release:`);
160
- console.log(` git push origin ${branch}`);
224
+ console.log(`\nCreated annotated tag ${version}. Push your branch and tag to trigger the release:`);
225
+ const branch = currentBranch(cwd);
226
+ if (branch) console.log(` git push origin ${branch}`);
227
+ console.log(` git push origin ${version}`);
161
228
  }
162
229
  }