@blamejs/exceptd-skills 0.19.33 → 0.19.34
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/CHANGELOG.md +10 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -152
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +35 -114
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -412
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +241 -241
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -140
package/scripts/release.js
CHANGED
|
@@ -1,44 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* or CI runner. It codifies the flow that CONTRIBUTING.md / the repo's
|
|
8
|
-
* release notes describe step by step, so a release can't skip the
|
|
9
|
-
* load-bearing ordering (CHANGELOG entry first, gates before tag, CI green
|
|
10
|
-
* before tag push, GUARD before tag).
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* node scripts/release.js prepare [--minor] [--with-content]
|
|
14
|
-
* # bump + sign + indexes + snapshot + sbom + baseline
|
|
15
|
-
* # --with-content: this release ships uncommitted work
|
|
16
|
-
* node scripts/release.js gates # npm test + 20-gate predeploy
|
|
17
|
-
* node scripts/release.js commit # release branch + signed commit
|
|
18
|
-
* node scripts/release.js push # push branch + open PR
|
|
19
|
-
* node scripts/release.js watch # CI watch + flag unresolved review threads
|
|
20
|
-
* node scripts/release.js merge # admin squash-merge if CLEAN + zero unresolved
|
|
21
|
-
* node scripts/release.js tag # GUARD + signed tag + push tag + verify
|
|
22
|
-
* node scripts/release.js release # watch release.yml + npm/global/tarball verify
|
|
23
|
-
* node scripts/release.js all [--minor] # all eight in sequence
|
|
24
|
-
* node scripts/release.js status # what phase the current branch is in
|
|
25
|
-
* node scripts/release.js help # this banner
|
|
26
|
-
*
|
|
27
|
-
* Pre-conditions the script enforces rather than assumes:
|
|
28
|
-
* - prepare runs only on a clean `main`, and refuses unless CHANGELOG.md
|
|
29
|
-
* already carries a `## <next-version>` heading (the operator writes the
|
|
30
|
-
* behavior-framed notes by hand; they don't auto-generate from a diff).
|
|
31
|
-
* - The three-version invariant (package.json == manifest.json ==
|
|
32
|
-
* CHANGELOG top heading) is established by prepare and re-checked by tag.
|
|
33
|
-
* - tag refuses unless local HEAD == origin/main and the version matches
|
|
34
|
-
* and no such tag exists (the GUARD that prevents tag-on-stale-HEAD).
|
|
35
|
-
*
|
|
36
|
-
* Patch is the default bump. --minor requires the explicit flag AND is a
|
|
37
|
-
* deliberate choice — the project default is patch-only.
|
|
38
|
-
*
|
|
39
|
-
* The judgment-requiring parts stay manual: writing the CHANGELOG entry,
|
|
40
|
-
* reviewing/fixing CI-surfaced review-thread findings (watch flags them and
|
|
41
|
-
* stops), and choosing patch vs minor.
|
|
4
|
+
* Orchestrates the release as idempotent per-phase subcommands, each enforcing
|
|
5
|
+
* its own preconditions so the load-bearing ordering cannot be skipped. `help`
|
|
6
|
+
* lists them.
|
|
42
7
|
*/
|
|
43
8
|
|
|
44
9
|
var fs = require("node:fs");
|
|
@@ -49,27 +14,18 @@ var ROOT = path.resolve(__dirname, "..");
|
|
|
49
14
|
var REPO = "blamejs/exceptd-skills";
|
|
50
15
|
var PKG_NAME = "@blamejs/exceptd-skills";
|
|
51
16
|
|
|
52
|
-
// Known-flaky CI jobs that warrant an auto-rerun rather than a hard fail:
|
|
53
|
-
// the macOS playbook-runner job and the offline-CLI F1 check both flake on
|
|
54
|
-
// fresh runners. watch reruns them up to twice before surfacing a failure.
|
|
55
17
|
var RERUN_LIMIT = 2;
|
|
56
18
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
// Windows resolves `npm` / `npx` as `.cmd` shims, which child_process can
|
|
60
|
-
// only invoke through a shell. `git`, `gh`, `node` are native exes that
|
|
61
|
-
// spawn directly — keeping shell off avoids the implicit arg-quoting risk.
|
|
19
|
+
// Windows resolves `npm` / `npx` as `.cmd` shims, which child_process can only
|
|
20
|
+
// invoke through a shell; `git`, `gh` and `node` are native exes that spawn directly.
|
|
62
21
|
function _needsShell(cmd) {
|
|
63
22
|
if (process.platform !== "win32") return false;
|
|
64
23
|
return cmd === "npm" || cmd === "npx";
|
|
65
24
|
}
|
|
66
25
|
|
|
67
26
|
// spawnSync with shell:true AND an args array concatenates the args without
|
|
68
|
-
// escaping (Node DEP0190 —
|
|
69
|
-
//
|
|
70
|
-
// string with no args array, which is the correct shell-invocation form. The
|
|
71
|
-
// only commands routed through the shell here are npm with static token args
|
|
72
|
-
// (verb names + flags, no spaces), so the single-string join is unambiguous.
|
|
27
|
+
// escaping (Node DEP0190 — an injection surface), so the shell path passes the
|
|
28
|
+
// whole invocation as one command string. Only npm with static token args goes there.
|
|
73
29
|
function _spawn(cmd, args, opts) {
|
|
74
30
|
opts = opts || {};
|
|
75
31
|
var useShell = _needsShell(cmd);
|
|
@@ -114,9 +70,8 @@ function _readJsonVersion(file) {
|
|
|
114
70
|
return JSON.parse(fs.readFileSync(path.join(ROOT, file), "utf8")).version;
|
|
115
71
|
}
|
|
116
72
|
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// manifest.json's hand-maintained shape).
|
|
73
|
+
// Rewrites only the "version" line: a full JSON.stringify would reflow
|
|
74
|
+
// manifest.json's hand-maintained key order and formatting.
|
|
120
75
|
function _writeJsonVersion(file, next) {
|
|
121
76
|
var p = path.join(ROOT, file);
|
|
122
77
|
var content = fs.readFileSync(p, "utf8");
|
|
@@ -136,7 +91,6 @@ function _bump(version, kind) {
|
|
|
136
91
|
return parts[0] + "." + parts[1] + "." + (parts[2] + 1);
|
|
137
92
|
}
|
|
138
93
|
|
|
139
|
-
// Topmost `## X.Y.Z` heading in CHANGELOG.md.
|
|
140
94
|
function _changelogTopVersion() {
|
|
141
95
|
var lines = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8").split(/\r?\n/);
|
|
142
96
|
for (var i = 0; i < lines.length; i++) {
|
|
@@ -146,8 +100,6 @@ function _changelogTopVersion() {
|
|
|
146
100
|
return null;
|
|
147
101
|
}
|
|
148
102
|
|
|
149
|
-
// Extract the body of a CHANGELOG section (between its `## X.Y.Z` heading
|
|
150
|
-
// and the next `## ` heading) — used to compose the commit + PR body.
|
|
151
103
|
function _changelogSection(version) {
|
|
152
104
|
var lines = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8").split(/\r?\n/);
|
|
153
105
|
var out = [];
|
|
@@ -164,10 +116,8 @@ function _changelogSection(version) {
|
|
|
164
116
|
return out.join("\n").trim();
|
|
165
117
|
}
|
|
166
118
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// headline field), so take the first SENTENCE and cap the length rather than
|
|
170
|
-
// dump the whole paragraph as the subject. The operator can always amend.
|
|
119
|
+
// "vX.Y.Z: <subject>" for the commit and PR title. CHANGELOG entries carry no
|
|
120
|
+
// headline field, so the subject is the first sentence, length-capped.
|
|
171
121
|
function _releaseSubject(version, section) {
|
|
172
122
|
var firstLine = (section.split(/\r?\n/).find(function (l) { return l.trim(); }) || "").trim();
|
|
173
123
|
var firstSentence = firstLine.split(/(?<=[.!?])\s/)[0] || firstLine;
|
|
@@ -182,10 +132,8 @@ function _gitOnMain() { return _gitBranch() === "main"; }
|
|
|
182
132
|
function _gitOnRelease() { return /^release-v\d+\.\d+\.\d+$/.test(_gitBranch()); }
|
|
183
133
|
function _releaseBranchFor(version) { return "release-v" + version; }
|
|
184
134
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// human-readable `%G? %GS` line. main is under required_signatures, so an
|
|
188
|
-
// unsigned commit can't be pushed — fail loudly here rather than at push.
|
|
135
|
+
// `git verify-commit` is the boolean GitHub's required_signatures ruleset checks;
|
|
136
|
+
// main is under that ruleset, so fail here rather than at push.
|
|
189
137
|
function _verifyCommitSignature(label) {
|
|
190
138
|
var verify = _capture("git", ["verify-commit", "HEAD"]);
|
|
191
139
|
if (verify.status !== 0) {
|
|
@@ -205,9 +153,8 @@ function _openPrNumber(branch) {
|
|
|
205
153
|
"--json", "number", "--jq", ".[0].number"]).stdout;
|
|
206
154
|
}
|
|
207
155
|
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// branch-protection merge block (conversation-resolution required).
|
|
156
|
+
// Conversation resolution is branch-protection-required, so an unresolved
|
|
157
|
+
// thread is a hard merge block.
|
|
211
158
|
function _unresolvedThreads(prNum) {
|
|
212
159
|
var q = 'query { repository(owner:"blamejs",name:"exceptd-skills") { pullRequest(number:' +
|
|
213
160
|
prNum + ') { reviewThreads(first:50) { nodes { isResolved comments(first:1) ' +
|
|
@@ -217,32 +164,11 @@ function _unresolvedThreads(prNum) {
|
|
|
217
164
|
try { return JSON.parse(rv.stdout || "[]"); } catch (_e) { return []; }
|
|
218
165
|
}
|
|
219
166
|
|
|
220
|
-
// Open CodeQL
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
// transient failure fails OPEN rather than blocking a release — the pre-flight
|
|
226
|
-
// checklist is the human backstop.
|
|
227
|
-
// Two refs, and BOTH are load-bearing — each alone has a blind spot that has
|
|
228
|
-
// already bitten:
|
|
229
|
-
//
|
|
230
|
-
// refs/pull/<N>/merge answers "did this PR introduce an alert". Scoping to
|
|
231
|
-
// it ALONE let two medium alerts sitting on refs/heads/main since
|
|
232
|
-
// 2026-08-08 return zero, riding through eight consecutive releases while
|
|
233
|
-
// the phase printed "zero open CodeQL alerts" — GitHub only annotates a PR
|
|
234
|
-
// with alerts that PR introduces.
|
|
235
|
-
// (no ref) answers against the DEFAULT BRANCH, catching exactly
|
|
236
|
-
// those pre-existing findings — but on its own it misses an alert this PR
|
|
237
|
-
// introduces that is not on main yet, which is the case the PR-scoped query
|
|
238
|
-
// was there for.
|
|
239
|
-
//
|
|
240
|
-
// So the gate blocks on the union. Replacing one query with the other just
|
|
241
|
-
// trades which blind spot you have.
|
|
242
|
-
//
|
|
243
|
-
// Filtered to tool_name=CodeQL deliberately: OpenSSF Scorecard writes to the
|
|
244
|
-
// same code-scanning surface, and its accepted-out-of-scope policy alerts would
|
|
245
|
-
// otherwise make this list permanently non-empty and therefore useless.
|
|
167
|
+
// Open CodeQL alerts. Returns the alert array, or null when the query cannot run
|
|
168
|
+
// at all, so a transient API failure fails OPEN rather than blocking a release.
|
|
169
|
+
// The union of two refs is required: refs/pull/<N>/merge misses an alert already
|
|
170
|
+
// sitting on main, and the default-branch query misses one this PR introduces.
|
|
171
|
+
// tool_name=CodeQL excludes Scorecard's accepted-out-of-scope policy alerts.
|
|
246
172
|
function _codeqlAlertsForRef(ref) {
|
|
247
173
|
var args = ["api", "repos/:owner/:repo/code-scanning/alerts", "-X", "GET",
|
|
248
174
|
"-f", "state=open", "-f", "tool_name=CodeQL", "-f", "per_page=100"];
|
|
@@ -258,9 +184,7 @@ function _codeqlAlertsForRef(ref) {
|
|
|
258
184
|
function _openCodeqlAlerts(prNum) {
|
|
259
185
|
var onDefault = _codeqlAlertsForRef(null);
|
|
260
186
|
var onPr = _codeqlAlertsForRef("refs/pull/" + prNum + "/merge");
|
|
261
|
-
//
|
|
262
|
-
// lookup read as "no alerts" — that is the same absent-input-passes shape
|
|
263
|
-
// this whole gate exists to close.
|
|
187
|
+
// A failed lookup must never read as "no alerts".
|
|
264
188
|
if (onDefault === null || onPr === null) return null;
|
|
265
189
|
|
|
266
190
|
var byNumber = new Map();
|
|
@@ -272,19 +196,11 @@ function _openCodeqlAlerts(prNum) {
|
|
|
272
196
|
return [...byNumber.values()];
|
|
273
197
|
}
|
|
274
198
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
// The derived-artifact regeneration, in the one order that is correct. Shared
|
|
278
|
-
// by `prepare` and `regen` so the two cannot drift: any source edit after a
|
|
279
|
-
// prepare — a review finding fixed on the release branch, most often a data
|
|
280
|
-
// correction — invalidates the signatures, indexes and SBOM hashes, and
|
|
281
|
-
// re-running the four commands from memory is where the ordering gets lost.
|
|
199
|
+
// Shared by `prepare` and `regen` so the two cannot drift.
|
|
282
200
|
function _regenArtifacts() {
|
|
283
201
|
_section("regen artifacts");
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// shipped tree (incl. README) — regenerating it before a later source edit
|
|
287
|
-
// strands the hashes (the recurring "refresh-sbom last" lesson).
|
|
202
|
+
// sign-all first, since it rewrites the manifest; refresh-sbom LAST, since it
|
|
203
|
+
// hashes the shipped tree (README included) and any later edit strands the hashes.
|
|
288
204
|
_run("node", ["lib/sign.js", "sign-all"]);
|
|
289
205
|
_run("npm", ["run", "build-indexes"]);
|
|
290
206
|
_run("npm", ["run", "refresh-snapshot"]);
|
|
@@ -292,16 +208,12 @@ function _regenArtifacts() {
|
|
|
292
208
|
_ok("signed + indexes + snapshot + sbom regenerated");
|
|
293
209
|
}
|
|
294
210
|
|
|
295
|
-
// Re-
|
|
296
|
-
//
|
|
297
|
-
// this phase exists precisely for a dirty tree. It refuses on main, where the
|
|
298
|
-
// bump belongs to `prepare`.
|
|
211
|
+
// Re-derives artifacts after editing an already-prepared release branch: no
|
|
212
|
+
// bump, no CHANGELOG requirement, and a dirty tree is the point.
|
|
299
213
|
function cmdRegen() {
|
|
300
214
|
_section("regen");
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// accidental run from a feature branch or a detached HEAD would produce
|
|
304
|
-
// release artifacts from a tree that is not the release.
|
|
215
|
+
// A release branch is positively required rather than main merely rejected: a
|
|
216
|
+
// feature branch or detached HEAD would build artifacts out of the wrong tree.
|
|
305
217
|
if (!_gitOnRelease()) {
|
|
306
218
|
throw new Error("release: regen must run on a release-vX.Y.Z branch (on " + _gitBranch() + "). " +
|
|
307
219
|
"On main the regeneration belongs to `prepare`.");
|
|
@@ -319,22 +231,10 @@ function cmdRegen() {
|
|
|
319
231
|
function cmdPrepare(opts) {
|
|
320
232
|
_section("prepare");
|
|
321
233
|
if (!_gitOnMain()) throw new Error("release: prepare must run on main (on " + _gitBranch() + ")");
|
|
322
|
-
// The
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
// (prepare is about to bump versions + regenerate artifacts — it must start
|
|
327
|
-
// from an otherwise-clean main so the release commit captures only the
|
|
328
|
-
// intended change set).
|
|
329
|
-
//
|
|
330
|
-
// `--with-content` widens that to a release which SHIPS an uncommitted change
|
|
331
|
-
// — a curation batch written into data/, a fix folded in alongside the bump.
|
|
332
|
-
// Without it, such a release cannot use this phase at all and gets hand-run
|
|
333
|
-
// instead, which is how the steps below drift: a hand-rolled sequence dropped
|
|
334
|
-
// the shrinkage gate two lines above the baseline refresh and rebaselined
|
|
335
|
-
// without ever checking. The flag keeps the phase authoritative for that flow
|
|
336
|
-
// rather than leaving it to memory. It still prints what it is carrying, so
|
|
337
|
-
// an unintended file in the tree is visible rather than silently released.
|
|
234
|
+
// The `## <next>` CHANGELOG entry is written by hand before prepare runs, so a
|
|
235
|
+
// CHANGELOG.md-only dirty tree is allowed and anything else is refused.
|
|
236
|
+
// `--with-content` widens that to a release which SHIPS uncommitted work, and
|
|
237
|
+
// prints what it carries so an unintended file is visible rather than released.
|
|
338
238
|
var dirty = _capture("git", ["status", "--porcelain"]).stdout
|
|
339
239
|
.split(/\r?\n/)
|
|
340
240
|
.filter(function (l) { return l.trim() && !/\bCHANGELOG\.md$/.test(l); });
|
|
@@ -352,24 +252,18 @@ function cmdPrepare(opts) {
|
|
|
352
252
|
var next = _bump(current, opts.minor ? "minor" : "patch");
|
|
353
253
|
console.log("current: " + current + " next: " + next + " (" + (opts.minor ? "minor" : "patch") + ")");
|
|
354
254
|
|
|
355
|
-
//
|
|
356
|
-
// narrative). Refuse if it isn't there — the three-version invariant the
|
|
357
|
-
// bootstrap-mode test enforces would otherwise fail at gates time.
|
|
255
|
+
// Without the entry, tests/bootstrap-mode's three-version invariant fails at gates.
|
|
358
256
|
var top = _changelogTopVersion();
|
|
359
257
|
if (top !== next) {
|
|
360
|
-
// Throw
|
|
361
|
-
//
|
|
362
|
-
// rather than continuing past a failed prepare. Matches the throw-style
|
|
363
|
-
// guards above (clean-tree / on-main); the dispatcher maps it to exit 1.
|
|
258
|
+
// Throw, never process.exit: the exit can truncate a piped stdout write, and
|
|
259
|
+
// `release all` must abort here. The dispatcher maps it to exit 1.
|
|
364
260
|
throw new Error(
|
|
365
261
|
"CHANGELOG.md top heading is '## " + top + "', expected '## " + next + "'. " +
|
|
366
262
|
"Write the " + next + " entry first (terse, behavior-change framed, no internal " +
|
|
367
263
|
"narrative), then re-run prepare. Example heading: ## " + next + " — <YYYY-MM-DD>");
|
|
368
264
|
}
|
|
369
265
|
|
|
370
|
-
// The
|
|
371
|
-
// passes the operator-facing lint (the release workflow publishes it verbatim
|
|
372
|
-
// as the GitHub Release body). Fail fast here rather than at the gates phase.
|
|
266
|
+
// The release workflow publishes this section verbatim as the GitHub Release body.
|
|
373
267
|
_run("node", ["scripts/check-changelog-extract.js", next]);
|
|
374
268
|
|
|
375
269
|
_writeJsonVersion("package.json", next);
|
|
@@ -379,22 +273,14 @@ function cmdPrepare(opts) {
|
|
|
379
273
|
_regenArtifacts();
|
|
380
274
|
|
|
381
275
|
_section("test-count baseline");
|
|
382
|
-
// Check BEFORE refreshing
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
// against itself and passed. Releasing was the one path that disarmed the
|
|
386
|
-
// guard, which is the path it exists to guard. Run the gate first so a drop
|
|
387
|
-
// stops the release here; refresh only once it has agreed nothing was lost,
|
|
388
|
-
// which still captures growth. A deliberate removal is re-baselined by hand
|
|
389
|
-
// with the command the gate's own failure message prints.
|
|
276
|
+
// Check BEFORE refreshing: `--update-baseline` writes whatever it observes, so
|
|
277
|
+
// refreshing first rebaselines a shrunken suite downward and the shrinkage gate
|
|
278
|
+
// in `gates` then compares the new count against itself.
|
|
390
279
|
_run("node", ["scripts/check-test-count.js"]);
|
|
391
280
|
_run("node", ["scripts/check-test-count.js", "--update-baseline"]);
|
|
392
281
|
|
|
393
282
|
_section("codebase-patterns currency (advisory)");
|
|
394
|
-
//
|
|
395
|
-
// patterns test) has grown a class exceptd hasn't triaged yet — the same
|
|
396
|
-
// forcing function the actions/vendor currency checks give those surfaces.
|
|
397
|
-
// Advisory: never blocks; skips cleanly when the sibling repo is absent.
|
|
283
|
+
// Flags a pattern class the sibling blamejs catalog grew; never blocks.
|
|
398
284
|
_run("node", ["scripts/check-codebase-patterns-currency.js"], { allowFail: true });
|
|
399
285
|
|
|
400
286
|
console.log("\nnext: node scripts/release.js gates");
|
|
@@ -402,9 +288,6 @@ function cmdPrepare(opts) {
|
|
|
402
288
|
|
|
403
289
|
function cmdGates() {
|
|
404
290
|
_section("gates");
|
|
405
|
-
// predeploy runs the full suite + every publish gate (signatures, catalog
|
|
406
|
-
// schema, snapshot, lint, sbom currency, indexes, tarball verify, diff
|
|
407
|
-
// coverage, ...). It is the authoritative pre-publish check.
|
|
408
291
|
_run("npm", ["run", "predeploy"]);
|
|
409
292
|
_ok("predeploy gates passed");
|
|
410
293
|
console.log("\nnext: node scripts/release.js commit");
|
|
@@ -416,8 +299,7 @@ function cmdCommit() {
|
|
|
416
299
|
var branch = _releaseBranchFor(next);
|
|
417
300
|
var current = _gitBranch();
|
|
418
301
|
|
|
419
|
-
// Resumable: a
|
|
420
|
-
// branch in place — switch to it instead of refusing.
|
|
302
|
+
// Resumable: a failed commit leaves the branch in place, so switch to it.
|
|
421
303
|
if (current === branch) {
|
|
422
304
|
_ok("already on " + branch + " (resume mode)");
|
|
423
305
|
} else if (current === "main") {
|
|
@@ -433,8 +315,7 @@ function cmdCommit() {
|
|
|
433
315
|
throw new Error("release: commit must run on main or " + branch + " (on " + current + ")");
|
|
434
316
|
}
|
|
435
317
|
|
|
436
|
-
//
|
|
437
|
-
// just verify the signature.
|
|
318
|
+
// HEAD already carrying this release's commit means verify, not re-commit.
|
|
438
319
|
var headSubject = _capture("git", ["log", "-1", "--pretty=%s"]).stdout;
|
|
439
320
|
if (headSubject.indexOf("v" + next + ":") === 0) {
|
|
440
321
|
_ok("HEAD already carries a v" + next + " commit (resume mode)");
|
|
@@ -443,8 +324,6 @@ function cmdCommit() {
|
|
|
443
324
|
return;
|
|
444
325
|
}
|
|
445
326
|
|
|
446
|
-
// Compose the commit body from the CHANGELOG section — the operator can
|
|
447
|
-
// amend, but the default mirrors the shipped notes.
|
|
448
327
|
var section = _changelogSection(next);
|
|
449
328
|
var subject = _releaseSubject(next, section);
|
|
450
329
|
var bodyPath = path.join(ROOT, ".scratch");
|
|
@@ -487,24 +366,16 @@ function cmdWatch() {
|
|
|
487
366
|
if (!prNum) throw new Error("release: no open PR for " + branch);
|
|
488
367
|
console.log("PR #" + prNum);
|
|
489
368
|
|
|
490
|
-
//
|
|
491
|
-
// run doesn't throw before we get to inspect + rerun it.
|
|
369
|
+
// Blocks until the checks settle; allowFail so failures are inspected below.
|
|
492
370
|
_run("gh", ["pr", "checks", prNum, "--watch"], { allowFail: true });
|
|
493
371
|
|
|
494
|
-
// Gate on check CONCLUSIONS, not only review threads.
|
|
495
|
-
//
|
|
496
|
-
// point of the watch phase) beats advancing to "next: merge" and letting
|
|
497
|
-
// cmdMerge reject it. Bucket is gh's normalized verdict: pass / fail /
|
|
498
|
-
// pending / skipping / cancel.
|
|
372
|
+
// Gate on check CONCLUSIONS, not only review threads. Bucket is gh's normalized
|
|
373
|
+
// verdict — pass / fail / pending / skipping / cancel.
|
|
499
374
|
var checksRaw = _capture("gh", ["pr", "checks", prNum, "--json", "name,bucket,link"]).stdout;
|
|
500
375
|
var checks = [];
|
|
501
376
|
try { checks = JSON.parse(checksRaw || "[]"); } catch (_e) { checks = []; }
|
|
502
|
-
// An empty or still-pending check set is NOT a pass
|
|
503
|
-
// returns immediately
|
|
504
|
-
// still being scheduled — and the filters below then find nothing wrong,
|
|
505
|
-
// so the phase printed "next: merge" for a PR whose CI had not started.
|
|
506
|
-
// Absence of a failure is not evidence of success; require that checks exist
|
|
507
|
-
// and have settled before reading anything into them.
|
|
377
|
+
// An empty or still-pending check set is NOT a pass: `gh pr checks --watch`
|
|
378
|
+
// returns immediately while the workflows are still being scheduled.
|
|
508
379
|
if (checks.length === 0) {
|
|
509
380
|
console.log("\nno checks have registered on this PR yet — they are probably still scheduling.");
|
|
510
381
|
console.log("Wait a moment, then re-run: node scripts/release.js watch");
|
|
@@ -526,8 +397,7 @@ function cmdWatch() {
|
|
|
526
397
|
process.exit(3); // allow:process-exit-after-stdout-write — maintainer-run release orchestrator; the guidance line above is human-read on a TTY, not a piped result channel
|
|
527
398
|
}
|
|
528
399
|
|
|
529
|
-
//
|
|
530
|
-
// alert blocks the release the same way an unresolved codex thread does.
|
|
400
|
+
// An open CodeQL alert blocks the release like an unresolved review thread.
|
|
531
401
|
var codeqlAlerts = _openCodeqlAlerts(prNum);
|
|
532
402
|
if (codeqlAlerts === null) {
|
|
533
403
|
console.log("\nnote: could not query CodeQL alerts (code scanning unavailable / API error) — " +
|
|
@@ -578,15 +448,13 @@ function cmdMerge() {
|
|
|
578
448
|
throw new Error("release: PR #" + prNum + " not mergeable (state=" +
|
|
579
449
|
state.mergeStateStatus + " mergeable=" + state.mergeable + ")");
|
|
580
450
|
}
|
|
581
|
-
//
|
|
582
|
-
// between watch and merge.
|
|
451
|
+
// A reviewer can open a thread between watch and merge.
|
|
583
452
|
var unresolved = _unresolvedThreads(prNum);
|
|
584
453
|
if (unresolved.length > 0) {
|
|
585
454
|
throw new Error("release: refusing to merge PR #" + prNum + " — " +
|
|
586
455
|
unresolved.length + " unresolved review thread(s); run watch again");
|
|
587
456
|
}
|
|
588
|
-
// Solo-maintainer protection requires 0 approvals; --admin
|
|
589
|
-
// remaining required checks gate without a second reviewer.
|
|
457
|
+
// Solo-maintainer protection requires 0 approvals; --admin covers required checks.
|
|
590
458
|
_run("gh", ["pr", "merge", prNum, "--squash", "--admin", "--delete-branch"]);
|
|
591
459
|
_ok("PR #" + prNum + " squash-merged");
|
|
592
460
|
|
|
@@ -601,10 +469,8 @@ function cmdTag() {
|
|
|
601
469
|
var next = _readJsonVersion("package.json");
|
|
602
470
|
var tag = "v" + next;
|
|
603
471
|
|
|
604
|
-
// GUARD against tag-on-stale-HEAD: a
|
|
605
|
-
//
|
|
606
|
-
// wrong commit and the release workflow's version-match gate would reject
|
|
607
|
-
// it (burning a version slot, since the v* ruleset blocks tag rewrites).
|
|
472
|
+
// GUARD against tag-on-stale-HEAD: a tag on the wrong commit burns a version
|
|
473
|
+
// slot, since the v* ruleset blocks tag rewrites.
|
|
608
474
|
try { fs.rmSync(path.join(ROOT, ".git", "index.lock"), { force: true }); } catch (_e) { /* ignore */ }
|
|
609
475
|
_run("git", ["fetch", "origin", "main"]);
|
|
610
476
|
var local = _capture("git", ["rev-parse", "HEAD"]).stdout;
|
|
@@ -628,12 +494,9 @@ function cmdTag() {
|
|
|
628
494
|
}
|
|
629
495
|
_ok("GUARD passed (HEAD==origin/main, 3-version match, no existing tag)");
|
|
630
496
|
|
|
631
|
-
// `-s` forces a signed tag
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
// expect a signature. Verify BEFORE pushing so an unsigned tag never
|
|
635
|
-
// reaches origin (the v* ruleset blocks tag rewrites, so a bad push would
|
|
636
|
-
// burn the version slot).
|
|
497
|
+
// `-s` forces a signed tag whatever tag.gpgsign is set to; `-a` silently
|
|
498
|
+
// produces an UNSIGNED annotated tag when the config is absent. Verify before
|
|
499
|
+
// pushing — an unsigned tag on origin burns the version slot.
|
|
637
500
|
_run("git", ["tag", "-s", tag, "-m", tag]);
|
|
638
501
|
var verify = _capture("git", ["tag", "-v", tag]);
|
|
639
502
|
if (verify.stderr.indexOf("Good") === -1 && verify.stdout.indexOf("Good") === -1) {
|
|
@@ -653,13 +516,9 @@ function cmdRelease() {
|
|
|
653
516
|
var next = _readJsonVersion("package.json");
|
|
654
517
|
|
|
655
518
|
_section("release workflow");
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
// uniquely identifies this run even when a prior tag points at the same
|
|
660
|
-
// commit (so it's preferable to headSha matching, and needs no extra git
|
|
661
|
-
// call). Bounded retries cover the few-seconds window GitHub needs to
|
|
662
|
-
// register the run after the tag push.
|
|
519
|
+
// Selects the release.yml run created by THIS tag push: gh reports the tag ref
|
|
520
|
+
// as headBranch for tag-triggered runs, and event=="push" excludes
|
|
521
|
+
// workflow_dispatch. The bounded retries cover GitHub registering the run.
|
|
663
522
|
var tag = "v" + next;
|
|
664
523
|
var runId = "";
|
|
665
524
|
for (var _i = 0; _i < 30 && !runId; _i++) {
|
|
@@ -667,20 +526,13 @@ function cmdRelease() {
|
|
|
667
526
|
"--event=push", "--json", "databaseId,headBranch,event",
|
|
668
527
|
"--jq", '[.[] | select(.headBranch=="' + tag + '")] | sort_by(.databaseId) | last | .databaseId']).stdout;
|
|
669
528
|
if (!runId && _i < 29) {
|
|
670
|
-
// Short bounded wait between polls — the run usually appears within
|
|
671
|
-
// a few seconds of the tag push.
|
|
672
529
|
_spawn(process.execPath, ["-e", "setTimeout(function(){},2000)"], { stdio: "ignore" });
|
|
673
530
|
}
|
|
674
531
|
}
|
|
675
532
|
if (runId) {
|
|
676
533
|
_run("gh", ["run", "watch", runId, "--exit-status"], { allowFail: true });
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
// release whose three jobs had all succeeded and whose package was already
|
|
680
|
-
// on npm — a false alarm on a good publish, twice, because an empty stdout
|
|
681
|
-
// (transient API error, or a run still settling) was treated as a verdict.
|
|
682
|
-
// Retry a few times before concluding anything: the value we want is a
|
|
683
|
-
// terminal state, and asking again is cheap next to a wrong answer.
|
|
534
|
+
// "The workflow failed" and "the lookup failed" are different answers: an
|
|
535
|
+
// empty stdout is the second, not a verdict. Retry for a terminal state.
|
|
684
536
|
var concl = "";
|
|
685
537
|
var lookupOk = false;
|
|
686
538
|
for (var _c = 0; _c < 5; _c++) {
|
|
@@ -688,11 +540,8 @@ function cmdRelease() {
|
|
|
688
540
|
"--jq", ".status + \"|\" + (.conclusion // \"\")"]);
|
|
689
541
|
if (rv.status === 0 && rv.stdout) {
|
|
690
542
|
var parts = rv.stdout.split("|");
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
// run can briefly report one too while the value settles — taking
|
|
694
|
-
// either as an answer reproduces the false failed-publish this retry
|
|
695
|
-
// exists to prevent. Keep polling until a real verdict appears.
|
|
543
|
+
// Only a completed run WITH a non-empty conclusion counts: an in-progress
|
|
544
|
+
// run reports an empty one, as does a completed one before it settles.
|
|
696
545
|
if (parts[0] === "completed" && parts[1]) { concl = parts[1]; lookupOk = true; break; }
|
|
697
546
|
}
|
|
698
547
|
if (_c < 4) _spawn(process.execPath, ["-e", "setTimeout(function(){},3000)"], { stdio: "ignore" });
|
|
@@ -716,20 +565,14 @@ function cmdRelease() {
|
|
|
716
565
|
_section("verify npm");
|
|
717
566
|
var npmVersion = _capture("npm", ["view", PKG_NAME, "version"]).stdout;
|
|
718
567
|
console.log("npm " + PKG_NAME + ": " + (npmVersion || "(unable to query)") + " (expected " + next + ")");
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
// verify). An empty stdout (registry/auth/network failure) is treated as a
|
|
722
|
-
// mismatch — an unconfirmable publish is a failure, not a success.
|
|
568
|
+
// Positive confirmation only: an empty stdout is a mismatch, not a pass. The
|
|
569
|
+
// hard failure is asserted at the end of the phase, after the tarball verify.
|
|
723
570
|
if (npmVersion === next) _ok("npm matches " + next);
|
|
724
571
|
|
|
725
572
|
_section("fresh-tarball signature verify");
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
// runs lib/verify.js against the extracted tree. This is the load-bearing
|
|
730
|
-
// post-publish check: a broken artifact/signature here means the release
|
|
731
|
-
// is broken, so it is a HARD gate — _run (no allowFail) throws on failure
|
|
732
|
-
// and the phase exits non-zero rather than reporting a clean release.
|
|
573
|
+
// Verifies the EXACT bytes a downstream consumer installs. A source-tree verify
|
|
574
|
+
// is necessary but not sufficient — a signature can diverge at pack time. HARD
|
|
575
|
+
// gate: _run throws rather than let the phase call the release clean.
|
|
733
576
|
var wrapper = path.join(ROOT, "scripts", "verify-shipped-tarball.js");
|
|
734
577
|
if (fs.existsSync(wrapper)) {
|
|
735
578
|
_run("node", [wrapper]);
|
|
@@ -738,12 +581,8 @@ function cmdRelease() {
|
|
|
738
581
|
throw new Error("release: scripts/verify-shipped-tarball.js missing — cannot verify the shipped artifact");
|
|
739
582
|
}
|
|
740
583
|
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
// so a stalled/failed/unconfirmable publish can't read as a completed
|
|
744
|
-
// release. (A genuinely in-flight publish is caught by the workflow-
|
|
745
|
-
// conclusion check above; by the time we query npm post-watch the version
|
|
746
|
-
// should be live.) The message reports the value actually queried.
|
|
584
|
+
// The workflow has finished by now, so an empty or mismatched version is not
|
|
585
|
+
// propagation lag — it must not read as a completed release.
|
|
747
586
|
if (npmVersion !== next) {
|
|
748
587
|
throw new Error("release: npm shows " + (npmVersion || "(unable to query)") + " but expected " + next +
|
|
749
588
|
" — publish did not complete or could not be confirmed; re-check release.yml before treating the release as done");
|
|
@@ -797,8 +636,6 @@ function cmdHelp() {
|
|
|
797
636
|
console.log("Patch is the default. --minor is a deliberate, explicit choice.");
|
|
798
637
|
}
|
|
799
638
|
|
|
800
|
-
// ---- Dispatch ------------------------------------------------------------
|
|
801
|
-
|
|
802
639
|
var sub = process.argv[2] || "help";
|
|
803
640
|
var opts = {
|
|
804
641
|
minor: process.argv.slice(3).indexOf("--minor") !== -1,
|