toolroll 0.9.93 → 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 +68 -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-ui.js +1 -1
- package/dist/assignment.js +5 -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-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +45 -2
- 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 +53 -24
- 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 +3 -1
- package/dist/check-facts.js +57 -11
- 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 +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/cli-help.d.ts +4 -1
- package/dist/cli-help.js +47 -30
- package/dist/cli.js +11 -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 +10 -3
- package/dist/contracts/console-api.js +10 -3
- 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/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/dispatch.js +18 -15
- package/dist/dispose.js +8 -2
- package/dist/easy-start.js +2 -3
- 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 +75 -9
- package/dist/flaky.js +255 -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 +27 -8
- package/dist/needs-you.js +96 -39
- 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 +402 -52
- 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 +8 -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/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 +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 +39 -9
- 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 +76 -8
- 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 +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 +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 +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 +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 +67 -67
- package/dist/server/render-tasks.d.ts +13 -2
- package/dist/server/render-tasks.js +153 -136
- 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 +45 -12
- package/dist/server/settings.js +129 -22
- package/dist/server/task-views.js +61 -26
- package/dist/server/tasks.d.ts +2 -6
- package/dist/server/tasks.js +174 -321
- 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.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 +80 -5
- package/dist/store.js +296 -87
- package/dist/surface.js +45 -4
- 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 +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 +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.js +17 -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 +76 -62
- 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
|
@@ -131,6 +131,8 @@ export type AutoObservers = {
|
|
|
131
131
|
/** Whether main's protection holds for the account that merges (readMainProtection); null when unreadable. */
|
|
132
132
|
upToDateRequired: () => Promise<boolean | null>;
|
|
133
133
|
evidenceRoot: string;
|
|
134
|
+
/** The cases that ran in `sha`'s macOS CI (fenced-check-coverage.ts); null when they can't be read. Absent: never read. */
|
|
135
|
+
macosCases?: (sha: string) => Promise<ReadonlyMap<string, boolean> | null>;
|
|
134
136
|
};
|
|
135
137
|
/**
|
|
136
138
|
* Every check run GitHub holds for one commit — every attempt, not only the latest (`filter=all`; GitHub's default
|
|
@@ -169,6 +171,13 @@ export declare function readMergeBase(run: Exec, gh: string, repo: string, base:
|
|
|
169
171
|
* never from a ref in a checkout a build can write.
|
|
170
172
|
*/
|
|
171
173
|
export declare function readMainHead(run: Exec, gh: string, repo: string, branch?: string): Promise<string>;
|
|
174
|
+
/** GitHub's own "Update branch", with its expected head: a push that got there first refuses. */
|
|
175
|
+
export declare function updatePullRequestBranch(run: Exec, gh: string, repo: string, pr: number, expectedHead: string): Promise<{
|
|
176
|
+
ok: true;
|
|
177
|
+
} | {
|
|
178
|
+
ok: false;
|
|
179
|
+
why: string;
|
|
180
|
+
}>;
|
|
172
181
|
/** Whether `sha` already holds `ancestor` (GitHub's compare: `sha` is not behind it), as GitHub answers now. */
|
|
173
182
|
export declare function readContains(run: Exec, gh: string, repo: string, sha: string, ancestor: string): Promise<boolean>;
|
|
174
183
|
/**
|
package/dist/release-adapters.js
CHANGED
|
@@ -24,6 +24,10 @@ import { databasePath, finishReleaseOperation, requireFreshReleaseApproval, open
|
|
|
24
24
|
import { acquireDeploymentLock } from "./deploy-lock.js";
|
|
25
25
|
import { autoReleaseApprovalOf, liveReleaseOf } from "./scope.js";
|
|
26
26
|
import { recheckReleaseTask, releaseCheckStanding } from "./check-failure.js";
|
|
27
|
+
import { CHECK_REPORT_CAPTURE, readSealedJson } from "./check-report.js";
|
|
28
|
+
import { readCheckReport } from "./contracts/release-check.js";
|
|
29
|
+
import { macosCases, skipCoverage, skippedCases, uncoveredWords } from "./fenced-check-coverage.js";
|
|
30
|
+
import { runCheckStamp } from "./store.js";
|
|
27
31
|
import { noteCiRunning, releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
28
32
|
import { autoPublishHeld, holdAutoPublish, liftCiHold, authorizeAutoCheck, giveUpAutoApproval, noteAutoCheckFailed, noteAutoRelease, releaseAutoApproves, sealAutoGreen } from "./release-auto.js";
|
|
29
33
|
import { proveCarry } from "./release-carry.js";
|
|
@@ -436,6 +440,7 @@ export function releaseAdapters(options) {
|
|
|
436
440
|
};
|
|
437
441
|
const release = storeReleaseAdapter(() => openStore(database), store => store.close(), options.now ?? (() => new Date()), undefined, { observe: sha => observeCi(sha), mergeBase: (base, sha) => readMergeBase(reads, ciGh, releaseRepo().slug, base, sha),
|
|
438
442
|
upToDateRequired: () => readUpToDateRequired(reads, ciGh, releaseRepo().slug), evidenceRoot: join(dirname(database), "evidence"),
|
|
443
|
+
macosCases: sha => macosCasesOf(run, ciGh, releaseRepo().slug, sha),
|
|
439
444
|
// A successful Actions job authenticates the job, not the candidate-writable JSON it uploads. Until the
|
|
440
445
|
// executor protects that reporting channel, production releases must take the person's approval road.
|
|
441
446
|
reportsProtected: false },
|
|
@@ -660,6 +665,7 @@ export function releaseAdapters(options) {
|
|
|
660
665
|
async complete(repo, sha) { return readSuitesComplete(reads, ciGh, repo, sha); },
|
|
661
666
|
async mainHead(repo) { return readMainHead(reads, ciGh, repo); },
|
|
662
667
|
async contains(repo, sha, ancestor) { return readContains(reads, ciGh, repo, sha, ancestor); },
|
|
668
|
+
async updateBranch(repo, pr, expectedHead) { return updatePullRequestBranch(reads, ciGh, repo, pr, expectedHead); },
|
|
663
669
|
async upToDateRequired(repo) { return readMainProtection(reads, ciGh, repo); },
|
|
664
670
|
async merge(repo, pr, head, message) {
|
|
665
671
|
// The squash commit's message lists every commit that landed after the approval (release-once.md). Never asked
|
|
@@ -989,6 +995,33 @@ nightlyReads) {
|
|
|
989
995
|
return answer.action === "rerun" ? { action: "rerun", why: answer.why } : answer.action === "pending" ? { action: "pending" } : { action: "stop", class: answer.class, why: answer.why };
|
|
990
996
|
});
|
|
991
997
|
},
|
|
998
|
+
async fencedCoverage(_taskId, run, candidate) {
|
|
999
|
+
// Today's road, read first and answered at once: a check that ran unsandboxed skips nothing for this reason. A run
|
|
1000
|
+
// from before stamps (a release resumed across an update) is judged by the level its dispatch recorded.
|
|
1001
|
+
const asked = withStore(store => {
|
|
1002
|
+
if (store.runCheckAccess(run) === "unsandboxed")
|
|
1003
|
+
return null;
|
|
1004
|
+
const reading = runCheckStamp(store, run);
|
|
1005
|
+
if (reading.state !== "current")
|
|
1006
|
+
return { refused: "how its check ran wasn't recorded, so what it skipped can't be judged" };
|
|
1007
|
+
if (reading.stamp.level === "unsandboxed")
|
|
1008
|
+
return null;
|
|
1009
|
+
const sealed = readSealedJson(store, auto.evidenceRoot, run, CHECK_REPORT_CAPTURE);
|
|
1010
|
+
const report = sealed === null ? null : readCheckReport(sealed.value);
|
|
1011
|
+
return report?.ok === true ? { skipped: skippedCases(report.value) } : { refused: "its report can't be read, so what it skipped can't be judged" };
|
|
1012
|
+
});
|
|
1013
|
+
if (asked === null)
|
|
1014
|
+
return { ok: true, covered: 0 };
|
|
1015
|
+
if ("refused" in asked)
|
|
1016
|
+
return { ok: false, why: asked.refused };
|
|
1017
|
+
if (asked.skipped.length === 0)
|
|
1018
|
+
return { ok: true, covered: 0 };
|
|
1019
|
+
const macos = await auto.macosCases?.(candidate) ?? null;
|
|
1020
|
+
if (macos === null)
|
|
1021
|
+
return { ok: false, why: "this commit's macOS CI reports couldn't be read, so what the sandboxed check skipped isn't covered" };
|
|
1022
|
+
const coverage = skipCoverage(asked.skipped, macos);
|
|
1023
|
+
return coverage.uncovered.length > 0 ? { ok: false, why: uncoveredWords(coverage.uncovered) } : { ok: true, covered: coverage.covered.length };
|
|
1024
|
+
},
|
|
992
1025
|
async standing(taskId, run) {
|
|
993
1026
|
return withStore(store => {
|
|
994
1027
|
const standing = releaseCheckStanding(store, auto.evidenceRoot, { taskId, run });
|
|
@@ -1317,6 +1350,13 @@ export async function readMainHead(run, gh, repo, branch = "main") {
|
|
|
1317
1350
|
throw new Error(`GitHub named no commit for ${branch}`);
|
|
1318
1351
|
return match[1];
|
|
1319
1352
|
}
|
|
1353
|
+
/** GitHub's own "Update branch", with its expected head: a push that got there first refuses. */
|
|
1354
|
+
export async function updatePullRequestBranch(run, gh, repo, pr, expectedHead) {
|
|
1355
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) || !Number.isSafeInteger(pr) || pr < 1 || !/^[0-9a-f]{40}$/.test(expectedHead))
|
|
1356
|
+
return { ok: false, why: "not a pull request GitHub can be asked to update" };
|
|
1357
|
+
const ran = await run(gh, ["api", "--hostname", "github.com", "--method", "PUT", `repos/${repo}/pulls/${pr}/update-branch`, "-f", `expected_head_sha=${expectedHead}`], { cwd: "/", env: ciEnv() });
|
|
1358
|
+
return ran.code === 0 ? { ok: true } : { ok: false, why: (ran.stderr.trim().split("\n")[0] ?? "").slice(0, 200) || `gh exited ${ran.code}` };
|
|
1359
|
+
}
|
|
1320
1360
|
/** Whether `sha` already holds `ancestor` (GitHub's compare: `sha` is not behind it), as GitHub answers now. */
|
|
1321
1361
|
export async function readContains(run, gh, repo, sha, ancestor) {
|
|
1322
1362
|
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) || !/^[0-9a-f]{40}$/.test(sha) || !/^[0-9a-f]{40}$/.test(ancestor))
|
|
@@ -1530,3 +1570,39 @@ export function releaseProgressRecorder(open, done) {
|
|
|
1530
1570
|
}
|
|
1531
1571
|
};
|
|
1532
1572
|
}
|
|
1573
|
+
/** The macOS CI's cases on exactly `sha`: the newest pull-request run of ci.yml there, each shard's newest attempt. Null
|
|
1574
|
+
* when they can't all be read. The folder is this function's own, removed before it returns. */
|
|
1575
|
+
async function macosCasesOf(run, gh, slug, sha) {
|
|
1576
|
+
const folder = mkdtempSync(join(tmpdir(), "so-macos-ci-"));
|
|
1577
|
+
try {
|
|
1578
|
+
const listed = await run(gh, ["api", `repos/${slug}/actions/runs?head_sha=${sha}&event=pull_request&per_page=100`], { timeoutMs: 60_000 });
|
|
1579
|
+
if (listed.code !== 0)
|
|
1580
|
+
return null;
|
|
1581
|
+
const runs = JSON.parse(listed.stdout).workflow_runs ?? [];
|
|
1582
|
+
const newest = runs.filter(one => typeof one.id === "number" && one.head_sha === sha && (one.path === ".github/workflows/ci.yml" || String(one.path).startsWith(".github/workflows/ci.yml@"))).map(one => one.id).sort((a, b) => b - a)[0];
|
|
1583
|
+
if (newest === undefined)
|
|
1584
|
+
return null;
|
|
1585
|
+
const got = await run(gh, ["run", "download", String(newest), "--repo", slug, "--pattern", "macos-shard-*", "--dir", folder], { timeoutMs: 180_000 });
|
|
1586
|
+
if (got.code !== 0)
|
|
1587
|
+
return null;
|
|
1588
|
+
const shards = new Map();
|
|
1589
|
+
for (const name of readdirSync(folder)) {
|
|
1590
|
+
const match = /^macos-(shard-\d+-\d+)-a([1-9]\d*)$/.exec(name);
|
|
1591
|
+
if (match === null)
|
|
1592
|
+
return null;
|
|
1593
|
+
const was = shards.get(match[1]);
|
|
1594
|
+
if (was === undefined || Number(match[2]) > was.attempt)
|
|
1595
|
+
shards.set(match[1], { attempt: Number(match[2]), name });
|
|
1596
|
+
}
|
|
1597
|
+
if (shards.size === 0)
|
|
1598
|
+
return null;
|
|
1599
|
+
const reports = [...shards.values()].map(one => JSON.parse(readFileSync(join(folder, one.name, "shard-report.json"), "utf8")));
|
|
1600
|
+
return macosCases(reports, `/Users/runner/work/${slug.split("/")[1]}/${slug.split("/")[1]}`);
|
|
1601
|
+
}
|
|
1602
|
+
catch {
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
finally {
|
|
1606
|
+
rmSync(folder, { recursive: true, force: true });
|
|
1607
|
+
}
|
|
1608
|
+
}
|
package/dist/release-auto.js
CHANGED
|
@@ -19,7 +19,7 @@ import { sealedRun } from "./release-recheck.js";
|
|
|
19
19
|
import { runCheckLevel } from "./check-levels.js";
|
|
20
20
|
import { readVerifiedCommit, verifiedBlobs, verifiedTreeDiff, verifiedTreeEntries } from "./commit-extract.js";
|
|
21
21
|
import { EVAL_HOOK_FILES, evalHookProblem, LEARNING_HOOK_FILES, learningHookProblem, storeRewriteProblem } from "./eval-hooks.js";
|
|
22
|
-
import { mightName, testInfrastructure } from "./test-infra.js";
|
|
22
|
+
import { isTestPath, mightName, testImportProblem, testInfrastructure } from "./test-infra.js";
|
|
23
23
|
import { chatControlHref } from "./chat-controls.js";
|
|
24
24
|
import { INFRA_RERUNS_PER_CANDIDATE, REQUIRED_PR_CHECKS, REQUIRED_PR_CHECKS_WORKFLOW, prChecksVerdict } from "./release.js";
|
|
25
25
|
import { releaseCheckStanding } from "./check-failure.js";
|
|
@@ -359,18 +359,18 @@ function changeProblem(repo, base, candidate) {
|
|
|
359
359
|
// Taking a test away is never automatic, wherever it lives.
|
|
360
360
|
if (change.kind === "deleted" && TEST_FILE.test(rulePath(change.path)))
|
|
361
361
|
return `${change.path} was deleted (removing a test needs a person)`;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
362
|
+
// Counts cannot prove that an assertion survived, and an appended line can disarm what it follows (`vi.mock` hoists,
|
|
363
|
+
// a top-level hook reaches every case): a test file that was there stays exactly as it was (H4).
|
|
364
|
+
if (change.before !== null && TEST_FILE.test(rulePath(change.path)))
|
|
365
|
+
return `${change.path} changes existing test code (new tests go in a new file; rewriting tests needs a person)`;
|
|
366
|
+
// A new test may not reach into another: it would run that file's code, and its mocks, here. From the verified tree.
|
|
367
|
+
// Any test extension: `src/` passes autoPathProblem whatever the file is called, so `.test.ts` alone would miss `.test.tsx`.
|
|
368
|
+
if (change.kind === "added" && isTestPath(rulePath(change.path))) {
|
|
369
|
+
const read = verifiedBlobs(gitDir, after.value.tree, [change.path]);
|
|
370
|
+
const bytes = read.ok ? read.value.get(change.path)?.bytes : undefined;
|
|
371
|
+
const why = bytes === undefined ? `${change.path}'s test code couldn't be read` : testImportProblem(change.path, bytes.toString("utf8"));
|
|
372
|
+
if (why !== null)
|
|
373
|
+
return `${why} (a person decides)`;
|
|
374
374
|
}
|
|
375
375
|
const why = autoPathProblem(change.path);
|
|
376
376
|
if (why !== null)
|
package/dist/release-card.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReleaseCheckPurpose, type ReleaseTerms } from "./contracts/release-terms.js";
|
|
2
2
|
import type { Scope } from "./scope.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type Store } from "./store.js";
|
|
4
4
|
/** One signed term as the card says it. `key` names the scope field or the release term's path. */
|
|
5
5
|
export type TermLine = {
|
|
6
6
|
key: string;
|
|
@@ -151,7 +151,6 @@ export type ReleaseCard = {
|
|
|
151
151
|
};
|
|
152
152
|
/** The card digest: everything the person is shown that can change under them. */
|
|
153
153
|
export declare function cardDigestOf(scopeDigest: string, formDigest: string, changes: Pick<ChangeList, "baseline" | "digest">): string;
|
|
154
|
-
/** The release card for a deploy gate, against the live records now. */
|
|
155
154
|
export declare function releaseCardOf(store: Store, scope: Scope, formDigest: string, words: {
|
|
156
155
|
profile: (profile: Scope["profile"]) => string | null;
|
|
157
156
|
}, read?: PullRequestReader): ReleaseCard;
|
package/dist/release-card.js
CHANGED
|
@@ -20,6 +20,7 @@ import { createHash } from "node:crypto";
|
|
|
20
20
|
import { lstatSync, realpathSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { checkAccessWords } from "./check-access.js";
|
|
23
|
+
import { ranWords } from "./check-stamp.js";
|
|
23
24
|
import { CHECK_LEVEL_WORDS, effectiveCheckLevel } from "./check-levels.js";
|
|
24
25
|
import { verifiedRange } from "./commit-extract.js";
|
|
25
26
|
import { releaseCheckPurposeSchema, releaseTermsSchema } from "./contracts/release-terms.js";
|
|
@@ -29,6 +30,7 @@ import { routeFromJson } from "./phase-routing.js";
|
|
|
29
30
|
import { projectName } from "./project.js";
|
|
30
31
|
import { RELEASE_CI_RUNNING_WORDS, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
31
32
|
import { RELEASE_PATH, releaseConfigProblem, releaseGitEnv, releaseRemote, releaseTools } from "./release-git.js";
|
|
33
|
+
import { taskCheckStamp } from "./store.js";
|
|
32
34
|
import { nightlyReleaseLines } from "./nightly-github.js";
|
|
33
35
|
const NONE = "none";
|
|
34
36
|
/** A budget in dollars from integer micro-dollars, in integer arithmetic: cents when it is whole cents, else every digit. */
|
|
@@ -382,6 +384,13 @@ function changeWords(changes, terms, cap) {
|
|
|
382
384
|
return [head, ...changes.commits.slice(0, cap).map(one => `• ${one.sha.slice(0, 7)} ${one.subject.slice(0, 100)}`), ...(changes.commits.length > cap ? [`• and ${changes.commits.length - cap} more`] : [])];
|
|
383
385
|
}
|
|
384
386
|
/** The release card for a deploy gate, against the live records now. */
|
|
387
|
+
/** How this task's last check ran, when one did and it wasn't plainly sandboxed: the sealed level above is what will run;
|
|
388
|
+
* this is what did. Nothing for a release no check has run for. */
|
|
389
|
+
function lastCheckWords(store, repo, taskId) {
|
|
390
|
+
const ran = repo === null ? null : taskCheckStamp(store, repo, taskId);
|
|
391
|
+
const words = ran === null ? null : ranWords(ran.reading);
|
|
392
|
+
return words === null ? [] : [`Last check: ${words}.`];
|
|
393
|
+
}
|
|
385
394
|
export function releaseCardOf(store, scope, formDigest, words, read) {
|
|
386
395
|
const terms = scope.releaseTerms ?? null;
|
|
387
396
|
// Either sealed form (Codex round 2, finding 1): the identity both share, for the head lines and the change list.
|
|
@@ -400,6 +409,8 @@ export function releaseCardOf(store, scope, formDigest, words, read) {
|
|
|
400
409
|
: `Carry later commits: ${terms.policy.carry ? "yes" : "no"} · checks: ${value("checks")} · expires ${terms.expiresAt}`,
|
|
401
410
|
// What an unsandboxed check means, before consent (Codex round 2, finding 1).
|
|
402
411
|
...(identity === null || releaseAccessWords(identity.access) === null ? [] : [releaseAccessWords(identity.access)]),
|
|
412
|
+
// A check that already ran for this task, from its stamp (H2): never read as sandboxed when it can't be shown.
|
|
413
|
+
...lastCheckWords(store, identity?.repo ?? null, scope.taskId),
|
|
403
414
|
// Option A's disclosure, on the card as on the approval sheet: never approved from chat without it.
|
|
404
415
|
...(terms !== null && terms.policy.carry && terms.access !== "sandboxed" ? ["This project's checks don't run in the sandbox: later test-only commits will run outside the sandbox without being shown to you first."] : []),
|
|
405
416
|
// Filed while CI runs (Alex, 2026-10-29): a person may approve now, and is told plainly what still waits.
|
package/dist/release-carry.d.ts
CHANGED
|
@@ -44,8 +44,13 @@ export declare function stricterOf(a: ReleasePolicy | null, b: ReleasePolicy | n
|
|
|
44
44
|
* the rules don't rely on that).
|
|
45
45
|
*/
|
|
46
46
|
export declare function rulePath(path: string): string;
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Why one changed path doesn't carry, in words, or null when it does. Every rule reads the normalized path. `fresh`: the
|
|
49
|
+
* paths that didn't exist at the approved commit. A test that was there stays exactly as it was (H4): new tests go in new
|
|
50
|
+
* files. Only "unchanged" holds up, since `vi.mock` hoists and a top-level hook reaches every case above it, so an
|
|
51
|
+
* appended line can disarm what it follows.
|
|
52
|
+
*/
|
|
53
|
+
export declare function changeRefusal(change: TreeChange, policy: ReleasePolicy, fresh?: ReadonlySet<string>): string | null;
|
|
49
54
|
/**
|
|
50
55
|
* A release scope's terms, from the plane's own records: the project's repository, the approved commit and its
|
|
51
56
|
* verified tree, the effective policy, the live release check grant and the check access (only the fence), and the
|
package/dist/release-carry.js
CHANGED
|
@@ -26,14 +26,17 @@ import { join, sep } from "node:path";
|
|
|
26
26
|
import { execFileSync } from "node:child_process";
|
|
27
27
|
import { checkLaunchRoot, fenceFor, linuxFenceAvailable, macosFenceAvailable, runFolders } from "./agent-fence.js";
|
|
28
28
|
import { touchesProtected } from "./approval-policy.js";
|
|
29
|
-
import { extractCommit, readVerifiedCommit, verifiedFirstParentWalk, verifiedTreeDiff } from "./commit-extract.js";
|
|
29
|
+
import { extractCommit, readVerifiedCommit, verifiedBlobs, verifiedFirstParentWalk, verifiedTreeDiff } from "./commit-extract.js";
|
|
30
|
+
import { testImportProblem } from "./test-infra.js";
|
|
30
31
|
import { projectGitDir } from "./check-report.js";
|
|
32
|
+
import { checkAccessOf } from "./check-access.js";
|
|
33
|
+
import { stampRefusal } from "./check-stamp.js";
|
|
31
34
|
import { CARRY_RULES_VERSION, readReleasePolicy, releasePolicyDigest } from "./contracts/release-terms.js";
|
|
32
35
|
import { run } from "./exec.js";
|
|
33
36
|
import { proxyEnv, startEgressProxy } from "./egress.js";
|
|
34
37
|
import { liveReleaseOf } from "./scope.js";
|
|
35
38
|
import { UNSANDBOXED_CARRY_ACTION, carriesUnsandboxed } from "./release-authority.js";
|
|
36
|
-
import { carryReleaseApproval, releaseAuthority, releaseCarryTo, sealAnchorArtifact, } from "./store.js";
|
|
39
|
+
import { carryReleaseApproval, releaseAuthority, releaseCarryTo, sealAnchorArtifact, taskCheckStamp, } from "./store.js";
|
|
37
40
|
// ------------------------------------------------------------------ the rules
|
|
38
41
|
/** The journey scripts: tests, by Alex's decision (they carry). */
|
|
39
42
|
export const JOURNEY_SCRIPTS = Object.freeze(["scripts/app-e2e.mjs", "scripts/flows-e2e.mjs"]);
|
|
@@ -111,8 +114,15 @@ export function stricterOf(a, b) {
|
|
|
111
114
|
export function rulePath(path) {
|
|
112
115
|
return path.toLowerCase().split("/").map(part => part.replace(/[. ]+$/, "")).filter(part => part !== "" && part !== ".").join("/");
|
|
113
116
|
}
|
|
114
|
-
/**
|
|
115
|
-
|
|
117
|
+
/** Tests by the rules' classes: what the existing-test rule holds still. */
|
|
118
|
+
const carriedTest = (normal) => CARRY_RULES.classes.some(one => (one.name === "unit tests" || one.name === "journey scripts") && one.matches(normal));
|
|
119
|
+
/**
|
|
120
|
+
* Why one changed path doesn't carry, in words, or null when it does. Every rule reads the normalized path. `fresh`: the
|
|
121
|
+
* paths that didn't exist at the approved commit. A test that was there stays exactly as it was (H4): new tests go in new
|
|
122
|
+
* files. Only "unchanged" holds up, since `vi.mock` hoists and a top-level hook reaches every case above it, so an
|
|
123
|
+
* appended line can disarm what it follows.
|
|
124
|
+
*/
|
|
125
|
+
export function changeRefusal(change, policy, fresh = new Set()) {
|
|
116
126
|
const path = change.path;
|
|
117
127
|
const normal = rulePath(path);
|
|
118
128
|
if (change.kind === "deleted")
|
|
@@ -133,6 +143,8 @@ export function changeRefusal(change, policy) {
|
|
|
133
143
|
return `${path} is protected in this project`;
|
|
134
144
|
if (!CARRY_RULES.classes.some(one => one.matches(normal)))
|
|
135
145
|
return `${path} isn't a test, a doc or the changelog`;
|
|
146
|
+
if (change.kind === "modified" && carriedTest(normal) && !fresh.has(path))
|
|
147
|
+
return `${path} changes an existing test, and new tests go in a new file`;
|
|
136
148
|
return null;
|
|
137
149
|
}
|
|
138
150
|
// ------------------------------------------------------------------ release terms
|
|
@@ -211,6 +223,11 @@ export function preflightCarry(store, input) {
|
|
|
211
223
|
if ((a.accessDigest !== "sandboxed" || store.projectCheckAccess(a.repo) !== "sandboxed") && policy.unsandboxedCarry !== true) {
|
|
212
224
|
return refuse("rules", "This project's checks run outside the sandbox, and carrying release approvals then wasn't on when this release was approved, or is off now.");
|
|
213
225
|
}
|
|
226
|
+
// A carry leans on the check the release already had: it must be shown to have stayed within what was approved (H2).
|
|
227
|
+
const ran = taskCheckStamp(store, a.repo, a.tipTask);
|
|
228
|
+
const unshown = ran === null ? null : stampRefusal(ran.reading, checkAccessOf(a.accessDigest));
|
|
229
|
+
if (unshown !== null)
|
|
230
|
+
return refuse("rules", `This release's approval can't carry: ${unshown}.`);
|
|
214
231
|
if (!/^[0-9a-f]{40}([0-9a-f]{24})?$/.test(input.to))
|
|
215
232
|
return refuse("ancestry", `${input.to} isn't a full commit id.`);
|
|
216
233
|
if (input.to === a.tipSha)
|
|
@@ -234,6 +251,10 @@ export function preflightCarry(store, input) {
|
|
|
234
251
|
treeOf.set(commit.id, commit.tree);
|
|
235
252
|
const commits = [];
|
|
236
253
|
let churn = 0;
|
|
254
|
+
const whole = verifiedTreeDiff(gitDir, a.approvedTree, chain[0].tree);
|
|
255
|
+
if (!whole.ok)
|
|
256
|
+
return refuse("shape", `The change since the approval couldn't be read: ${whole.message}`);
|
|
257
|
+
const fresh = new Set(whole.value.filter(one => one.kind === "added").map(one => one.path));
|
|
237
258
|
for (const commit of ordered) {
|
|
238
259
|
const parentTree = treeOf.get(commit.parents[0]);
|
|
239
260
|
if (parentTree === undefined)
|
|
@@ -242,21 +263,30 @@ export function preflightCarry(store, input) {
|
|
|
242
263
|
if (!diff.ok)
|
|
243
264
|
return refuse("shape", `${commit.id.slice(0, 7)} couldn't be read: ${diff.message}`);
|
|
244
265
|
for (const change of diff.value) {
|
|
245
|
-
const why = changeRefusal(change, policy);
|
|
266
|
+
const why = changeRefusal(change, policy, fresh);
|
|
246
267
|
if (why !== null)
|
|
247
268
|
return refuse("rules", `${commit.id.slice(0, 7)}: ${why}.`, change.path);
|
|
248
269
|
}
|
|
249
270
|
churn += diff.value.reduce((n, one) => n + one.added + one.deleted, 0);
|
|
250
271
|
commits.push({ sha: commit.id, files: diff.value.map(one => ({ path: one.path, added: one.added, deleted: one.deleted })) });
|
|
251
272
|
}
|
|
252
|
-
const whole = verifiedTreeDiff(gitDir, a.approvedTree, chain[0].tree);
|
|
253
|
-
if (!whole.ok)
|
|
254
|
-
return refuse("shape", `The change since the approval couldn't be read: ${whole.message}`);
|
|
255
273
|
for (const change of whole.value) {
|
|
256
|
-
const why = changeRefusal(change, policy);
|
|
274
|
+
const why = changeRefusal(change, policy, fresh);
|
|
257
275
|
if (why !== null)
|
|
258
276
|
return refuse("rules", `Since the approval: ${why}.`, change.path);
|
|
259
277
|
}
|
|
278
|
+
// No new test may reach into another (H4). Read from the verified tree, never from a checkout.
|
|
279
|
+
// By the rules' own classifier, so everything that carries as a test is read (a new journey script too).
|
|
280
|
+
const newTests = whole.value.filter(one => one.kind === "added" && carriedTest(rulePath(one.path))).map(one => one.path);
|
|
281
|
+
const added = newTests.length === 0 ? null : verifiedBlobs(gitDir, chain[0].tree, newTests);
|
|
282
|
+
if (added !== null && !added.ok)
|
|
283
|
+
return refuse("shape", `The new tests couldn't be read: ${added.message}`);
|
|
284
|
+
for (const path of newTests) {
|
|
285
|
+
const bytes = added !== null && added.ok ? added.value.get(path)?.bytes : undefined;
|
|
286
|
+
const why = bytes === undefined ? `${path} couldn't be read` : testImportProblem(path, bytes.toString("utf8"));
|
|
287
|
+
if (why !== null)
|
|
288
|
+
return refuse("rules", `Since the approval: ${why}.`, path);
|
|
289
|
+
}
|
|
260
290
|
const changedLines = whole.value.reduce((n, one) => n + one.added + one.deleted, 0);
|
|
261
291
|
if (changedLines > policy.limits.lines)
|
|
262
292
|
return refuse("limits", `${changedLines} lines changed since the approval; at most ${policy.limits.lines} carry.`);
|
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>;
|