toolroll 0.9.93 → 0.9.95
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 +79 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +10 -0
- package/dist/agent-fence.js +6 -3
- 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 -1
- package/dist/assignment.js +5 -0
- package/dist/attest.d.ts +3 -0
- package/dist/attest.js +10 -0
- 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 +1 -1
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-crew.js +2 -0
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +50 -2
- package/dist/browser-workspace.js +3 -3
- package/dist/build-review.js +2 -0
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +11 -2
- package/dist/builder.js +234 -101
- package/dist/card-catalog.d.ts +209 -0
- package/dist/card-catalog.js +133 -0
- package/dist/card-cli.d.ts +43 -0
- package/dist/card-cli.js +44 -0
- package/dist/card-render.d.ts +26 -0
- package/dist/card-render.js +223 -0
- package/dist/card-sources.d.ts +68 -0
- package/dist/card-sources.js +234 -0
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +106 -9
- package/dist/chat-actions.js +473 -46
- package/dist/chat-approval.d.ts +13 -0
- package/dist/chat-approval.js +39 -9
- package/dist/chat-card-part.d.ts +11 -0
- package/dist/chat-card-part.js +18 -0
- package/dist/chat-card.d.ts +46 -0
- package/dist/chat-card.js +99 -0
- package/dist/chat-channel.d.ts +2 -0
- package/dist/chat-channel.js +37 -14
- package/dist/chat-controls.d.ts +4 -0
- package/dist/chat-controls.js +4 -0
- package/dist/chat-decide.d.ts +10 -16
- package/dist/chat-decide.js +79 -62
- package/dist/chat-delivery.js +13 -4
- package/dist/chat-flow.d.ts +0 -27
- package/dist/chat-flow.js +31 -2
- package/dist/chat-quiet.js +4 -4
- package/dist/chat-task-actions.d.ts +3 -2
- package/dist/chat-task-actions.js +19 -5
- package/dist/chat-terms.d.ts +14 -2
- package/dist/chat-terms.js +35 -0
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +3 -1
- package/dist/check-facts.js +57 -11
- package/dist/check-failure.d.ts +24 -3
- package/dist/check-failure.js +89 -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-repair.d.ts +47 -0
- package/dist/check-repair.js +240 -0
- package/dist/check-report.d.ts +2 -0
- package/dist/check-report.js +1 -1
- 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/claim.d.ts +6 -0
- package/dist/claim.js +13 -2
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +49 -30
- package/dist/cli.js +15 -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 +3 -1
- package/dist/coding-provider.js +12 -7
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.d.ts +1 -1
- package/dist/coding-workspace.js +8 -8
- 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 +92 -0
- package/dist/contracts/chat-actions.js +20 -0
- package/dist/contracts/chat-card.d.ts +91 -0
- package/dist/contracts/chat-card.js +54 -0
- package/dist/contracts/chat-content.d.ts +191 -0
- package/dist/contracts/chat-content.js +4 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +12 -3
- package/dist/contracts/console-api.js +12 -3
- package/dist/contracts/flow-propose.d.ts +8 -1
- package/dist/contracts/flow-propose.js +3 -1
- package/dist/contracts/flow-send.d.ts +126 -0
- package/dist/contracts/flow-send.js +3 -0
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +234 -0
- package/dist/contracts/lead-tools.js +25 -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/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +18 -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/discord-chat.js +14 -0
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +25 -16
- package/dist/dispose.d.ts +19 -0
- package/dist/dispose.js +55 -9
- package/dist/easy-start.js +2 -3
- package/dist/eval-oracle.js +4 -1
- package/dist/evals.js +2 -1
- package/dist/evidence-pair.d.ts +53 -0
- package/dist/evidence-pair.js +128 -0
- package/dist/exhaustion.d.ts +17 -4
- package/dist/exhaustion.js +44 -4
- package/dist/export.js +1 -1
- package/dist/failing-first.d.ts +31 -0
- package/dist/failing-first.js +44 -0
- package/dist/fallback-schema.d.ts +8 -0
- package/dist/fallback-schema.js +40 -0
- package/dist/fallback.d.ts +138 -0
- package/dist/fallback.js +605 -0
- 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/findings.d.ts +224 -0
- package/dist/findings.js +449 -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 +75 -9
- package/dist/flaky.js +255 -0
- package/dist/fleet-schema.d.ts +17 -0
- package/dist/fleet-schema.js +135 -0
- package/dist/flow-browser.js +2 -2
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +9 -5
- package/dist/flow-result-card.d.ts +22 -0
- package/dist/flow-result-card.js +55 -0
- package/dist/flow-send.js +12 -9
- package/dist/flow-steps.js +1 -1
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +2 -2
- package/dist/flow-unattended.d.ts +26 -0
- package/dist/flow-unattended.js +12 -3
- package/dist/flows-ui.js +10 -2
- package/dist/follow-through-brief.d.ts +12 -0
- package/dist/follow-through-brief.js +31 -0
- package/dist/follow-through-cli.d.ts +23 -0
- package/dist/follow-through-cli.js +153 -0
- package/dist/git-incident.d.ts +16 -0
- package/dist/git-incident.js +69 -6
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/handover-note.d.ts +33 -0
- package/dist/handover-note.js +106 -0
- package/dist/invoke.d.ts +25 -2
- package/dist/invoke.js +157 -58
- 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-cli.js +26 -24
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +3 -2
- package/dist/lead-doors.d.ts +4 -0
- package/dist/lead-doors.js +2 -2
- package/dist/lead-progress.js +5 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +162 -34
- package/dist/lead.js +2 -2
- 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 +16 -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/modes.d.ts +6 -6
- package/dist/modes.js +3 -1
- package/dist/needs-you.d.ts +31 -9
- package/dist/needs-you.js +110 -41
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +7 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +565 -64
- 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/plan-login.d.ts +14 -0
- package/dist/plan-login.js +58 -0
- package/dist/planner.d.ts +1 -1
- package/dist/planner.js +4 -2
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +19 -2
- package/dist/project-delete-ui.js +29 -1
- package/dist/project-delete.js +14 -1
- 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/project-tools.d.ts +4 -4
- package/dist/project-tools.js +23 -7
- package/dist/proof.js +30 -5
- package/dist/protected-storage-look.d.ts +1 -0
- package/dist/protected-storage-look.js +13 -0
- package/dist/protected-storage.d.ts +70 -39
- package/dist/protected-storage.js +196 -147
- package/dist/provider-auth.js +1 -1
- package/dist/provider-capture.d.ts +34 -0
- package/dist/provider-capture.js +53 -0
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +18 -2
- package/dist/push.js +40 -16
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +76 -0
- package/dist/release-auto.js +13 -13
- package/dist/release-card.d.ts +1 -2
- package/dist/release-card.js +11 -0
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +49 -10
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +29 -22
- 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 +20 -1
- package/dist/release.js +85 -9
- 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 +27 -19
- 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 +28 -4
- package/dist/scope.js +92 -28
- package/dist/scout.d.ts +1 -1
- package/dist/scout.js +2 -1
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +26 -18
- package/dist/server/chat.js +16 -14
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +73 -0
- package/dist/server/guards.js +3 -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 +16 -3
- 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-follow-through.d.ts +11 -0
- package/dist/server/render-follow-through.js +68 -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 +2 -0
- package/dist/server/render-release.js +3 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +68 -68
- package/dist/server/render-tasks.d.ts +16 -2
- package/dist/server/render-tasks.js +177 -141
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +48 -12
- package/dist/server/settings.js +166 -34
- package/dist/server/task-views.js +66 -26
- package/dist/server/tasks.d.ts +2 -6
- package/dist/server/tasks.js +250 -321
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.d.ts +1 -0
- package/dist/settings-groups-ui.js +4 -2
- package/dist/settings-kit.js +3 -2
- package/dist/settings-proofs.js +3 -1
- package/dist/slack-chat.js +14 -1
- package/dist/steer-mailbox.d.ts +8 -1
- package/dist/steer-mailbox.js +164 -6
- package/dist/stop-proof.js +6 -4
- 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 +89 -7
- package/dist/store.js +386 -114
- package/dist/surface.js +67 -6
- 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.d.ts +2 -1
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +17 -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 +45 -49
- package/dist/teardown-git.d.ts +18 -0
- package/dist/teardown-git.js +105 -21
- package/dist/telegram-lead.js +23 -6
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/telegram.js +12 -1
- 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.js +19 -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 +95 -66
- package/dist/work-summary.d.ts +1 -1
- package/dist/work-summary.js +7 -2
- package/dist/workspace-ui.d.ts +7 -3
- package/dist/workspace-ui.js +38 -23
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +91 -14
- package/package.json +2 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
package/dist/release-cli.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { type ReleaseAdapters } from "./release.js";
|
|
9
9
|
import { type GateReleaseWorld } from "./release-gate.js";
|
|
10
10
|
import { type RescueWorld } from "./deploy-rescue.js";
|
|
11
|
+
import { type RunLockSeams, type SettleWorld } from "./release-next.js";
|
|
11
12
|
export declare const RELEASE_HELP: string;
|
|
12
13
|
/** A runner lease id (claim.ts mints randomUUID()). */
|
|
13
14
|
export declare const LEASE_SHAPED: RegExp;
|
|
@@ -23,6 +24,9 @@ export type ReleaseCliDeps = {
|
|
|
23
24
|
gateWorld?: GateReleaseWorld;
|
|
24
25
|
/** What --recover-deploy reads and runs (deploy-rescue.ts); the installation's own by default. */
|
|
25
26
|
rescueWorld?: RescueWorld;
|
|
27
|
+
/** What a stopped deployment left, settled before the release goes on (release-next.ts). Tests that inject adapters skip it. */
|
|
28
|
+
settle?: SettleWorld;
|
|
29
|
+
runLock?: RunLockSeams;
|
|
26
30
|
/** The plane's database for the authority commands (default: this installation's). */
|
|
27
31
|
database?: string;
|
|
28
32
|
now?: () => Date;
|
package/dist/release-cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { RELEASE_LIMITS, runRelease } from "./release.js";
|
|
|
16
16
|
import { processes, releaseAdapters } from "./release-adapters.js";
|
|
17
17
|
import { journals, releaseGate } from "./release-gate.js";
|
|
18
18
|
import { rescueDeployment } from "./deploy-rescue.js";
|
|
19
|
+
import { settleDeployment, takeReleaseRun } from "./release-next.js";
|
|
19
20
|
import { databasePath, elapsedSeconds, latestReleaseStepTask, openStore, releaseProgressFor, revokeRelease } from "./store.js";
|
|
20
21
|
import { dur } from "./design/kit.js";
|
|
21
22
|
import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxedCarry } from "./release-carry.js";
|
|
@@ -34,7 +35,10 @@ export const RELEASE_HELP = `toolroll release <branch> — release Toolroll itse
|
|
|
34
35
|
squash-merges, deletes the matching remote branch, verifies the merged tree and tags. Both macOS legs gate npm
|
|
35
36
|
publication; Homebrew waits for its checks to register and all pass before merging.
|
|
36
37
|
|
|
37
|
-
Run the same command to
|
|
38
|
+
Run the same command to go on after any stop. It reads where the release got to and does the one thing that's next:
|
|
39
|
+
finishes a deployment forward, puts one back, lifts a pause left before a swap, waits for another run of the release
|
|
40
|
+
(naming its computer and process), or waits for your approval. It says what it did. A branch behind main is brought up
|
|
41
|
+
to date for you; that new commit needs its own approval, CI and check.
|
|
38
42
|
|
|
39
43
|
A branch whose name is one of this command's own words (approve, status, nightly, carry…) is named with --branch <name>
|
|
40
44
|
or after --: \`toolroll release --branch status\`.
|
|
@@ -47,19 +51,6 @@ toolroll release approve [<branch>] [--repo <path>] — approve the release wait
|
|
|
47
51
|
toolroll release status [<task>] — the steps of a release (the newest one recorded, or the release task named): each
|
|
48
52
|
step's state and how long it took, and where it stopped
|
|
49
53
|
|
|
50
|
-
toolroll release --release-gate <id> [--yes] — lift the update pause a deployment left before its swap
|
|
51
|
-
|
|
52
|
-
Shows why it is safe first: the pause is that deployment's, its journal stopped before the swap, and the
|
|
53
|
-
database, service definition and running service are all still the previous build's. --yes lifts that one pause.
|
|
54
|
-
Any missing or contradicting evidence refuses and leaves the pause in place.
|
|
55
|
-
|
|
56
|
-
toolroll release --recover-deploy [--stage <dir>] [--yes] — put back a deployment that stopped half way
|
|
57
|
-
|
|
58
|
-
Finds the one unfinished deployment (or the stage named) and shows what its recovery will do; --yes does it. When
|
|
59
|
-
the old service isn't proved stopped, it boots the service out and stops what it left running, but only processes
|
|
60
|
-
proved, by exact runtime path and arguments, to be that deployment's own; then it starts the previous service
|
|
61
|
-
again, waits for it, and lifts the update pause. Anything it can't prove refuses. Safe to run again.
|
|
62
|
-
|
|
63
54
|
toolroll release carry <release> --to <commit> [--request <id>] — carry a person's release approval to a later commit
|
|
64
55
|
|
|
65
56
|
Only tests, docs and the changelog may change after the approval, within the limits, with an identical production
|
|
@@ -100,10 +91,6 @@ Options
|
|
|
100
91
|
--new release a new commit on the branch, setting the earlier unfinished release aside
|
|
101
92
|
--limit <step>=<min> change one step's time limit (${Object.entries(RELEASE_LIMITS).map(([step, minutes]) => `${step} ${minutes}`).join(", ")})
|
|
102
93
|
--tap <owner/name> the Homebrew tap (default ap9000/homebrew-toolroll)
|
|
103
|
-
--release-gate <id> check (and with --yes, lift) one update pause left before a deployment's swap
|
|
104
|
-
--yes with --release-gate: lift the pause once it is proved safe; with --recover-deploy: recover
|
|
105
|
-
--recover-deploy recover the unfinished deployment (see above)
|
|
106
|
-
--stage <dir> with --recover-deploy: the deployment's staging folder, when more than one is unfinished
|
|
107
94
|
--json answer with one machine envelope
|
|
108
95
|
|
|
109
96
|
A runner's lease is returned with \`toolroll worker release <lease>\`.`;
|
|
@@ -270,8 +257,23 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
270
257
|
const adapters = deps.adapters === undefined
|
|
271
258
|
? releaseAdapters({ checkout, say, ...(tap === undefined ? {} : { tap }), ...(limits["deploy"] === undefined ? {} : { deployMinutes: limits["deploy"] }) })
|
|
272
259
|
: { ...deps.adapters, say };
|
|
273
|
-
|
|
274
|
-
|
|
260
|
+
// One command: wait out another run of this release, settle what a stopped deployment left, then go on from the journal.
|
|
261
|
+
const taken = await takeReleaseRun(stateDir, branch, say, deps.runLock);
|
|
262
|
+
if (!taken.ok)
|
|
263
|
+
return refuse("running", `Another release of ${branch} is still running on ${taken.holder.host} (process ${taken.holder.pid}). Nothing was changed.`, 3);
|
|
264
|
+
try {
|
|
265
|
+
const world = deps.settle ?? (deps.adapters === undefined ? { rescue: installedRescueWorld(checkout), gate: installedGateWorld() } : null);
|
|
266
|
+
if (world !== null) {
|
|
267
|
+
const settled = await settleDeployment(world, say);
|
|
268
|
+
if (!settled.ok)
|
|
269
|
+
return refuse("deployment", settled.message, 3);
|
|
270
|
+
}
|
|
271
|
+
const outcome = await runRelease({ branch, checkout, stateDir, limits, fresh: bare.has("--new"), noCarry: bare.has("--no-carry") }, adapters);
|
|
272
|
+
return report(outcome, write, json, lines);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
taken.release();
|
|
276
|
+
}
|
|
275
277
|
}
|
|
276
278
|
function report(outcome, write, json, lines) {
|
|
277
279
|
const release = outcome.journal === null ? null : {
|
|
@@ -590,10 +592,15 @@ async function runStatusCommand(task, write, json, deps) {
|
|
|
590
592
|
}
|
|
591
593
|
const word = { running: "running", waiting: "waiting", done: "done", failed: "failed" };
|
|
592
594
|
write(`Release ${progress.version} (${progress.tag})${progress.pr === null ? "" : ` · pull request #${progress.pr}`} · task ${taskId}`);
|
|
593
|
-
for (const one of steps)
|
|
594
|
-
|
|
595
|
+
for (const one of steps) {
|
|
596
|
+
// A failure the release has gone on from is history, as on the task page.
|
|
597
|
+
const passed = one.state === "failed" && progress.stopped === null;
|
|
598
|
+
write(` ${one.step.padEnd(9)} ${(passed ? "went on" : word[one.state]).padEnd(8)} ${one.seconds === null ? "–" : dur(one.seconds)}${one.detail === null ? "" : ` ${one.detail}`}`);
|
|
599
|
+
}
|
|
595
600
|
if (progress.stopped !== null)
|
|
596
601
|
write(`Stopped at ${progress.stopped.step}: ${progress.stopped.message}`);
|
|
602
|
+
if (progress.building !== null)
|
|
603
|
+
write(`Build #${progress.building.run} is running the check.`);
|
|
597
604
|
return 0;
|
|
598
605
|
}
|
|
599
606
|
finally {
|
package/dist/release-coverage.js
CHANGED
|
@@ -26,7 +26,7 @@ export const gitIsAncestor = (repo, ancestor, descendant) => {
|
|
|
26
26
|
let answer;
|
|
27
27
|
try {
|
|
28
28
|
// Every git version through a frozen read view, bounded (eval-harness review 1, finding 7).
|
|
29
|
-
const status = viewedGitSync(repo, ["merge-base", "--is-ancestor", ancestor, descendant], { timeoutMs: 5_000 }).status;
|
|
29
|
+
const status = viewedGitSync(repo, ["merge-base", "--is-ancestor", ancestor, descendant], { timeoutMs: 5_000, binding: { repo } }).status;
|
|
30
30
|
if (status !== 0)
|
|
31
31
|
throw Object.assign(new Error("git merge-base"), { status });
|
|
32
32
|
answer = true;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type RescueWorld } from "./deploy-rescue.js";
|
|
2
|
+
import { type GateReleaseWorld } from "./release-gate.js";
|
|
3
|
+
import { type ReleaseJournal } from "./release.js";
|
|
4
|
+
import type { Store } from "./store.js";
|
|
5
|
+
/** How long a run waits for another before it says so and stops. */
|
|
6
|
+
export declare const RELEASE_WAIT_MS: number;
|
|
7
|
+
export type RunHolder = {
|
|
8
|
+
host: string;
|
|
9
|
+
pid: number;
|
|
10
|
+
};
|
|
11
|
+
export type RunLockSeams = {
|
|
12
|
+
host?: string;
|
|
13
|
+
pid?: number;
|
|
14
|
+
alive?: (pid: number) => boolean;
|
|
15
|
+
sleep?: (ms: number) => Promise<void>;
|
|
16
|
+
now?: () => number;
|
|
17
|
+
waitMs?: number;
|
|
18
|
+
pollMs?: number;
|
|
19
|
+
};
|
|
20
|
+
/** Take this branch's run lock, waiting for a live holder. Answers how to give it back, or who kept it. */
|
|
21
|
+
export declare function takeReleaseRun(stateDir: string, branch: string, say: (line: string) => void, seams?: RunLockSeams): Promise<{
|
|
22
|
+
ok: true;
|
|
23
|
+
release: () => void;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
holder: RunHolder;
|
|
27
|
+
}>;
|
|
28
|
+
export type SettleWorld = {
|
|
29
|
+
rescue: RescueWorld;
|
|
30
|
+
gate: GateReleaseWorld;
|
|
31
|
+
};
|
|
32
|
+
export type DeployAction = "none" | "lift-pause" | "forward" | "roll-back" | "refuse";
|
|
33
|
+
/** The one action a deployment's journal calls for. */
|
|
34
|
+
export declare function deployActionOf(phase: unknown): DeployAction;
|
|
35
|
+
/** Settle what a stopped deployment left, saying what was done. A refusal stops the release before it does anything. */
|
|
36
|
+
export declare function settleDeployment(world: SettleWorld, say: (line: string) => void): Promise<{
|
|
37
|
+
ok: true;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
message: string;
|
|
41
|
+
}>;
|
|
42
|
+
export declare const RELEASE_RESUMED_ACTION = "release resumed";
|
|
43
|
+
/**
|
|
44
|
+
* A person let a held release go on (release-auto.ts allowAutoPublish; the Release sheet's button or `publish-ok`): each
|
|
45
|
+
* sweep, its journal goes on from where it stopped, once per that person's act, under the same run lock. It was merged
|
|
46
|
+
* already, so only the tag, publication and Homebrew are left, and each re-proves itself.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resumeAllowedReleases(store: Store, stateDir: string, run: (journal: ReleaseJournal) => Promise<unknown>, now: Date): Promise<number>;
|
|
@@ -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,15 @@ 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
|
+
}>;
|
|
335
346
|
/** One-tap release approval (docs/plans/trusted-release-reports.md, slice A): what the plane tells a person and
|
|
336
347
|
* keeps for the approval sheet. Best effort: a failure here never changes the release. Absent on an older plane. */
|
|
337
348
|
ready?: ReleaseReadyAdapter;
|
|
@@ -536,6 +547,14 @@ export type ReleaseAdapters = {
|
|
|
536
547
|
mainHead(repo: string): Promise<string>;
|
|
537
548
|
/** Whether `sha` already holds `ancestor`, by GitHub's own comparison. */
|
|
538
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
|
+
}>;
|
|
539
558
|
/** Whether GitHub refuses to merge a branch that is behind main ("Require branches to be up to date before
|
|
540
559
|
* merging"); null when it can't be read. Read only. */
|
|
541
560
|
upToDateRequired(repo: string): Promise<MainProtection | null>;
|
package/dist/release.js
CHANGED
|
@@ -93,6 +93,8 @@ export const ADVISORY_PR_CHECKS = {
|
|
|
93
93
|
"youiyoux · UI drift": ".github/workflows/youiyoux.yml",
|
|
94
94
|
"journeys · app (scripted)": ".github/workflows/journeys.yml",
|
|
95
95
|
"journeys · flows (scripted)": ".github/workflows/journeys.yml",
|
|
96
|
+
"upgrade path · scope": ".github/workflows/upgrade-path.yml",
|
|
97
|
+
"upgrade path": ".github/workflows/upgrade-path.yml",
|
|
96
98
|
};
|
|
97
99
|
/** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
|
|
98
100
|
* the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
|
|
@@ -101,8 +103,10 @@ export const ADVISORY_PR_CHECKS = {
|
|
|
101
103
|
export const RELEASE_TAG_CHECKS = ["release proof", "publish"];
|
|
102
104
|
export const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
|
|
103
105
|
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
|
-
|
|
106
|
+
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). The
|
|
107
|
+
* check's covers the first attempt and the two same-commit reruns the plane may give it, each of which a busy machine
|
|
108
|
+
* may stretch to two hours (check-load.ts). */
|
|
109
|
+
export const RELEASE_LIMITS = { gate: 60, approval: 24 * 60, check: 360, quiet: 60, deploy: 30, ci: 60, publish: 45, homebrew: 45 };
|
|
106
110
|
const POLL_MS = 15_000;
|
|
107
111
|
/**
|
|
108
112
|
* Failure classes replace a flat flake allowance (docs/plans/eval-harness.md; check-failure.ts says the classes): only a
|
|
@@ -129,9 +133,12 @@ class HomebrewFailed extends Error {
|
|
|
129
133
|
/** The branch moved before the deploy reservation: carry the approval to the new head, or file it for a person. */
|
|
130
134
|
class Restart extends Error {
|
|
131
135
|
head;
|
|
132
|
-
|
|
136
|
+
updated;
|
|
137
|
+
/** `updated`: Toolroll itself brought the branch up to main; the new commit is a new candidate and nothing carries. */
|
|
138
|
+
constructor(head, updated = false) {
|
|
133
139
|
super(`restart at ${head}`);
|
|
134
140
|
this.head = head;
|
|
141
|
+
this.updated = updated;
|
|
135
142
|
}
|
|
136
143
|
}
|
|
137
144
|
/** Every commit that landed after the approval, in words: the summary and the merge message list them all. */
|
|
@@ -244,6 +251,8 @@ export async function runRelease(options, adapters) {
|
|
|
244
251
|
let step = "preflight";
|
|
245
252
|
let freshApproval = false;
|
|
246
253
|
let supersededTask = null;
|
|
254
|
+
/** The commit whose check stopped the release being set aside. The stop tells a person to approve it again with --new. */
|
|
255
|
+
let stoppedCheck = null;
|
|
247
256
|
const save = () => { journal.updatedAt = adapters.now().toISOString(); writeJournal(file, journal); };
|
|
248
257
|
/** Tell the progress record about a step (v146). Never throws and never waits: the release goes on whatever it says. */
|
|
249
258
|
const mark = (at, state, detail = null) => {
|
|
@@ -317,6 +326,8 @@ export async function runRelease(options, adapters) {
|
|
|
317
326
|
say(`Kept the earlier release journal at ${aside}.`);
|
|
318
327
|
freshApproval = journal.needsApproval === true;
|
|
319
328
|
supersededTask = journal.task.id;
|
|
329
|
+
if (journal.stopped?.reason === "check-failed")
|
|
330
|
+
stoppedCheck = journal.candidate.sha;
|
|
320
331
|
journal = null;
|
|
321
332
|
}
|
|
322
333
|
if (journal?.step === "done") {
|
|
@@ -331,8 +342,10 @@ export async function runRelease(options, adapters) {
|
|
|
331
342
|
journal = await begin(options, adapters, adapters.now());
|
|
332
343
|
// The earlier attempt needs a person's fresh approval (a CI failure after its deploy): this one files a task of its
|
|
333
344
|
// own for the same commit, so the old approval can't carry it.
|
|
345
|
+
// A check that stopped this same commit is the same: its task keeps that failure, so reusing it would only stop again
|
|
346
|
+
// (a passing run on it, run 3333, can't erase it). A new task reruns the check and asks a person, never the plane.
|
|
334
347
|
// Only an id that has never existed, numbered after the superseded one: never a task whose approval still stands.
|
|
335
|
-
if (freshApproval) {
|
|
348
|
+
if (freshApproval || journal.candidate.sha === stoppedCheck) {
|
|
336
349
|
const base = journal.task.id;
|
|
337
350
|
const suffix = supersededTask === null ? null : supersededTask === base ? 1 : supersededTask.startsWith(`${base}-`) ? Number(supersededTask.slice(base.length + 1)) : null;
|
|
338
351
|
let picked = null;
|
|
@@ -626,11 +639,41 @@ export async function runRelease(options, adapters) {
|
|
|
626
639
|
/** identity(), on a plane that keeps release authority; a legacy plane keeps the old rules. */
|
|
627
640
|
const watch = async (action) => { if (rel !== undefined)
|
|
628
641
|
await identity(action, true); };
|
|
629
|
-
|
|
642
|
+
/**
|
|
643
|
+
* Behind main (plain-toolroll.md P2.6): update the branch under this release's lease and the head it was gated on.
|
|
644
|
+
* A concurrent push aborts it. The approval stops first, so nothing can ride it across the update; the updated head
|
|
645
|
+
* is a new candidate with a new approval, fresh CI and a fresh check, and there is no carry.
|
|
646
|
+
*/
|
|
647
|
+
const updateFromMain = async () => {
|
|
648
|
+
const update = adapters.github.updateBranch;
|
|
649
|
+
if (update === undefined)
|
|
650
|
+
return null;
|
|
651
|
+
if (rel !== undefined && j.authority) {
|
|
652
|
+
if (epoch === null || !await rel.renew(j.authority.id, owner, epoch))
|
|
653
|
+
throw new Stop("lease", "Another release coordinator took over this release.");
|
|
654
|
+
}
|
|
655
|
+
const pr = await adapters.github.pullRequest(j.github, j.branch);
|
|
656
|
+
if (pr === null || pr.number !== j.pr || pr.state !== "open" || pr.head !== j.candidate.sha)
|
|
657
|
+
throw new Stop("drift", `${j.branch} moved while it was being brought up to main. Nothing was changed; rerun \`${resume}\`.`);
|
|
658
|
+
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)) {
|
|
659
|
+
throw new Stop("lease", "The release's approval couldn't be stopped before the update, so the branch wasn't updated.");
|
|
660
|
+
}
|
|
661
|
+
say(` ${j.branch} is behind main: bringing it up to date`);
|
|
662
|
+
const moved = await update(j.github, j.pr, j.candidate.sha);
|
|
663
|
+
if (!moved.ok)
|
|
664
|
+
throw new Stop("drift", `${j.branch} couldn't be brought up to main: ${moved.why}. Rerun \`${resume}\`.`);
|
|
665
|
+
return waitFor("ci", `${j.branch} to move past ${j.candidate.sha.slice(0, 7)}`, async () => {
|
|
666
|
+
const head = await adapters.repo.remoteHead(j.branch);
|
|
667
|
+
return head !== null && head !== j.candidate.sha ? head : null;
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
const restart = async (head, updated = false) => {
|
|
630
671
|
// The step this commit was at ends here: the release goes on with the new commit (and, filed again, a new task).
|
|
631
672
|
mark(j.step, "failed", `${j.branch} moved to ${head.slice(0, 7)}`);
|
|
632
673
|
const tree = await adapters.repo.tree(head);
|
|
633
|
-
if (
|
|
674
|
+
if (updated)
|
|
675
|
+
say(` ${j.branch} is at ${head.slice(0, 7)}: a new commit, so it needs its own approval, CI and check`);
|
|
676
|
+
else if (rel !== undefined && j.authority && epoch !== null) {
|
|
634
677
|
say(` ${j.branch} moved to ${head.slice(0, 7)}: checking whether the approval carries`);
|
|
635
678
|
const carried = await rel.carry(j.authority.id, head, randomUUID(), epoch);
|
|
636
679
|
if (carried.ok) {
|
|
@@ -808,7 +851,17 @@ export async function runRelease(options, adapters) {
|
|
|
808
851
|
}
|
|
809
852
|
// The tree CI tested (the pull request's head, not GitHub's merge commit) is the tree that merges only when the
|
|
810
853
|
// branch already contains main's head: refused here, before anything is filed, and again right before merging.
|
|
811
|
-
|
|
854
|
+
try {
|
|
855
|
+
await containsMain(adapters, j, "Nothing was filed or sent for approval");
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
if (!(error instanceof Stop) || error.reason !== "base-moved")
|
|
859
|
+
throw error;
|
|
860
|
+
const head = await updateFromMain();
|
|
861
|
+
if (head === null)
|
|
862
|
+
throw error;
|
|
863
|
+
throw new Restart(head, true);
|
|
864
|
+
}
|
|
812
865
|
if (j.intents.gate === undefined)
|
|
813
866
|
await mainProtection(adapters, j, false, "Nothing was filed or sent for approval");
|
|
814
867
|
const existing = await adapters.toolroll.gate(j.task.id);
|
|
@@ -963,6 +1016,15 @@ export async function runRelease(options, adapters) {
|
|
|
963
1016
|
else if (standing.retried.length > 0)
|
|
964
1017
|
say(` run ${gate.result.run} passed, but only on a retry: ${standing.retried.join(", ")}`);
|
|
965
1018
|
}
|
|
1019
|
+
// A sandboxed Full check must show that what it skipped passed elsewhere (trusted-release-reports.md H3). The plane
|
|
1020
|
+
// answers at once for a check that ran unsandboxed, as Toolroll's own do: nothing there skips for this reason.
|
|
1021
|
+
if (rel?.fencedCoverage !== undefined) {
|
|
1022
|
+
const covered = await rel.fencedCoverage(j.task.id, gate.result.run, j.candidate.sha);
|
|
1023
|
+
if (!covered.ok)
|
|
1024
|
+
throw new Stop("check-failed", `Run ${gate.result.run} passed on ${j.candidate.sha.slice(0, 7)}, but ${covered.why}.`);
|
|
1025
|
+
if (covered.covered > 0)
|
|
1026
|
+
say(` ${covered.covered} cases skipped inside the sandbox passed in this commit's macOS CI`);
|
|
1027
|
+
}
|
|
966
1028
|
return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree, head };
|
|
967
1029
|
};
|
|
968
1030
|
const result = await waitFor("check", `the release check of ${j.candidate.sha.slice(0, 7)}`, async () => {
|
|
@@ -1175,7 +1237,21 @@ export async function runRelease(options, adapters) {
|
|
|
1175
1237
|
await identity("merge", false);
|
|
1176
1238
|
await pinned();
|
|
1177
1239
|
// A squash merge only reproduces the gated (and CI-tested) tree when main hasn't moved past the branch's base.
|
|
1178
|
-
|
|
1240
|
+
let main;
|
|
1241
|
+
try {
|
|
1242
|
+
main = await containsMain(adapters, j, "Nothing was merged");
|
|
1243
|
+
}
|
|
1244
|
+
catch (error) {
|
|
1245
|
+
if (!(error instanceof Stop) || error.reason !== "base-moved")
|
|
1246
|
+
throw error;
|
|
1247
|
+
// Past the deploy: the updated commit isn't what's running, so it starts over, with a person.
|
|
1248
|
+
const head = await updateFromMain();
|
|
1249
|
+
if (head === null)
|
|
1250
|
+
throw error;
|
|
1251
|
+
j.needsApproval = true;
|
|
1252
|
+
save();
|
|
1253
|
+
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\`.`);
|
|
1254
|
+
}
|
|
1179
1255
|
// Every release merges only where GitHub itself refuses a branch behind main.
|
|
1180
1256
|
await mainProtection(adapters, j, j.auto?.state === "approved", "Nothing was merged");
|
|
1181
1257
|
// The checks again, at the moment of merging: a rerun that failed since must not merge.
|
|
@@ -1290,7 +1366,7 @@ export async function runRelease(options, adapters) {
|
|
|
1290
1366
|
catch (error) {
|
|
1291
1367
|
if (!(error instanceof Restart))
|
|
1292
1368
|
throw error;
|
|
1293
|
-
await restart(error.head);
|
|
1369
|
+
await restart(error.head, error.updated);
|
|
1294
1370
|
}
|
|
1295
1371
|
}
|
|
1296
1372
|
const left = homebrewLeft;
|
|
@@ -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>;
|