@entro314labs/release-kit 2.1.0 → 2.2.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.
Files changed (3) hide show
  1. package/README.md +65 -21
  2. package/package.json +1 -1
  3. package/release.mjs +34 -1
package/README.md CHANGED
@@ -65,45 +65,70 @@ Released v2.5.0
65
65
 
66
66
  ## 📦 Install
67
67
 
68
- **As a devDependency** — the normal choice. Updates arrive through your package manager.
68
+ Pick by what the project is, not by preference.
69
+
70
+ ### Node projects — devDependency
71
+
72
+ Pins the version, so every machine and CI run behave identically.
69
73
 
70
74
  ```sh
71
75
  pnpm add -D @entro314labs/release-kit
72
76
  ```
73
77
 
74
78
  ```json
75
- {
76
- "scripts": {
77
- "release": "release-kit"
78
- }
79
- }
79
+ { "scripts": { "release": "release-kit" } }
80
+ ```
81
+
82
+ ### Non-Node projects — global install
83
+
84
+ A Rust, Python or Go repository has no manifest to hang a devDependency on, so install it
85
+ once and use it everywhere.
86
+
87
+ ```sh
88
+ npm i -g @entro314labs/release-kit
89
+ release-kit minor
80
90
  ```
81
91
 
82
- **Without installing** — for a one-off release, or a project you do not want to add a
83
- dependency to:
92
+ ### CI, any language — pinned npx
93
+
94
+ No global state to drift, no install step, and the version is explicit in the command.
84
95
 
85
96
  ```sh
86
- npx @entro314labs/release-kit --dry-run
97
+ npx @entro314labs/release-kit@2.1.0 minor --yes
87
98
  ```
88
99
 
89
- **Vendored** — for a project that should not depend on the registry it is about to publish
90
- to, or one that needs releases to work offline. `--sync` copies the file into
91
- `scripts/release.mjs`:
100
+ ### Vendored — no registry at release time
101
+
102
+ For a project that should not depend on the registry it is about to publish to, or that
103
+ needs releases to work offline. The file is self-contained, so a copy is a complete install.
92
104
 
93
105
  ```sh
94
- npx @entro314labs/release-kit --sync .
106
+ npx @entro314labs/release-kit --sync . # writes scripts/release.mjs
95
107
  ```
96
108
 
97
109
  ```json
98
- {
99
- "scripts": {
100
- "release": "node scripts/release.mjs"
101
- }
102
- }
110
+ { "scripts": { "release": "node scripts/release.mjs" } }
103
111
  ```
104
112
 
