@eclipse-docks/cli 0.7.118 → 0.7.124

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,22 +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 --since v1.2.0 # notes from that version (bump still from latest)
13
- npx @eclipse-docks/cli release --dry-run # print what would be committed
14
- 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
15
15
  ```
16
16
 
17
17
  ## `release`
18
18
 
19
- Creates an empty commit whose subject is the next version (`vX.Y.Z`) and pushes it.
20
- Pair this with a CI workflow that tags/releases on that commit subject (see
21
- [`.github/workflows/app-release.yml`](../../.github/workflows/app-release.yml) for a
22
- 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.
23
22
 
24
- The last version is taken from the most recent commit whose subject is exactly `vX.Y.Z`
25
- or the legacy form `Release: vX.Y.Z`. That drives the default bump.
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.
26
30
 
27
31
  If no release notes are given via `-m`, an end-user-facing summary is generated from the
28
- commits since that version (or since `--since vX.Y.Z` when set), grouped under
29
- **Features**, **Fixes**, **Docs**, and **Chore/Refactor**, and offered as the default
30
- (press Enter on an empty first line to 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.118",
3
+ "version": "0.7.124",
4
4
  "description": "Developer CLI for Eclipse Docks apps (release automation and more)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,10 +1,8 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline/promises';
3
3
 
4
- /** CLI arg / new release commit subject: exact `vX.Y.Z`. */
4
+ /** CLI arg / release tag name: exact `vX.Y.Z`. */
5
5
  const VERSION_RE = /^v\d+\.\d+\.\d+$/;
6
- /** Discover last release from exact `vX.Y.Z` or legacy `Release: vX.Y.Z`. */
7
- const VERSION_IN_SUBJECT_RE = /^(?:Release:\s*)?(v\d+\.\d+\.\d+)$/i;
8
6
 
9
7
  function git(args, cwd) {
10
8
  const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
@@ -19,12 +17,6 @@ function gitOrNull(args, cwd) {
19
17
  return result.status === 0 ? result.stdout.trim() : null;
20
18
  }
21
19
 
22
- function parseVersionSubject(subject) {
23
- const m = subject.trim().match(VERSION_IN_SUBJECT_RE);
24
- if (!m) return null;
25
- return `v${m[1].slice(1)}`;
26
- }
27
-
28
20
  function usage() {
29
21
  return 'Usage: docks release [major|minor|patch|vX.Y.Z] [-m "release notes"] [--since vX.Y.Z] [--dry-run] [--no-push]';
30
22
  }
@@ -61,21 +53,57 @@ function parseArgs(argv) {
61
53
  }
62
54
 
63
55
  function findLastVersion(cwd) {
64
- const log = gitOrNull(['log', '--pretty=%s'], cwd);
65
- if (!log) return null;
66
- for (const subject of log.split('\n')) {
67
- const version = parseVersionSubject(subject);
68
- if (version) return version;
69
- }
70
- return null;
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;
71
60
  }
72
61
 
73
62
  function findVersionCommit(cwd, version) {
74
- const escaped = version.replace(/\./g, '\\.');
75
- return gitOrNull(
76
- ['log', '-E', '-i', '--pretty=%H', '-1', `--grep=^(Release:[[:space:]]*)?${escaped}$`],
77
- cwd,
78
- );
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;
102
+ }
103
+
104
+ console.log(`Pushing ${status.ahead} commit(s) to origin ${branch}...`);
105
+ git(['push', 'origin', branch], cwd);
106
+ return branch;
79
107
  }
80
108
 
81
109
  function bumpVersion(last, bump) {
@@ -93,7 +121,7 @@ function bumpVersion(last, bump) {
93
121
  function generateSummary(cwd, since) {
94
122
  const sinceCommit = since ? findVersionCommit(cwd, since) : null;
95
123
  if (since && !sinceCommit) {
96
- throw new Error(`No commit found for --since ${since} (expected subject "${since}" or "Release: ${since}").`);
124
+ throw new Error(`No tag found for --since ${since}.`);
97
125
  }
98
126
  const range = sinceCommit ? `${sinceCommit}..HEAD` : 'HEAD';
99
127
 
@@ -101,7 +129,7 @@ function generateSummary(cwd, since) {
101
129
  const buckets = { features: [], fixes: [], docs: [], chores: [] };
102
130
 
103
131
  for (const subject of log.split('\n')) {
104
- if (!subject || parseVersionSubject(subject)) continue;
132
+ if (!subject || VERSION_RE.test(subject.trim())) continue;
105
133
 
106
134
  const stripped = subject.includes(': ') ? subject.slice(subject.indexOf(': ') + 2) : subject;
107
135
  const text = stripped.charAt(0).toUpperCase() + stripped.slice(1);
@@ -154,7 +182,7 @@ export async function release(argv, cwd = process.cwd()) {
154
182
  let version = opts.version;
155
183
  if (!version) {
156
184
  version = bumpVersion(last ?? 'v0.0.0', opts.bump);
157
- console.log(`Last version commit: ${last ?? 'v0.0.0'}`);
185
+ console.log(`Last release tag: ${last ?? 'v0.0.0'}`);
158
186
  }
159
187
 
160
188
  if (gitOrNull(['rev-parse', version], cwd)) {
@@ -164,29 +192,38 @@ export async function release(argv, cwd = process.cwd()) {
164
192
  let notes = opts.notes;
165
193
  if (!notes) {
166
194
  const since = opts.since || last;
167
- if (opts.since) console.log(`Summarizing commits since ${opts.since}`);
195
+ if (since) console.log(`Summarizing commits since ${since}`);
168
196
  const summary = generateSummary(cwd, since);
169
197
  notes = await promptNotes(version, summary);
170
198
  }
171
199
 
172
200
  if (opts.dryRun) {
173
- 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`);
174
203
  console.log(notes.replace(/^/gm, ' '));
175
204
  if (opts.push) {
176
- const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
177
- console.log(`[dry run] would push to origin ${branch}`);
205
+ console.log(`[dry run] would push tag to origin ${version}`);
178
206
  }
179
207
  return;
180
208
  }
181
209
 
182
- 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);
183
219
 
184
- const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
185
220
  if (opts.push) {
186
- git(['push', 'origin', branch], cwd);
187
- 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}. Release runs after CI succeeds on main, or immediately if CI already passed on this commit.`);
188
223
  } else {
189
- console.log(`\nCreated release commit for ${version}. Push it to trigger the release:`);
190
- 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}`);
191
228
  }
192
229
  }