toolroll 0.9.92 → 0.9.94
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 +87 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +4 -0
- package/dist/agent-fence.js +4 -1
- package/dist/approval-authority.d.ts +7 -1
- package/dist/approval-authority.js +11 -2
- package/dist/approval-password.d.ts +31 -0
- package/dist/approval-password.js +26 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +3 -3
- package/dist/assignment.js +16 -3
- package/dist/boot-identity.d.ts +2 -1
- package/dist/boot-identity.js +6 -4
- package/dist/bounded-spawn.d.ts +57 -0
- package/dist/bounded-spawn.js +220 -0
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +2 -2
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +48 -7
- package/dist/browser-workspace.js +3 -3
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +73 -25
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +12 -2
- package/dist/chat-actions.js +25 -2
- package/dist/chat-channel.js +11 -0
- package/dist/chat-decide.d.ts +7 -2
- package/dist/chat-decide.js +37 -9
- package/dist/chat-quiet.js +3 -3
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +126 -0
- package/dist/check-facts.js +318 -0
- package/dist/check-failure.d.ts +22 -3
- package/dist/check-failure.js +57 -13
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +10 -1
- package/dist/check-load.d.ts +85 -0
- package/dist/check-load.js +147 -0
- package/dist/check-only-standing.d.ts +36 -0
- package/dist/check-only-standing.js +39 -0
- package/dist/check-only.d.ts +15 -0
- package/dist/check-only.js +99 -0
- package/dist/check-report.d.ts +7 -0
- package/dist/check-report.js +4 -3
- package/dist/check-stamp.d.ts +46 -0
- package/dist/check-stamp.js +93 -0
- package/dist/checkout-cleanup.d.ts +36 -5
- package/dist/checkout-cleanup.js +166 -14
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +47 -30
- package/dist/cli.js +16 -1
- package/dist/code-extract.d.ts +44 -0
- package/dist/code-extract.js +88 -0
- package/dist/code-map-acts.d.ts +24 -0
- package/dist/code-map-acts.js +53 -0
- package/dist/code-map-admission.d.ts +52 -0
- package/dist/code-map-admission.js +295 -0
- package/dist/code-map-build.d.ts +63 -0
- package/dist/code-map-build.js +415 -0
- package/dist/code-map-cli.d.ts +153 -0
- package/dist/code-map-cli.js +103 -0
- package/dist/code-map-main.d.ts +1 -0
- package/dist/code-map-main.js +48 -0
- package/dist/code-map-read.d.ts +106 -0
- package/dist/code-map-read.js +171 -0
- package/dist/code-map-report.d.ts +37 -0
- package/dist/code-map-report.js +95 -0
- package/dist/code-map-shape.d.ts +172 -0
- package/dist/code-map-shape.js +327 -0
- package/dist/code-map-store.d.ts +136 -0
- package/dist/code-map-store.js +233 -0
- package/dist/coding-provider.d.ts +2 -0
- package/dist/coding-provider.js +3 -2
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.js +4 -4
- package/dist/console-chrome.js +2 -2
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +14 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +12 -5
- package/dist/contracts/console-api.js +12 -5
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +61 -0
- package/dist/contracts/lead-tools.js +10 -1
- package/dist/contracts/project-knowledge.d.ts +38 -5
- package/dist/contracts/project-knowledge.js +11 -5
- package/dist/contracts/project-memory.d.ts +3 -1
- package/dist/contracts/project-memory.js +3 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/youiyoux-request.d.ts +1 -1
- package/dist/contracts/youiyoux-request.js +1 -1
- package/dist/desktop-access.js +6 -2
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispatch.js +18 -15
- package/dist/dispose.js +8 -2
- package/dist/easy-start.js +2 -3
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/evals.js +2 -1
- package/dist/fast-start.d.ts +136 -0
- package/dist/fast-start.js +431 -0
- package/dist/fast-terms.d.ts +9 -0
- package/dist/fast-terms.js +23 -0
- package/dist/fenced-check-coverage.d.ts +62 -0
- package/dist/fenced-check-coverage.js +54 -0
- package/dist/first-run.js +1 -1
- package/dist/flaky-ci.d.ts +31 -0
- package/dist/flaky-ci.js +149 -0
- package/dist/flaky-cli.d.ts +60 -0
- package/dist/flaky-cli.js +60 -0
- package/dist/flaky.d.ts +83 -0
- package/dist/flaky.js +264 -0
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/invoke.d.ts +5 -1
- package/dist/invoke.js +49 -14
- package/dist/job-object-helper.ps1 +10 -3
- package/dist/keymap.d.ts +41 -0
- package/dist/keymap.js +51 -0
- package/dist/knowledge-cli.js +1 -1
- package/dist/knowledge-ui.d.ts +21 -4
- package/dist/knowledge-ui.js +75 -7
- package/dist/laptop-background.d.ts +16 -0
- package/dist/laptop-background.js +108 -2
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-progress.js +3 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +51 -2
- package/dist/lead.js +1 -1
- package/dist/lesson-notes.d.ts +81 -0
- package/dist/lesson-notes.js +336 -0
- package/dist/limits-ui.js +1 -1
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/machine-check.d.ts +16 -0
- package/dist/machine-check.js +26 -0
- package/dist/machine-identity.d.ts +75 -0
- package/dist/machine-identity.js +170 -0
- package/dist/mcp-core.js +11 -1
- package/dist/memory-cli.d.ts +300 -1
- package/dist/memory-cli.js +62 -9
- package/dist/memory-pass.d.ts +16 -0
- package/dist/memory-pass.js +67 -3
- package/dist/memory-tidy.d.ts +181 -0
- package/dist/memory-tidy.js +523 -0
- package/dist/memory-use.d.ts +35 -0
- package/dist/memory-use.js +109 -0
- package/dist/merge-checklist.d.ts +55 -0
- package/dist/merge-checklist.js +182 -0
- package/dist/needs-you.d.ts +32 -13
- package/dist/needs-you.js +108 -49
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +6 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +540 -70
- package/dist/phase-routing.js +2 -2
- package/dist/plain-schema.d.ts +23 -0
- package/dist/plain-schema.js +94 -0
- package/dist/plain-words.d.ts +20 -0
- package/dist/plain-words.js +66 -0
- package/dist/planner.js +2 -1
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +7 -2
- package/dist/project-delete-ui.js +12 -1
- package/dist/project-delete.js +13 -2
- package/dist/project-knowledge.d.ts +18 -11
- package/dist/project-knowledge.js +39 -9
- package/dist/project-learning.d.ts +2 -1
- package/dist/project-learning.js +15 -45
- package/dist/project-memory.d.ts +5 -0
- package/dist/project-memory.js +22 -2
- package/dist/proof.js +30 -5
- package/dist/provider-auth.js +1 -1
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +26 -3
- package/dist/push.js +55 -15
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +87 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +26 -17
- package/dist/release-card.d.ts +74 -6
- package/dist/release-card.js +179 -23
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +39 -9
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +38 -23
- package/dist/release-coverage.js +1 -1
- package/dist/release-next.d.ts +48 -0
- package/dist/release-next.js +161 -0
- package/dist/release.d.ts +50 -1
- package/dist/release.js +118 -11
- package/dist/remote-destination.d.ts +36 -0
- package/dist/remote-destination.js +217 -0
- package/dist/remote-main.d.ts +48 -0
- package/dist/remote-main.js +84 -5
- package/dist/repository-context.d.ts +3 -2
- package/dist/repository-context.js +112 -70
- package/dist/restart-certification.d.ts +1 -0
- package/dist/restart-certification.js +8 -6
- package/dist/result-acts.d.ts +29 -21
- package/dist/result-acts.js +36 -25
- package/dist/result-follow-ups.d.ts +2 -2
- package/dist/result-follow-ups.js +18 -4
- package/dist/result-review.d.ts +1 -1
- package/dist/route-change-check.d.ts +4 -0
- package/dist/route-change-check.js +13 -0
- package/dist/route-change.d.ts +22 -0
- package/dist/route-change.js +89 -0
- package/dist/route-hooks.js +3 -2
- package/dist/route-results.d.ts +6 -1
- package/dist/route-results.js +14 -5
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +7 -5
- package/dist/runner-process.js +3 -2
- package/dist/runner.js +2 -1
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +22 -9
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +24 -18
- package/dist/server/chat.js +14 -12
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +23 -0
- package/dist/server/guards.js +4 -0
- package/dist/server/home.d.ts +6 -0
- package/dist/server/home.js +21 -13
- package/dist/server/http.js +1 -1
- package/dist/server/kit.js +1 -1
- package/dist/server/merge-ready-view.d.ts +10 -0
- package/dist/server/merge-ready-view.js +30 -0
- package/dist/server/pages-assets.d.ts +1 -0
- package/dist/server/pages-assets.js +8 -5
- package/dist/server/pages.js +103 -82
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-code-map.d.ts +30 -0
- package/dist/server/render-code-map.js +137 -0
- package/dist/server/render-flaky.d.ts +20 -0
- package/dist/server/render-flaky.js +59 -0
- package/dist/server/render-history.js +1 -1
- package/dist/server/render-new-task.d.ts +26 -1
- package/dist/server/render-new-task.js +30 -5
- package/dist/server/render-pages.d.ts +1 -175
- package/dist/server/render-pages.js +38 -313
- package/dist/server/render-projects.d.ts +2 -1
- package/dist/server/render-projects.js +9 -7
- package/dist/server/render-release.d.ts +19 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +2 -2
- package/dist/server/render-results.js +71 -73
- package/dist/server/render-tasks.d.ts +13 -2
- package/dist/server/render-tasks.js +156 -139
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +4 -1
- package/dist/server/route-table.js +52 -13
- package/dist/server/settings.js +129 -22
- package/dist/server/task-views.js +61 -26
- package/dist/server/tasks.d.ts +17 -6
- package/dist/server/tasks.js +273 -338
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +295 -0
- package/dist/storage-sweep-process.js +1 -1
- package/dist/storage-sweep.d.ts +5 -3
- package/dist/storage-sweep.js +30 -9
- package/dist/storage-ui.js +28 -17
- package/dist/storage.d.ts +27 -2
- package/dist/storage.js +39 -2
- package/dist/store.d.ts +148 -34
- package/dist/store.js +626 -177
- package/dist/surface.js +46 -5
- package/dist/sweep-quarantine.d.ts +1 -0
- package/dist/sweep-quarantine.js +6 -4
- package/dist/tamper-scan.d.ts +4 -0
- package/dist/tamper-scan.js +90 -0
- package/dist/task-aliases.d.ts +5 -0
- package/dist/task-aliases.js +9 -0
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +36 -17
- package/dist/task-sizing.d.ts +5 -0
- package/dist/task-sizing.js +2 -0
- package/dist/task-source.d.ts +7 -0
- package/dist/task-source.js +14 -0
- package/dist/task-status.d.ts +9 -0
- package/dist/task-status.js +40 -49
- package/dist/teardown-git.d.ts +16 -0
- package/dist/teardown-git.js +63 -5
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/test-infra.d.ts +8 -0
- package/dist/test-infra.js +18 -0
- package/dist/tidy.js +1 -1
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +18 -5
- package/dist/tui-contract.d.ts +24 -0
- package/dist/tui-source.js +67 -8
- package/dist/tui.js +51 -51
- package/dist/vanished-retry.d.ts +30 -0
- package/dist/vanished-retry.js +174 -0
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +82 -70
- package/dist/work-summary.d.ts +2 -2
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +28 -22
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +68 -13
- package/package.json +1 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `toolroll release <branch>` does before its journal's next step (docs/plans/plain-toolroll.md P2.6): one command,
|
|
3
|
+
* so a person never chooses between --recover-deploy, --release-gate and rerunning.
|
|
4
|
+
*
|
|
5
|
+
* - **Another run**: a run lock per branch, by host and pid. A live one is waited for, named; two coordinators never act
|
|
6
|
+
* on one release at once. A lock whose process is proved gone on this host is taken over.
|
|
7
|
+
* - **A deployment left open**: the journal decides. Before the swap: its pause is lifted, once proved. Past the new
|
|
8
|
+
* service's start: forward, by the release. Between: rolled back through its own recovery. Each of those re-proves
|
|
9
|
+
* itself, so a crash at any phase ends the same on the next run.
|
|
10
|
+
*/
|
|
11
|
+
import { closeSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeSync } from "node:fs";
|
|
12
|
+
import { hostname } from "node:os";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { FORWARD_ONLY, rescueDeployment } from "./deploy-rescue.js";
|
|
15
|
+
import { BEFORE_SWAP, releaseGate } from "./release-gate.js";
|
|
16
|
+
import { journalFile, readJournal } from "./release.js";
|
|
17
|
+
import { autoPublishHeld, PUBLISH_ALLOWED_ACTION } from "./release-auto.js";
|
|
18
|
+
import { sameMachineHost } from "./machine-identity.js";
|
|
19
|
+
const lockFile = (stateDir, branch) => journalFile(stateDir, branch).replace(/\.json$/, ".running");
|
|
20
|
+
/** How long a run waits for another before it says so and stops. */
|
|
21
|
+
export const RELEASE_WAIT_MS = 30 * 60_000;
|
|
22
|
+
const processAlive = (pid) => {
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return error.code === "EPERM";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function readHolder(file) {
|
|
32
|
+
try {
|
|
33
|
+
const read = JSON.parse(readFileSync(file, "utf8"));
|
|
34
|
+
return typeof read.host === "string" && Number.isSafeInteger(read.pid) ? { host: read.host, pid: read.pid } : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Take this branch's run lock, waiting for a live holder. Answers how to give it back, or who kept it. */
|
|
41
|
+
export async function takeReleaseRun(stateDir, branch, say, seams = {}) {
|
|
42
|
+
const file = lockFile(stateDir, branch);
|
|
43
|
+
const host = seams.host ?? hostname(), pid = seams.pid ?? process.pid;
|
|
44
|
+
const alive = seams.alive ?? processAlive, sleep = seams.sleep ?? (ms => new Promise(done => setTimeout(done, ms)));
|
|
45
|
+
const now = seams.now ?? Date.now;
|
|
46
|
+
const deadline = now() + (seams.waitMs ?? RELEASE_WAIT_MS);
|
|
47
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
48
|
+
let said = false;
|
|
49
|
+
for (;;) {
|
|
50
|
+
try {
|
|
51
|
+
const fd = openSync(file, "wx", 0o600);
|
|
52
|
+
try {
|
|
53
|
+
writeSync(fd, JSON.stringify({ host, pid }));
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
closeSync(fd);
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, release: () => { const mine = readHolder(file); if (mine?.pid === pid && mine.host === host)
|
|
59
|
+
rmSync(file, { force: true }); } };
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code !== "EEXIST")
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const holder = readHolder(file);
|
|
66
|
+
// A lock with no readable holder, or a process proved gone on this computer, holds nothing. Another computer's can't
|
|
67
|
+
// be proved gone from here, so it is waited for.
|
|
68
|
+
if (holder === null || (sameMachineHost(holder.host, host) && !alive(holder.pid))) {
|
|
69
|
+
rmSync(file, { force: true });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!said) {
|
|
73
|
+
say(`Waiting: another release of ${branch} is running on ${holder.host} (process ${holder.pid}).`);
|
|
74
|
+
said = true;
|
|
75
|
+
}
|
|
76
|
+
if (now() >= deadline)
|
|
77
|
+
return { ok: false, holder };
|
|
78
|
+
await sleep(seams.pollMs ?? 5_000);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** The one action a deployment's journal calls for. */
|
|
82
|
+
export function deployActionOf(phase) {
|
|
83
|
+
if (typeof phase !== "string")
|
|
84
|
+
return "refuse";
|
|
85
|
+
if (phase === "released" || phase === "deployed")
|
|
86
|
+
return "none";
|
|
87
|
+
if (BEFORE_SWAP.includes(phase))
|
|
88
|
+
return "lift-pause";
|
|
89
|
+
if (FORWARD_ONLY.includes(phase))
|
|
90
|
+
return "forward";
|
|
91
|
+
return "roll-back";
|
|
92
|
+
}
|
|
93
|
+
/** Settle what a stopped deployment left, saying what was done. A refusal stops the release before it does anything. */
|
|
94
|
+
export async function settleDeployment(world, say) {
|
|
95
|
+
const open = world.rescue.journals().filter(one => deployActionOf(one.journal?.["phase"]) !== "none");
|
|
96
|
+
if (open.length === 0)
|
|
97
|
+
return { ok: true };
|
|
98
|
+
if (open.length > 1)
|
|
99
|
+
return { ok: false, message: `${open.length} deployments are unfinished: ${open.map(one => one.stage).join(", ")}. Nothing was changed.` };
|
|
100
|
+
const { stage, journal } = open[0];
|
|
101
|
+
const action = deployActionOf(journal?.["phase"]);
|
|
102
|
+
if (action === "refuse")
|
|
103
|
+
return { ok: false, message: `The deployment journal in ${stage} can't be read. Nothing was changed.` };
|
|
104
|
+
if (action === "forward") {
|
|
105
|
+
say("Finishing the deployment that was under way.");
|
|
106
|
+
return { ok: true };
|
|
107
|
+
}
|
|
108
|
+
if (action === "lift-pause") {
|
|
109
|
+
const lifted = releaseGate(String(journal?.["id"] ?? ""), world.gate, true);
|
|
110
|
+
if (!lifted.ok)
|
|
111
|
+
return { ok: false, message: lifted.message };
|
|
112
|
+
say("Lifted the pause an unfinished deployment left before its swap.");
|
|
113
|
+
return { ok: true };
|
|
114
|
+
}
|
|
115
|
+
const back = await rescueDeployment(stage, true, world.rescue);
|
|
116
|
+
if (!back.ok)
|
|
117
|
+
return { ok: false, message: back.message };
|
|
118
|
+
say("Put back the deployment that stopped half way.");
|
|
119
|
+
return { ok: true };
|
|
120
|
+
}
|
|
121
|
+
// ---- a held release resumes itself -----------------------------------------------------------------------------------
|
|
122
|
+
export const RELEASE_RESUMED_ACTION = "release resumed";
|
|
123
|
+
/**
|
|
124
|
+
* A person let a held release go on (release-auto.ts allowAutoPublish; the Release sheet's button or `publish-ok`): each
|
|
125
|
+
* sweep, its journal goes on from where it stopped, once per that person's act, under the same run lock. It was merged
|
|
126
|
+
* already, so only the tag, publication and Homebrew are left, and each re-proves itself.
|
|
127
|
+
*/
|
|
128
|
+
export async function resumeAllowedReleases(store, stateDir, run, now) {
|
|
129
|
+
let folder;
|
|
130
|
+
try {
|
|
131
|
+
folder = readdirSync(join(stateDir, "releases")).filter(name => name.endsWith(".json") && !name.includes(".superseded-"));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
let resumed = 0;
|
|
137
|
+
for (const name of folder) {
|
|
138
|
+
const journal = readJournal(join(stateDir, "releases", name));
|
|
139
|
+
if (journal === null || !["tag", "publish", "homebrew"].includes(journal.step) || autoPublishHeld(store, journal.task.id) !== null)
|
|
140
|
+
continue;
|
|
141
|
+
const allowed = store.handle.prepare("SELECT id FROM action_ledger WHERE task_id = ? AND action = ? ORDER BY id DESC LIMIT 1").get(journal.task.id, PUBLISH_ALLOWED_ACTION);
|
|
142
|
+
if (allowed === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
// Once per act: a resume that stops again waits for a person, never for the next sweep.
|
|
145
|
+
const done = store.handle.prepare("SELECT 1 AS hit FROM action_ledger WHERE task_id = ? AND action = ? AND outcome = ?").get(journal.task.id, RELEASE_RESUMED_ACTION, String(allowed["id"]));
|
|
146
|
+
if (done !== undefined)
|
|
147
|
+
continue;
|
|
148
|
+
const taken = await takeReleaseRun(stateDir, journal.branch, () => { }, { waitMs: 0 });
|
|
149
|
+
if (!taken.ok)
|
|
150
|
+
continue;
|
|
151
|
+
try {
|
|
152
|
+
store.recordAction({ at: now.toISOString(), actor: "system", repo: store.lookupRef(journal.task.id)?.repo ?? null, taskId: journal.task.id, runId: null, action: RELEASE_RESUMED_ACTION, outcome: String(allowed["id"]), source: "work", detail: journal.tag });
|
|
153
|
+
await run(journal);
|
|
154
|
+
resumed++;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
taken.release();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return resumed;
|
|
161
|
+
}
|
package/dist/release.d.ts
CHANGED
|
@@ -77,7 +77,9 @@ export declare const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
|
|
|
77
77
|
export declare const RELEASE_STEPS: readonly ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
|
78
78
|
export type ReleaseStep = typeof RELEASE_STEPS[number];
|
|
79
79
|
type WaitingStep = "gate" | "approval" | "check" | "quiet" | "deploy" | "ci" | "publish" | "homebrew";
|
|
80
|
-
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it).
|
|
80
|
+
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). The
|
|
81
|
+
* check's covers the first attempt and the two same-commit reruns the plane may give it, each of which a busy machine
|
|
82
|
+
* may stretch to two hours (check-load.ts). */
|
|
81
83
|
export declare const RELEASE_LIMITS: Readonly<Record<WaitingStep, number>>;
|
|
82
84
|
/** One check attempt; `app` is the GitHub App that made it (github-actions for a workflow), when known; `id` its check
|
|
83
85
|
* run id, `suite` its check suite (one workflow run: its re-runs stay in it) and `head` the commit GitHub filed it
|
|
@@ -332,6 +334,43 @@ export type ReleaseAuthorityAdapter = {
|
|
|
332
334
|
why: string;
|
|
333
335
|
warning: string | null;
|
|
334
336
|
}>;
|
|
337
|
+
/** Whether a sandboxed Full check's skipped cases are covered (fenced-check-coverage.ts, H3). Run `run`'s stamped level
|
|
338
|
+
* decides: an unsandboxed check answers at once. Absent: a plane that can't say, as for every older one. */
|
|
339
|
+
fencedCoverage?(taskId: string, run: number, candidate: string): Promise<{
|
|
340
|
+
ok: true;
|
|
341
|
+
covered: number;
|
|
342
|
+
} | {
|
|
343
|
+
ok: false;
|
|
344
|
+
why: string;
|
|
345
|
+
}>;
|
|
346
|
+
/** One-tap release approval (docs/plans/trusted-release-reports.md, slice A): what the plane tells a person and
|
|
347
|
+
* keeps for the approval sheet. Best effort: a failure here never changes the release. Absent on an older plane. */
|
|
348
|
+
ready?: ReleaseReadyAdapter;
|
|
349
|
+
};
|
|
350
|
+
/** What the coordinator tells the plane for a person's release (release-approval.ts). Ids and its own readings only. */
|
|
351
|
+
export type ReleaseReadyAdapter = {
|
|
352
|
+
/** The approval step was entered (a fresh filing or a resumed one): one notification per task and scope digest. */
|
|
353
|
+
asked(taskId: string): Promise<void>;
|
|
354
|
+
/** The pull request's required checks on the candidate, as this coordinator just read them. */
|
|
355
|
+
ci(input: {
|
|
356
|
+
taskId: string;
|
|
357
|
+
candidate: string;
|
|
358
|
+
state: "passed" | "running" | "failed";
|
|
359
|
+
failed: readonly string[];
|
|
360
|
+
runs: readonly CheckRun[];
|
|
361
|
+
}): Promise<void>;
|
|
362
|
+
/** A person's release stopped: one attention notification per task, step and reason. */
|
|
363
|
+
stopped(input: {
|
|
364
|
+
taskId: string;
|
|
365
|
+
step: string;
|
|
366
|
+
reason: string;
|
|
367
|
+
message: string;
|
|
368
|
+
}): Promise<void>;
|
|
369
|
+
/** Main's merge commit was proved to hold the approved tree: the plane checks it again and keeps the release's record. */
|
|
370
|
+
verified(input: {
|
|
371
|
+
taskId: string;
|
|
372
|
+
merge: string;
|
|
373
|
+
}): Promise<void>;
|
|
335
374
|
};
|
|
336
375
|
/** The plane's automatic release approval, as the coordinator asks it. Every answer is the plane's own: the coordinator
|
|
337
376
|
* hands it ids, never evidence (the plane reads GitHub CI itself for `approve`). */
|
|
@@ -451,11 +490,13 @@ export type ReleaseAdapters = {
|
|
|
451
490
|
}): Promise<{
|
|
452
491
|
scopeDigest: string;
|
|
453
492
|
}>;
|
|
493
|
+
/** `reason` "stopping": Toolroll is still proving the check's build stopped (stop-proof.ts); it clears on its own. */
|
|
454
494
|
complete(taskId: string, receipt: string): Promise<{
|
|
455
495
|
ok: true;
|
|
456
496
|
} | {
|
|
457
497
|
ok: false;
|
|
458
498
|
message: string;
|
|
499
|
+
reason?: string;
|
|
459
500
|
}>;
|
|
460
501
|
busy(): Promise<{
|
|
461
502
|
running: number;
|
|
@@ -506,6 +547,14 @@ export type ReleaseAdapters = {
|
|
|
506
547
|
mainHead(repo: string): Promise<string>;
|
|
507
548
|
/** Whether `sha` already holds `ancestor`, by GitHub's own comparison. */
|
|
508
549
|
contains(repo: string, sha: string, ancestor: string): Promise<boolean>;
|
|
550
|
+
/** Bring a pull request's branch up to main, only while its head is `expectedHead` (GitHub's own compare-and-swap: a
|
|
551
|
+
* push that got there first refuses). Absent: a behind branch stops, for a person to update. */
|
|
552
|
+
updateBranch?(repo: string, pr: number, expectedHead: string): Promise<{
|
|
553
|
+
ok: true;
|
|
554
|
+
} | {
|
|
555
|
+
ok: false;
|
|
556
|
+
why: string;
|
|
557
|
+
}>;
|
|
509
558
|
/** Whether GitHub refuses to merge a branch that is behind main ("Require branches to be up to date before
|
|
510
559
|
* merging"); null when it can't be read. Read only. */
|
|
511
560
|
upToDateRequired(repo: string): Promise<MainProtection | null>;
|
package/dist/release.js
CHANGED
|
@@ -101,8 +101,10 @@ export const ADVISORY_PR_CHECKS = {
|
|
|
101
101
|
export const RELEASE_TAG_CHECKS = ["release proof", "publish"];
|
|
102
102
|
export const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
|
|
103
103
|
export const RELEASE_STEPS = ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
|
104
|
-
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it).
|
|
105
|
-
|
|
104
|
+
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). The
|
|
105
|
+
* check's covers the first attempt and the two same-commit reruns the plane may give it, each of which a busy machine
|
|
106
|
+
* may stretch to two hours (check-load.ts). */
|
|
107
|
+
export const RELEASE_LIMITS = { gate: 60, approval: 24 * 60, check: 360, quiet: 60, deploy: 30, ci: 60, publish: 45, homebrew: 45 };
|
|
106
108
|
const POLL_MS = 15_000;
|
|
107
109
|
/**
|
|
108
110
|
* Failure classes replace a flat flake allowance (docs/plans/eval-harness.md; check-failure.ts says the classes): only a
|
|
@@ -129,9 +131,12 @@ class HomebrewFailed extends Error {
|
|
|
129
131
|
/** The branch moved before the deploy reservation: carry the approval to the new head, or file it for a person. */
|
|
130
132
|
class Restart extends Error {
|
|
131
133
|
head;
|
|
132
|
-
|
|
134
|
+
updated;
|
|
135
|
+
/** `updated`: Toolroll itself brought the branch up to main; the new commit is a new candidate and nothing carries. */
|
|
136
|
+
constructor(head, updated = false) {
|
|
133
137
|
super(`restart at ${head}`);
|
|
134
138
|
this.head = head;
|
|
139
|
+
this.updated = updated;
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
/** Every commit that landed after the approval, in words: the summary and the merge message list them all. */
|
|
@@ -453,6 +458,20 @@ export async function runRelease(options, adapters) {
|
|
|
453
458
|
};
|
|
454
459
|
// ---- one approval per release (release-once.md) -------------------------------------------------------------
|
|
455
460
|
const rel = adapters.release;
|
|
461
|
+
/** One-tap release approval: tell the plane, never stopping or changing the release whatever it answers. */
|
|
462
|
+
const tell = async (act) => { if (rel?.ready === undefined)
|
|
463
|
+
return; try {
|
|
464
|
+
await act(rel.ready);
|
|
465
|
+
}
|
|
466
|
+
catch { /* best effort */ } };
|
|
467
|
+
/** The last pull request checks this run read on the candidate (the gate reads them before the task exists). */
|
|
468
|
+
let lastCi = null;
|
|
469
|
+
const noteCi = async (runs, verdict) => {
|
|
470
|
+
lastCi = { candidate: j.candidate.sha, state: verdict.state === "pending" ? "running" : verdict.state, failed: verdict.failed ?? [], runs: runs.filter(run => REQUIRED_PR_CHECKS.includes(run.name)) };
|
|
471
|
+
const seen = lastCi;
|
|
472
|
+
await tell(ready => ready.ci({ taskId: j.task.id, ...seen }));
|
|
473
|
+
};
|
|
474
|
+
const currentCi = () => lastCi;
|
|
456
475
|
let epoch = null;
|
|
457
476
|
// This invocation's own lease owner (review round 1): two runs of the same journal never share an epoch.
|
|
458
477
|
const owner = `${j.id}:${randomUUID()}`;
|
|
@@ -612,11 +631,41 @@ export async function runRelease(options, adapters) {
|
|
|
612
631
|
/** identity(), on a plane that keeps release authority; a legacy plane keeps the old rules. */
|
|
613
632
|
const watch = async (action) => { if (rel !== undefined)
|
|
614
633
|
await identity(action, true); };
|
|
615
|
-
|
|
634
|
+
/**
|
|
635
|
+
* Behind main (plain-toolroll.md P2.6): update the branch under this release's lease and the head it was gated on.
|
|
636
|
+
* A concurrent push aborts it. The approval stops first, so nothing can ride it across the update; the updated head
|
|
637
|
+
* is a new candidate with a new approval, fresh CI and a fresh check, and there is no carry.
|
|
638
|
+
*/
|
|
639
|
+
const updateFromMain = async () => {
|
|
640
|
+
const update = adapters.github.updateBranch;
|
|
641
|
+
if (update === undefined)
|
|
642
|
+
return null;
|
|
643
|
+
if (rel !== undefined && j.authority) {
|
|
644
|
+
if (epoch === null || !await rel.renew(j.authority.id, owner, epoch))
|
|
645
|
+
throw new Stop("lease", "Another release coordinator took over this release.");
|
|
646
|
+
}
|
|
647
|
+
const pr = await adapters.github.pullRequest(j.github, j.branch);
|
|
648
|
+
if (pr === null || pr.number !== j.pr || pr.state !== "open" || pr.head !== j.candidate.sha)
|
|
649
|
+
throw new Stop("drift", `${j.branch} moved while it was being brought up to main. Nothing was changed; rerun \`${resume}\`.`);
|
|
650
|
+
if (rel !== undefined && j.authority && !await rel.needsApproval(j.authority.id, "the branch was brought up to main, so its new commit needs a fresh approval", epoch)) {
|
|
651
|
+
throw new Stop("lease", "The release's approval couldn't be stopped before the update, so the branch wasn't updated.");
|
|
652
|
+
}
|
|
653
|
+
say(` ${j.branch} is behind main: bringing it up to date`);
|
|
654
|
+
const moved = await update(j.github, j.pr, j.candidate.sha);
|
|
655
|
+
if (!moved.ok)
|
|
656
|
+
throw new Stop("drift", `${j.branch} couldn't be brought up to main: ${moved.why}. Rerun \`${resume}\`.`);
|
|
657
|
+
return waitFor("ci", `${j.branch} to move past ${j.candidate.sha.slice(0, 7)}`, async () => {
|
|
658
|
+
const head = await adapters.repo.remoteHead(j.branch);
|
|
659
|
+
return head !== null && head !== j.candidate.sha ? head : null;
|
|
660
|
+
});
|
|
661
|
+
};
|
|
662
|
+
const restart = async (head, updated = false) => {
|
|
616
663
|
// The step this commit was at ends here: the release goes on with the new commit (and, filed again, a new task).
|
|
617
664
|
mark(j.step, "failed", `${j.branch} moved to ${head.slice(0, 7)}`);
|
|
618
665
|
const tree = await adapters.repo.tree(head);
|
|
619
|
-
if (
|
|
666
|
+
if (updated)
|
|
667
|
+
say(` ${j.branch} is at ${head.slice(0, 7)}: a new commit, so it needs its own approval, CI and check`);
|
|
668
|
+
else if (rel !== undefined && j.authority && epoch !== null) {
|
|
620
669
|
say(` ${j.branch} moved to ${head.slice(0, 7)}: checking whether the approval carries`);
|
|
621
670
|
const carried = await rel.carry(j.authority.id, head, randomUUID(), epoch);
|
|
622
671
|
if (carried.ok) {
|
|
@@ -693,6 +742,7 @@ export async function runRelease(options, adapters) {
|
|
|
693
742
|
throw error;
|
|
694
743
|
}
|
|
695
744
|
const verdict = prChecksVerdict(runs, j.candidate.sha, REQUIRED_PR_CHECKS, { workflow: REQUIRED_PR_CHECKS_WORKFLOW });
|
|
745
|
+
await noteCi(runs, verdict);
|
|
696
746
|
if (verdict.state === "failed") {
|
|
697
747
|
throw new Stop("ci-failed", `GitHub's checks failed on ${j.candidate.sha.slice(0, 7)}: ${verdict.failed.join(", ")}. ${nothing}. ${j.approval === null ? "" : `The approval covers ${j.candidate.sha.slice(0, 7)} only. `}Rerun the failed job on GitHub and run \`${resume}\` again, or fix the branch and release the new commit with \`toolroll release ${releaseBranchArg(j.branch)} --repo ${j.checkout} --new\`.`);
|
|
698
748
|
}
|
|
@@ -793,7 +843,17 @@ export async function runRelease(options, adapters) {
|
|
|
793
843
|
}
|
|
794
844
|
// The tree CI tested (the pull request's head, not GitHub's merge commit) is the tree that merges only when the
|
|
795
845
|
// branch already contains main's head: refused here, before anything is filed, and again right before merging.
|
|
796
|
-
|
|
846
|
+
try {
|
|
847
|
+
await containsMain(adapters, j, "Nothing was filed or sent for approval");
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
if (!(error instanceof Stop) || error.reason !== "base-moved")
|
|
851
|
+
throw error;
|
|
852
|
+
const head = await updateFromMain();
|
|
853
|
+
if (head === null)
|
|
854
|
+
throw error;
|
|
855
|
+
throw new Restart(head, true);
|
|
856
|
+
}
|
|
797
857
|
if (j.intents.gate === undefined)
|
|
798
858
|
await mainProtection(adapters, j, false, "Nothing was filed or sent for approval");
|
|
799
859
|
const existing = await adapters.toolroll.gate(j.task.id);
|
|
@@ -810,7 +870,7 @@ export async function runRelease(options, adapters) {
|
|
|
810
870
|
// The gate only stops on a check that already failed: the task is filed and a person asked while CI runs, and
|
|
811
871
|
// the release check runs beside it (pipeline-speed.md, D2 and "Decision: a person's release asks during
|
|
812
872
|
// CI"). The automatic approval waits for CI (the check step); nothing deploys before it passes (the deploy).
|
|
813
|
-
const settled = await earlyChecks(adapters, j, (what, probe) => waitFor("gate", what, probe), { pendingIsEnough: true });
|
|
873
|
+
const settled = await earlyChecks(adapters, j, (what, probe) => waitFor("gate", what, probe), { pendingIsEnough: true, seen: (runs, verdict) => noteCi(runs, verdict) });
|
|
814
874
|
// Recorded before the task exists, so its approval card says CI still runs from the first moment.
|
|
815
875
|
if (!settled)
|
|
816
876
|
await rel?.auto?.ciRunning?.(j.task.id, j.candidate.sha);
|
|
@@ -830,6 +890,10 @@ export async function runRelease(options, adapters) {
|
|
|
830
890
|
...(rel === undefined ? {} : { release: { version: j.packageVersion, pr: j.pr, branch: j.branch, noCarry: options.noCarry === true } }),
|
|
831
891
|
});
|
|
832
892
|
j.task.scopeDigest = filed.scopeDigest;
|
|
893
|
+
// The checks the gate read before the task existed, kept for its approval sheet now that it does.
|
|
894
|
+
const seen = currentCi();
|
|
895
|
+
if (seen !== null)
|
|
896
|
+
await tell(ready => ready.ci({ taskId: j.task.id, ...seen }));
|
|
833
897
|
}
|
|
834
898
|
advance("approval");
|
|
835
899
|
break;
|
|
@@ -862,7 +926,11 @@ export async function runRelease(options, adapters) {
|
|
|
862
926
|
// A check that already failed asks nobody (as at the gate).
|
|
863
927
|
if (j.ciAhead === true)
|
|
864
928
|
await ciWhileRunning("Nothing was deployed or merged", true);
|
|
929
|
+
// One push, on every entry to this step (a fresh filing or a resumed one): the plane files it once per task and
|
|
930
|
+
// scope digest, so a crash between filing and here is healed by the next run.
|
|
931
|
+
await tell(ready => ready.asked(j.task.id));
|
|
865
932
|
say(` The release scope needs your approval: \`toolroll task approve ${j.task.id} --digest ${j.task.scopeDigest}\`, or open the task in the console.`);
|
|
933
|
+
say(` Or approve it from here: \`toolroll release approve ${releaseBranchArg(j.branch)}\`.`);
|
|
866
934
|
if (j.ciAhead === true)
|
|
867
935
|
say(` ${CI_RUNNING_WORDS}`);
|
|
868
936
|
const approved = await waitFor("approval", `a person to approve task ${j.task.id}`, async () => {
|
|
@@ -940,6 +1008,15 @@ export async function runRelease(options, adapters) {
|
|
|
940
1008
|
else if (standing.retried.length > 0)
|
|
941
1009
|
say(` run ${gate.result.run} passed, but only on a retry: ${standing.retried.join(", ")}`);
|
|
942
1010
|
}
|
|
1011
|
+
// A sandboxed Full check must show that what it skipped passed elsewhere (trusted-release-reports.md H3). The plane
|
|
1012
|
+
// answers at once for a check that ran unsandboxed, as Toolroll's own do: nothing there skips for this reason.
|
|
1013
|
+
if (rel?.fencedCoverage !== undefined) {
|
|
1014
|
+
const covered = await rel.fencedCoverage(j.task.id, gate.result.run, j.candidate.sha);
|
|
1015
|
+
if (!covered.ok)
|
|
1016
|
+
throw new Stop("check-failed", `Run ${gate.result.run} passed on ${j.candidate.sha.slice(0, 7)}, but ${covered.why}.`);
|
|
1017
|
+
if (covered.covered > 0)
|
|
1018
|
+
say(` ${covered.covered} cases skipped inside the sandbox passed in this commit's macOS CI`);
|
|
1019
|
+
}
|
|
943
1020
|
return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree, head };
|
|
944
1021
|
};
|
|
945
1022
|
const result = await waitFor("check", `the release check of ${j.candidate.sha.slice(0, 7)}`, async () => {
|
|
@@ -1007,7 +1084,12 @@ export async function runRelease(options, adapters) {
|
|
|
1007
1084
|
await checked(gate, "complete");
|
|
1008
1085
|
if (gate.completion?.digest !== check.receipt) {
|
|
1009
1086
|
intend("complete");
|
|
1010
|
-
|
|
1087
|
+
// Toolroll proves the check's build stopped by itself (stop-proof.ts) a short while after it finished: the
|
|
1088
|
+
// release waits for that, never for a person to confirm it.
|
|
1089
|
+
const done = await waitFor("quiet", "Toolroll to make sure the release check stopped", async () => {
|
|
1090
|
+
const tried = await adapters.toolroll.complete(j.task.id, check.receipt);
|
|
1091
|
+
return !tried.ok && tried.reason === "stopping" ? null : tried;
|
|
1092
|
+
});
|
|
1011
1093
|
if (!done.ok)
|
|
1012
1094
|
throw new Stop("complete", `Couldn't mark task ${j.task.id} complete: ${done.message} Mark that exact result complete in the console, then rerun.`);
|
|
1013
1095
|
}
|
|
@@ -1147,7 +1229,21 @@ export async function runRelease(options, adapters) {
|
|
|
1147
1229
|
await identity("merge", false);
|
|
1148
1230
|
await pinned();
|
|
1149
1231
|
// A squash merge only reproduces the gated (and CI-tested) tree when main hasn't moved past the branch's base.
|
|
1150
|
-
|
|
1232
|
+
let main;
|
|
1233
|
+
try {
|
|
1234
|
+
main = await containsMain(adapters, j, "Nothing was merged");
|
|
1235
|
+
}
|
|
1236
|
+
catch (error) {
|
|
1237
|
+
if (!(error instanceof Stop) || error.reason !== "base-moved")
|
|
1238
|
+
throw error;
|
|
1239
|
+
// Past the deploy: the updated commit isn't what's running, so it starts over, with a person.
|
|
1240
|
+
const head = await updateFromMain();
|
|
1241
|
+
if (head === null)
|
|
1242
|
+
throw error;
|
|
1243
|
+
j.needsApproval = true;
|
|
1244
|
+
save();
|
|
1245
|
+
throw new Stop("approval", `${j.branch} was behind main, so it's now at ${head.slice(0, 7)}. Nothing was merged. The new commit needs a person's approval: run \`toolroll release ${j.branch} --repo ${j.checkout} --new\`.`);
|
|
1246
|
+
}
|
|
1151
1247
|
// Every release merges only where GitHub itself refuses a branch behind main.
|
|
1152
1248
|
await mainProtection(adapters, j, j.auto?.state === "approved", "Nothing was merged");
|
|
1153
1249
|
// The checks again, at the moment of merging: a rerun that failed since must not merge.
|
|
@@ -1202,6 +1298,8 @@ export async function runRelease(options, adapters) {
|
|
|
1202
1298
|
merge.tree = tree;
|
|
1203
1299
|
if (tree !== j.candidate.tree)
|
|
1204
1300
|
throw new Stop("tree-mismatch", `Main's merge commit ${merge.sha.slice(0, 7)} has tree ${tree.slice(0, 12)}, not the gated ${j.candidate.tree.slice(0, 12)}: main changed what was checked. Nothing was tagged; the deployed build stays as it is.`);
|
|
1301
|
+
// The release's verified record (trusted-release-reports.md A1): the next release's change list starts here.
|
|
1302
|
+
await tell(ready => ready.verified({ taskId: j.task.id, merge: merge.sha }));
|
|
1205
1303
|
advance("tag");
|
|
1206
1304
|
break;
|
|
1207
1305
|
}
|
|
@@ -1260,7 +1358,7 @@ export async function runRelease(options, adapters) {
|
|
|
1260
1358
|
catch (error) {
|
|
1261
1359
|
if (!(error instanceof Restart))
|
|
1262
1360
|
throw error;
|
|
1263
|
-
await restart(error.head);
|
|
1361
|
+
await restart(error.head, error.updated);
|
|
1264
1362
|
}
|
|
1265
1363
|
}
|
|
1266
1364
|
const left = homebrewLeft;
|
|
@@ -1288,6 +1386,14 @@ export async function runRelease(options, adapters) {
|
|
|
1288
1386
|
}
|
|
1289
1387
|
catch { /* the stop is still reported */ }
|
|
1290
1388
|
mark(journal.step, "failed", stop.message);
|
|
1389
|
+
// A person's release that stops tells them (trusted-release-reports.md A5); the automatic road keeps its own notices.
|
|
1390
|
+
const ready = adapters.release?.ready;
|
|
1391
|
+
if (ready !== undefined && journal.auto?.state !== "checking" && journal.auto?.state !== "approved") {
|
|
1392
|
+
try {
|
|
1393
|
+
await ready.stopped({ taskId: journal.task.id, step: journal.step, reason: stop.reason, message: stop.message });
|
|
1394
|
+
}
|
|
1395
|
+
catch { /* best effort */ }
|
|
1396
|
+
}
|
|
1291
1397
|
}
|
|
1292
1398
|
return { ok: false, reason: stop.reason, message: stop.message, step: journal?.step ?? step, journal, file, resume };
|
|
1293
1399
|
}
|
|
@@ -1428,6 +1534,7 @@ async function earlyChecks(adapters, j, wait, options = {}) {
|
|
|
1428
1534
|
return null;
|
|
1429
1535
|
}
|
|
1430
1536
|
const verdict = prChecksVerdict(runs.filter(run => required.includes(run.name)), j.candidate.sha, required, { workflow: REQUIRED_PR_CHECKS_WORKFLOW });
|
|
1537
|
+
await options.seen?.(runs, verdict);
|
|
1431
1538
|
if (verdict.state === "failed") {
|
|
1432
1539
|
throw new Stop("ci-failed", `Pull request checks already failed on ${sha}: ${verdict.failed.join(", ")}. ${nothing}. Fix the branch, then release the new commit with \`toolroll release ${j.branch} --repo ${j.checkout} --new\` (or rerun the failed job on GitHub and run this command again).`);
|
|
1433
1540
|
}
|
|
@@ -1616,6 +1723,6 @@ export function workflowIs(path, file) {
|
|
|
1616
1723
|
return /^[A-Za-z0-9._+/-]+$/.test(ref) && !/\.ya?ml$/i.test(ref);
|
|
1617
1724
|
}
|
|
1618
1725
|
/** The words `toolroll release` reads as its own (release-cli.ts), never as a branch: such a branch is named with --branch. */
|
|
1619
|
-
const RELEASE_WORDS = new Set(["status", "nightly", "carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"]);
|
|
1726
|
+
const RELEASE_WORDS = new Set(["status", "nightly", "carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok", "approve"]);
|
|
1620
1727
|
/** How a person names this branch to `toolroll release` again. */
|
|
1621
1728
|
export function releaseBranchArg(branch) { return RELEASE_WORDS.has(branch) ? `--branch ${branch}` : branch; }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type Lookup = (host: string) => Promise<readonly string[]>;
|
|
2
|
+
export declare const lookupAll: Lookup;
|
|
3
|
+
export type Destination = {
|
|
4
|
+
scheme: "https" | "ssh";
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
literal: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type Pin = {
|
|
10
|
+
/** `-c` settings for git, or nothing for ssh. */
|
|
11
|
+
gitArgs: string[];
|
|
12
|
+
/** Environment on top of remote-main.ts's ENV. */
|
|
13
|
+
env: Record<string, string>;
|
|
14
|
+
/** The proxy's host name when the read goes through the operator's proxy. */
|
|
15
|
+
proxy: string | null;
|
|
16
|
+
};
|
|
17
|
+
export type PinResult = {
|
|
18
|
+
ok: true;
|
|
19
|
+
pin: Pin;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
why: string;
|
|
23
|
+
};
|
|
24
|
+
/** Whether an address is loopback, unspecified, link-local, or an IPv4-mapped or NAT64 form of these. */
|
|
25
|
+
export declare function addressRefused(address: string): boolean;
|
|
26
|
+
/** The host, effective port and scheme a recorded URL connects to; null when it can't be read. The caller has already
|
|
27
|
+
* passed remoteUrlProblem. Node's URL parser normalizes every spelling of a literal host. */
|
|
28
|
+
export declare function destinationOf(url: string): Destination | null;
|
|
29
|
+
/** Whether curl would skip the proxy for this host; "unknown" when an entry can't be decided. */
|
|
30
|
+
export declare function noProxyApplies(noProxy: string, host: string): boolean | "unknown";
|
|
31
|
+
/**
|
|
32
|
+
* Decide the route and pin it. `env` is what git will see (the names remote-main.ts's ENV_ALLOWLIST passes). ssh never
|
|
33
|
+
* uses a proxy. Through a proxy, the proxy's own address is checked and pinned; the operator's proxy then chooses the
|
|
34
|
+
* final destination, and the caller says so.
|
|
35
|
+
*/
|
|
36
|
+
export declare function pinDestination(url: string, env: Readonly<Record<string, string | undefined>>, lookup?: Lookup): Promise<PinResult>;
|