@blamejs/exceptd-skills 0.19.33 → 0.19.35
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 +28 -0
- package/bin/exceptd.js +895 -2828
- 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 +170 -76
- package/lib/collectors/cicd-pipeline-compromise.js +113 -136
- 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 +198 -211
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +130 -118
- package/lib/collectors/scan-excludes.js +33 -139
- 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 -155
- 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 +39 -113
- 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 +88 -236
- package/lib/playbook-runner.js +759 -2107
- package/lib/prefetch.js +101 -376
- package/lib/refresh-external.js +199 -633
- package/lib/refresh-network.js +78 -311
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +85 -146
- 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 +28 -27
- package/lib/upstream-check-cli.js +36 -29
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +52 -121
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +78 -286
- 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 -413
- 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 +242 -242
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +29 -28
- 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 +21 -31
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +26 -57
- 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 +63 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +62 -81
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +83 -198
- 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 +7 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +7 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +7 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +63 -148
- package/scripts/release.js +69 -234
- package/scripts/run-e2e-scenarios.js +26 -73
- 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 -141
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,16 @@ var ROOT = path.resolve(__dirname, "..");
|
|
|
49
14
|
var REPO = "blamejs/exceptd-skills";
|
|
50
15
|
var PKG_NAME = "@blamejs/exceptd-skills";
|
|
51
16
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// fresh runners. watch reruns them up to twice before surfacing a failure.
|
|
55
|
-
var RERUN_LIMIT = 2;
|
|
56
|
-
|
|
57
|
-
// ---- Helpers -------------------------------------------------------------
|
|
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.
|
|
17
|
+
// Windows resolves `npm` / `npx` as `.cmd` shims, which child_process can only
|
|
18
|
+
// invoke through a shell; `git`, `gh` and `node` are native exes that spawn directly.
|
|
62
19
|
function _needsShell(cmd) {
|
|
63
20
|
if (process.platform !== "win32") return false;
|
|
64
21
|
return cmd === "npm" || cmd === "npx";
|
|
65
22
|
}
|
|
66
23
|
|
|
67
24
|
// 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.
|
|
25
|
+
// escaping (Node DEP0190 — an injection surface), so the shell path passes the
|
|
26
|
+
// whole invocation as one command string. Only npm with static token args goes there.
|
|
73
27
|
function _spawn(cmd, args, opts) {
|
|
74
28
|
opts = opts || {};
|
|
75
29
|
var useShell = _needsShell(cmd);
|
|
@@ -114,9 +68,8 @@ function _readJsonVersion(file) {
|
|
|
114
68
|
return JSON.parse(fs.readFileSync(path.join(ROOT, file), "utf8")).version;
|
|
115
69
|
}
|
|
116
70
|
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// manifest.json's hand-maintained shape).
|
|
71
|
+
// Rewrites only the "version" line: a full JSON.stringify would reflow
|
|
72
|
+
// manifest.json's hand-maintained key order and formatting.
|
|
120
73
|
function _writeJsonVersion(file, next) {
|
|
121
74
|
var p = path.join(ROOT, file);
|
|
122
75
|
var content = fs.readFileSync(p, "utf8");
|
|
@@ -136,7 +89,6 @@ function _bump(version, kind) {
|
|
|
136
89
|
return parts[0] + "." + parts[1] + "." + (parts[2] + 1);
|
|
137
90
|
}
|
|
138
91
|
|
|
139
|
-
// Topmost `## X.Y.Z` heading in CHANGELOG.md.
|
|
140
92
|
function _changelogTopVersion() {
|
|
141
93
|
var lines = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8").split(/\r?\n/);
|
|
142
94
|
for (var i = 0; i < lines.length; i++) {
|
|
@@ -146,8 +98,6 @@ function _changelogTopVersion() {
|
|
|
146
98
|
return null;
|
|
147
99
|
}
|
|
148
100
|
|
|
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
101
|
function _changelogSection(version) {
|
|
152
102
|
var lines = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8").split(/\r?\n/);
|
|
153
103
|
var out = [];
|
|
@@ -164,10 +114,8 @@ function _changelogSection(version) {
|
|
|
164
114
|
return out.join("\n").trim();
|
|
165
115
|
}
|
|
166
116
|
|
|
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.
|
|
117
|
+
// "vX.Y.Z: <subject>" for the commit and PR title. CHANGELOG entries carry no
|
|
118
|
+
// headline field, so the subject is the first sentence, length-capped.
|
|
171
119
|
function _releaseSubject(version, section) {
|
|
172
120
|
var firstLine = (section.split(/\r?\n/).find(function (l) { return l.trim(); }) || "").trim();
|
|
173
121
|
var firstSentence = firstLine.split(/(?<=[.!?])\s/)[0] || firstLine;
|
|
@@ -182,10 +130,8 @@ function _gitOnMain() { return _gitBranch() === "main"; }
|
|
|
182
130
|
function _gitOnRelease() { return /^release-v\d+\.\d+\.\d+$/.test(_gitBranch()); }
|
|
183
131
|
function _releaseBranchFor(version) { return "release-v" + version; }
|
|
184
132
|
|
|
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.
|
|
133
|
+
// `git verify-commit` is the boolean GitHub's required_signatures ruleset checks;
|
|
134
|
+
// main is under that ruleset, so fail here rather than at push.
|
|
189
135
|
function _verifyCommitSignature(label) {
|
|
190
136
|
var verify = _capture("git", ["verify-commit", "HEAD"]);
|
|
191
137
|
if (verify.status !== 0) {
|
|
@@ -205,9 +151,8 @@ function _openPrNumber(branch) {
|
|
|
205
151
|
"--json", "number", "--jq", ".[0].number"]).stdout;
|
|
206
152
|
}
|
|
207
153
|
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// branch-protection merge block (conversation-resolution required).
|
|
154
|
+
// Conversation resolution is branch-protection-required, so an unresolved
|
|
155
|
+
// thread is a hard merge block.
|
|
211
156
|
function _unresolvedThreads(prNum) {
|
|
212
157
|
var q = 'query { repository(owner:"blamejs",name:"exceptd-skills") { pullRequest(number:' +
|
|
213
158
|
prNum + ') { reviewThreads(first:50) { nodes { isResolved comments(first:1) ' +
|
|
@@ -217,32 +162,11 @@ function _unresolvedThreads(prNum) {
|
|
|
217
162
|
try { return JSON.parse(rv.stdout || "[]"); } catch (_e) { return []; }
|
|
218
163
|
}
|
|
219
164
|
|
|
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.
|
|
165
|
+
// Open CodeQL alerts. Returns the alert array, or null when the query cannot run
|
|
166
|
+
// at all, so a transient API failure fails OPEN rather than blocking a release.
|
|
167
|
+
// The union of two refs is required: refs/pull/<N>/merge misses an alert already
|
|
168
|
+
// sitting on main, and the default-branch query misses one this PR introduces.
|
|
169
|
+
// tool_name=CodeQL excludes Scorecard's accepted-out-of-scope policy alerts.
|
|
246
170
|
function _codeqlAlertsForRef(ref) {
|
|
247
171
|
var args = ["api", "repos/:owner/:repo/code-scanning/alerts", "-X", "GET",
|
|
248
172
|
"-f", "state=open", "-f", "tool_name=CodeQL", "-f", "per_page=100"];
|
|
@@ -258,9 +182,7 @@ function _codeqlAlertsForRef(ref) {
|
|
|
258
182
|
function _openCodeqlAlerts(prNum) {
|
|
259
183
|
var onDefault = _codeqlAlertsForRef(null);
|
|
260
184
|
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.
|
|
185
|
+
// A failed lookup must never read as "no alerts".
|
|
264
186
|
if (onDefault === null || onPr === null) return null;
|
|
265
187
|
|
|
266
188
|
var byNumber = new Map();
|
|
@@ -272,19 +194,11 @@ function _openCodeqlAlerts(prNum) {
|
|
|
272
194
|
return [...byNumber.values()];
|
|
273
195
|
}
|
|
274
196
|
|
|
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.
|
|
197
|
+
// Shared by `prepare` and `regen` so the two cannot drift.
|
|
282
198
|
function _regenArtifacts() {
|
|
283
199
|
_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).
|
|
200
|
+
// sign-all first, since it rewrites the manifest; refresh-sbom LAST, since it
|
|
201
|
+
// hashes the shipped tree (README included) and any later edit strands the hashes.
|
|
288
202
|
_run("node", ["lib/sign.js", "sign-all"]);
|
|
289
203
|
_run("npm", ["run", "build-indexes"]);
|
|
290
204
|
_run("npm", ["run", "refresh-snapshot"]);
|
|
@@ -292,16 +206,12 @@ function _regenArtifacts() {
|
|
|
292
206
|
_ok("signed + indexes + snapshot + sbom regenerated");
|
|
293
207
|
}
|
|
294
208
|
|
|
295
|
-
// Re-
|
|
296
|
-
//
|
|
297
|
-
// this phase exists precisely for a dirty tree. It refuses on main, where the
|
|
298
|
-
// bump belongs to `prepare`.
|
|
209
|
+
// Re-derives artifacts after editing an already-prepared release branch: no
|
|
210
|
+
// bump, no CHANGELOG requirement, and a dirty tree is the point.
|
|
299
211
|
function cmdRegen() {
|
|
300
212
|
_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.
|
|
213
|
+
// A release branch is positively required rather than main merely rejected: a
|
|
214
|
+
// feature branch or detached HEAD would build artifacts out of the wrong tree.
|
|
305
215
|
if (!_gitOnRelease()) {
|
|
306
216
|
throw new Error("release: regen must run on a release-vX.Y.Z branch (on " + _gitBranch() + "). " +
|
|
307
217
|
"On main the regeneration belongs to `prepare`.");
|
|
@@ -319,22 +229,10 @@ function cmdRegen() {
|
|
|
319
229
|
function cmdPrepare(opts) {
|
|
320
230
|
_section("prepare");
|
|
321
231
|
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.
|
|
232
|
+
// The `## <next>` CHANGELOG entry is written by hand before prepare runs, so a
|
|
233
|
+
// CHANGELOG.md-only dirty tree is allowed and anything else is refused.
|
|
234
|
+
// `--with-content` widens that to a release which SHIPS uncommitted work, and
|
|
235
|
+
// prints what it carries so an unintended file is visible rather than released.
|
|
338
236
|
var dirty = _capture("git", ["status", "--porcelain"]).stdout
|
|
339
237
|
.split(/\r?\n/)
|
|
340
238
|
.filter(function (l) { return l.trim() && !/\bCHANGELOG\.md$/.test(l); });
|
|
@@ -352,24 +250,18 @@ function cmdPrepare(opts) {
|
|
|
352
250
|
var next = _bump(current, opts.minor ? "minor" : "patch");
|
|
353
251
|
console.log("current: " + current + " next: " + next + " (" + (opts.minor ? "minor" : "patch") + ")");
|
|
354
252
|
|
|
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.
|
|
253
|
+
// Without the entry, tests/bootstrap-mode's three-version invariant fails at gates.
|
|
358
254
|
var top = _changelogTopVersion();
|
|
359
255
|
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.
|
|
256
|
+
// Throw, never process.exit: the exit can truncate a piped stdout write, and
|
|
257
|
+
// `release all` must abort here. The dispatcher maps it to exit 1.
|
|
364
258
|
throw new Error(
|
|
365
259
|
"CHANGELOG.md top heading is '## " + top + "', expected '## " + next + "'. " +
|
|
366
260
|
"Write the " + next + " entry first (terse, behavior-change framed, no internal " +
|
|
367
261
|
"narrative), then re-run prepare. Example heading: ## " + next + " — <YYYY-MM-DD>");
|
|
368
262
|
}
|
|
369
263
|
|
|
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.
|
|
264
|
+
// The release workflow publishes this section verbatim as the GitHub Release body.
|
|
373
265
|
_run("node", ["scripts/check-changelog-extract.js", next]);
|
|
374
266
|
|
|
375
267
|
_writeJsonVersion("package.json", next);
|
|
@@ -379,22 +271,14 @@ function cmdPrepare(opts) {
|
|
|
379
271
|
_regenArtifacts();
|
|
380
272
|
|
|
381
273
|
_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.
|
|
274
|
+
// Check BEFORE refreshing: `--update-baseline` writes whatever it observes, so
|
|
275
|
+
// refreshing first rebaselines a shrunken suite downward and the shrinkage gate
|
|
276
|
+
// in `gates` then compares the new count against itself.
|
|
390
277
|
_run("node", ["scripts/check-test-count.js"]);
|
|
391
278
|
_run("node", ["scripts/check-test-count.js", "--update-baseline"]);
|
|
392
279
|
|
|
393
280
|
_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.
|
|
281
|
+
// Flags a pattern class the sibling blamejs catalog grew; never blocks.
|
|
398
282
|
_run("node", ["scripts/check-codebase-patterns-currency.js"], { allowFail: true });
|
|
399
283
|
|
|
400
284
|
console.log("\nnext: node scripts/release.js gates");
|
|
@@ -402,9 +286,6 @@ function cmdPrepare(opts) {
|
|
|
402
286
|
|
|
403
287
|
function cmdGates() {
|
|
404
288
|
_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
289
|
_run("npm", ["run", "predeploy"]);
|
|
409
290
|
_ok("predeploy gates passed");
|
|
410
291
|
console.log("\nnext: node scripts/release.js commit");
|
|
@@ -416,8 +297,7 @@ function cmdCommit() {
|
|
|
416
297
|
var branch = _releaseBranchFor(next);
|
|
417
298
|
var current = _gitBranch();
|
|
418
299
|
|
|
419
|
-
// Resumable: a
|
|
420
|
-
// branch in place — switch to it instead of refusing.
|
|
300
|
+
// Resumable: a failed commit leaves the branch in place, so switch to it.
|
|
421
301
|
if (current === branch) {
|
|
422
302
|
_ok("already on " + branch + " (resume mode)");
|
|
423
303
|
} else if (current === "main") {
|
|
@@ -433,8 +313,7 @@ function cmdCommit() {
|
|
|
433
313
|
throw new Error("release: commit must run on main or " + branch + " (on " + current + ")");
|
|
434
314
|
}
|
|
435
315
|
|
|
436
|
-
//
|
|
437
|
-
// just verify the signature.
|
|
316
|
+
// HEAD already carrying this release's commit means verify, not re-commit.
|
|
438
317
|
var headSubject = _capture("git", ["log", "-1", "--pretty=%s"]).stdout;
|
|
439
318
|
if (headSubject.indexOf("v" + next + ":") === 0) {
|
|
440
319
|
_ok("HEAD already carries a v" + next + " commit (resume mode)");
|
|
@@ -443,8 +322,6 @@ function cmdCommit() {
|
|
|
443
322
|
return;
|
|
444
323
|
}
|
|
445
324
|
|
|
446
|
-
// Compose the commit body from the CHANGELOG section — the operator can
|
|
447
|
-
// amend, but the default mirrors the shipped notes.
|
|
448
325
|
var section = _changelogSection(next);
|
|
449
326
|
var subject = _releaseSubject(next, section);
|
|
450
327
|
var bodyPath = path.join(ROOT, ".scratch");
|
|
@@ -487,24 +364,16 @@ function cmdWatch() {
|
|
|
487
364
|
if (!prNum) throw new Error("release: no open PR for " + branch);
|
|
488
365
|
console.log("PR #" + prNum);
|
|
489
366
|
|
|
490
|
-
//
|
|
491
|
-
// run doesn't throw before we get to inspect + rerun it.
|
|
367
|
+
// Blocks until the checks settle; allowFail so failures are inspected below.
|
|
492
368
|
_run("gh", ["pr", "checks", prNum, "--watch"], { allowFail: true });
|
|
493
369
|
|
|
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.
|
|
370
|
+
// Gate on check CONCLUSIONS, not only review threads. Bucket is gh's normalized
|
|
371
|
+
// verdict — pass / fail / pending / skipping / cancel.
|
|
499
372
|
var checksRaw = _capture("gh", ["pr", "checks", prNum, "--json", "name,bucket,link"]).stdout;
|
|
500
373
|
var checks = [];
|
|
501
374
|
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.
|
|
375
|
+
// An empty or still-pending check set is NOT a pass: `gh pr checks --watch`
|
|
376
|
+
// returns immediately while the workflows are still being scheduled.
|
|
508
377
|
if (checks.length === 0) {
|
|
509
378
|
console.log("\nno checks have registered on this PR yet — they are probably still scheduling.");
|
|
510
379
|
console.log("Wait a moment, then re-run: node scripts/release.js watch");
|
|
@@ -526,8 +395,7 @@ function cmdWatch() {
|
|
|
526
395
|
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
396
|
}
|
|
528
397
|
|
|
529
|
-
//
|
|
530
|
-
// alert blocks the release the same way an unresolved codex thread does.
|
|
398
|
+
// An open CodeQL alert blocks the release like an unresolved review thread.
|
|
531
399
|
var codeqlAlerts = _openCodeqlAlerts(prNum);
|
|
532
400
|
if (codeqlAlerts === null) {
|
|
533
401
|
console.log("\nnote: could not query CodeQL alerts (code scanning unavailable / API error) — " +
|
|
@@ -578,15 +446,13 @@ function cmdMerge() {
|
|
|
578
446
|
throw new Error("release: PR #" + prNum + " not mergeable (state=" +
|
|
579
447
|
state.mergeStateStatus + " mergeable=" + state.mergeable + ")");
|
|
580
448
|
}
|
|
581
|
-
//
|
|
582
|
-
// between watch and merge.
|
|
449
|
+
// A reviewer can open a thread between watch and merge.
|
|
583
450
|
var unresolved = _unresolvedThreads(prNum);
|
|
584
451
|
if (unresolved.length > 0) {
|
|
585
452
|
throw new Error("release: refusing to merge PR #" + prNum + " — " +
|
|
586
453
|
unresolved.length + " unresolved review thread(s); run watch again");
|
|
587
454
|
}
|
|
588
|
-
// Solo-maintainer protection requires 0 approvals; --admin
|
|
589
|
-
// remaining required checks gate without a second reviewer.
|
|
455
|
+
// Solo-maintainer protection requires 0 approvals; --admin covers required checks.
|
|
590
456
|
_run("gh", ["pr", "merge", prNum, "--squash", "--admin", "--delete-branch"]);
|
|
591
457
|
_ok("PR #" + prNum + " squash-merged");
|
|
592
458
|
|
|
@@ -601,10 +467,8 @@ function cmdTag() {
|
|
|
601
467
|
var next = _readJsonVersion("package.json");
|
|
602
468
|
var tag = "v" + next;
|
|
603
469
|
|
|
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).
|
|
470
|
+
// GUARD against tag-on-stale-HEAD: a tag on the wrong commit burns a version
|
|
471
|
+
// slot, since the v* ruleset blocks tag rewrites.
|
|
608
472
|
try { fs.rmSync(path.join(ROOT, ".git", "index.lock"), { force: true }); } catch (_e) { /* ignore */ }
|
|
609
473
|
_run("git", ["fetch", "origin", "main"]);
|
|
610
474
|
var local = _capture("git", ["rev-parse", "HEAD"]).stdout;
|
|
@@ -628,12 +492,9 @@ function cmdTag() {
|
|
|
628
492
|
}
|
|
629
493
|
_ok("GUARD passed (HEAD==origin/main, 3-version match, no existing tag)");
|
|
630
494
|
|
|
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).
|
|
495
|
+
// `-s` forces a signed tag whatever tag.gpgsign is set to; `-a` silently
|
|
496
|
+
// produces an UNSIGNED annotated tag when the config is absent. Verify before
|
|
497
|
+
// pushing — an unsigned tag on origin burns the version slot.
|
|
637
498
|
_run("git", ["tag", "-s", tag, "-m", tag]);
|
|
638
499
|
var verify = _capture("git", ["tag", "-v", tag]);
|
|
639
500
|
if (verify.stderr.indexOf("Good") === -1 && verify.stdout.indexOf("Good") === -1) {
|
|
@@ -653,13 +514,9 @@ function cmdRelease() {
|
|
|
653
514
|
var next = _readJsonVersion("package.json");
|
|
654
515
|
|
|
655
516
|
_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.
|
|
517
|
+
// Selects the release.yml run created by THIS tag push: gh reports the tag ref
|
|
518
|
+
// as headBranch for tag-triggered runs, and event=="push" excludes
|
|
519
|
+
// workflow_dispatch. The bounded retries cover GitHub registering the run.
|
|
663
520
|
var tag = "v" + next;
|
|
664
521
|
var runId = "";
|
|
665
522
|
for (var _i = 0; _i < 30 && !runId; _i++) {
|
|
@@ -667,20 +524,13 @@ function cmdRelease() {
|
|
|
667
524
|
"--event=push", "--json", "databaseId,headBranch,event",
|
|
668
525
|
"--jq", '[.[] | select(.headBranch=="' + tag + '")] | sort_by(.databaseId) | last | .databaseId']).stdout;
|
|
669
526
|
if (!runId && _i < 29) {
|
|
670
|
-
// Short bounded wait between polls — the run usually appears within
|
|
671
|
-
// a few seconds of the tag push.
|
|
672
527
|
_spawn(process.execPath, ["-e", "setTimeout(function(){},2000)"], { stdio: "ignore" });
|
|
673
528
|
}
|
|
674
529
|
}
|
|
675
530
|
if (runId) {
|
|
676
531
|
_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.
|
|
532
|
+
// "The workflow failed" and "the lookup failed" are different answers: an
|
|
533
|
+
// empty stdout is the second, not a verdict. Retry for a terminal state.
|
|
684
534
|
var concl = "";
|
|
685
535
|
var lookupOk = false;
|
|
686
536
|
for (var _c = 0; _c < 5; _c++) {
|
|
@@ -688,11 +538,8 @@ function cmdRelease() {
|
|
|
688
538
|
"--jq", ".status + \"|\" + (.conclusion // \"\")"]);
|
|
689
539
|
if (rv.status === 0 && rv.stdout) {
|
|
690
540
|
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.
|
|
541
|
+
// Only a completed run WITH a non-empty conclusion counts: an in-progress
|
|
542
|
+
// run reports an empty one, as does a completed one before it settles.
|
|
696
543
|
if (parts[0] === "completed" && parts[1]) { concl = parts[1]; lookupOk = true; break; }
|
|
697
544
|
}
|
|
698
545
|
if (_c < 4) _spawn(process.execPath, ["-e", "setTimeout(function(){},3000)"], { stdio: "ignore" });
|
|
@@ -716,20 +563,14 @@ function cmdRelease() {
|
|
|
716
563
|
_section("verify npm");
|
|
717
564
|
var npmVersion = _capture("npm", ["view", PKG_NAME, "version"]).stdout;
|
|
718
565
|
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.
|
|
566
|
+
// Positive confirmation only: an empty stdout is a mismatch, not a pass. The
|
|
567
|
+
// hard failure is asserted at the end of the phase, after the tarball verify.
|
|
723
568
|
if (npmVersion === next) _ok("npm matches " + next);
|
|
724
569
|
|
|
725
570
|
_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.
|
|
571
|
+
// Verifies the EXACT bytes a downstream consumer installs. A source-tree verify
|
|
572
|
+
// is necessary but not sufficient — a signature can diverge at pack time. HARD
|
|
573
|
+
// gate: _run throws rather than let the phase call the release clean.
|
|
733
574
|
var wrapper = path.join(ROOT, "scripts", "verify-shipped-tarball.js");
|
|
734
575
|
if (fs.existsSync(wrapper)) {
|
|
735
576
|
_run("node", [wrapper]);
|
|
@@ -738,12 +579,8 @@ function cmdRelease() {
|
|
|
738
579
|
throw new Error("release: scripts/verify-shipped-tarball.js missing — cannot verify the shipped artifact");
|
|
739
580
|
}
|
|
740
581
|
|
|
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.
|
|
582
|
+
// The workflow has finished by now, so an empty or mismatched version is not
|
|
583
|
+
// propagation lag — it must not read as a completed release.
|
|
747
584
|
if (npmVersion !== next) {
|
|
748
585
|
throw new Error("release: npm shows " + (npmVersion || "(unable to query)") + " but expected " + next +
|
|
749
586
|
" — publish did not complete or could not be confirmed; re-check release.yml before treating the release as done");
|
|
@@ -797,8 +634,6 @@ function cmdHelp() {
|
|
|
797
634
|
console.log("Patch is the default. --minor is a deliberate, explicit choice.");
|
|
798
635
|
}
|
|
799
636
|
|
|
800
|
-
// ---- Dispatch ------------------------------------------------------------
|
|
801
|
-
|
|
802
637
|
var sub = process.argv[2] || "help";
|
|
803
638
|
var opts = {
|
|
804
639
|
minor: process.argv.slice(3).indexOf("--minor") !== -1,
|