105
- All three run the same file. Zero-config works on the conventions below; add a
106
- [`release.config.json`](#️-configuration) only for what differs.
113
+ ### Piped — nothing installed at all
114
+
115
+ `release.mjs` runs straight from stdin, arguments and all. Useful for a one-off release on a
116
+ machine you do not want to install anything on.
117
+
118
+ ```sh
119
+ curl -fsSL https://raw.githubusercontent.com/entro314-labs/release-kit/v2.1.0/release.mjs \
120
+ | node - minor --yes
121
+ ```
122
+
123
+ Pin the URL to a tag, never `main`: piping an unpinned remote script into an interpreter
124
+ means whatever is at that URL runs against your repository and your credentials. `--sync` is
125
+ the one thing that does not work this way — copying itself needs a file on disk.
126
+
127
+ > **All five paths run the same file and need Node 18+.** That includes the Rust, Python and
128
+ > Go projects: `release-kit` is a Node program regardless of what it is releasing.
129
+
130
+ Zero-config works on the conventions below; add a [`release.config.json`](#️-configuration)
131
+ only for what differs.
107
132
 
108
133
  ## ⚡ Usage
109
134
 
@@ -224,6 +249,7 @@ rather than stopping at the first problem.
224
249
  - The remote exists, is reachable, and the branch is not behind it
225
250
  - The tag is free — or already exists at `HEAD`, in which case it is reused
226
251
  - `gh` is installed and authenticated
252
+ - Commit and tag signing can actually sign, when `commit.gpgsign` or `tag.gpgsign` is on
227
253
  - The publishing CLI is authenticated, and the version is not already on the registry
228
254
  - Configured release assets exist
229
255
  - A changelog section for the version exists _(a warning, not a failure — it falls back
@@ -320,6 +346,23 @@ name, `%d` npm dist-tag. In the `publish` command line the substituted values ar
320
346
  shell-quoted, so a version carrying shell metacharacters is passed through as one literal
321
347
  argument.
322
348
 
349
+ ### Signing
350
+
351
+ Signing is git's, not this tool's: commits and tags are made with plain `git commit` and
352
+ `git tag`, so they are signed exactly when `commit.gpgsign` and `tag.gpgsign` say to, with
353
+ whatever key `user.signingkey` resolves to. There is no key handling here to get wrong.
354
+
355
+ What it does add is a preflight check, because an unusable key otherwise fails at the commit
356
+ step with the version already written. For CI, where a signing key usually is not present,
357
+ disable signing for that run rather than configuring keys:
358
+
359
+ ```sh
360
+ git -c commit.gpgsign=false -c tag.gpgsign=false release-kit minor --yes
361
+ ```
362
+
363
+ For commits to show as **Verified** on GitHub, the SSH key must be registered as a _signing_
364
+ key in your account, which is a separate list from authentication keys.
365
+
323
366
  ### Publishing and authentication
324
367
 
325
368
  The registry preflight (`whoami`, the already-published lookup) runs with whichever CLI the
@@ -445,7 +488,8 @@ including a directory that is not a repository.
445
488
 
446
489
  ## 📋 Requirements
447
490
 
448
- - Node 18+ (uses `node:readline/promises` and `Array.prototype.at`)
491
+ - **Node 18+ — including for Rust, Python and Go projects.** `release-kit` is a Node
492
+ program whatever it releases; there is no standalone binary.
449
493
  - `git`
450
494
  - `gh`, authenticated — only when creating GitHub releases
451
495
  - Whatever the `publish` command needs — for the default, a live `npm login` session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entro314labs/release-kit",
3
- "version": "2.1.0",
3
+ "version": "2.2.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",
package/release.mjs CHANGED
@@ -33,7 +33,7 @@
33
33
 
34
34
  import { execFileSync, execSync } from 'node:child_process'
35
35
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
36
- import { tmpdir } from 'node:os'
36
+ import { homedir, tmpdir } from 'node:os'
37
37
  import { basename, join, relative, resolve, sep } from 'node:path'
38
38
  import { createInterface } from 'node:readline/promises'
39
39
 
@@ -762,6 +762,15 @@ if (flag('--help') || flag('-h')) {
762
762
  // --sync copies this file into other projects and exits; it touches no git state.
763
763
  if (flag('--sync')) {
764
764
  const self = new URL(import.meta.url).pathname
765
+ // Piped from stdin (`curl … | node -`) there is no file to copy: import.meta.url points
766
+ // at a synthetic [eval] path. Say so instead of failing on a missing file.
767
+ if (!existsSync(self)) {
768
+ abort(
769
+ '--sync copies this script from disk, and it was piped from stdin so there is no ' +
770
+ 'file to copy.\n Run it from an installed copy instead: ' +
771
+ 'npx @entro314labs/release-kit --sync <dir>',
772
+ )
773
+ }
765
774
  const targets = argv.slice(argv.indexOf('--sync') + 1).filter((a) => !a.startsWith('-'))
766
775
  if (!targets.length) abort('--sync needs at least one project directory')
767
776
 
@@ -1122,6 +1131,30 @@ if (!succeeds('git', ['remote', 'get-url', config.remote])) {
1122
1131
  }
1123
1132
  }
1124
1133
 
1134
+ // Signing is configured per repository and inherited, never managed here — git already
1135
+ // owns that. But a signing setup that cannot produce a signature fails at the commit step,
1136
+ // after the version has been written, so it is worth catching before anything mutates.
1137
+ const signsSomething = ['commit', 'version', 'changelog', 'tag'].some(runs)
1138
+ const signingKeys = ['commit.gpgsign', 'tag.gpgsign'].filter(
1139
+ (key) => tryRead('git', ['config', '--get', key]) === 'true',
1140
+ )
1141
+ if (signsSomething && signingKeys.length) {
1142
+ const format = tryRead('git', ['config', '--get', 'gpg.format']) || 'openpgp'
1143
+ const signingKey = tryRead('git', ['config', '--get', 'user.signingkey'])
1144
+ const keyPath = signingKey?.replace(/^~/, homedir())
1145
+ if (!signingKey) {
1146
+ fail(`${signingKeys.join(' and ')} enabled but user.signingkey is not set`)
1147
+ } else if (format === 'ssh' && /^[~/.]/.test(signingKey) && !existsSync(keyPath)) {
1148
+ fail(
1149
+ `signing key ${signingKey} does not exist.\n` +
1150
+ ' Point user.signingkey at a key that is present, or disable signing for this ' +
1151
+ 'run with `git -c commit.gpgsign=false -c tag.gpgsign=false`.',
1152
+ )
1153
+ } else {
1154
+ ok(`signing commits and tags (${format})`)
1155
+ }
1156
+ }
1157
+
1125
1158
  const head = tryRead('git', ['rev-parse', 'HEAD'])
1126
1159
  const taggedCommit = tryRead('git', ['rev-list', '-n', '1', tag])
1127
1160
  if (!runs('tag')) {