toolroll 0.9.79 → 0.9.82
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 +47 -0
- package/dist/cli.js +1 -0
- package/dist/commit-extract.d.ts +18 -0
- package/dist/commit-extract.js +39 -1
- package/dist/converse.d.ts +12 -0
- package/dist/converse.js +18 -2
- package/dist/deploy-gate.js +8 -0
- package/dist/eval-gate.d.ts +51 -0
- package/dist/eval-gate.js +199 -0
- package/dist/eval-hooks.d.ts +39 -0
- package/dist/eval-hooks.js +477 -0
- package/dist/eval-oracle-main.d.ts +1 -0
- package/dist/eval-oracle-main.js +31 -0
- package/dist/eval-oracle.d.ts +60 -0
- package/dist/eval-oracle.js +217 -0
- package/dist/eval-world.d.ts +17 -0
- package/dist/eval-world.js +97 -0
- package/dist/evals.d.ts +299 -0
- package/dist/evals.js +964 -0
- package/dist/exec.js +15 -1
- package/dist/flow-draft.js +4 -0
- package/dist/flow-sort.js +4 -0
- package/dist/github-jobs.d.ts +25 -0
- package/dist/github-jobs.js +22 -0
- package/dist/guides.js +12 -9
- package/dist/invoke.d.ts +1 -1
- package/dist/invoke.js +14 -0
- package/dist/lead.js +4 -1
- package/dist/memory-pass.js +4 -0
- package/dist/nightly-check.d.ts +4 -1
- package/dist/nightly-check.js +5 -2
- package/dist/nightly-github.d.ts +316 -0
- package/dist/nightly-github.js +1022 -0
- package/dist/operate.d.ts +91 -1
- package/dist/operate.js +158 -1
- package/dist/project-knowledge.d.ts +4 -4
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +9 -2
- package/dist/release-adapters.d.ts +21 -21
- package/dist/release-adapters.js +53 -20
- package/dist/release-auto.d.ts +1 -1
- package/dist/release-auto.js +26 -0
- package/dist/release-card.js +3 -0
- package/dist/release-cli.js +59 -3
- package/dist/release-view.d.ts +3 -0
- package/dist/release-view.js +2 -0
- package/dist/release.d.ts +36 -17
- package/dist/release.js +74 -24
- package/dist/reported-usage.d.ts +69 -0
- package/dist/reported-usage.js +97 -0
- package/dist/server/chat.js +4 -0
- package/dist/server/render-tasks.js +1 -0
- package/dist/server/tasks.js +8 -1
- package/dist/store.d.ts +109 -1
- package/dist/store.js +394 -4
- package/dist/subagents.js +4 -0
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +52 -13
- package/dist/task-sizing.js +4 -0
- package/dist/teardown-git.d.ts +9 -2
- package/dist/teardown-git.js +56 -33
- package/evals/cases/catalog/case.json +25 -0
- package/evals/cases/catalog/fixture/README.md +3 -0
- package/evals/cases/catalog/fixture/package.json +8 -0
- package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
- package/evals/cases/catalog/oracle.mjs +23 -0
- package/evals/cases/catalog/probe.mjs +10 -0
- package/evals/cases/catalog/source.md +9 -0
- package/evals/cases/csv-quoting/case.json +23 -0
- package/evals/cases/csv-quoting/fixture/README.md +3 -0
- package/evals/cases/csv-quoting/fixture/csv.js +1 -0
- package/evals/cases/csv-quoting/fixture/package.json +8 -0
- package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
- package/evals/cases/csv-quoting/oracle.mjs +14 -0
- package/evals/cases/csv-quoting/probe.mjs +9 -0
- package/evals/cases/csv-quoting/source.md +9 -0
- package/evals/cases/dedupe/case.json +23 -0
- package/evals/cases/dedupe/fixture/README.md +3 -0
- package/evals/cases/dedupe/fixture/dedupe.js +1 -0
- package/evals/cases/dedupe/fixture/package.json +8 -0
- package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
- package/evals/cases/dedupe/oracle.mjs +16 -0
- package/evals/cases/dedupe/probe.mjs +13 -0
- package/evals/cases/dedupe/source.md +9 -0
- package/evals/cases/endpoint/case.json +25 -0
- package/evals/cases/endpoint/fixture/README.md +3 -0
- package/evals/cases/endpoint/fixture/package.json +8 -0
- package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
- package/evals/cases/endpoint/oracle.mjs +27 -0
- package/evals/cases/endpoint/probe.mjs +10 -0
- package/evals/cases/endpoint/source.md +11 -0
- package/evals/cases/math-divide/case.json +23 -0
- package/evals/cases/math-divide/fixture/README.md +3 -0
- package/evals/cases/math-divide/fixture/package.json +8 -0
- package/evals/cases/math-divide/fixture/src/math.js +1 -0
- package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
- package/evals/cases/math-divide/oracle.mjs +13 -0
- package/evals/cases/math-divide/probe.mjs +9 -0
- package/evals/cases/math-divide/source.md +9 -0
- package/evals/cases/math-multiply/case.json +23 -0
- package/evals/cases/math-multiply/fixture/README.md +3 -0
- package/evals/cases/math-multiply/fixture/package.json +8 -0
- package/evals/cases/math-multiply/fixture/src/math.js +2 -0
- package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
- package/evals/cases/math-multiply/oracle.mjs +12 -0
- package/evals/cases/math-multiply/probe.mjs +9 -0
- package/evals/cases/math-multiply/source.md +9 -0
- package/evals/cases/math-subtract/case.json +23 -0
- package/evals/cases/math-subtract/fixture/README.md +3 -0
- package/evals/cases/math-subtract/fixture/package.json +8 -0
- package/evals/cases/math-subtract/fixture/src/math.js +1 -0
- package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
- package/evals/cases/math-subtract/oracle.mjs +12 -0
- package/evals/cases/math-subtract/probe.mjs +9 -0
- package/evals/cases/math-subtract/source.md +9 -0
- package/evals/cases/merge-config/case.json +23 -0
- package/evals/cases/merge-config/fixture/README.md +3 -0
- package/evals/cases/merge-config/fixture/config.js +1 -0
- package/evals/cases/merge-config/fixture/package.json +8 -0
- package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
- package/evals/cases/merge-config/oracle.mjs +28 -0
- package/evals/cases/merge-config/probe.mjs +15 -0
- package/evals/cases/merge-config/source.md +9 -0
- package/evals/cases/paginate/case.json +23 -0
- package/evals/cases/paginate/fixture/README.md +3 -0
- package/evals/cases/paginate/fixture/package.json +8 -0
- package/evals/cases/paginate/fixture/paginate.js +1 -0
- package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
- package/evals/cases/paginate/oracle.mjs +18 -0
- package/evals/cases/paginate/probe.mjs +13 -0
- package/evals/cases/paginate/source.md +9 -0
- package/evals/cases/round2-asks/case.json +24 -0
- package/evals/cases/round2-asks/fixture/README.md +3 -0
- package/evals/cases/round2-asks/fixture/package.json +8 -0
- package/evals/cases/round2-asks/fixture/src/math.js +1 -0
- package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
- package/evals/cases/round2-asks/source.md +11 -0
- package/evals/manifest.json +15 -0
- package/package.json +2 -1
package/dist/release-auto.js
CHANGED
|
@@ -17,6 +17,7 @@ import { fullCheckProblem, projectGitDir } from "./check-report.js";
|
|
|
17
17
|
import { sealedRun } from "./release-recheck.js";
|
|
18
18
|
import { runCheckLevel } from "./check-levels.js";
|
|
19
19
|
import { readVerifiedCommit, verifiedBlobs, verifiedTreeDiff, verifiedTreeEntries } from "./commit-extract.js";
|
|
20
|
+
import { EVAL_HOOK_FILES, evalHookProblem, storeRewriteProblem } from "./eval-hooks.js";
|
|
20
21
|
import { mightName, testInfrastructure } from "./test-infra.js";
|
|
21
22
|
import { chatControlHref } from "./chat-controls.js";
|
|
22
23
|
import { INFRA_RERUNS_PER_CANDIDATE, REQUIRED_PR_CHECKS, REQUIRED_PR_CHECKS_WORKFLOW, prChecksVerdict } from "./release.js";
|
|
@@ -27,6 +28,7 @@ import { autoReleaseApprovalOf, personApprovalOf } from "./scope.js";
|
|
|
27
28
|
import { releaseOperations, revokeRelease } from "./store.js";
|
|
28
29
|
import { RELEASE_CI_HELD_ACTION, RELEASE_CI_PASSED_ACTION, RELEASE_CI_RUNNING_WORDS, releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
29
30
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
31
|
+
import { nightlyDebtHold } from "./nightly-github.js";
|
|
30
32
|
export { RELEASE_APPROVAL_ACTION, releaseApprovalSetting, releaseAutoApproves };
|
|
31
33
|
/** The setting's words, one place. */
|
|
32
34
|
export const RELEASE_APPROVAL_WORDS = { ask: "Ask me", auto: "Approve automatically when checks pass" };
|
|
@@ -177,6 +179,11 @@ export function authorizeAutoCheck(store, input) {
|
|
|
177
179
|
const SRC_TEST_INFRA = /^src\/(fixtures\/|(.*\/)?(test-[^/]*|[^/]*-test-support[^/]*|[^/]*fixtures?[^/]*)\.[cm]?[jt]sx?$)/;
|
|
178
180
|
/** A test file (normalized path): `*.test.ts`, never inside a fixtures folder. */
|
|
179
181
|
const TEST_FILE = /^(?!.*(^|\/)fixtures?\/)[^]*\.test\.ts$/;
|
|
182
|
+
/** The eval set's and GitHub nightly's policy code (docs/plans/evals-nightly.md, part C, finding 10): the cases and
|
|
183
|
+
* oracles, the evaluator, its gate, hooks and world, reviewer scoring, nightly ingestion, their contracts, and the
|
|
184
|
+
* accounting readings evals trust (reported-usage.ts, run-efficiency.ts). Where they are called from, and the store's
|
|
185
|
+
* accounting reads, are held by eval-hooks.ts instead (evalHookProblem). */
|
|
186
|
+
const EVAL_POLICY = /^(evals\/|src\/evals[^/]*\.ts$|src\/eval-[^/]*\.ts$|src\/reviewer-evals\.ts$|src\/nightly-github\.ts$|src\/contracts\/evals[^/]*\.ts$|src\/reported-usage\.ts$|src\/run-efficiency\.ts$)/;
|
|
180
187
|
/** Build or type configuration anywhere: a person approves it. */
|
|
181
188
|
const BUILD_CONFIG = /(^|\/)(tsconfig[^/]*\.json|[^/]*\.config\.[cm]?[jt]s|\.npmrc|\.nvmrc|\.node-version)$/;
|
|
182
189
|
/**
|
|
@@ -201,6 +208,8 @@ export function autoPathProblem(path) {
|
|
|
201
208
|
}
|
|
202
209
|
// Test files under test/ carry the same trust as src's (Alex: test authors are trusted) — only the `.test.ts` files
|
|
203
210
|
// themselves, never a helper, fixture, setup or config file beside them, nor one inside a fixtures folder.
|
|
211
|
+
if (EVAL_POLICY.test(normal))
|
|
212
|
+
return "it's eval or nightly policy code";
|
|
204
213
|
if (testUnderTest)
|
|
205
214
|
return null;
|
|
206
215
|
if (BUILD_CONFIG.test(normal))
|
|
@@ -312,6 +321,19 @@ function changeProblem(repo, base, candidate) {
|
|
|
312
321
|
scans.push(found);
|
|
313
322
|
}
|
|
314
323
|
const infra = new Set(scans.flatMap(one => [...one.infra]));
|
|
324
|
+
// The eval and nightly enforcement calls the base holds must all still be in place at the candidate (eval-hooks.ts).
|
|
325
|
+
// One verified read of every listed file per tree (an unreadable tree reads as every file missing: a person decides).
|
|
326
|
+
const blobAt = (tree) => { const blobs = verifiedBlobs(gitDir, tree, EVAL_HOOK_FILES); return (file) => blobs.ok ? blobs.value.get(file)?.bytes ?? null : null; };
|
|
327
|
+
const hooks = evalHookProblem(blobAt(after.value.tree), blobAt(before.value.tree));
|
|
328
|
+
if (hooks !== null)
|
|
329
|
+
return `${hooks}: a person decides`;
|
|
330
|
+
// No changed file may write over a pinned Store method from outside store.ts (eval-hooks.ts storeRewriteProblem).
|
|
331
|
+
const changed = diff.value.filter(change => change.after !== null).map(change => change.path);
|
|
332
|
+
// A failed read is no reader at all (a person decides), never files that read as absent.
|
|
333
|
+
const changedAt = (tree) => { const blobs = verifiedBlobs(gitDir, tree, changed); return blobs.ok ? (file) => blobs.value.get(file)?.bytes ?? null : null; };
|
|
334
|
+
const rewrites = storeRewriteProblem(changed, changedAt(after.value.tree), changedAt(before.value.tree));
|
|
335
|
+
if (rewrites !== null)
|
|
336
|
+
return `${rewrites}: a person decides`;
|
|
315
337
|
for (const change of diff.value) {
|
|
316
338
|
const modes = [change.before?.mode, change.after?.mode].filter((one) => one !== undefined);
|
|
317
339
|
if (modes.includes("120000") || modes.includes("160000"))
|
|
@@ -461,6 +483,10 @@ export function sealAutoGreen(store, input) {
|
|
|
461
483
|
if (changed !== null)
|
|
462
484
|
return refuse(changed);
|
|
463
485
|
// GitHub CI, as the plane's own gh saw it moments ago, on exactly this commit.
|
|
486
|
+
// GitHub's nightly lane debt (evals-nightly.md, part A2): dormant while automatic approval is refused, held here too.
|
|
487
|
+
const nightly = nightlyDebtHold(store, { repo: facts.repo, taskId: input.taskId, candidate: input.candidate, phase: "auto", release: true, now: input.now });
|
|
488
|
+
if (nightly !== null)
|
|
489
|
+
return refuse(nightly.why);
|
|
464
490
|
const ci = ciVerdict(input.ci, input.candidate, input.now);
|
|
465
491
|
if (!ci.ok)
|
|
466
492
|
return refuse(`CI: ${ci.problem}`);
|
package/dist/release-card.js
CHANGED
|
@@ -27,6 +27,7 @@ import { routeFromJson } from "./phase-routing.js";
|
|
|
27
27
|
import { projectName } from "./project.js";
|
|
28
28
|
import { RELEASE_CI_RUNNING_WORDS, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
29
29
|
import { RELEASE_PATH, releaseConfigProblem, releaseGitEnv, releaseRemote, releaseTools } from "./release-git.js";
|
|
30
|
+
import { nightlyReleaseLines } from "./nightly-github.js";
|
|
30
31
|
const NONE = "none";
|
|
31
32
|
/** A budget in dollars from integer micro-dollars, in integer arithmetic: cents when it is whole cents, else every digit. */
|
|
32
33
|
const money = (micro) => {
|
|
@@ -258,6 +259,8 @@ export function releaseCardOf(store, scope, formDigest, words) {
|
|
|
258
259
|
...(terms !== null && terms.policy.carry && terms.access !== "sandboxed" ? ["This project's checks don't run in the sandbox: later test-only commits will run outside the sandbox without being shown to you first."] : []),
|
|
259
260
|
// Filed while CI runs (Alex, 2026-10-29): a person may approve now, and is told plainly what still waits.
|
|
260
261
|
...(releaseCiRunningOf(store, scope.taskId) === null ? [] : [RELEASE_CI_RUNNING_WORDS]),
|
|
262
|
+
// GitHub's nightly (nightly-github.ts): what holds this release, or why it can't be read. Not a signed term.
|
|
263
|
+
...(terms === null ? [] : nightlyReleaseLines(store, { repo: terms.repo, taskId: scope.taskId, candidate: scope.candidate ?? null, now: new Date() })),
|
|
261
264
|
"",
|
|
262
265
|
...changeWords(changes, terms, 5),
|
|
263
266
|
"",
|
package/dist/release-cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxed
|
|
|
21
21
|
import { authenticateApprover, machineActing, personProof, proveCredential } from "./scope.js";
|
|
22
22
|
import { allowAutoPublish, setReleaseApproval } from "./release-auto.js";
|
|
23
23
|
import { createNightlyCheckFlow } from "./nightly-check.js";
|
|
24
|
+
import { aliasNightlyLane, optInGithubNightly, retireNightlyLane } from "./nightly-github.js";
|
|
24
25
|
import { parseSchedule } from "./flow-schedule.js";
|
|
25
26
|
export const RELEASE_HELP = `toolroll release <branch> — release Toolroll itself, from a pushed branch with an open pull request
|
|
26
27
|
|
|
@@ -58,6 +59,12 @@ toolroll release nightly --repo <path> --as <you> --token <password> [--schedule
|
|
|
58
59
|
— the full release check of main's head every night, approved once with your password; it never deploys,
|
|
59
60
|
merges or completes, and a failure tells you and holds composed releases until a full check passes
|
|
60
61
|
|
|
62
|
+
toolroll release nightly --github --repo <path> --as <you> --token <password> — read GitHub's Nightly runs on main
|
|
63
|
+
for this project (its recorded GitHub remote): a lane that fails its tests holds every deploy of the project, a
|
|
64
|
+
release's merge and its tag until a later nightly passes it, or the release's own CI re-ran it after the failure and
|
|
65
|
+
passed. --lane-alias <lane-id> <new name> says a renamed job is the same lane; --lane-retire <lane-id> retires a
|
|
66
|
+
lane no required check names any more (both with your password)
|
|
67
|
+
|
|
61
68
|
toolroll release ask-every-commit on|off --repo <path> [--as <you> --token <password>] — this project's releases ask
|
|
62
69
|
for every later commit (on), or carry test-only commits again (off: your password, never the lead's token)
|
|
63
70
|
|
|
@@ -91,8 +98,8 @@ A runner's lease is returned with \`toolroll worker release <lease>\`.`;
|
|
|
91
98
|
/** A runner lease id (claim.ts mints randomUUID()). */
|
|
92
99
|
export const LEASE_SHAPED = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
93
100
|
export const LEASE_DEPRECATION = "`toolroll release <lease>` is now `toolroll worker release <lease>`; the old form goes in the next minor version.";
|
|
94
|
-
const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage"]);
|
|
95
|
-
const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start", "--recover-deploy"]);
|
|
101
|
+
const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage", "--lane-retire"]);
|
|
102
|
+
const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start", "--recover-deploy", "--github"]);
|
|
96
103
|
export async function runReleaseCommand(args, write, deps = {}) {
|
|
97
104
|
// The flag, not a secret's value that happens to read "--json".
|
|
98
105
|
// A secret is never exactly one of these names (the loop below refuses that as a missing value).
|
|
@@ -104,12 +111,21 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
104
111
|
};
|
|
105
112
|
const positional = [];
|
|
106
113
|
const limits = {};
|
|
107
|
-
let stage;
|
|
114
|
+
let stage, laneRetire, laneAlias;
|
|
108
115
|
let repo, tap, gate, to, request, as, token, scheduleFlag;
|
|
109
116
|
for (let i = 0; i < args.length; i += 1) {
|
|
110
117
|
const raw = args[i];
|
|
111
118
|
if (BARE_FLAGS.has(raw))
|
|
112
119
|
continue;
|
|
120
|
+
// `--lane-alias <lane-id> <new name>`: two values, neither a flag of this command.
|
|
121
|
+
if (raw === "--lane-alias") {
|
|
122
|
+
const [id, name] = [args[i + 1], args[i + 2]];
|
|
123
|
+
if (id === undefined || name === undefined || id.startsWith("--") || name.startsWith("--"))
|
|
124
|
+
return refuse("usage", "--lane-alias takes <lane-id> <new name>.", 2);
|
|
125
|
+
laneAlias = [id, name];
|
|
126
|
+
i += 2;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
113
129
|
// `--flag=value` for every value flag; a secret (--token) is also the next argument even when it starts with "--".
|
|
114
130
|
const equals = raw.startsWith("--") ? raw.indexOf("=") : -1;
|
|
115
131
|
const arg = equals === -1 ? raw : raw.slice(0, equals);
|
|
@@ -139,6 +155,8 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
139
155
|
gate = value;
|
|
140
156
|
else if (arg === "--stage")
|
|
141
157
|
stage = value;
|
|
158
|
+
else if (arg === "--lane-retire")
|
|
159
|
+
laneRetire = value;
|
|
142
160
|
else {
|
|
143
161
|
const match = /^([a-z]+)=([0-9]+)$/.exec(value);
|
|
144
162
|
if (match === null || !(match[1] in RELEASE_LIMITS) || Number(match[2]) < 1)
|
|
@@ -169,6 +187,13 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
169
187
|
return refuse("usage", "--stage goes with --recover-deploy.", 2);
|
|
170
188
|
if (bare.has("--yes"))
|
|
171
189
|
return refuse("usage", "--yes goes with --release-gate <id> or --recover-deploy; a release needs no confirmation beyond the owner's approval.", 2);
|
|
190
|
+
if (bare.has("--github") || laneAlias !== undefined || laneRetire !== undefined) {
|
|
191
|
+
if (positional[0] !== "nightly" || positional.length !== 1)
|
|
192
|
+
return refuse("usage", "--github, --lane-alias and --lane-retire go with `toolroll release nightly`.", 2);
|
|
193
|
+
if ([bare.has("--github"), laneAlias !== undefined, laneRetire !== undefined].filter(Boolean).length !== 1 || scheduleFlag !== undefined || bare.has("--start"))
|
|
194
|
+
return refuse("usage", "Use one of --github, --lane-alias or --lane-retire, without --schedule or --start.", 2);
|
|
195
|
+
return runGithubNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, alias: laneAlias, retire: laneRetire }, write, json, deps);
|
|
196
|
+
}
|
|
172
197
|
if (positional[0] === "nightly" && positional.length === 1)
|
|
173
198
|
return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start: bare.has("--start") }, write, json, deps);
|
|
174
199
|
if (["carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"].includes(positional[0] ?? "") && positional.length === 2) {
|
|
@@ -463,3 +488,34 @@ async function runNightlyCommand(flags, write, json, deps) {
|
|
|
463
488
|
store.close();
|
|
464
489
|
}
|
|
465
490
|
}
|
|
491
|
+
/** `toolroll release nightly --github | --lane-alias | --lane-retire`: GitHub's nightly as release debt
|
|
492
|
+
* (nightly-github.ts), each a person's step proved with their password now. */
|
|
493
|
+
async function runGithubNightlyCommand(flags, write, json, deps) {
|
|
494
|
+
const answer = (ok, fields, words, code) => { write(json ? checkedEnvelopeJson({ ok, command: "release nightly", ...fields }) : words); return code; };
|
|
495
|
+
if (flags.repo === undefined)
|
|
496
|
+
return answer(false, { reason: "usage", message: "Name the project with --repo <path>." }, "Name the project with --repo <path>.", 2);
|
|
497
|
+
if (flags.as === undefined || flags.token === undefined)
|
|
498
|
+
return answer(false, { reason: "usage", message: "This needs your password: add --as <you> --token <password>." }, "This needs your password: add --as <you> --token <password>.", 2);
|
|
499
|
+
if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined || process.env["TOOLROLL_REMOTE_TOKEN"] !== undefined)
|
|
500
|
+
return answer(false, { reason: "person-only", message: "Only a person changes how GitHub's nightly holds releases." }, "Only a person changes how GitHub's nightly holds releases.", 1);
|
|
501
|
+
const store = openStore(deps.database ?? databasePath(process.env, homedir()));
|
|
502
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
503
|
+
try {
|
|
504
|
+
if (flags.alias !== undefined) {
|
|
505
|
+
const [laneId, name] = flags.alias;
|
|
506
|
+
const done = aliasNightlyLane(store, { repo: flags.repo, laneId, name, as: flags.as, token: flags.token }, now);
|
|
507
|
+
return done.ok ? answer(true, { lane: laneId, name }, `Jobs named "${name}" now count for lane ${laneId}.`, 0) : answer(false, { reason: "refused", message: done.message }, done.message, 1);
|
|
508
|
+
}
|
|
509
|
+
if (flags.retire !== undefined) {
|
|
510
|
+
const done = retireNightlyLane(store, { repo: flags.repo, laneId: flags.retire, as: flags.as, token: flags.token }, now);
|
|
511
|
+
return done.ok ? answer(true, { lane: flags.retire, closed: done.closed }, `Retired lane ${flags.retire}${done.closed > 0 ? `; ${done.closed} open failure${done.closed === 1 ? "" : "s"} closed` : ""}.`, 0)
|
|
512
|
+
: answer(false, { reason: "refused", message: done.message }, done.message, 1);
|
|
513
|
+
}
|
|
514
|
+
const made = optInGithubNightly(store, { repo: flags.repo, as: flags.as, token: flags.token }, now);
|
|
515
|
+
return made.ok ? answer(true, { github: made.slug, lanes: made.lanes }, `GitHub's nightly for ${made.slug} now holds this project's deploys, release merges and tags while a lane fails (${made.lanes} lanes).`, 0)
|
|
516
|
+
: answer(false, { reason: "refused", message: made.message }, made.message, 1);
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
store.close();
|
|
520
|
+
}
|
|
521
|
+
}
|
package/dist/release-view.d.ts
CHANGED
|
@@ -30,5 +30,8 @@ export type ReleaseView = {
|
|
|
30
30
|
outcome: string | null;
|
|
31
31
|
} | null;
|
|
32
32
|
openMainFailures: number;
|
|
33
|
+
/** GitHub's nightly (nightly-github.ts): each open lane failure, held or exempted for this release's tip, and a
|
|
34
|
+
* warning when it couldn't be read or hasn't run lately. */
|
|
35
|
+
githubNightly: string[];
|
|
33
36
|
};
|
|
34
37
|
export declare function releaseViewOf(store: Store, taskId: string, now: Date, obligations?: (authority: number) => string[]): ReleaseView | null;
|
package/dist/release-view.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* main; for a carried task, whose release approval it was carried from. Read-only, from the store.
|
|
5
5
|
*/
|
|
6
6
|
import { liveReleaseOf } from "./scope.js";
|
|
7
|
+
import { nightlyReleaseLines } from "./nightly-github.js";
|
|
7
8
|
import { releaseAuthorityOfTask, releaseCarries } from "./store.js";
|
|
8
9
|
export function releaseViewOf(store, taskId, now, obligations = () => []) {
|
|
9
10
|
const found = releaseAuthorityOfTask(store, taskId);
|
|
@@ -23,5 +24,6 @@ export function releaseViewOf(store, taskId, now, obligations = () => []) {
|
|
|
23
24
|
obligations: obligations(a.id),
|
|
24
25
|
nightly: nightly === undefined ? null : { at: String(nightly["at"]), sha: String(nightly["sha"]), outcome: nightly["outcome"] == null ? null : String(nightly["outcome"]) },
|
|
25
26
|
openMainFailures: open,
|
|
27
|
+
githubNightly: nightlyReleaseLines(store, { repo: a.repo, taskId: a.tipTask, candidate: a.tipSha, now }),
|
|
26
28
|
};
|
|
27
29
|
}
|
package/dist/release.d.ts
CHANGED
|
@@ -32,16 +32,18 @@
|
|
|
32
32
|
* Everything outside this module is an adapter (release-adapters.ts), so the state machine is tested without git,
|
|
33
33
|
* GitHub, npm or a running Toolroll.
|
|
34
34
|
*/
|
|
35
|
-
/** The pull-request checks a release waits for, by exact job name (
|
|
36
|
-
* concluded `success` on the gated commit; src/ci-workflow.test.ts holds the
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
/** The pull-request checks a release waits for, by exact job name (docs/plans/test-matrix.md). Every one must have
|
|
36
|
+
* concluded `success` — executed, never skipped — on the gated commit; src/ci-workflow.test.ts holds the workflows to
|
|
37
|
+
* this list, and publish.yml's release proof to the same list. */
|
|
38
|
+
export declare const REQUIRED_PR_CHECKS: readonly ["node 24 · ubuntu-latest", "linux native containment · delegated cgroup v2", "linux fence · sandbox-runtime", "Windows Job Object containment", "git 2.41 · debian trixie", "git 2.47 · debian trixie", "node 24 · macos-latest", "node 22 · ubuntu-latest", "node 22 · macos-latest"];
|
|
39
|
+
/** The workflow file each REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
|
|
39
40
|
* added beside it) never stands for it (Codex review round 1, inherited CI limitation). */
|
|
40
|
-
export declare const REQUIRED_PR_CHECKS_WORKFLOW
|
|
41
|
+
export declare const REQUIRED_PR_CHECKS_WORKFLOW: Readonly<Record<typeof REQUIRED_PR_CHECKS[number], string>>;
|
|
41
42
|
/**
|
|
42
|
-
* The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1)
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1).
|
|
44
|
+
* Empty since the test matrix (docs/plans/test-matrix.md): publication's release proof trusts what main's protection
|
|
45
|
+
* required at the merge, so main's protection must name every REQUIRED_PR_CHECKS check, and a release refuses to merge
|
|
46
|
+
* (and so to publish) until it does.
|
|
45
47
|
*/
|
|
46
48
|
export declare const PROTECTION_REPORT_ONLY_CHECKS: readonly string[];
|
|
47
49
|
/** The checks main's protection must require at minimum: every required check that isn't report-only. */
|
|
@@ -57,10 +59,11 @@ export type MainProtection = {
|
|
|
57
59
|
missing: string[];
|
|
58
60
|
notYetRequired: string[];
|
|
59
61
|
};
|
|
60
|
-
/** The checks a release's publication waits for on its tag (.github/workflows/publish.yml,
|
|
61
|
-
* the
|
|
62
|
-
*
|
|
63
|
-
|
|
62
|
+
/** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
|
|
63
|
+
* the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
|
|
64
|
+
* the publish job that needs it. The tag runs no tests again. Nothing else reported on the merge commit (main-push.yml)
|
|
65
|
+
* holds publication. */
|
|
66
|
+
export declare const RELEASE_TAG_CHECKS: readonly ["release proof", "publish"];
|
|
64
67
|
export declare const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
|
|
65
68
|
export declare const RELEASE_STEPS: readonly ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
|
66
69
|
export type ReleaseStep = typeof RELEASE_STEPS[number];
|
|
@@ -304,6 +307,22 @@ export type ReleaseAuthorityAdapter = {
|
|
|
304
307
|
/** Releases that approve themselves when green (docs/plans/auto-approve-green.md): the plane's own seals, each proven
|
|
305
308
|
* again inside the plane. Absent: a plane that can't, and every release asks a person as before. */
|
|
306
309
|
auto?: AutoReleaseAdapter;
|
|
310
|
+
/** GitHub's nightly lane debt (nightly-github.ts, docs/plans/evals-nightly.md part A) right before a deploy, the merge
|
|
311
|
+
* and the tag: one bounded read of GitHub (20 s), this candidate's exemptions read again, then the plane's stored
|
|
312
|
+
* debt. A warning never holds; a hold stops the release. Absent: a plane that can't read it. */
|
|
313
|
+
nightly?(input: {
|
|
314
|
+
taskId: string;
|
|
315
|
+
candidate: string;
|
|
316
|
+
pr: number;
|
|
317
|
+
phase: "deploy" | "merge" | "tag";
|
|
318
|
+
}): Promise<{
|
|
319
|
+
ok: true;
|
|
320
|
+
warning: string | null;
|
|
321
|
+
} | {
|
|
322
|
+
ok: false;
|
|
323
|
+
why: string;
|
|
324
|
+
warning: string | null;
|
|
325
|
+
}>;
|
|
307
326
|
};
|
|
308
327
|
/** The plane's automatic release approval, as the coordinator asks it. Every answer is the plane's own: the coordinator
|
|
309
328
|
* hands it ids, never evidence (the plane reads GitHub CI itself for `approve`). */
|
|
@@ -662,10 +681,10 @@ export declare function checksVerdict(runs: readonly CheckRun[], required: reado
|
|
|
662
681
|
failed: string[];
|
|
663
682
|
};
|
|
664
683
|
/**
|
|
665
|
-
* Publication's verdict on the tag commit (
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
684
|
+
* Publication's verdict on the tag commit (test-matrix.md): only RELEASE_TAG_CHECKS, from GitHub Actions runs of
|
|
685
|
+
* publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's own
|
|
686
|
+
* push run — is not waited for: the candidate's checks already passed on its exact head before the merge, the merged
|
|
687
|
+
* tree was proved equal to it, and publish.yml's release proof checks both again from GitHub's record.
|
|
669
688
|
*/
|
|
670
689
|
export declare function tagChecksVerdict(runs: readonly CheckRun[]): ReturnType<typeof checksVerdict>;
|
|
671
690
|
/** Run the release from wherever its journal stopped. */
|
|
@@ -686,7 +705,7 @@ export declare function transientNetworkError(error: unknown): boolean;
|
|
|
686
705
|
*/
|
|
687
706
|
export declare function prChecksVerdict(runs: readonly CheckRun[], head: string, required: readonly string[], options?: {
|
|
688
707
|
auto?: true;
|
|
689
|
-
workflow?: string
|
|
708
|
+
workflow?: string | Readonly<Record<string, string>>;
|
|
690
709
|
}): {
|
|
691
710
|
state: "passed";
|
|
692
711
|
failures: CheckFailure[];
|
package/dist/release.js
CHANGED
|
@@ -38,37 +38,54 @@ import { dirname, join } from "node:path";
|
|
|
38
38
|
import { personProof } from "./person-proof.js";
|
|
39
39
|
import { RELEASE_CI_RUNNING_WORDS as CI_RUNNING_WORDS } from "./release-ci-hold.js";
|
|
40
40
|
import { transientFailure } from "./release-net.js";
|
|
41
|
-
/** The pull-request checks a release waits for, by exact job name (
|
|
42
|
-
* concluded `success` on the gated commit; src/ci-workflow.test.ts holds the
|
|
41
|
+
/** The pull-request checks a release waits for, by exact job name (docs/plans/test-matrix.md). Every one must have
|
|
42
|
+
* concluded `success` — executed, never skipped — on the gated commit; src/ci-workflow.test.ts holds the workflows to
|
|
43
|
+
* this list, and publish.yml's release proof to the same list. */
|
|
43
44
|
export const REQUIRED_PR_CHECKS = [
|
|
44
|
-
|
|
45
|
+
// The unit suite on Node 24 (ci.yml "test-shard" and "test"), on every pull request.
|
|
45
46
|
"node 24 · ubuntu-latest",
|
|
47
|
+
// The area lanes (ci.yml), on every pull request except a documentation-only one (scripts/ci-scope.mjs), where they
|
|
48
|
+
// are skipped; a release's pull request runs them.
|
|
46
49
|
"linux native containment · delegated cgroup v2",
|
|
47
50
|
"linux fence · sandbox-runtime",
|
|
48
51
|
"Windows Job Object containment",
|
|
49
52
|
// The git lanes (ci.yml job "git"): the declared floor (git-floor.ts) and Debian trixie's 2.47.
|
|
50
53
|
"git 2.41 · debian trixie",
|
|
51
54
|
"git 2.47 · debian trixie",
|
|
52
|
-
// The unit suite on macOS (ci.yml jobs "macos-shard" and "macos"; pipeline-speed.md, item 3a)
|
|
53
|
-
// before it merges, no longer only on the tag after.
|
|
54
|
-
"node 22 · macos-latest",
|
|
55
|
+
// The unit suite on macOS (ci.yml jobs "macos-shard" and "macos"; pipeline-speed.md, item 3a), Node 24.
|
|
55
56
|
"node 24 · macos-latest",
|
|
57
|
+
// Node 22, the engines floor, on Linux and macOS (release-candidate.yml): on a release-* branch's pull request,
|
|
58
|
+
// skipped on every other pull request.
|
|
59
|
+
"node 22 · ubuntu-latest",
|
|
60
|
+
"node 22 · macos-latest",
|
|
56
61
|
];
|
|
57
|
-
/** The workflow file
|
|
62
|
+
/** The workflow file each REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
|
|
58
63
|
* added beside it) never stands for it (Codex review round 1, inherited CI limitation). */
|
|
59
|
-
export const REQUIRED_PR_CHECKS_WORKFLOW =
|
|
64
|
+
export const REQUIRED_PR_CHECKS_WORKFLOW = {
|
|
65
|
+
"node 24 · ubuntu-latest": ".github/workflows/ci.yml",
|
|
66
|
+
"linux native containment · delegated cgroup v2": ".github/workflows/ci.yml",
|
|
67
|
+
"linux fence · sandbox-runtime": ".github/workflows/ci.yml",
|
|
68
|
+
"Windows Job Object containment": ".github/workflows/ci.yml",
|
|
69
|
+
"git 2.41 · debian trixie": ".github/workflows/ci.yml",
|
|
70
|
+
"git 2.47 · debian trixie": ".github/workflows/ci.yml",
|
|
71
|
+
"node 24 · macos-latest": ".github/workflows/ci.yml",
|
|
72
|
+
"node 22 · ubuntu-latest": ".github/workflows/release-candidate.yml",
|
|
73
|
+
"node 22 · macos-latest": ".github/workflows/release-candidate.yml",
|
|
74
|
+
};
|
|
60
75
|
/**
|
|
61
|
-
* The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1)
|
|
62
|
-
*
|
|
63
|
-
*
|
|
76
|
+
* The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1).
|
|
77
|
+
* Empty since the test matrix (docs/plans/test-matrix.md): publication's release proof trusts what main's protection
|
|
78
|
+
* required at the merge, so main's protection must name every REQUIRED_PR_CHECKS check, and a release refuses to merge
|
|
79
|
+
* (and so to publish) until it does.
|
|
64
80
|
*/
|
|
65
|
-
export const PROTECTION_REPORT_ONLY_CHECKS = [
|
|
81
|
+
export const PROTECTION_REPORT_ONLY_CHECKS = [];
|
|
66
82
|
/** The checks main's protection must require at minimum: every required check that isn't report-only. */
|
|
67
83
|
export const PROTECTION_MINIMUM_CHECKS = REQUIRED_PR_CHECKS.filter(name => !PROTECTION_REPORT_ONLY_CHECKS.includes(name));
|
|
68
|
-
/** The checks a release's publication waits for on its tag (.github/workflows/publish.yml,
|
|
69
|
-
* the
|
|
70
|
-
*
|
|
71
|
-
|
|
84
|
+
/** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
|
|
85
|
+
* the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
|
|
86
|
+
* the publish job that needs it. The tag runs no tests again. Nothing else reported on the merge commit (main-push.yml)
|
|
87
|
+
* holds publication. */
|
|
88
|
+
export const RELEASE_TAG_CHECKS = ["release proof", "publish"];
|
|
72
89
|
export const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
|
|
73
90
|
export const RELEASE_STEPS = ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
|
74
91
|
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
|
|
@@ -192,10 +209,10 @@ export function checksVerdict(runs, required) {
|
|
|
192
209
|
return waiting.length > 0 ? { state: "pending", waiting } : { state: "passed" };
|
|
193
210
|
}
|
|
194
211
|
/**
|
|
195
|
-
* Publication's verdict on the tag commit (
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
212
|
+
* Publication's verdict on the tag commit (test-matrix.md): only RELEASE_TAG_CHECKS, from GitHub Actions runs of
|
|
213
|
+
* publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's own
|
|
214
|
+
* push run — is not waited for: the candidate's checks already passed on its exact head before the merge, the merged
|
|
215
|
+
* tree was proved equal to it, and publish.yml's release proof checks both again from GitHub's record.
|
|
199
216
|
*/
|
|
200
217
|
export function tagChecksVerdict(runs) {
|
|
201
218
|
const names = RELEASE_TAG_CHECKS;
|
|
@@ -553,6 +570,16 @@ export async function runRelease(options, adapters) {
|
|
|
553
570
|
if (held !== null)
|
|
554
571
|
throw new Stop("approval", `${held}. It was merged; nothing more is tagged, published or sent to Homebrew until a person lets it go on: \`toolroll release publish-ok ${j.task.id} --as <you> --token <password>\`, then rerun.`);
|
|
555
572
|
};
|
|
573
|
+
/** GitHub's nightly lane debt before a deploy, the merge or the tag (evals-nightly.md, part A2). */
|
|
574
|
+
const nightlyHolds = async (phase) => {
|
|
575
|
+
if (rel?.nightly === undefined)
|
|
576
|
+
return;
|
|
577
|
+
const answer = await rel.nightly({ taskId: j.task.id, candidate: j.candidate.sha, pr: j.pr, phase });
|
|
578
|
+
if (answer.warning !== null)
|
|
579
|
+
say(` ${answer.warning}`);
|
|
580
|
+
if (!answer.ok)
|
|
581
|
+
throw new Stop("nightly-debt", `${answer.why} Nothing was ${phase === "deploy" ? "deployed" : phase === "merge" ? "merged" : "tagged or published"}; rerun the release once it passes.`);
|
|
582
|
+
};
|
|
556
583
|
/** identity(), on a plane that keeps release authority; a legacy plane keeps the old rules. */
|
|
557
584
|
const watch = async (action) => { if (rel !== undefined)
|
|
558
585
|
await identity(action, true); };
|
|
@@ -983,6 +1010,7 @@ export async function runRelease(options, adapters) {
|
|
|
983
1010
|
await watch("deploy");
|
|
984
1011
|
// Every deploy reservation, whatever the journal saved: CI must still be green on the exact commit.
|
|
985
1012
|
await autoCiHolds("the deploy");
|
|
1013
|
+
await nightlyHolds("deploy");
|
|
986
1014
|
const reserved = await rel.reserve({ authority: j.authority.id, kind: "deploy", taskId: j.task.id, candidate: j.candidate.sha, epoch });
|
|
987
1015
|
if (!reserved.ok)
|
|
988
1016
|
throw new Stop("release", `The deploy couldn't be reserved: ${reserved.why}`);
|
|
@@ -1000,6 +1028,7 @@ export async function runRelease(options, adapters) {
|
|
|
1000
1028
|
await pinned();
|
|
1001
1029
|
// Every deploy issued, whatever the journal saved (a retry after a rollback included): CI still green.
|
|
1002
1030
|
await autoCiHolds("the deploy");
|
|
1031
|
+
await nightlyHolds("deploy");
|
|
1003
1032
|
let operation, secret = "";
|
|
1004
1033
|
if (rel !== undefined && j.authority && epoch !== null) {
|
|
1005
1034
|
const reserved = await rel.reserve({ authority: j.authority.id, kind: "deploy", taskId: j.task.id, candidate: j.candidate.sha, epoch });
|
|
@@ -1093,6 +1122,9 @@ export async function runRelease(options, adapters) {
|
|
|
1093
1122
|
const verdict = prChecksVerdict(await adapters.github.checks(j.github, j.candidate.sha), j.candidate.sha, REQUIRED_PR_CHECKS, { ...autoCap(), workflow: REQUIRED_PR_CHECKS_WORKFLOW });
|
|
1094
1123
|
if (verdict.state !== "passed")
|
|
1095
1124
|
await ciFailedAfterDeploy(`Pull request checks are no longer all passing on ${j.candidate.sha.slice(0, 7)}`);
|
|
1125
|
+
// GitHub's nightly lane debt (a bounded read), before main's last read so it doesn't widen that window; the
|
|
1126
|
+
// reservation below checks the stored debt again in its own transaction.
|
|
1127
|
+
await nightlyHolds("merge");
|
|
1096
1128
|
// Main once more, from GitHub, the last thing before the merge is reserved: moved since the comparison, it
|
|
1097
1129
|
// stops here (finding 4). GitHub's own up-to-date rule refuses the rest of the window.
|
|
1098
1130
|
const now = await adapters.github.mainHead(j.github);
|
|
@@ -1149,6 +1181,7 @@ export async function runRelease(options, adapters) {
|
|
|
1149
1181
|
throw new Stop("conflict", `${j.tag} already points at ${at.slice(0, 7)}, not the merge ${merge.sha.slice(0, 7)}.`);
|
|
1150
1182
|
if (at === null) {
|
|
1151
1183
|
await autoCiHolds("the tag");
|
|
1184
|
+
await nightlyHolds("tag");
|
|
1152
1185
|
intend("tag");
|
|
1153
1186
|
await adapters.github.createTag(j.github, j.tag, merge.sha);
|
|
1154
1187
|
}
|
|
@@ -1422,9 +1455,12 @@ export function prChecksVerdict(runs, head, required, options = {}) {
|
|
|
1422
1455
|
}
|
|
1423
1456
|
// A required name counts only from the workflow that defines it: a job of the same name in another workflow (or
|
|
1424
1457
|
// one whose workflow GitHub doesn't name) fails the verdict rather than stand in for it.
|
|
1425
|
-
if (options.workflow !== undefined && required.includes(run.name)
|
|
1426
|
-
|
|
1427
|
-
|
|
1458
|
+
if (options.workflow !== undefined && required.includes(run.name)) {
|
|
1459
|
+
const want = typeof options.workflow === "string" ? options.workflow : options.workflow[run.name];
|
|
1460
|
+
if (want === undefined || !workflowIs(run.workflowPath, want)) {
|
|
1461
|
+
failed.push(`${run.name} (from ${run.workflowPath ?? "a workflow GitHub doesn't name"}, not ${want ?? "a workflow this release knows for it"})`);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1428
1464
|
}
|
|
1429
1465
|
if (run.app === "github-actions" && (typeof run.suite !== "number" || !Number.isSafeInteger(run.suite))) {
|
|
1430
1466
|
failed.push(`${run.name} (no workflow run)`);
|
|
@@ -1468,6 +1504,13 @@ export function prChecksVerdict(runs, head, required, options = {}) {
|
|
|
1468
1504
|
waiting.push(`${name} (${latest.status})`);
|
|
1469
1505
|
continue;
|
|
1470
1506
|
}
|
|
1507
|
+
// Skipped is never success (test-matrix.md): GitHub's protection reads a skipped job as passing a merge, a release
|
|
1508
|
+
// never does. A documentation-only pull request skips the area lanes, and any pull request but a release-* branch's
|
|
1509
|
+
// skips Node 22, so a release needs every lane executed on its exact head.
|
|
1510
|
+
if (latest.conclusion === "skipped" && required.includes(name)) {
|
|
1511
|
+
failed.push(`${name} (skipped: a release needs it run on this exact commit; release from a branch named release-*, whose pull request runs every lane)`);
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1471
1514
|
if (latest.conclusion !== "success") {
|
|
1472
1515
|
failed.push(`${name} (${latest.conclusion ?? "no conclusion"})`);
|
|
1473
1516
|
continue;
|
|
@@ -1525,5 +1568,12 @@ export function checkFailedWords(input) {
|
|
|
1525
1568
|
}
|
|
1526
1569
|
/** Whether GitHub's workflow-run `path` names exactly `file` (it may carry the ref it ran from, after an `@`). */
|
|
1527
1570
|
export function workflowIs(path, file) {
|
|
1528
|
-
|
|
1571
|
+
if (typeof path !== "string")
|
|
1572
|
+
return false;
|
|
1573
|
+
if (path === file)
|
|
1574
|
+
return true;
|
|
1575
|
+
// GitHub may append @ref, but ci.yml@evil.yml is also a valid *different* workflow filename. Multiple @ signs
|
|
1576
|
+
// likewise cannot distinguish that filename from a ref. Refuse ambiguous suffixes instead of granting its checks.
|
|
1577
|
+
const ref = path.startsWith(`${file}@`) ? path.slice(file.length + 1) : "";
|
|
1578
|
+
return /^[A-Za-z0-9._+/-]+$/.test(ref) && !/\.ya?ml$/i.test(ref);
|
|
1529
1579
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Billing } from "./spend.js";
|
|
2
|
+
/** Several reported models are kept as their sorted ids joined by this (it has spaces, so no one id can equal it):
|
|
3
|
+
* a run whose calls ran different models never reads as any one of them. */
|
|
4
|
+
export declare const MODELS_JOIN = " + ";
|
|
5
|
+
export declare function reportedModelOf(value: unknown): string | null;
|
|
6
|
+
/** The one model a Claude result's `modelUsage` names; null when it names none, several, or anything malformed. */
|
|
7
|
+
export declare function soleReportedModel(modelUsage: unknown): string | null;
|
|
8
|
+
/** Two reports of the same piece of work (a resumed turn, a second init): unknown if either is, the model when they
|
|
9
|
+
* agree, otherwise every model named, sorted (never one chosen). */
|
|
10
|
+
export declare function combineReportedModels(a: string | null, b: string | null): string | null;
|
|
11
|
+
export declare function providerTurnsOf(value: unknown): number | null;
|
|
12
|
+
export declare function reportedCostUsdOf(value: unknown): number | null;
|
|
13
|
+
/** One provider call's own report. `tokensIn` is everything the model read (cached included) when the provider
|
|
14
|
+
* reported every part of it (run-efficiency.ts usageOf), else null. */
|
|
15
|
+
export type ProviderReport = {
|
|
16
|
+
modelReported: string | null;
|
|
17
|
+
providerTurns: number | null;
|
|
18
|
+
reportedCostUsd: number | null;
|
|
19
|
+
tokensIn: number | null;
|
|
20
|
+
cacheReadTokens: number | null;
|
|
21
|
+
cacheWriteTokens: number | null;
|
|
22
|
+
};
|
|
23
|
+
/** What the session's own `system`/`init` events named: the one model they agree on, or that they disagree. */
|
|
24
|
+
export type ClaudeInitModel = {
|
|
25
|
+
model: string | null;
|
|
26
|
+
conflict: boolean;
|
|
27
|
+
};
|
|
28
|
+
/** A Claude primary `result` (streamed or buffered `--output-format json`), with what the session's inits named. The
|
|
29
|
+
* init's model is the session's own; inits that disagree leave it unknown; with no init naming one, the result's
|
|
30
|
+
* `modelUsage` counts only when it names exactly one model. */
|
|
31
|
+
export declare function claudeResultReport(result: Record<string, unknown>, init?: ClaudeInitModel): ProviderReport;
|
|
32
|
+
/** Codex's `turn.completed` usage: its input already counts what came from cache; it reports no cache writes, no
|
|
33
|
+
* model and no cost in `exec --json`, so those stay unknown. */
|
|
34
|
+
export declare function codexUsageReport(usage: unknown): ProviderReport;
|
|
35
|
+
/** One of the three amounts, never added to another. */
|
|
36
|
+
export type ReportedAmount = {
|
|
37
|
+
kind: "api";
|
|
38
|
+
microusd: number;
|
|
39
|
+
}
|
|
40
|
+
/** The provider's own reported cost of work a subscription covers: usage, not billed (null: it reported none). */
|
|
41
|
+
| {
|
|
42
|
+
kind: "subscription";
|
|
43
|
+
reportedUsd: number | null;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "unknown";
|
|
46
|
+
};
|
|
47
|
+
/** A run's amount, from its fixed billing and its priced spend (run_spend). Subscription work reads its provider's
|
|
48
|
+
* reported cost as usage, never as dollars; billed work without a reported or priced cost is unknown. v141 (Codex
|
|
49
|
+
* round 1 #4, #10): billing this computer's default guessed (`basis` not evidence) is unknown, and so is a run whose
|
|
50
|
+
* cost the provider never reported (its `cost_usd` then holds a conservative charge, never a measurement). */
|
|
51
|
+
export declare function runAmountOf(run: {
|
|
52
|
+
billing: Billing | null;
|
|
53
|
+
spendMicrousd: number | null;
|
|
54
|
+
costUsd: number | null;
|
|
55
|
+
basis?: "evidence" | "default" | null;
|
|
56
|
+
costUnknown?: boolean;
|
|
57
|
+
}): ReportedAmount;
|
|
58
|
+
/** A chat request's amount: a subscription chat's reported cost is usage; an API chat's settled charge is dollars,
|
|
59
|
+
* unless its spend is unknown (then the amount is, whatever was settled against the reservation). v141 (Codex round 1
|
|
60
|
+
* #4): a subscription harness's request is subscription usage only when its own events said it was billed to the plan
|
|
61
|
+
* (`billingReported`); one they said used a key is API dollars at its reported cost; one they said nothing of is
|
|
62
|
+
* unknown. Absent `billingReported` keeps the older reading for callers that predate it. */
|
|
63
|
+
export declare function chatTurnAmountOf(turn: {
|
|
64
|
+
provider: string;
|
|
65
|
+
settledMicrousd: number | null;
|
|
66
|
+
unknownSpend: boolean;
|
|
67
|
+
reportedCostUsd: number | null;
|
|
68
|
+
billingReported?: "subscription" | "api-key" | null;
|
|
69
|
+
}): ReportedAmount;
|