@eclipse-docks/cli 0.7.118 → 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 +16 -12
- package/package.json +1 -1
- package/src/commands/release.js +71 -34
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
|
|
13
|
-
npx @eclipse-docks/cli release --dry-run # print what would be
|
|
14
|
-
npx @eclipse-docks/cli release --no-push #
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
29
|
-
**
|
|
30
|
-
|
|
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
package/src/commands/release.js
CHANGED
|
@@ -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 /
|
|
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
|
|
65
|
-
if (!
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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 ||
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
187
|
-
console.log(`\nPushed ${version}
|
|
221
|
+
git(['push', 'origin', version], cwd);
|
|
222
|
+
console.log(`\nPushed tag ${version}; the release workflow will take over.`);
|
|
188
223
|
} else {
|
|
189
|
-
console.log(`\nCreated
|
|
190
|
-
|
|
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
|
}
|