@entro314labs/release-kit 2.2.2 → 2.3.1
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 +46 -24
- package/package.json +1 -1
- package/release.mjs +211 -17
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ release-kit minor
|
|
|
94
94
|
No global state to drift, no install step, and the version is explicit in the command.
|
|
95
95
|
|
|
96
96
|
```sh
|
|
97
|
-
npx @entro314labs/release-kit@2.
|
|
97
|
+
npx @entro314labs/release-kit@2.3.0 minor --yes
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
### Vendored — no registry at release time
|
|
@@ -116,7 +116,7 @@ npx @entro314labs/release-kit --sync . # writes scripts/release.mjs
|
|
|
116
116
|
machine you do not want to install anything on.
|
|
117
117
|
|
|
118
118
|
```sh
|
|
119
|
-
curl -fsSL https://raw.githubusercontent.com/entro314-labs/release-kit/v2.
|
|
119
|
+
curl -fsSL https://raw.githubusercontent.com/entro314-labs/release-kit/v2.3.0/release.mjs \
|
|
120
120
|
| node - minor --yes
|
|
121
121
|
```
|
|
122
122
|
|
|
@@ -144,6 +144,15 @@ pnpm release -- --help
|
|
|
144
144
|
The target is optional. With no target it releases whatever version `package.json`
|
|
145
145
|
already says — which is the mode to use when a version bump landed in an earlier commit.
|
|
146
146
|
|
|
147
|
+
| `auto` | derived from the Conventional Commits since the last tag |
|
|
148
|
+
|
|
149
|
+
`auto` follows release-please's rules: a breaking change (`!` or a `BREAKING CHANGE:`
|
|
150
|
+
footer) is a major, a `feat:` is a minor, anything else is a patch. Below `1.0.0` that is
|
|
151
|
+
softened — a breaking change bumps the minor rather than jumping to `1.0.0`. A commit body
|
|
152
|
+
containing `Release-As: 2.0.0` pins the version outright. It always prints what it inferred
|
|
153
|
+
and why before doing anything. Set `"versioning"` to `always-patch`, `always-minor` or
|
|
154
|
+
`always-major` to never infer.
|
|
155
|
+
|
|
147
156
|
| Target | From `1.2.3` | From `2.0.0-beta.1` |
|
|
148
157
|
| ------------------------------------ | ------------------------------------------------ | ------------------- |
|
|
149
158
|
| _(none)_ | `1.2.3` | `2.0.0-beta.1` |
|
|
@@ -163,16 +172,20 @@ Prerelease bumps need `--preid` unless the current version already carries one t
|
|
|
163
172
|
|
|
164
173
|
### Flags
|
|
165
174
|
|
|
166
|
-
| Flag
|
|
167
|
-
|
|
|
168
|
-
| `--
|
|
169
|
-
| `--
|
|
170
|
-
| `--
|
|
171
|
-
| `--
|
|
172
|
-
| `--
|
|
173
|
-
| `--
|
|
174
|
-
| `--
|
|
175
|
-
| `--
|
|
175
|
+
| Flag | Effect |
|
|
176
|
+
| ---------------------------- | -------------------------------------------------------------------------- |
|
|
177
|
+
| `--only <steps>` | Run only these steps, comma-separated. |
|
|
178
|
+
| `--skip <steps>` | Run every step except these. |
|
|
179
|
+
| `--commit` | Add the opt-in `commit` step: commit a dirty tree with a drafted message. |
|
|
180
|
+
| `--dry-run` | Print every step, execute nothing. Preflight still runs and still reports. |
|
|
181
|
+
| `--yes`, `-y` | Skip the confirmation prompt. |
|
|
182
|
+
| `--preid <id>` | Prerelease identifier: `alpha`, `beta`, `rc`, `next`, `nightly`, `canary`. |
|
|
183
|
+
| `--dist-tag <name>` | Override the npm dist-tag. Always wins over the derived one. |
|
|
184
|
+
| `--assistant <name>` | Drafting CLI: `auto`, `none`, `claude`, `codex`. |
|
|
185
|
+
| `--assistant-model <name>` | Model the assistant runs with. |
|
|
186
|
+
| `--assistant-effort <level>` | Reasoning effort the assistant runs with. |
|
|
187
|
+
| `--sync <dir>...` | Copy this script into other projects and exit. Touches no git state. |
|
|
188
|
+
| `--help`, `-h` | Full flag list. |
|
|
176
189
|
|
|
177
190
|
## 🧩 Steps
|
|
178
191
|
|
|
@@ -217,7 +230,11 @@ Notes resolve in this order:
|
|
|
217
230
|
section ends at the next `##` heading or `---` rule.
|
|
218
231
|
2. The `## [Unreleased]` section, if the version has no section of its own — this is the
|
|
219
232
|
same content that step 2 above is about to promote.
|
|
220
|
-
3.
|
|
233
|
+
3. The commits grouped by Conventional Commit type — Features, Bug Fixes, Performance
|
|
234
|
+
Improvements, Reverts, with breaking changes first and chores, CI and docs hidden. This
|
|
235
|
+
is deterministic and needs nothing installed, so decent notes are the default rather
|
|
236
|
+
than something that requires an assistant.
|
|
237
|
+
4. Otherwise GitHub generates them from the commits since the previous tag.
|
|
221
238
|
|
|
222
239
|
The same text becomes the tag annotation, the GitHub release body, and (when rolled) the
|
|
223
240
|
changelog entry. It is written once and lands in three places.
|
|
@@ -243,17 +260,18 @@ falling through to `latest` would put a prerelease on the stable line where ever
|
|
|
243
260
|
Every check runs and every failure is reported before it aborts once with the whole list,
|
|
244
261
|
rather than stopping at the first problem.
|
|
245
262
|
|
|
246
|
-
- The target version is greater than the current one
|
|
247
|
-
|
|
248
|
-
-
|
|
263
|
+
- The target version is greater than the current one — and for `auto`, which bump the
|
|
264
|
+
commits imply and why
|
|
265
|
+
- Working tree is clean, or listed for commit when the `commit` step runs
|
|
266
|
+
- On the configured branch, and not on a detached HEAD
|
|
249
267
|
- The remote exists, is reachable, and the branch is not behind it
|
|
250
268
|
- The tag is free — or already exists at `HEAD`, in which case it is reused
|
|
251
269
|
- `gh` is installed and authenticated
|
|
252
|
-
- Commit and tag signing can actually sign,
|
|
253
|
-
- The publishing CLI is authenticated, and the version is not already
|
|
270
|
+
- Commit and tag signing can actually sign, and the key is one GitHub will accept
|
|
271
|
+
- The publishing CLI is authenticated, and the version is not already published
|
|
254
272
|
- Configured release assets exist
|
|
255
|
-
- A
|
|
256
|
-
|
|
273
|
+
- A shallow clone is reported, since it truncates the history notes come from _(warning)_
|
|
274
|
+
- A changelog section for the version exists _(warning — it falls back to generated notes)_
|
|
257
275
|
|
|
258
276
|
Under `--dry-run` the failures are reported and then the remaining steps are shown anyway,
|
|
259
277
|
so you can see the whole plan without fixing the blockers first.
|
|
@@ -335,12 +353,16 @@ rather than being silently ignored.
|
|
|
335
353
|
|
|
336
354
|
| Key | Default | Meaning |
|
|
337
355
|
| --------------- | ------------------------ | ------------------------------------------------------------ |
|
|
356
|
+
| `steps` | all but `commit` | Which steps run; the order is fixed |
|
|
338
357
|
| `tagPrefix` | `"v"` | Prepended to the version to form the tag |
|
|
339
358
|
| `branch` | `"main"` | The only branch a release may run from; `null` allows any |
|
|
340
359
|
| `remote` | `"origin"` | Git remote to push to |
|
|
341
|
-
| `changelog` | `"CHANGELOG.md"` | Changelog path; `null`
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
360
|
+
| `changelog` | `"CHANGELOG.md"` | Changelog path; `null` for a project without one |
|
|
361
|
+
| `versionFile` | detected | Where the version lives; `null` versions by tag alone |
|
|
362
|
+
| `versionFiles` | `[]` | Further files kept in sync; a path or `{ path, pattern }` |
|
|
363
|
+
| `publish` | `"npm publish --tag %d"` | Publish command; `null` means none is configured |
|
|
364
|
+
| `versioning` | `"conventional"` | How `auto` infers; or `always-patch` / `-minor` / `-major` |
|
|
365
|
+
| `assistant` | `null` | Drafting CLI: a name, `"auto"`, or `{ tool, model, effort }` |
|
|
344
366
|
| `commitMessage` | `"chore(release): %t"` | Release commit subject |
|
|
345
367
|
| `releaseTitle` | `"%t"` | GitHub release title |
|
|
346
368
|
| `assets` | `[]` | Files attached to the GitHub release |
|
|
@@ -360,7 +382,7 @@ Non-interactive by default: the confirmation prompt is skipped when stdin is not
|
|
|
360
382
|
with:
|
|
361
383
|
fetch-depth: 0 # release notes and the last-tag lookup need real history
|
|
362
384
|
- id: release
|
|
363
|
-
run: npx @entro314labs/release-kit@2.
|
|
385
|
+
run: npx @entro314labs/release-kit@2.3.0 minor --yes
|
|
364
386
|
env:
|
|
365
387
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
366
388
|
- run: echo "shipped ${{ steps.release.outputs.tag }} ${{ steps.release.outputs.release-url }}"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entro314labs/release-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Single-file, zero-dependency release mechanism for JS/TS/Node projects: version bump, changelog roll, commit, annotated tag, push, publish, GitHub release",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"changelog",
|
package/release.mjs
CHANGED
|
@@ -66,6 +66,8 @@ import { createInterface } from 'node:readline/promises'
|
|
|
66
66
|
* commitMessage string release commit subject
|
|
67
67
|
* releaseTitle string GitHub release title
|
|
68
68
|
* assets string[] files attached to the GitHub release
|
|
69
|
+
* versioning string how `auto` derives a bump: "conventional", or
|
|
70
|
+
* always-patch / always-minor / always-major to never infer
|
|
69
71
|
* assistant string|object drafting CLI for commit messages and notes. A key of
|
|
70
72
|
* ASSISTANTS, "auto" for the first available, or null. The
|
|
71
73
|
* object form { tool, model, effort } also pins which model and
|
|
@@ -105,6 +107,7 @@ const DEFAULTS = {
|
|
|
105
107
|
releaseTitle: '%t',
|
|
106
108
|
assets: [],
|
|
107
109
|
assistant: null,
|
|
110
|
+
versioning: 'conventional',
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
/**
|
|
@@ -226,6 +229,22 @@ function read(command, args) {
|
|
|
226
229
|
}).trim()
|
|
227
230
|
}
|
|
228
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Whether GitHub knows the key git signs with.
|
|
234
|
+
*
|
|
235
|
+
* Compares key material rather than fingerprints, so no extra process is spawned per key.
|
|
236
|
+
*
|
|
237
|
+
* @returns {boolean | null} null when it cannot be determined — gh missing, token without
|
|
238
|
+
* the scope to list signing keys, or no network. An unanswerable check is not a failure.
|
|
239
|
+
*/
|
|
240
|
+
function signingKeyRegistered(keyPath) {
|
|
241
|
+
const local = readFileSync(keyPath, 'utf8').trim().split(/\s+/)[1]
|
|
242
|
+
if (!local) return null
|
|
243
|
+
const listed = tryRead('gh', ['api', 'user/ssh_signing_keys', '--jq', '.[].key'])
|
|
244
|
+
if (listed === null) return null
|
|
245
|
+
return listed.split('\n').some((key) => key.trim().split(/\s+/)[1] === local)
|
|
246
|
+
}
|
|
247
|
+
|
|
229
248
|
/** Read-only command → trimmed stdout, or null when it exits non-zero. */
|
|
230
249
|
function tryRead(command, args) {
|
|
231
250
|
try {
|
|
@@ -360,16 +379,122 @@ function runAssistant(prompt) {
|
|
|
360
379
|
function commitsSinceLastTag() {
|
|
361
380
|
const lastTag = tryRead('git', ['describe', '--tags', '--abbrev=0'])
|
|
362
381
|
const range = lastTag ? `${lastTag}..HEAD` : 'HEAD'
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
)
|
|
382
|
+
// %B is the whole message: bodies carry BREAKING CHANGE and Release-As footers. A record
|
|
383
|
+
// separator keeps multi-line messages parseable when splitting the log back apart.
|
|
384
|
+
const raw = tryRead('git', ['log', `--format=%B%x1e`, range]) ?? ''
|
|
385
|
+
const commits = raw
|
|
386
|
+
.split('\u001e')
|
|
387
|
+
.map((entry) => entry.trim())
|
|
388
|
+
.filter(Boolean)
|
|
389
|
+
.map((entry) => {
|
|
390
|
+
const [subject, ...rest] = entry.split('\n')
|
|
391
|
+
return { subject: subject.trim(), body: rest.join('\n').trim() }
|
|
392
|
+
})
|
|
393
|
+
.filter(
|
|
394
|
+
({ subject }) =>
|
|
395
|
+
subject &&
|
|
396
|
+
!/^chore\(release\)/i.test(subject) &&
|
|
397
|
+
!/^Merge (branch|pull request|remote)/i.test(subject) &&
|
|
398
|
+
!/^wip\b/i.test(subject) &&
|
|
399
|
+
!/^(fixup|squash)!/.test(subject),
|
|
400
|
+
)
|
|
401
|
+
return { lastTag, commits, subjects: commits.map((c) => c.subject) }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Conventional Commit types and the changelog heading each lands under, following
|
|
406
|
+
* release-please's defaults. Types marked hidden are real changes but not release notes:
|
|
407
|
+
* a reader upgrading does not need to know the CI config moved.
|
|
408
|
+
*/
|
|
409
|
+
const CHANGELOG_SECTIONS = [
|
|
410
|
+
{ type: 'feat', section: 'Features' },
|
|
411
|
+
{ type: 'fix', section: 'Bug Fixes' },
|
|
412
|
+
{ type: 'perf', section: 'Performance Improvements' },
|
|
413
|
+
{ type: 'revert', section: 'Reverts' },
|
|
414
|
+
{ type: 'docs', section: 'Documentation', hidden: true },
|
|
415
|
+
{ type: 'style', section: 'Styles', hidden: true },
|
|
416
|
+
{ type: 'refactor', section: 'Code Refactoring', hidden: true },
|
|
417
|
+
{ type: 'test', section: 'Tests', hidden: true },
|
|
418
|
+
{ type: 'build', section: 'Build System', hidden: true },
|
|
419
|
+
{ type: 'ci', section: 'Continuous Integration', hidden: true },
|
|
420
|
+
{ type: 'chore', section: 'Miscellaneous Chores', hidden: true },
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Parse a commit into the parts a release cares about.
|
|
425
|
+
*
|
|
426
|
+
* @returns {{type: string, scope: string|null, breaking: boolean, subject: string,
|
|
427
|
+
* releaseAs: string|null} | null} null when the subject is not Conventional Commits
|
|
428
|
+
*/
|
|
429
|
+
function parseCommit(subject, body = '') {
|
|
430
|
+
const match = /^([a-z]+)(?:\(([^)]+)\))?(!)?: (.+)$/i.exec(subject)
|
|
431
|
+
if (!match) return null
|
|
432
|
+
const [, type, scope, bang, text] = match
|
|
433
|
+
// A breaking change is marked either by `!` in the header or a BREAKING CHANGE footer.
|
|
434
|
+
const breaking = !!bang || /^BREAKING[ -]CHANGE:/m.test(body)
|
|
435
|
+
// `Release-As: 1.2.3` in a commit body pins the next version, so the decision can live
|
|
436
|
+
// in git history rather than on the command line.
|
|
437
|
+
const releaseAs = /^Release-As:\s*v?(\S+)/im.exec(body)?.[1] ?? null
|
|
438
|
+
return { type: type.toLowerCase(), scope: scope ?? null, breaking, subject: text, releaseAs }
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The bump implied by a set of commits, following release-please's default strategy:
|
|
443
|
+
* a breaking change is major, a feature is minor, anything else is a patch.
|
|
444
|
+
*
|
|
445
|
+
* Below 1.0.0 that is usually wrong — a breaking change in a 0.x project conventionally
|
|
446
|
+
* bumps the minor, not to 1.0.0 — so `preMajor` softens both by one level.
|
|
447
|
+
*
|
|
448
|
+
* @returns {{bump: string, breaking: string[], features: string[], releaseAs: string|null}}
|
|
449
|
+
*/
|
|
450
|
+
function inferBump(commits, currentVersion, strategy = 'conventional') {
|
|
451
|
+
const parsed = commits.map((c) => parseCommit(c.subject, c.body)).filter(Boolean)
|
|
452
|
+
const releaseAs = parsed.find((c) => c.releaseAs)?.releaseAs ?? null
|
|
453
|
+
const breaking = parsed.filter((c) => c.breaking).map((c) => c.subject)
|
|
454
|
+
const features = parsed
|
|
455
|
+
.filter((c) => !c.breaking && /^feat(ure)?$/.test(c.type))
|
|
456
|
+
.map((c) => c.subject)
|
|
457
|
+
|
|
458
|
+
if (strategy !== 'conventional') {
|
|
459
|
+
return { bump: strategy.replace(/^always-/, ''), breaking, features, releaseAs }
|
|
372
460
|
}
|
|
461
|
+
|
|
462
|
+
const preMajor = currentVersion ? parseVersion(currentVersion)?.major === 0 : false
|
|
463
|
+
let bump = 'patch'
|
|
464
|
+
if (breaking.length) bump = preMajor ? 'minor' : 'major'
|
|
465
|
+
else if (features.length) bump = 'minor'
|
|
466
|
+
return { bump, breaking, features, releaseAs }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Group commits into changelog sections, hiding the types that are not release notes.
|
|
471
|
+
* Deterministic and offline: this is what a project gets without an assistant.
|
|
472
|
+
*
|
|
473
|
+
* @returns {string | null} markdown body, or null when nothing visible changed
|
|
474
|
+
*/
|
|
475
|
+
function changelogFromCommits(commits) {
|
|
476
|
+
const parsed = commits.map((c) => parseCommit(c.subject, c.body)).filter(Boolean)
|
|
477
|
+
const lines = []
|
|
478
|
+
|
|
479
|
+
const breaking = parsed.filter((c) => c.breaking)
|
|
480
|
+
if (breaking.length) {
|
|
481
|
+
lines.push('### ⚠ BREAKING CHANGES', '')
|
|
482
|
+
for (const c of breaking) lines.push(`- ${c.scope ? `**${c.scope}:** ` : ''}${c.subject}`)
|
|
483
|
+
lines.push('')
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
for (const { type, section, hidden } of CHANGELOG_SECTIONS) {
|
|
487
|
+
if (hidden) continue
|
|
488
|
+
const inSection = parsed.filter(
|
|
489
|
+
(c) => c.type === type || (type === 'feat' && c.type === 'feature'),
|
|
490
|
+
)
|
|
491
|
+
if (!inSection.length) continue
|
|
492
|
+
lines.push(`### ${section}`, '')
|
|
493
|
+
for (const c of inSection) lines.push(`- ${c.scope ? `**${c.scope}:** ` : ''}${c.subject}`)
|
|
494
|
+
lines.push('')
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return lines.length ? lines.join('\n').trim() : null
|
|
373
498
|
}
|
|
374
499
|
|
|
375
500
|
const CONVENTIONAL_TYPES = 'build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test'
|
|
@@ -742,7 +867,16 @@ function writeVersionInto(entry, version) {
|
|
|
742
867
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
743
868
|
|
|
744
869
|
const argv = process.argv.slice(2)
|
|
745
|
-
const BUMPS = new Set([
|
|
870
|
+
const BUMPS = new Set([
|
|
871
|
+
'auto',
|
|
872
|
+
'major',
|
|
873
|
+
'minor',
|
|
874
|
+
'patch',
|
|
875
|
+
'premajor',
|
|
876
|
+
'preminor',
|
|
877
|
+
'prepatch',
|
|
878
|
+
'prerelease',
|
|
879
|
+
])
|
|
746
880
|
|
|
747
881
|
const flag = (name) => argv.includes(name)
|
|
748
882
|
const option = (name) => {
|
|
@@ -987,6 +1121,9 @@ console.log(
|
|
|
987
1121
|
(dryRun ? ` ${yellow('(dry run — nothing will execute)')}` : ''),
|
|
988
1122
|
)
|
|
989
1123
|
|
|
1124
|
+
/** What `auto` inferred, kept so preflight can show the reasoning. */
|
|
1125
|
+
let autoBump = null
|
|
1126
|
+
|
|
990
1127
|
let version
|
|
991
1128
|
if (!target) {
|
|
992
1129
|
if (!currentVersion) {
|
|
@@ -996,6 +1133,29 @@ if (!target) {
|
|
|
996
1133
|
)
|
|
997
1134
|
}
|
|
998
1135
|
version = currentVersion
|
|
1136
|
+
} else if (target === 'auto') {
|
|
1137
|
+
if (!currentVersion) {
|
|
1138
|
+
abort('auto needs a versionFile to bump from. Pass a version explicitly instead.')
|
|
1139
|
+
}
|
|
1140
|
+
const { commits, lastTag } = commitsSinceLastTag()
|
|
1141
|
+
if (!commits.length) {
|
|
1142
|
+
abort(
|
|
1143
|
+
`no releasable commits since ${lastTag ?? 'the start of the project'} — nothing to release`,
|
|
1144
|
+
)
|
|
1145
|
+
}
|
|
1146
|
+
autoBump = inferBump(commits, currentVersion, config.versioning)
|
|
1147
|
+
if (autoBump.releaseAs) {
|
|
1148
|
+
if (!parseVersion(autoBump.releaseAs)) {
|
|
1149
|
+
abort(`Release-As: ${autoBump.releaseAs} in a commit is not a semver version`)
|
|
1150
|
+
}
|
|
1151
|
+
version = autoBump.releaseAs
|
|
1152
|
+
} else {
|
|
1153
|
+
version = incrementVersion(
|
|
1154
|
+
currentVersion,
|
|
1155
|
+
autoBump.bump,
|
|
1156
|
+
requestedPreid ?? preidOf(currentVersion),
|
|
1157
|
+
)
|
|
1158
|
+
}
|
|
999
1159
|
} else if (BUMPS.has(target)) {
|
|
1000
1160
|
if (!currentVersion) {
|
|
1001
1161
|
abort(`a ${target} bump needs a versionFile to bump from. Pass a version explicitly instead.`)
|
|
@@ -1098,6 +1258,20 @@ const fail = (message) => {
|
|
|
1098
1258
|
problems.push(message)
|
|
1099
1259
|
}
|
|
1100
1260
|
|
|
1261
|
+
if (autoBump) {
|
|
1262
|
+
const reason = autoBump.releaseAs
|
|
1263
|
+
? `Release-As: ${autoBump.releaseAs} in a commit`
|
|
1264
|
+
: autoBump.breaking.length
|
|
1265
|
+
? `${autoBump.breaking.length} breaking change(s)`
|
|
1266
|
+
: autoBump.features.length
|
|
1267
|
+
? `${autoBump.features.length} feature(s), no breaking changes`
|
|
1268
|
+
: 'no features or breaking changes'
|
|
1269
|
+
ok(`auto: ${autoBump.bump} — ${reason}`)
|
|
1270
|
+
for (const subject of [...autoBump.breaking, ...autoBump.features].slice(0, 5)) {
|
|
1271
|
+
console.log(dim(` ${subject}`))
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1101
1275
|
if (bumping && compareVersions(version, currentVersion) <= 0) {
|
|
1102
1276
|
fail(`${version} is not greater than the current version ${currentVersion}`)
|
|
1103
1277
|
} else if (bumping) {
|
|
@@ -1131,9 +1305,9 @@ if (runs('commit') && !assistant) {
|
|
|
1131
1305
|
.join(', ')
|
|
1132
1306
|
ok(`assistant: ${assistantName}${detail ? ` (${detail})` : ''}`)
|
|
1133
1307
|
if (assistantModel && !assistant.model)
|
|
1134
|
-
warn(`${assistantName} takes no model flag — --model ignored`)
|
|
1308
|
+
warn(`${assistantName} takes no model flag — --assistant-model ignored`)
|
|
1135
1309
|
if (assistantEffort && !assistant.effort)
|
|
1136
|
-
warn(`${assistantName} takes no effort flag — --effort ignored`)
|
|
1310
|
+
warn(`${assistantName} takes no effort flag — --assistant-effort ignored`)
|
|
1137
1311
|
}
|
|
1138
1312
|
|
|
1139
1313
|
const branch = tryRead('git', ['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
@@ -1193,6 +1367,22 @@ if (signsSomething && signingKeys.length) {
|
|
|
1193
1367
|
)
|
|
1194
1368
|
} else {
|
|
1195
1369
|
ok(`signing commits and tags (${format})`)
|
|
1370
|
+
// git signs happily with a key GitHub has never seen, which is how a repository fills
|
|
1371
|
+
// up with commits that are locally valid and permanently "Unverified". Only GitHub can
|
|
1372
|
+
// answer this, and only when the token carries the scope to list signing keys — so an
|
|
1373
|
+
// unanswerable check stays silent rather than guessing.
|
|
1374
|
+
if (format === 'ssh' && keyPath && existsSync(keyPath) && runs('release')) {
|
|
1375
|
+
const registered = signingKeyRegistered(keyPath)
|
|
1376
|
+
if (registered === false) {
|
|
1377
|
+
warn(
|
|
1378
|
+
'this signing key is not registered with GitHub, so the commits will show as ' +
|
|
1379
|
+
'Unverified.\n Add it: gh ssh-key add ' +
|
|
1380
|
+
`${signingKey} --type signing`,
|
|
1381
|
+
)
|
|
1382
|
+
} else if (registered === true) {
|
|
1383
|
+
ok('signing key is registered with GitHub')
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1196
1386
|
}
|
|
1197
1387
|
}
|
|
1198
1388
|
|
|
@@ -1269,11 +1459,15 @@ let draftedNotes = null
|
|
|
1269
1459
|
*/
|
|
1270
1460
|
const notesDeferred = !!(dirty && runs('commit') && assistant)
|
|
1271
1461
|
|
|
1272
|
-
/**
|
|
1462
|
+
/**
|
|
1463
|
+
* Notes for a version, in descending order of how much they can be trusted:
|
|
1464
|
+
* an assistant's prose when one is configured, otherwise the commits grouped by
|
|
1465
|
+
* Conventional Commit type. Only when neither yields anything does GitHub generate them.
|
|
1466
|
+
*/
|
|
1273
1467
|
function draftNotesFor(v) {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
if (!
|
|
1468
|
+
const { lastTag, subjects, commits } = commitsSinceLastTag()
|
|
1469
|
+
if (!commits.length) return null
|
|
1470
|
+
if (!assistant) return changelogFromCommits(commits)
|
|
1277
1471
|
if (shallow) {
|
|
1278
1472
|
warn(
|
|
1279
1473
|
`shallow clone: only ${subjects.length} commit(s) are visible, so the notes will ` +
|
|
@@ -1281,7 +1475,7 @@ function draftNotesFor(v) {
|
|
|
1281
1475
|
)
|
|
1282
1476
|
}
|
|
1283
1477
|
note(`drafting notes from ${subjects.length} commit(s) with ${assistantName}...`)
|
|
1284
|
-
return draftReleaseNotes(v, subjects, lastTag)
|
|
1478
|
+
return draftReleaseNotes(v, subjects, lastTag) ?? changelogFromCommits(commits)
|
|
1285
1479
|
}
|
|
1286
1480
|
if (config.changelog && existsSync(config.changelog)) {
|
|
1287
1481
|
const text = readFileSync(config.changelog, 'utf8')
|