@entro314labs/release-kit 2.2.0 → 2.2.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 +31 -0
- package/package.json +1 -1
- package/release.mjs +61 -3
package/README.md
CHANGED
|
@@ -346,6 +346,37 @@ name, `%d` npm dist-tag. In the `publish` command line the substituted values ar
|
|
|
346
346
|
shell-quoted, so a version carrying shell metacharacters is passed through as one literal
|
|
347
347
|
argument.
|
|
348
348
|
|
|
349
|
+
### Continuous integration
|
|
350
|
+
|
|
351
|
+
Non-interactive by default: the confirmation prompt is skipped when stdin is not a TTY, and
|
|
352
|
+
`gh` picks up `GITHUB_TOKEN` on its own. Pass `--yes` to be explicit.
|
|
353
|
+
|
|
354
|
+
```yaml
|
|
355
|
+
- uses: actions/checkout@v5
|
|
356
|
+
with:
|
|
357
|
+
fetch-depth: 0 # release notes and the last-tag lookup need real history
|
|
358
|
+
- id: release
|
|
359
|
+
run: npx @entro314labs/release-kit@2.1.0 minor --yes
|
|
360
|
+
env:
|
|
361
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
362
|
+
- run: echo "shipped ${{ steps.release.outputs.tag }} ${{ steps.release.outputs.release-url }}"
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
On success it writes to `$GITHUB_OUTPUT`, so later steps can act on what happened instead of
|
|
366
|
+
re-deriving it: `version`, `tag`, `name`, `dist-tag`, `steps`, `published`, `release-url`.
|
|
367
|
+
Nothing is written on a dry run, and an unwritable `$GITHUB_OUTPUT` never fails a release
|
|
368
|
+
that already completed.
|
|
369
|
+
|
|
370
|
+
Three things CI does that are worth knowing about:
|
|
371
|
+
|
|
372
|
+
- **`fetch-depth: 0`.** The default checkout is a shallow clone, which hides the history
|
|
373
|
+
release notes are drafted from. It still releases correctly, but the notes describe a
|
|
374
|
+
fraction of the work, so a shallow clone is called out as a warning.
|
|
375
|
+
- **Detached HEAD.** Tag and pull-request checkouts leave no branch to push, which is a
|
|
376
|
+
preflight failure rather than a confusing push error.
|
|
377
|
+
- **Signing.** Runners have no signing key, so disable it for the run rather than shipping
|
|
378
|
+
keys around: `git -c commit.gpgsign=false -c tag.gpgsign=false`.
|
|
379
|
+
|
|
349
380
|
### Signing
|
|
350
381
|
|
|
351
382
|
Signing is git's, not this tool's: commits and tags are made with plain `git commit` and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entro314labs/release-kit",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.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
|
@@ -32,7 +32,14 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
import { execFileSync, execSync } from 'node:child_process'
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
appendFileSync,
|
|
37
|
+
existsSync,
|
|
38
|
+
mkdirSync,
|
|
39
|
+
mkdtempSync,
|
|
40
|
+
readFileSync,
|
|
41
|
+
writeFileSync,
|
|
42
|
+
} from 'node:fs'
|
|
36
43
|
import { homedir, tmpdir } from 'node:os'
|
|
37
44
|
import { basename, join, relative, resolve, sep } from 'node:path'
|
|
38
45
|
import { createInterface } from 'node:readline/promises'
|
|
@@ -1106,11 +1113,21 @@ if (runs('commit') && !assistant) {
|
|
|
1106
1113
|
}
|
|
1107
1114
|
|
|
1108
1115
|
const branch = tryRead('git', ['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
1116
|
+
// A detached HEAD has no branch to push, and reports itself as the literal "HEAD", which
|
|
1117
|
+
// would otherwise be compared against config.branch and pushed as a ref of that name.
|
|
1118
|
+
const detached = branch === 'HEAD'
|
|
1109
1119
|
if (!branch) fail('could not read the current branch')
|
|
1110
|
-
else if (
|
|
1120
|
+
else if (detached) {
|
|
1121
|
+
fail('HEAD is detached — a release needs a branch to push. Check one out first.')
|
|
1122
|
+
} else if (config.branch && branch !== config.branch) {
|
|
1111
1123
|
fail(`on '${branch}', expected '${config.branch}'`)
|
|
1112
1124
|
} else ok(`on ${branch}`)
|
|
1113
1125
|
|
|
1126
|
+
// A shallow clone (CI checkouts default to depth 1) hides the history that release notes
|
|
1127
|
+
// and the last-tag lookup are derived from. It still releases correctly; the notes just
|
|
1128
|
+
// silently describe a fraction of the work, so say so before that happens.
|
|
1129
|
+
const shallow = tryRead('git', ['rev-parse', '--is-shallow-repository']) === 'true'
|
|
1130
|
+
|
|
1114
1131
|
if (!succeeds('git', ['remote', 'get-url', config.remote])) {
|
|
1115
1132
|
fail(`no '${config.remote}' remote configured`)
|
|
1116
1133
|
} else {
|
|
@@ -1118,7 +1135,7 @@ if (!succeeds('git', ['remote', 'get-url', config.remote])) {
|
|
|
1118
1135
|
// Fetch so the tag and behind-remote checks below see the real remote state.
|
|
1119
1136
|
if (!succeeds('git', ['fetch', '--quiet', '--tags', config.remote])) {
|
|
1120
1137
|
fail(`could not fetch from ${config.remote}`)
|
|
1121
|
-
} else if (branch) {
|
|
1138
|
+
} else if (branch && !detached) {
|
|
1122
1139
|
const upstream = `${config.remote}/${branch}`
|
|
1123
1140
|
if (!succeeds('git', ['rev-parse', '--verify', '--quiet', `refs/remotes/${upstream}`])) {
|
|
1124
1141
|
note(`${upstream} does not exist yet — the push will create it`)
|
|
@@ -1233,6 +1250,12 @@ function draftNotesFor(v) {
|
|
|
1233
1250
|
if (!assistant) return null
|
|
1234
1251
|
const { lastTag, subjects } = commitsSinceLastTag()
|
|
1235
1252
|
if (!subjects.length) return null
|
|
1253
|
+
if (shallow) {
|
|
1254
|
+
warn(
|
|
1255
|
+
`shallow clone: only ${subjects.length} commit(s) are visible, so the notes will ` +
|
|
1256
|
+
'describe part of the release. Check out with full history (fetch-depth: 0).',
|
|
1257
|
+
)
|
|
1258
|
+
}
|
|
1236
1259
|
note(`drafting notes from ${subjects.length} commit(s) with ${assistantName}...`)
|
|
1237
1260
|
return draftReleaseNotes(v, subjects, lastTag)
|
|
1238
1261
|
}
|
|
@@ -1418,6 +1441,41 @@ if (runs('release') && !releaseExists) {
|
|
|
1418
1441
|
mutate('gh', args, notes ? { input: `${notes}\n` } : {})
|
|
1419
1442
|
}
|
|
1420
1443
|
|
|
1444
|
+
/**
|
|
1445
|
+
* Hand the result back to whatever is orchestrating this. GitHub Actions reads key=value
|
|
1446
|
+
* pairs from $GITHUB_OUTPUT, so a workflow can gate later steps on what actually happened
|
|
1447
|
+
* rather than re-deriving it from the repository.
|
|
1448
|
+
*/
|
|
1449
|
+
function emitOutputs() {
|
|
1450
|
+
const file = process.env.GITHUB_OUTPUT
|
|
1451
|
+
if (!file || dryRun) return
|
|
1452
|
+
const releaseUrl = runs('release')
|
|
1453
|
+
? (tryRead('gh', ['release', 'view', tag, '--json', 'url', '--jq', '.url']) ?? '')
|
|
1454
|
+
: ''
|
|
1455
|
+
const outputs = {
|
|
1456
|
+
version,
|
|
1457
|
+
tag,
|
|
1458
|
+
name: projectName,
|
|
1459
|
+
'dist-tag': distTag,
|
|
1460
|
+
steps: STEPS.filter(runs).join(','),
|
|
1461
|
+
published: String(!!publishCommand && !alreadyPublished),
|
|
1462
|
+
'release-url': releaseUrl,
|
|
1463
|
+
}
|
|
1464
|
+
try {
|
|
1465
|
+
appendFileSync(
|
|
1466
|
+
file,
|
|
1467
|
+
`${Object.entries(outputs)
|
|
1468
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
1469
|
+
.join('\n')}\n`,
|
|
1470
|
+
)
|
|
1471
|
+
note(`wrote ${Object.keys(outputs).length} outputs to $GITHUB_OUTPUT`)
|
|
1472
|
+
} catch {
|
|
1473
|
+
// Outputs are a convenience; never fail a completed release over them.
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
emitOutputs()
|
|
1478
|
+
|
|
1421
1479
|
console.log(
|
|
1422
1480
|
`\n${green(bold(dryRun ? 'Dry run complete — nothing was changed.' : `Released ${tag}`))}`,
|
|
1423
1481
|
)
|