toolroll 0.9.71 → 0.9.73
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 +155 -0
- package/dist/agent-fence.d.ts +10 -2
- package/dist/agent-fence.js +47 -9
- package/dist/approval-authority.d.ts +20 -0
- package/dist/approval-authority.js +67 -0
- package/dist/assignment.js +26 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +28 -28
- package/dist/builder.js +48 -6
- package/dist/chat-actions.js +13 -8
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +186 -5
- package/dist/check-access.js +2 -2
- package/dist/check-report.d.ts +109 -0
- package/dist/check-report.js +205 -0
- package/dist/claim.js +1 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +111 -0
- package/dist/commit-extract.js +514 -0
- package/dist/contracts/console-api.d.ts +4 -4
- package/dist/contracts/console-api.js +5 -4
- package/dist/contracts/flow.d.ts +16 -0
- package/dist/contracts/flow.js +3 -0
- package/dist/contracts/release-check.d.ts +335 -0
- package/dist/contracts/release-check.js +159 -0
- package/dist/contracts/release-terms.d.ts +66 -0
- package/dist/contracts/release-terms.js +74 -0
- package/dist/contracts/verification-receipt.d.ts +42 -8
- package/dist/contracts/verification-receipt.js +24 -7
- package/dist/contracts/verification-view.d.ts +1 -0
- package/dist/deploy-gate.d.ts +17 -0
- package/dist/deploy-gate.js +68 -0
- package/dist/dispatch.js +3 -10
- package/dist/dispose.js +4 -1
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +20 -2
- package/dist/evidence.d.ts +1 -0
- package/dist/evidence.js +2 -1
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +7 -3
- package/dist/flow-engine.js +5 -1
- package/dist/flow-gallery-ui.js +4 -2
- package/dist/flow-gallery.d.ts +3 -0
- package/dist/flow-gallery.js +6 -2
- package/dist/flow-pull-request.d.ts +5 -2
- package/dist/flow-pull-request.js +14 -5
- package/dist/flow-schedule.js +25 -6
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/flows-ui.js +7 -1
- package/dist/guides.js +10 -1
- package/dist/invoke.d.ts +17 -6
- package/dist/invoke.js +48 -18
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/lead-status.js +5 -2
- package/dist/live-terminal.d.ts +10 -6
- package/dist/live-terminal.js +55 -14
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/nightly-check.d.ts +104 -0
- package/dist/nightly-check.js +362 -0
- package/dist/operate.js +39 -2
- package/dist/person-proof.d.ts +2 -0
- package/dist/person-proof.js +14 -0
- package/dist/plan-auto.js +4 -4
- package/dist/planner-source.d.ts +1 -1
- package/dist/project-delete-ui.js +1 -1
- package/dist/project-tools.js +3 -1
- package/dist/proof.js +2 -0
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/publish.js +156 -144
- package/dist/pull-request-flow.d.ts +2 -0
- package/dist/pull-request-flow.js +24 -2
- package/dist/release-adapters.d.ts +16 -1
- package/dist/release-adapters.js +292 -29
- package/dist/release-authority.d.ts +118 -0
- package/dist/release-authority.js +135 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +236 -0
- package/dist/release-carry.js +664 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +158 -6
- package/dist/release-coverage.js +3 -0
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -0
- package/dist/release-recheck.d.ts +146 -0
- package/dist/release-recheck.js +657 -0
- package/dist/release-view.d.ts +34 -0
- package/dist/release-view.js +27 -0
- package/dist/release.d.ts +139 -3
- package/dist/release.js +641 -250
- package/dist/remote-main.d.ts +2 -0
- package/dist/remote-main.js +1 -1
- package/dist/result-completion.d.ts +6 -0
- package/dist/result-completion.js +15 -1
- package/dist/result-follow-ups.js +29 -1
- package/dist/sandbox-runtime.d.ts +16 -5
- package/dist/sandbox-runtime.js +19 -6
- package/dist/scope.d.ts +122 -5
- package/dist/scope.js +236 -7
- package/dist/server/flows.js +40 -10
- package/dist/server/render-settings.js +1 -1
- package/dist/server/render-tasks.d.ts +8 -0
- package/dist/server/render-tasks.js +50 -9
- package/dist/server/route-table.js +1 -0
- package/dist/server/settings.js +26 -5
- package/dist/server/task-views.js +4 -0
- package/dist/server/tasks.js +12 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +230 -5
- package/dist/store.js +733 -37
- package/dist/subagent-memory.d.ts +1 -0
- package/dist/subagent-memory.js +1 -1
- package/dist/subagent-tools.d.ts +22 -6
- package/dist/subagent-tools.js +44 -8
- package/dist/subagent-work.d.ts +2 -0
- package/dist/subagent-work.js +5 -2
- package/dist/subagents-ui.js +6 -4
- package/dist/task-outcome-cli.js +7 -3
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +25 -1
- package/dist/verification-evidence.js +57 -2
- package/dist/work-index.js +3 -6
- package/package.json +1 -1
package/dist/publish.js
CHANGED
|
@@ -26,6 +26,7 @@ import { tmpdir } from "node:os";
|
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import { run as execRun } from "./exec.js";
|
|
28
28
|
import { summarizeChecks } from "./pulls.js";
|
|
29
|
+
import { releasePublication } from "./store.js";
|
|
29
30
|
import { headWithin } from "./names.js";
|
|
30
31
|
export const BODY_TEMPLATE_VERSION = 2;
|
|
31
32
|
/** After this many failed attempts a publication stops retrying and pages instead. */
|
|
@@ -529,168 +530,179 @@ export async function sweepMerges(store, options) {
|
|
|
529
530
|
const method = grant.mergeMethod;
|
|
530
531
|
const termsHash = mergeTermsHash(grant);
|
|
531
532
|
for (const publication of store.openedPublications()) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const prNumber = publication.prNumber;
|
|
535
|
-
const headSha = publication.headSha;
|
|
536
|
-
// The local-repo fence (finding 6): repo B's publication never rides
|
|
537
|
-
// repo A's sweep, whatever the remote terms coincide on.
|
|
538
|
-
const ref = store.refForId(publication.taskRef);
|
|
539
|
-
if (ref === null || ref.repo !== options.repo)
|
|
540
|
-
continue;
|
|
541
|
-
if (publication.githubRepo !== grant.githubRepo)
|
|
542
|
-
continue;
|
|
543
|
-
const blocker = store.mergeBlockerFor(publication.id);
|
|
544
|
-
if (blocker !== null) {
|
|
533
|
+
// A release's result merges only through `toolroll release` (release-once.md): the sweep never takes it up.
|
|
534
|
+
if (releasePublication(store, publication.id)) {
|
|
545
535
|
report.skipped++;
|
|
546
|
-
pagePerson(publication, "blocked", "PR #" + prNumber + " holds for a repair", "A CI repair (" + (blocker.taskId ?? "?") + ") is in flight. It merges nothing until you lift it: toolroll publish unblock " + prNumber + ".");
|
|
547
536
|
continue;
|
|
548
537
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
538
|
+
// One bad row never stops the rest of the sweep: every other publication, in every project, still merges.
|
|
539
|
+
try {
|
|
540
|
+
if (publication.prNumber === null || publication.headSha === null)
|
|
541
|
+
continue;
|
|
542
|
+
const prNumber = publication.prNumber;
|
|
543
|
+
const headSha = publication.headSha;
|
|
544
|
+
// The local-repo fence (finding 6): repo B's publication never rides
|
|
545
|
+
// repo A's sweep, whatever the remote terms coincide on.
|
|
546
|
+
const ref = store.refForId(publication.taskRef);
|
|
547
|
+
if (ref === null || ref.repo !== options.repo)
|
|
548
|
+
continue;
|
|
549
|
+
if (publication.githubRepo !== grant.githubRepo)
|
|
550
|
+
continue;
|
|
551
|
+
const blocker = store.mergeBlockerFor(publication.id);
|
|
552
|
+
if (blocker !== null) {
|
|
553
|
+
report.skipped++;
|
|
554
|
+
pagePerson(publication, "blocked", "PR #" + prNumber + " holds for a repair", "A CI repair (" + (blocker.taskId ?? "?") + ") is in flight. It merges nothing until you lift it: toolroll publish unblock " + prNumber + ".");
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
store.createMergeIntent({ publication: publication.id, repo: options.repo, grantTermsHash: termsHash, headSha, method, deleteBranch: grant.mergeDeleteBranch === true }, clock());
|
|
558
|
+
const intent = store.mergeIntentFor(publication.id);
|
|
559
|
+
if (intent === null || intent.state === "merged" || intent.state === "refused" || intent.state === "superseded")
|
|
560
|
+
continue;
|
|
561
|
+
// WAITING-HUMAN never fires itself (E1): the person is paged once,
|
|
562
|
+
// episodically, with the exact ceremony that releases it.
|
|
563
|
+
if (intent.state === "waiting-human") {
|
|
564
|
+
report.skipped++;
|
|
565
|
+
pagePerson(publication, "waiting-human", "PR #" + prNumber + " waits for your go-ahead", "Merges wait for you while this posture holds. When you want it merged: toolroll publish merge " + prNumber + " --as <you> --token <t>. It still merges only when CI is seen green on the exact head.");
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
// FIRING is one-shot (E1): a live firing row belongs to its owner.
|
|
569
|
+
// A STALE one (deadline passed) recovers by STRUCTURED REMOTE REREAD
|
|
570
|
+
// (F1): an observed terminal settles it; an OPEN same-head PR is
|
|
571
|
+
// NEVER auto-retried - the person gets the recovery ceremony.
|
|
572
|
+
if (intent.state === "firing") {
|
|
573
|
+
if (intent.firingDeadline !== null && intent.firingDeadline <= clock().toISOString()) {
|
|
574
|
+
const reread = await observeOne(store, exec, clock, grant.githubRepo, prNumber);
|
|
575
|
+
if (!reread.ok && reread.why === "pr-merged") {
|
|
576
|
+
store.settleMergeIntent(intent.id, intent.generation, "merged", clock(), {
|
|
577
|
+
receipt: "observed merged remotely after a stale firing claim", from: "firing", bumpGeneration: true,
|
|
578
|
+
});
|
|
579
|
+
report.merged++;
|
|
580
|
+
}
|
|
581
|
+
else if (!reread.ok && reread.why.startsWith("pr-")) {
|
|
582
|
+
store.settleMergeIntent(intent.id, intent.generation, "superseded", clock(), {
|
|
583
|
+
error: reread.why, from: "firing", bumpGeneration: true,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
else if (reread.ok && reread.headSha !== intent.headSha) {
|
|
587
|
+
store.settleMergeIntent(intent.id, intent.generation, "superseded", clock(), {
|
|
588
|
+
error: "the head moved while the firing claim was stale", from: "firing", bumpGeneration: true,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
report.skipped++;
|
|
593
|
+
pagePerson(publication, "half-fired", "PR #" + prNumber + " may have half-merged", "A merge was issued and its owner went silent; the PR is still open on the same commit. Nothing retries by itself. If you want it fired again: toolroll publish refire " + prNumber + " --as <you> --token <t>.");
|
|
594
|
+
}
|
|
572
595
|
}
|
|
573
|
-
else
|
|
574
|
-
|
|
575
|
-
error: reread.why, from: "firing", bumpGeneration: true,
|
|
576
|
-
});
|
|
596
|
+
else {
|
|
597
|
+
report.skipped++;
|
|
577
598
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const claim = store.claimMergeIntent(intent.id, who, MERGE_LEASE_MS, clock());
|
|
602
|
+
if (claim === null)
|
|
603
|
+
continue; // someone else's live claim
|
|
604
|
+
const settle = (state, detail = {}) => store.settleMergeIntent(intent.id, claim.generation, state, clock(), { from: "claimed", ...detail });
|
|
605
|
+
// The live grant, re-read AFTER the claim (finding 7): revocation,
|
|
606
|
+
// narrowing, or any term drift supersedes rather than merges.
|
|
607
|
+
const live = store.publicationGrantFor(options.repo);
|
|
608
|
+
if (live === null || live.merge !== true || mergeTermsHash(live) !== termsHash || intent.headSha !== headSha) {
|
|
609
|
+
settle("superseded", { error: "the grant or the head moved since this intent was written" });
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const queue = await proveNoMergeQueue(exec, grant.githubRepo, grant.base);
|
|
613
|
+
if (!queue.ok) {
|
|
614
|
+
settle("refused", { error: queue.why });
|
|
615
|
+
report.refused++;
|
|
616
|
+
pagePerson(publication, queue.why, "PR #" + prNumber + " will not auto-merge", queue.why === "merge-queue"
|
|
617
|
+
? "The base branch requires a merge queue - out of this release's scope. Merge it on GitHub, or lift the queue and run: toolroll publish rearm " + prNumber + "."
|
|
618
|
+
: "The branch's protection could not be READ well enough to prove no merge queue governs it - auto-merge stays paused. Check access, then run: toolroll publish rearm " + prNumber + ".");
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
// One fresh, winning observation - green on the EXACT publication head.
|
|
622
|
+
const seen = await observeOne(store, exec, clock, grant.githubRepo, prNumber);
|
|
623
|
+
if (!seen.ok) {
|
|
624
|
+
if (seen.why === "draft") {
|
|
625
|
+
settle("refused", { error: "draft" });
|
|
626
|
+
report.refused++;
|
|
627
|
+
pagePerson(publication, "draft", "PR #" + prNumber + " is a draft", "Drafts never merge themselves. Mark it ready on GitHub, then run: toolroll publish rearm " + prNumber + ".");
|
|
628
|
+
}
|
|
629
|
+
else if (seen.why.startsWith("pr-")) {
|
|
630
|
+
settle("superseded", { error: seen.why });
|
|
582
631
|
}
|
|
583
632
|
else {
|
|
633
|
+
settle("pending");
|
|
634
|
+
report.problems.push("PR #" + prNumber + ": " + seen.why);
|
|
635
|
+
}
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (seen.headSha !== headSha || seen.state !== "passing") {
|
|
639
|
+
settle(seen.headSha !== headSha ? "superseded" : "pending", {
|
|
640
|
+
error: seen.headSha !== headSha ? "the head moved" : "CI is " + seen.state + ", not green",
|
|
641
|
+
});
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
// The linearization point (E1): claimed -> firing as a DURABLE CAS
|
|
645
|
+
// re-proving grant terms, head, and basis-specific authority in one
|
|
646
|
+
// transaction. Before it commits, a revocation wins; after, the
|
|
647
|
+
// issued call is one-shot and no revocation demotes it.
|
|
648
|
+
const fired = store.fireMergeIntent(intent.id, claim.generation, { repo: options.repo, headSha, liveGrantTermsHash: mergeTermsHash(live), graceMs: FIRING_GRACE_MS }, clock());
|
|
649
|
+
if (!fired.ok) {
|
|
650
|
+
if (fired.outcome === "waiting-human") {
|
|
584
651
|
report.skipped++;
|
|
585
|
-
pagePerson(publication, "
|
|
652
|
+
pagePerson(publication, "waiting-human", "PR #" + prNumber + " waits for your go-ahead", fired.why + " - when you want it merged: toolroll publish merge " + prNumber + " --as <you> --token <t>.");
|
|
586
653
|
}
|
|
654
|
+
continue;
|
|
587
655
|
}
|
|
588
|
-
|
|
589
|
-
|
|
656
|
+
const merged = await exec("gh", [
|
|
657
|
+
"pr", "merge", String(prNumber),
|
|
658
|
+
"--repo", grant.githubRepo,
|
|
659
|
+
"--" + method,
|
|
660
|
+
"--match-head-commit", headSha,
|
|
661
|
+
...(grant.mergeDeleteBranch === true ? ["--delete-branch"] : []),
|
|
662
|
+
], { timeoutMs: EXEC_TIMEOUT_MS });
|
|
663
|
+
if (merged.code === 0) {
|
|
664
|
+
store.transact(() => {
|
|
665
|
+
settle("merged", { receipt: "merged " + headSha.slice(0, 12) + " at " + clock().toISOString(), countAttempt: true, from: "firing" });
|
|
666
|
+
store.recordPublicationRemoteState(publication.id, "MERGED", clock());
|
|
667
|
+
store.resolveCiEpisodes(grant.githubRepo, prNumber, null, clock());
|
|
668
|
+
});
|
|
669
|
+
report.merged++;
|
|
670
|
+
store.resolveEpisodes(`merge-attn:${publication.id}`, clock());
|
|
671
|
+
page(publication, "merge:" + publication.id + ":merged", "merged: PR #" + prNumber, (publication.prUrl ?? grant.githubRepo) + " - " + method + " of " + headSha.slice(0, 12) + ", under the merge terms you approved.");
|
|
672
|
+
continue;
|
|
590
673
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
continue; // someone else's live claim
|
|
596
|
-
const settle = (state, detail = {}) => store.settleMergeIntent(intent.id, claim.generation, state, clock(), { from: "claimed", ...detail });
|
|
597
|
-
// The live grant, re-read AFTER the claim (finding 7): revocation,
|
|
598
|
-
// narrowing, or any term drift supersedes rather than merges.
|
|
599
|
-
const live = store.publicationGrantFor(options.repo);
|
|
600
|
-
if (live === null || live.merge !== true || mergeTermsHash(live) !== termsHash || intent.headSha !== headSha) {
|
|
601
|
-
settle("superseded", { error: "the grant or the head moved since this intent was written" });
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
const queue = await proveNoMergeQueue(exec, grant.githubRepo, grant.base);
|
|
605
|
-
if (!queue.ok) {
|
|
606
|
-
settle("refused", { error: queue.why });
|
|
607
|
-
report.refused++;
|
|
608
|
-
pagePerson(publication, queue.why, "PR #" + prNumber + " will not auto-merge", queue.why === "merge-queue"
|
|
609
|
-
? "The base branch requires a merge queue - out of this release's scope. Merge it on GitHub, or lift the queue and run: toolroll publish rearm " + prNumber + "."
|
|
610
|
-
: "The branch's protection could not be READ well enough to prove no merge queue governs it - auto-merge stays paused. Check access, then run: toolroll publish rearm " + prNumber + ".");
|
|
611
|
-
continue;
|
|
612
|
-
}
|
|
613
|
-
// One fresh, winning observation - green on the EXACT publication head.
|
|
614
|
-
const seen = await observeOne(store, exec, clock, grant.githubRepo, prNumber);
|
|
615
|
-
if (!seen.ok) {
|
|
616
|
-
if (seen.why === "draft") {
|
|
617
|
-
settle("refused", { error: "draft" });
|
|
674
|
+
// NEVER classify from the exit code alone (only auth=4 is distinct):
|
|
675
|
+
// re-read structured state and let the PR say what happened.
|
|
676
|
+
if (merged.code === 4) {
|
|
677
|
+
settle("refused", { error: "credential", countAttempt: true, from: "firing" });
|
|
618
678
|
report.refused++;
|
|
619
|
-
pagePerson(publication, "
|
|
679
|
+
pagePerson(publication, "credential", "PR #" + prNumber + ": gh is not signed in", "The merge acts as this machine's GitHub account, and it could not authenticate. Run gh auth login, then: toolroll publish rearm " + prNumber + ".");
|
|
680
|
+
continue;
|
|
620
681
|
}
|
|
621
|
-
|
|
622
|
-
|
|
682
|
+
const after = await observeOne(store, exec, clock, grant.githubRepo, prNumber);
|
|
683
|
+
if (!after.ok && after.why === "pr-merged") {
|
|
684
|
+
settle("merged", { receipt: "merged (confirmed by re-read after an ambiguous exit)", countAttempt: true, from: "firing" });
|
|
685
|
+
report.merged++;
|
|
623
686
|
}
|
|
624
|
-
else {
|
|
625
|
-
settle("
|
|
626
|
-
|
|
687
|
+
else if (!after.ok && (after.why === "draft" || after.why.startsWith("pr-"))) {
|
|
688
|
+
settle(after.why === "draft" ? "refused" : "superseded", { error: after.why, countAttempt: true, from: "firing" });
|
|
689
|
+
if (after.why === "draft")
|
|
690
|
+
report.refused++;
|
|
627
691
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (seen.headSha !== headSha || seen.state !== "passing") {
|
|
631
|
-
settle(seen.headSha !== headSha ? "superseded" : "pending", {
|
|
632
|
-
error: seen.headSha !== headSha ? "the head moved" : "CI is " + seen.state + ", not green",
|
|
633
|
-
});
|
|
634
|
-
continue;
|
|
635
|
-
}
|
|
636
|
-
// The linearization point (E1): claimed -> firing as a DURABLE CAS
|
|
637
|
-
// re-proving grant terms, head, and basis-specific authority in one
|
|
638
|
-
// transaction. Before it commits, a revocation wins; after, the
|
|
639
|
-
// issued call is one-shot and no revocation demotes it.
|
|
640
|
-
const fired = store.fireMergeIntent(intent.id, claim.generation, { repo: options.repo, headSha, liveGrantTermsHash: mergeTermsHash(live), graceMs: FIRING_GRACE_MS }, clock());
|
|
641
|
-
if (!fired.ok) {
|
|
642
|
-
if (fired.outcome === "waiting-human") {
|
|
643
|
-
report.skipped++;
|
|
644
|
-
pagePerson(publication, "waiting-human", "PR #" + prNumber + " waits for your go-ahead", fired.why + " - when you want it merged: toolroll publish merge " + prNumber + " --as <you> --token <t>.");
|
|
692
|
+
else if (after.ok && after.headSha !== headSha) {
|
|
693
|
+
settle("superseded", { error: "the head moved during the attempt", countAttempt: true, from: "firing" });
|
|
645
694
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const merged = await exec("gh", [
|
|
649
|
-
"pr", "merge", String(prNumber),
|
|
650
|
-
"--repo", grant.githubRepo,
|
|
651
|
-
"--" + method,
|
|
652
|
-
"--match-head-commit", headSha,
|
|
653
|
-
...(grant.mergeDeleteBranch === true ? ["--delete-branch"] : []),
|
|
654
|
-
], { timeoutMs: EXEC_TIMEOUT_MS });
|
|
655
|
-
if (merged.code === 0) {
|
|
656
|
-
store.transact(() => {
|
|
657
|
-
settle("merged", { receipt: "merged " + headSha.slice(0, 12) + " at " + clock().toISOString(), countAttempt: true, from: "firing" });
|
|
658
|
-
store.recordPublicationRemoteState(publication.id, "MERGED", clock());
|
|
659
|
-
store.resolveCiEpisodes(grant.githubRepo, prNumber, null, clock());
|
|
660
|
-
});
|
|
661
|
-
report.merged++;
|
|
662
|
-
store.resolveEpisodes(`merge-attn:${publication.id}`, clock());
|
|
663
|
-
page(publication, "merge:" + publication.id + ":merged", "merged: PR #" + prNumber, (publication.prUrl ?? grant.githubRepo) + " - " + method + " of " + headSha.slice(0, 12) + ", under the merge terms you approved.");
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
// NEVER classify from the exit code alone (only auth=4 is distinct):
|
|
667
|
-
// re-read structured state and let the PR say what happened.
|
|
668
|
-
if (merged.code === 4) {
|
|
669
|
-
settle("refused", { error: "credential", countAttempt: true, from: "firing" });
|
|
670
|
-
report.refused++;
|
|
671
|
-
pagePerson(publication, "credential", "PR #" + prNumber + ": gh is not signed in", "The merge acts as this machine's GitHub account, and it could not authenticate. Run gh auth login, then: toolroll publish rearm " + prNumber + ".");
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
const after = await observeOne(store, exec, clock, grant.githubRepo, prNumber);
|
|
675
|
-
if (!after.ok && after.why === "pr-merged") {
|
|
676
|
-
settle("merged", { receipt: "merged (confirmed by re-read after an ambiguous exit)", countAttempt: true, from: "firing" });
|
|
677
|
-
report.merged++;
|
|
678
|
-
}
|
|
679
|
-
else if (!after.ok && (after.why === "draft" || after.why.startsWith("pr-"))) {
|
|
680
|
-
settle(after.why === "draft" ? "refused" : "superseded", { error: after.why, countAttempt: true, from: "firing" });
|
|
681
|
-
if (after.why === "draft")
|
|
695
|
+
else if (intent.attempts + 1 >= MERGE_MAX_ATTEMPTS) {
|
|
696
|
+
settle("refused", { error: firstLine(merged.stderr) || "the merge was rejected", countAttempt: true, from: "firing" });
|
|
682
697
|
report.refused++;
|
|
698
|
+
pagePerson(publication, "rejected", "PR #" + prNumber + " would not merge", (firstLine(merged.stderr) || "GitHub rejected the merge") + " - likely branch protection, required reviews, or a conflict. Fix the cause, then run: toolroll publish rearm " + prNumber + ".");
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
settle("pending", { error: firstLine(merged.stderr) || "transport", countAttempt: true, from: "firing" });
|
|
702
|
+
}
|
|
683
703
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
else if (intent.attempts + 1 >= MERGE_MAX_ATTEMPTS) {
|
|
688
|
-
settle("refused", { error: firstLine(merged.stderr) || "the merge was rejected", countAttempt: true, from: "firing" });
|
|
689
|
-
report.refused++;
|
|
690
|
-
pagePerson(publication, "rejected", "PR #" + prNumber + " would not merge", (firstLine(merged.stderr) || "GitHub rejected the merge") + " - likely branch protection, required reviews, or a conflict. Fix the cause, then run: toolroll publish rearm " + prNumber + ".");
|
|
691
|
-
}
|
|
692
|
-
else {
|
|
693
|
-
settle("pending", { error: firstLine(merged.stderr) || "transport", countAttempt: true, from: "firing" });
|
|
704
|
+
catch (error) {
|
|
705
|
+
report.problems.push("publication " + publication.id + ": " + (error instanceof Error ? error.message : String(error)));
|
|
694
706
|
}
|
|
695
707
|
}
|
|
696
708
|
return report;
|
|
@@ -180,4 +180,6 @@ export type PullRequestView = {
|
|
|
180
180
|
export declare function pullRequestViewOf(store: Store, runId: number): PullRequestView | null;
|
|
181
181
|
/** The newest pull request any version of a task opened through Complete (versions oldest first), or null. */
|
|
182
182
|
export declare function newestPullRequestOf(store: Store, versions: readonly string[]): PullRequestView | null;
|
|
183
|
+
/** Why a machine credential's merge is refused (approval policy §1.7). */
|
|
184
|
+
export declare const MERGE_NEEDS_PERSON = "Only a person can merge. Merge it on the task page or from your paired chat.";
|
|
183
185
|
export {};
|
|
@@ -21,8 +21,10 @@ import { failingChecks, summarizeChecks } from "./pulls.js";
|
|
|
21
21
|
import { assignmentOf, checkAssignmentAsOperator } from "./assignment.js";
|
|
22
22
|
import { requestResultChanges } from "./result-actions.js";
|
|
23
23
|
import { revisionSourceOf } from "./result-review.js";
|
|
24
|
-
import { authenticateApprover } from "./scope.js";
|
|
24
|
+
import { authenticateApprover, personProof } from "./scope.js";
|
|
25
25
|
import { BRANCH_PREFIX, headWithin } from "./names.js";
|
|
26
|
+
import { NIGHTLY_CHECK_ONLY, RELEASE_MERGES_ONLY, nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
27
|
+
import { belongsToRelease } from "./release-authority.js";
|
|
26
28
|
import { fullCheckGate, requestFollowUpChecks } from "./result-follow-ups.js";
|
|
27
29
|
export const MERGE_METHODS = ["squash", "merge", "rebase"];
|
|
28
30
|
/** CI revisions one task may file across its revisions before a person is asked instead. */
|
|
@@ -143,6 +145,10 @@ export function pullRequestBlocker(store, runId) {
|
|
|
143
145
|
return "Pull requests aren't set up for this project.";
|
|
144
146
|
if (run.headRevision === null || run.committed === false)
|
|
145
147
|
return "This result has no commit to publish.";
|
|
148
|
+
if (nightlyTaskRef(store, run.taskRef))
|
|
149
|
+
return NIGHTLY_CHECK_ONLY;
|
|
150
|
+
if (releaseTaskRef(store, run.taskRef))
|
|
151
|
+
return RELEASE_MERGES_ONLY;
|
|
146
152
|
if (run.branch === null || !headWithin(run.branch, grant.headPrefix))
|
|
147
153
|
return "This result's branch isn't one Toolroll may push.";
|
|
148
154
|
if (store.hasRedactedTerminalDiff(runId))
|
|
@@ -370,8 +376,13 @@ export async function mergeAsPerson(store, input) {
|
|
|
370
376
|
const repo = run === null ? null : store.refById(run.taskRef)?.repo ?? null;
|
|
371
377
|
if (input.password === "")
|
|
372
378
|
return refuse("password", "Enter your password to merge.");
|
|
373
|
-
|
|
379
|
+
const proved = authenticateApprover(store, input.name, input.password, repo);
|
|
380
|
+
if (!proved.ok)
|
|
374
381
|
return refuse("password", "That password didn't work, or you can't merge in this project.");
|
|
382
|
+
// A merge by hand is a person's (approval policy §1.7): the lead's token or a remote API token proves a credential,
|
|
383
|
+
// not a person — `--anyway` included.
|
|
384
|
+
if (!personProof(proved.method))
|
|
385
|
+
return refuse("person-required", MERGE_NEEDS_PERSON);
|
|
375
386
|
return mergePullRequest(store, { runId: input.runId, by: input.name, ...(input.anyway === true ? { anyway: true } : {}), ...(input.exec === undefined ? {} : { exec: input.exec }), ...(input.clock === undefined ? {} : { clock: input.clock }) });
|
|
376
387
|
}
|
|
377
388
|
/**
|
|
@@ -382,6 +393,15 @@ export async function mergeAsPerson(store, input) {
|
|
|
382
393
|
export async function mergePullRequest(store, input) {
|
|
383
394
|
const exec = input.exec ?? execRun;
|
|
384
395
|
const clock = input.clock ?? (() => new Date());
|
|
396
|
+
// The nightly check of main (release-once.md, part 3) is a check only: it never merges.
|
|
397
|
+
const mergedRun = store.getRun(input.runId);
|
|
398
|
+
const mergedTask = mergedRun === null ? null : store.refById(mergedRun.taskRef)?.externalId ?? null;
|
|
399
|
+
if (mergedTask !== null && store.handle.prepare("SELECT 1 AS hit FROM nightly_check WHERE task = ?").get(mergedTask) !== undefined)
|
|
400
|
+
return refuse("check-only", "The nightly check of main is only a check; it never merges.");
|
|
401
|
+
// A release's task merges only through `toolroll release` (release-once.md), which proves the live release, reserves
|
|
402
|
+
// the merge at its lease's epoch and checks the exact pull request: never here, by hand or automatically.
|
|
403
|
+
if (mergedTask !== null && belongsToRelease(store, mergedTask))
|
|
404
|
+
return refuse("release", "This result belongs to a release; it merges only through `toolroll release`.");
|
|
385
405
|
const publication = store.publicationForRun(input.runId);
|
|
386
406
|
const follow = publication === null ? null : followOf(store, publication.id);
|
|
387
407
|
if (publication === null || follow === null || publication.prNumber === null)
|
|
@@ -505,3 +525,5 @@ export function newestPullRequestOf(store, versions) {
|
|
|
505
525
|
}
|
|
506
526
|
return null;
|
|
507
527
|
}
|
|
528
|
+
/** Why a machine credential's merge is refused (approval policy §1.7). */
|
|
529
|
+
export const MERGE_NEEDS_PERSON = "Only a person can merge. Merge it on the task page or from your paired chat.";
|
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
* the sign-ins this machine already has.
|
|
6
6
|
*/
|
|
7
7
|
import type { GateState, Installed, ReleaseAdapters } from "./release.js";
|
|
8
|
+
import { Store } from "./store.js";
|
|
9
|
+
import { type BuildRunner } from "./release-carry.js";
|
|
10
|
+
import { type ReleaseRepo, type ReleaseTools } from "./release-git.js";
|
|
11
|
+
export { RELEASE_PATH, RELEASE_REMOTE, releaseConfigProblem, releaseGitArgs, releaseGitEnv, releaseRemote, releaseTools, type ReleaseRepo, type ReleaseTools } from "./release-git.js";
|
|
8
12
|
export type Exec = (command: string, args: readonly string[], options?: {
|
|
9
13
|
cwd?: string;
|
|
10
14
|
timeoutMs?: number;
|
|
11
15
|
env?: NodeJS.ProcessEnv;
|
|
16
|
+
input?: string;
|
|
12
17
|
}) => Promise<{
|
|
13
18
|
code: number;
|
|
14
19
|
stdout: string;
|
|
@@ -38,8 +43,14 @@ export declare function stageGate(stage: string, owner: () => string | null): {
|
|
|
38
43
|
id: string;
|
|
39
44
|
phase: string;
|
|
40
45
|
} | null;
|
|
46
|
+
/** The recorded repository for a gate checkout, from the plane's database; throws in plain words when there is none. */
|
|
47
|
+
export declare function recordedReleaseRepo(database: string, checkout: string): ReleaseRepo;
|
|
41
48
|
export type AdapterOptions = {
|
|
42
49
|
checkout: string;
|
|
50
|
+
/** The recorded repository and the programs, when a caller (a test) supplies them; otherwise read from the plane's
|
|
51
|
+
* records and found on this machine when the first Git command runs. */
|
|
52
|
+
releaseRepo?: ReleaseRepo;
|
|
53
|
+
tools?: ReleaseTools;
|
|
43
54
|
/** The live database (default: this installation's). */
|
|
44
55
|
database?: string;
|
|
45
56
|
processes?: typeof processes;
|
|
@@ -66,4 +77,8 @@ export declare function releaseAdapters(options: AdapterOptions): ReleaseAdapter
|
|
|
66
77
|
export declare function workflowRunsOnChange(text: string): boolean;
|
|
67
78
|
/** The formula with its first `url` and `sha256` replaced; anything else is left exactly as it was. */
|
|
68
79
|
export declare function bumpFormula(text: string, url: string, sha256: string): string;
|
|
69
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The release's authority record through a store (release-once.md): what `toolroll release` reads and writes for its
|
|
82
|
+
* lease, its carries and its operations. `open` hands a store for each call and `done` lets it go.
|
|
83
|
+
*/
|
|
84
|
+
export declare function storeReleaseAdapter(open: () => Store, done: (store: Store) => void, now: () => Date, runner?: BuildRunner): NonNullable<ReleaseAdapters["release"]>;
|