toolroll 0.9.79 → 0.9.81
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 +26 -0
- package/dist/guides.js +12 -9
- package/dist/release-adapters.js +1 -1
- package/dist/release-auto.d.ts +1 -1
- package/dist/release.d.ts +20 -17
- package/dist/release.js +58 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.81 — 2026-10-30
|
|
4
|
+
|
|
5
|
+
- **The release proof works after the merge.** GitHub empties a CI run's
|
|
6
|
+
pull-request link once the pull request merges and its branch is deleted,
|
|
7
|
+
so 0.9.80's tag could never prove its checks and wasn't published. A run
|
|
8
|
+
with no link now stands only when its branch has had exactly one pull
|
|
9
|
+
request, ever, and that pull request merged into main. 0.9.80's changes
|
|
10
|
+
ship in this release.
|
|
11
|
+
|
|
12
|
+
## 0.9.80 — 2026-10-30
|
|
13
|
+
|
|
14
|
+
- **Each test runs once, where it adds something** (docs/plans/test-matrix.md).
|
|
15
|
+
Pull requests run the unit suite on Node 24 (Linux and macOS) and every
|
|
16
|
+
area lane (Windows, Linux fence and cgroup, git 2.41 and 2.47); a pull
|
|
17
|
+
request that only changes Markdown under `docs/`, the changelog or the
|
|
18
|
+
README skips the area lanes. Node 22 runs on Linux and macOS once per
|
|
19
|
+
release, on the `release-*` branch's pull request, in the new
|
|
20
|
+
Release candidate workflow. Release tags no longer rerun the macOS suites:
|
|
21
|
+
a release proof reads GitHub's record that the tagged tree passed every
|
|
22
|
+
required check on its pull request before npm publication. A push to main
|
|
23
|
+
typechecks and builds, and runs the full CI only when it can't prove the
|
|
24
|
+
merge commit was tested. The nightly runs everything. A release now needs
|
|
25
|
+
every required check executed, never skipped, and refuses to merge while
|
|
26
|
+
main's branch protection doesn't require all of them. Hosted unit shards
|
|
27
|
+
require their platform's native fence attacks to run.
|
|
28
|
+
|
|
3
29
|
## 0.9.79 — 2026-10-29
|
|
4
30
|
|
|
5
31
|
- **`toolroll release` rides out a short network failure instead of
|
package/dist/guides.js
CHANGED
|
@@ -473,20 +473,23 @@ with a time limit.
|
|
|
473
473
|
saved first; if either name doesn't answer as the new build, both go
|
|
474
474
|
back. It goes on only when the console, worker and both names run the
|
|
475
475
|
gated commit. A rerun relinks without deploying again.
|
|
476
|
-
8. **ci**: waits for every pull-request check on the gated commit:
|
|
477
|
-
|
|
478
|
-
|
|
476
|
+
8. **ci**: waits for every pull-request check on the gated commit: Node 24
|
|
477
|
+
on Ubuntu and macOS, the area lanes (Linux cgroup and fence, Windows,
|
|
478
|
+
git 2.41 and 2.47), and Node 22 on Ubuntu and macOS, which runs on a
|
|
479
|
+
release-* branch's pull request.
|
|
480
|
+
Pending, missing, skipped, cancelled or failed never merges, and main's
|
|
481
|
+
protection must require every one of them.
|
|
479
482
|
9. **merge**: squash-merges with the head pinned to the gated commit,
|
|
480
483
|
once main has nothing the branch lacks, then deletes the remote branch
|
|
481
484
|
only if its head still matches. A resumed merge also finishes this cleanup.
|
|
482
485
|
10. **tree**: main's merge commit must have exactly the gated tree, or
|
|
483
486
|
nothing is tagged.
|
|
484
|
-
11. **tag**: pushes \`v<version>\` on that merge commit.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
12. **publish**: waits for npm, the GitHub release and the tag's
|
|
489
|
-
|
|
487
|
+
11. **tag**: pushes \`v<version>\` on that merge commit. The tag runs no
|
|
488
|
+
tests again: publish.yml's release proof reads GitHub's record that the
|
|
489
|
+
tagged tree is the merged pull request's head tree and passed every
|
|
490
|
+
required check there, and npm and GitHub publication need it.
|
|
491
|
+
12. **publish**: waits for npm, the GitHub release and the tag's release
|
|
492
|
+
proof and publish jobs; not main's own push run on the merge commit.
|
|
490
493
|
13. **homebrew**: the release is published once npm and the GitHub release
|
|
491
494
|
are verified. It proposes the tap's formula change (checksum from npm's
|
|
492
495
|
tarball) and looks at its checks once: \`Published; Homebrew pending\`
|
package/dist/release-adapters.js
CHANGED
|
@@ -1290,7 +1290,7 @@ export async function readMainProtection(run, gh, repo, branch = "main") {
|
|
|
1290
1290
|
else {
|
|
1291
1291
|
const lacking = PROTECTION_MINIMUM_CHECKS.filter(name => !checks.includes(name));
|
|
1292
1292
|
if (lacking.length > 0)
|
|
1293
|
-
own.push(`${where}: required checks lack ${lacking.join(", ")}`);
|
|
1293
|
+
own.push(`${where}: required checks lack ${lacking.join(", ")} (add ${lacking.length === 1 ? "it" : "them"} to main's required status checks: a release's proof trusts what main required)`);
|
|
1294
1294
|
const unbound = PROTECTION_MINIMUM_CHECKS.filter(name => checks.includes(name) && !bound.includes(name));
|
|
1295
1295
|
if (unbound.length > 0)
|
|
1296
1296
|
own.push(`${where}: required checks must require GitHub Actions (app 15368) as their source: ${unbound.join(", ")}`);
|
package/dist/release-auto.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type ObservedCheckRun = {
|
|
|
19
19
|
suite?: number | null;
|
|
20
20
|
head?: string | null;
|
|
21
21
|
workflow?: number | null;
|
|
22
|
-
/** The workflow's file (GitHub's workflow-run `path`): a required check counts only from
|
|
22
|
+
/** The workflow's file (GitHub's workflow-run `path`): a required check counts only from its own (REQUIRED_PR_CHECKS_WORKFLOW). */
|
|
23
23
|
workflowPath?: string | null;
|
|
24
24
|
/** GitHub's job record shows this unsuccessful attempt ended before any workflow step ran (release-adapters.ts). */
|
|
25
25
|
beforeTests?: boolean | null;
|
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];
|
|
@@ -662,10 +665,10 @@ export declare function checksVerdict(runs: readonly CheckRun[], required: reado
|
|
|
662
665
|
failed: string[];
|
|
663
666
|
};
|
|
664
667
|
/**
|
|
665
|
-
* Publication's verdict on the tag commit (
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
668
|
+
* Publication's verdict on the tag commit (test-matrix.md): only RELEASE_TAG_CHECKS, from GitHub Actions runs of
|
|
669
|
+
* publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's own
|
|
670
|
+
* push run — is not waited for: the candidate's checks already passed on its exact head before the merge, the merged
|
|
671
|
+
* tree was proved equal to it, and publish.yml's release proof checks both again from GitHub's record.
|
|
669
672
|
*/
|
|
670
673
|
export declare function tagChecksVerdict(runs: readonly CheckRun[]): ReturnType<typeof checksVerdict>;
|
|
671
674
|
/** Run the release from wherever its journal stopped. */
|
|
@@ -686,7 +689,7 @@ export declare function transientNetworkError(error: unknown): boolean;
|
|
|
686
689
|
*/
|
|
687
690
|
export declare function prChecksVerdict(runs: readonly CheckRun[], head: string, required: readonly string[], options?: {
|
|
688
691
|
auto?: true;
|
|
689
|
-
workflow?: string
|
|
692
|
+
workflow?: string | Readonly<Record<string, string>>;
|
|
690
693
|
}): {
|
|
691
694
|
state: "passed";
|
|
692
695
|
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;
|
|
@@ -1422,9 +1439,12 @@ export function prChecksVerdict(runs, head, required, options = {}) {
|
|
|
1422
1439
|
}
|
|
1423
1440
|
// A required name counts only from the workflow that defines it: a job of the same name in another workflow (or
|
|
1424
1441
|
// 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
|
-
|
|
1442
|
+
if (options.workflow !== undefined && required.includes(run.name)) {
|
|
1443
|
+
const want = typeof options.workflow === "string" ? options.workflow : options.workflow[run.name];
|
|
1444
|
+
if (want === undefined || !workflowIs(run.workflowPath, want)) {
|
|
1445
|
+
failed.push(`${run.name} (from ${run.workflowPath ?? "a workflow GitHub doesn't name"}, not ${want ?? "a workflow this release knows for it"})`);
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1428
1448
|
}
|
|
1429
1449
|
if (run.app === "github-actions" && (typeof run.suite !== "number" || !Number.isSafeInteger(run.suite))) {
|
|
1430
1450
|
failed.push(`${run.name} (no workflow run)`);
|
|
@@ -1468,6 +1488,13 @@ export function prChecksVerdict(runs, head, required, options = {}) {
|
|
|
1468
1488
|
waiting.push(`${name} (${latest.status})`);
|
|
1469
1489
|
continue;
|
|
1470
1490
|
}
|
|
1491
|
+
// Skipped is never success (test-matrix.md): GitHub's protection reads a skipped job as passing a merge, a release
|
|
1492
|
+
// never does. A documentation-only pull request skips the area lanes, and any pull request but a release-* branch's
|
|
1493
|
+
// skips Node 22, so a release needs every lane executed on its exact head.
|
|
1494
|
+
if (latest.conclusion === "skipped" && required.includes(name)) {
|
|
1495
|
+
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)`);
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1471
1498
|
if (latest.conclusion !== "success") {
|
|
1472
1499
|
failed.push(`${name} (${latest.conclusion ?? "no conclusion"})`);
|
|
1473
1500
|
continue;
|
|
@@ -1525,5 +1552,12 @@ export function checkFailedWords(input) {
|
|
|
1525
1552
|
}
|
|
1526
1553
|
/** Whether GitHub's workflow-run `path` names exactly `file` (it may carry the ref it ran from, after an `@`). */
|
|
1527
1554
|
export function workflowIs(path, file) {
|
|
1528
|
-
|
|
1555
|
+
if (typeof path !== "string")
|
|
1556
|
+
return false;
|
|
1557
|
+
if (path === file)
|
|
1558
|
+
return true;
|
|
1559
|
+
// GitHub may append @ref, but ci.yml@evil.yml is also a valid *different* workflow filename. Multiple @ signs
|
|
1560
|
+
// likewise cannot distinguish that filename from a ref. Refuse ambiguous suffixes instead of granting its checks.
|
|
1561
|
+
const ref = path.startsWith(`${file}@`) ? path.slice(file.length + 1) : "";
|
|
1562
|
+
return /^[A-Za-z0-9._+/-]+$/.test(ref) && !/\.ya?ml$/i.test(ref);
|
|
1529
1563
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.81",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|