@entro314labs/release-kit 2.3.3 → 2.5.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 +51 -0
- package/package.json +5 -3
- package/release.mjs +63 -9
package/README.md
CHANGED
|
@@ -342,6 +342,11 @@ the version. Only the version itself is rewritten, so comments and formatting su
|
|
|
342
342
|
because the TOML match is anchored to the start of a line, a dependency's
|
|
343
343
|
`serde = { version = "1.0" }` is left alone.
|
|
344
344
|
|
|
345
|
+
**Lockfiles are scoped automatically.** A `Cargo.lock` records a version for every
|
|
346
|
+
dependency — hundreds of them — so matching the first `version = "…"` would rewrite an
|
|
347
|
+
unrelated crate. Listing one rewrites only the `[[package]]` block whose name matches the
|
|
348
|
+
crate in the sibling `Cargo.toml`; with no sibling to read, it refuses rather than guesses.
|
|
349
|
+
|
|
345
350
|
For anything else, give a pattern with one capture group around the version. `versionFiles`
|
|
346
351
|
takes the same entries, so several files stay in sync across formats:
|
|
347
352
|
|
|
@@ -352,6 +357,29 @@ takes the same entries, so several files stay in sync across formats:
|
|
|
352
357
|
}
|
|
353
358
|
```
|
|
354
359
|
|
|
360
|
+
A desktop app usually carries the same version in a lot of places at once — a workspace
|
|
361
|
+
manifest, per-platform bundle configs, a crate manifest and its lockfile. They stay in step
|
|
362
|
+
in one release, across three formats, with no scripting:
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"versionFiles": [
|
|
367
|
+
"apps/desktop/package.json",
|
|
368
|
+
"apps/desktop/src-tauri/tauri.conf.json",
|
|
369
|
+
"apps/desktop/src-tauri/tauri.macos.conf.json",
|
|
370
|
+
"apps/desktop/src-tauri/tauri.windows.conf.json",
|
|
371
|
+
"apps/desktop/src-tauri/tauri.linux.conf.json",
|
|
372
|
+
"apps/desktop/src-tauri/Cargo.toml",
|
|
373
|
+
"apps/desktop/src-tauri/Cargo.lock"
|
|
374
|
+
],
|
|
375
|
+
"publish": null,
|
|
376
|
+
"steps": ["version", "changelog", "tag", "push"]
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Stopping at `push` because the tag is what triggers the build pipeline — see
|
|
381
|
+
[Libraries versus apps](#-libraries-versus-apps).
|
|
382
|
+
|
|
355
383
|
The project name comes from the manifest when there is one (`name` in `package.json`,
|
|
356
384
|
`Cargo.toml` or `pyproject.toml`), and falls back to the repository directory.
|
|
357
385
|
|
|
@@ -593,6 +621,18 @@ jobs:
|
|
|
593
621
|
- run: goreleaser release --clean --release-notes dist-notes.md
|
|
594
622
|
```
|
|
595
623
|
|
|
624
|
+
A Go project has no version file at all — the tag is the version — so it is
|
|
625
|
+
`"versionFile": null` and the version is passed explicitly, or inferred with `auto`:
|
|
626
|
+
|
|
627
|
+
```sh
|
|
628
|
+
release-kit auto # bump from the commits, tag, push; goreleaser takes it from there
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**Only one of the two should write release notes.** goreleaser groups conventional commits
|
|
632
|
+
itself; `--release-notes` makes it use yours instead and skip its own generation. Leaving
|
|
633
|
+
both on means the notes in the GitHub release and the notes in your `CHANGELOG.md` are
|
|
634
|
+
generated by different code from the same commits, and they drift.
|
|
635
|
+
|
|
596
636
|
**Do not leave `release` in `steps` here.** goreleaser creates the GitHub release itself; if
|
|
597
637
|
release-kit has already created one for that tag, goreleaser fails. Exactly one of them
|
|
598
638
|
should own it, and it should be the one attaching the binaries.
|
|
@@ -642,6 +682,17 @@ with the reasoning — are in [ROADMAP.md](ROADMAP.md).
|
|
|
642
682
|
|
|
643
683
|
## 🤝 Contributing
|
|
644
684
|
|
|
685
|
+
```sh
|
|
686
|
+
pnpm install
|
|
687
|
+
pnpm test # 63 tests, node --test, no framework
|
|
688
|
+
pnpm check # format + lint + tests, the same gate CI runs
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
`test/` holds unit suites for the pure functions and an integration suite that builds real
|
|
692
|
+
throwaway repositories with a real bare remote and stubbed `gh`/`npm`. The integration tests
|
|
693
|
+
pin defects found in use, so a name like "refuses to reuse a tag while still producing a
|
|
694
|
+
commit" is describing something that actually happened.
|
|
695
|
+
|
|
645
696
|
The tool releases itself, so a change ships the same way it would in any consuming project:
|
|
646
697
|
add a `## [Unreleased]` entry to `CHANGELOG.md`, then run `pnpm release <bump>` from a clone.
|
|
647
698
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entro314labs/release-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
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",
|
|
@@ -41,12 +41,14 @@
|
|
|
41
41
|
"format:check": "oxfmt --check .",
|
|
42
42
|
"lint": "oxlint .",
|
|
43
43
|
"lint:ci": "oxlint --deny-warnings .",
|
|
44
|
-
"
|
|
44
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
45
|
+
"check": "pnpm run format:check && pnpm run lint:ci && pnpm run test",
|
|
45
46
|
"release": "node release.mjs"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"oxfmt": "^0.63.0",
|
|
49
|
-
"oxlint": "^1.78.0"
|
|
50
|
+
"oxlint": "^1.78.0",
|
|
51
|
+
"semver": "^7.8.5"
|
|
50
52
|
},
|
|
51
53
|
"engines": {
|
|
52
54
|
"node": ">=22"
|
package/release.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
writeFileSync,
|
|
42
42
|
} from 'node:fs'
|
|
43
43
|
import { homedir, tmpdir } from 'node:os'
|
|
44
|
-
import { basename, join, relative, resolve, sep } from 'node:path'
|
|
44
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
|
45
45
|
import { createInterface } from 'node:readline/promises'
|
|
46
46
|
|
|
47
47
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -419,6 +419,7 @@ const CHANGELOG_SECTIONS = [
|
|
|
419
419
|
{ type: 'fix', section: 'Bug Fixes' },
|
|
420
420
|
{ type: 'perf', section: 'Performance Improvements' },
|
|
421
421
|
{ type: 'revert', section: 'Reverts' },
|
|
422
|
+
{ type: 'deps', section: 'Dependencies' },
|
|
422
423
|
{ type: 'docs', section: 'Documentation', hidden: true },
|
|
423
424
|
{ type: 'style', section: 'Styles', hidden: true },
|
|
424
425
|
{ type: 'refactor', section: 'Code Refactoring', hidden: true },
|
|
@@ -435,7 +436,9 @@ const CHANGELOG_SECTIONS = [
|
|
|
435
436
|
* releaseAs: string|null} | null} null when the subject is not Conventional Commits
|
|
436
437
|
*/
|
|
437
438
|
function parseCommit(subject, body = '', hash = '') {
|
|
438
|
-
|
|
439
|
+
// Conventional Commits does not restrict the type to letters — `i18n:` and `a11y:` are
|
|
440
|
+
// types people really use, and `[a-z]+` silently failed to parse them at all.
|
|
441
|
+
const match = /^([a-z][a-z0-9-]*)(?:\(([^)]+)\))?(!)?: (.+)$/i.exec(subject)
|
|
439
442
|
if (!match) return null
|
|
440
443
|
const [, type, scope, bang, text] = match
|
|
441
444
|
const shortHash = hash.slice(0, 7)
|
|
@@ -519,6 +522,7 @@ function changelogFromCommits(commits, links = null) {
|
|
|
519
522
|
lines.push('')
|
|
520
523
|
}
|
|
521
524
|
|
|
525
|
+
const known = new Set(CHANGELOG_SECTIONS.map((s) => s.type))
|
|
522
526
|
for (const { type, section, hidden } of CHANGELOG_SECTIONS) {
|
|
523
527
|
if (hidden) continue
|
|
524
528
|
const inSection = parsed.filter(
|
|
@@ -530,6 +534,16 @@ function changelogFromCommits(commits, links = null) {
|
|
|
530
534
|
lines.push('')
|
|
531
535
|
}
|
|
532
536
|
|
|
537
|
+
// A conventional type nobody anticipated — `security:`, `i18n:` — is still a change
|
|
538
|
+
// someone made deliberately. Dropping it silently is how a security fix goes unmentioned.
|
|
539
|
+
// The hidden types are excluded because hiding them is the point.
|
|
540
|
+
const other = parsed.filter((c) => !known.has(c.type) && c.type !== 'feature')
|
|
541
|
+
if (other.length) {
|
|
542
|
+
lines.push('### Other Changes', '')
|
|
543
|
+
for (const c of other) lines.push(bullet(c, c.subject))
|
|
544
|
+
lines.push('')
|
|
545
|
+
}
|
|
546
|
+
|
|
533
547
|
return lines.length ? lines.join('\n').trim() : null
|
|
534
548
|
}
|
|
535
549
|
|
|
@@ -919,9 +933,29 @@ function readNameFrom(entry) {
|
|
|
919
933
|
/** Normalise a versionFile / versionFiles entry to { path, pattern }. */
|
|
920
934
|
const versionSource = (entry) => (typeof entry === 'string' ? { path: entry } : entry)
|
|
921
935
|
|
|
936
|
+
/**
|
|
937
|
+
* A lockfile records a version for every dependency — hundreds of them — so the first
|
|
938
|
+
* `version = "…"` in the file belongs to whichever crate sorts first, not to this project.
|
|
939
|
+
* Rewriting it corrupts an unrelated dependency, silently. Scope to the named package block.
|
|
940
|
+
*/
|
|
941
|
+
function cargoLockPattern(lockPath) {
|
|
942
|
+
const sibling = join(dirname(lockPath), 'Cargo.toml')
|
|
943
|
+
const crate = existsSync(sibling) ? readNameFrom({ path: sibling }) : null
|
|
944
|
+
if (!crate) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
`${lockPath} lists every dependency's version, so it needs to know which package is ` +
|
|
947
|
+
`yours.\n No Cargo.toml beside it to read the name from — give an explicit ` +
|
|
948
|
+
`pattern:\n { "path": "${lockPath}", "pattern": "name = \\"<crate>\\"\\nversion = ` +
|
|
949
|
+
`\\"(.+)\\"" }`,
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
return new RegExp(`\\[\\[package\\]\\]\\nname = "${escapeRe(crate)}"\\nversion = "([^"]*)"`)
|
|
953
|
+
}
|
|
954
|
+
|
|
922
955
|
/** The regex for a source, or null when the whole file is the version. */
|
|
923
956
|
function patternFor({ path, pattern }) {
|
|
924
957
|
if (pattern) return new RegExp(pattern, 'm')
|
|
958
|
+
if (basename(path) === 'Cargo.lock') return cargoLockPattern(path)
|
|
925
959
|
if (path.endsWith('.json')) return VERSION_PATTERNS.json
|
|
926
960
|
if (path.endsWith('.toml')) return VERSION_PATTERNS.toml
|
|
927
961
|
return null
|
|
@@ -941,9 +975,10 @@ function readVersionFrom(entry) {
|
|
|
941
975
|
* Replace the version in a source file, touching nothing else: only the captured range is
|
|
942
976
|
* rewritten, so formatting, key order and comments all survive.
|
|
943
977
|
*
|
|
978
|
+
* @param {{dryRun?: boolean}} [options] report the change without making it
|
|
944
979
|
* @returns {boolean} whether the file needed changing
|
|
945
980
|
*/
|
|
946
|
-
function writeVersionInto(entry, version) {
|
|
981
|
+
function writeVersionInto(entry, version, { dryRun = false } = {}) {
|
|
947
982
|
const source = versionSource(entry)
|
|
948
983
|
const text = readFileSync(source.path, 'utf8')
|
|
949
984
|
const pattern = patternFor(source)
|
|
@@ -1167,7 +1202,20 @@ if (versionFile && !existsSync(versionFile.path)) {
|
|
|
1167
1202
|
abort(`versionFile ${versionFile.path} does not exist`)
|
|
1168
1203
|
}
|
|
1169
1204
|
|
|
1170
|
-
|
|
1205
|
+
/**
|
|
1206
|
+
* Where a repository versions by tag alone — a Go module, a docs site — the latest tag is
|
|
1207
|
+
* the current version. Without this, `auto` and every bump have nothing to work from and
|
|
1208
|
+
* the version has to be typed out in full every time.
|
|
1209
|
+
*/
|
|
1210
|
+
function versionFromLastTag() {
|
|
1211
|
+
const tag = tryRead('git', ['describe', '--tags', '--abbrev=0'])
|
|
1212
|
+
if (!tag) return null
|
|
1213
|
+
const bare =
|
|
1214
|
+
config.tagPrefix && tag.startsWith(config.tagPrefix) ? tag.slice(config.tagPrefix.length) : tag
|
|
1215
|
+
return parseVersion(bare) ? bare : null
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const currentVersion = versionFile ? readVersionFrom(versionFile) : versionFromLastTag()
|
|
1171
1219
|
if (versionFile && !currentVersion) {
|
|
1172
1220
|
abort(`could not read a version from ${versionFile.path}`)
|
|
1173
1221
|
}
|
|
@@ -1259,14 +1307,17 @@ let version
|
|
|
1259
1307
|
if (!target) {
|
|
1260
1308
|
if (!currentVersion) {
|
|
1261
1309
|
abort(
|
|
1262
|
-
'this repository has no versionFile
|
|
1263
|
-
' Pass one explicitly: release-kit 1.2.3',
|
|
1310
|
+
'nothing to default to: this repository has no versionFile and no tag to read a ' +
|
|
1311
|
+
'version from.\n Pass one explicitly: release-kit 1.2.3',
|
|
1264
1312
|
)
|
|
1265
1313
|
}
|
|
1266
1314
|
version = currentVersion
|
|
1267
1315
|
} else if (target === 'auto') {
|
|
1268
1316
|
if (!currentVersion) {
|
|
1269
|
-
abort(
|
|
1317
|
+
abort(
|
|
1318
|
+
'auto has nothing to bump from: there is no versionFile and no tag to read a version ' +
|
|
1319
|
+
'from.\n Pass the first version explicitly: release-kit 0.1.0',
|
|
1320
|
+
)
|
|
1270
1321
|
}
|
|
1271
1322
|
const { commits, lastTag } = commitsSinceLastTag()
|
|
1272
1323
|
if (!commits.length) {
|
|
@@ -1289,7 +1340,10 @@ if (!target) {
|
|
|
1289
1340
|
}
|
|
1290
1341
|
} else if (BUMPS.has(target)) {
|
|
1291
1342
|
if (!currentVersion) {
|
|
1292
|
-
abort(
|
|
1343
|
+
abort(
|
|
1344
|
+
`a ${target} bump has nothing to bump from: no versionFile, and no tag to read a ` +
|
|
1345
|
+
'version from.\n Pass the version explicitly instead.',
|
|
1346
|
+
)
|
|
1293
1347
|
}
|
|
1294
1348
|
const preid = requestedPreid ?? preidOf(currentVersion)
|
|
1295
1349
|
if (target.startsWith('pre') && !preid) {
|
|
@@ -1754,7 +1808,7 @@ if (bumping) {
|
|
|
1754
1808
|
for (const entry of [versionFile, ...config.versionFiles]) {
|
|
1755
1809
|
const source = versionSource(entry)
|
|
1756
1810
|
if (!existsSync(source.path)) abort(`versionFiles entry ${source.path} does not exist`)
|
|
1757
|
-
if (writeVersionInto(source, version)) {
|
|
1811
|
+
if (writeVersionInto(source, version, { dryRun })) {
|
|
1758
1812
|
staged.push(source.path)
|
|
1759
1813
|
console.log(` ${dryRun ? yellow('would write') : dim('wrote')} ${source.path}`)
|
|
1760
1814
|
}
|