toolroll 0.9.85 → 0.9.87
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 +83 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.js +4 -3
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -68
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +363 -177
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.js +1 -0
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +4 -1
- package/dist/contracts/console-api.js +4 -1
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +21 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/coordinator.js +1 -0
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/deploy-gate.js +4 -0
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +54 -5
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +152 -32
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -6
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-flow.js +35 -4
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +21 -0
- package/dist/release.js +40 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/result-follow-ups.js +4 -0
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +8 -0
- package/dist/scope.js +13 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +20 -18
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +31 -2
- package/dist/server/flows.js +79 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +35 -190
- package/dist/server/render-pages.js +154 -563
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +43 -5
- package/dist/server/render-tasks.js +131 -91
- package/dist/server/route-table.js +31 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +90 -2
- package/dist/server/tasks.d.ts +18 -0
- package/dist/server/tasks.js +148 -41
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +182 -5
- package/dist/store.js +512 -38
- package/dist/surface.js +23 -3
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.d.ts +24 -0
- package/dist/work-index.js +56 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +8 -4
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* read must show the PR open, on the accepted commit, with checks passing; GitHub's head-commit match guards
|
|
15
15
|
* the race; the branch is deleted; the merge commit and who merged are recorded in the ledger.
|
|
16
16
|
*/
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
18
18
|
import { run as execRun } from "./exec.js";
|
|
19
19
|
import { bodyHashOf, publicationBody, publicationGrantOf } from "./publish.js";
|
|
20
20
|
import { failingChecks, summarizeChecks } from "./pulls.js";
|
|
@@ -23,7 +23,7 @@ import { requestResultChanges } from "./result-actions.js";
|
|
|
23
23
|
import { revisionSourceOf } from "./result-review.js";
|
|
24
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";
|
|
26
|
+
import { NIGHTLY_CHECK_ONLY, RELEASE_MERGES_ONLY, nightlyTaskRef, refArchived, releaseTaskRef } from "./store.js";
|
|
27
27
|
import { belongsToRelease } from "./release-authority.js";
|
|
28
28
|
import { autoReleaseSealed } from "./release-auto.js";
|
|
29
29
|
import { fullCheckGate, requestFollowUpChecks } from "./result-follow-ups.js";
|
|
@@ -292,6 +292,9 @@ export async function followPullRequests(store, seen, options) {
|
|
|
292
292
|
continue;
|
|
293
293
|
const pr = publication.prNumber;
|
|
294
294
|
const taskId = taskIdOf(store, publication);
|
|
295
|
+
// A put-away task (v145) gets no automatic revision, merge or ready notice: restoring it resumes following.
|
|
296
|
+
if (refArchived(store, publication.taskRef))
|
|
297
|
+
continue;
|
|
295
298
|
if (one.remoteState === "MERGED") {
|
|
296
299
|
updateFollow(store, publication.id, { merge_commit: one.mergeCommit ?? "", merged_by: "GitHub", merged_at: clock().toISOString() }, clock());
|
|
297
300
|
continue;
|
|
@@ -397,6 +400,8 @@ export async function mergePullRequest(store, input) {
|
|
|
397
400
|
// The nightly check of main (release-once.md, part 3) is a check only: it never merges.
|
|
398
401
|
const mergedRun = store.getRun(input.runId);
|
|
399
402
|
const mergedTask = mergedRun === null ? null : store.refById(mergedRun.taskRef)?.externalId ?? null;
|
|
403
|
+
if (mergedRun !== null && refArchived(store, mergedRun.taskRef))
|
|
404
|
+
return refuse("archived", "This task is put away. Restore it before merging.");
|
|
400
405
|
if (mergedTask !== null && store.handle.prepare("SELECT 1 AS hit FROM nightly_check WHERE task = ?").get(mergedTask) !== undefined)
|
|
401
406
|
return refuse("check-only", "The nightly check of main is only a check; it never merges.");
|
|
402
407
|
// A release's task merges only through `toolroll release` (release-once.md), which proves the live release, reserves
|
|
@@ -470,8 +475,34 @@ export async function mergePullRequest(store, input) {
|
|
|
470
475
|
return fail("moved", "The pull request has commits that weren't part of the completed result, so it won't be merged from here.");
|
|
471
476
|
if (before.checks !== "passing")
|
|
472
477
|
return fail("checks", before.checks === "failing" ? "Checks are failing, so it can't merge." : before.checks === "running" ? "Checks are still running. Merge once they pass." : "No checks have reported yet. Merge once they pass.");
|
|
473
|
-
|
|
474
|
-
|
|
478
|
+
// The merge is reserved with the put-away check in one write (v145): a put away that committed first refuses it, and
|
|
479
|
+
// one that comes after waits for GitHub's answer (the reservation counts as work going on).
|
|
480
|
+
// One merge at a time, each owning its reservation by its token: a second caller is refused, and each releases only its own.
|
|
481
|
+
const token = randomUUID();
|
|
482
|
+
const reserved = store.transact(() => {
|
|
483
|
+
if (refArchived(store, publication.taskRef))
|
|
484
|
+
return "archived";
|
|
485
|
+
if (store.handle.prepare("SELECT 1 FROM hold WHERE task_ref = ? AND owner_id LIKE 'project-delete:%' AND (until IS NULL OR until > ?)").get(publication.taskRef, clock().toISOString()) !== undefined)
|
|
486
|
+
return "deleting";
|
|
487
|
+
if (store.handle.prepare("SELECT 1 FROM merge_in_flight WHERE publication = ? AND at > ?").get(publication.id, new Date(clock().getTime() - 15 * 60_000).toISOString()) !== undefined)
|
|
488
|
+
return "merging";
|
|
489
|
+
store.handle.prepare("INSERT OR REPLACE INTO merge_in_flight (publication, task_ref, token, by, at) VALUES (?, ?, ?, ?, ?)").run(publication.id, publication.taskRef, token, input.by, clock().toISOString());
|
|
490
|
+
return "ok";
|
|
491
|
+
});
|
|
492
|
+
if (reserved === "archived")
|
|
493
|
+
return refuse("archived", "This task is put away. Restore it before merging.");
|
|
494
|
+
if (reserved === "deleting")
|
|
495
|
+
return refuse("deleting", "This task's project is being deleted, so nothing merges.");
|
|
496
|
+
if (reserved === "merging")
|
|
497
|
+
return refuse("merging", "This pull request is already being merged. Wait for that to finish.");
|
|
498
|
+
let merged, after;
|
|
499
|
+
try {
|
|
500
|
+
merged = await exec("gh", ["pr", "merge", pr, "--repo", publication.githubRepo, `--${method}`, "--match-head-commit", publication.headSha, "--delete-branch"], { timeoutMs: EXEC_TIMEOUT_MS, env: { GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1" } });
|
|
501
|
+
after = await view();
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
store.handle.prepare("DELETE FROM merge_in_flight WHERE publication = ? AND token = ?").run(publication.id, token);
|
|
505
|
+
}
|
|
475
506
|
if (after?.state === "MERGED") {
|
|
476
507
|
record(after.commit, input.by);
|
|
477
508
|
return { ok: true, commit: after.commit };
|
|
@@ -21,4 +21,5 @@ export type PullRequestSettingsView = {
|
|
|
21
21
|
problem: string | null;
|
|
22
22
|
};
|
|
23
23
|
export declare function pullRequestSettingsHtml(view: PullRequestSettingsView): Html;
|
|
24
|
-
|
|
24
|
+
/** Kept for the console's style bundle (server/chrome.ts); the page itself is kit markup. */
|
|
25
|
+
export declare const PULL_REQUEST_SETTINGS_CSS = "";
|
package/dist/pull-request-ui.js
CHANGED
|
@@ -1,44 +1,55 @@
|
|
|
1
1
|
/** Settings → Projects → Pull requests: one project's pull-request setup, in plain words. Off shows what setup
|
|
2
2
|
* found (or what to fix) and one Turn on; on shows where pull requests go and how they merge. Every change takes
|
|
3
3
|
* the password; turning on restates the exact repository and branch that were checked. */
|
|
4
|
-
import { html,
|
|
4
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
5
|
+
import { acts, btn, disclose, formRow, infoLine, note, notice, select, settingsSection, switchInput } from "./server/kit.js";
|
|
6
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
5
7
|
const METHOD_WORDS = { squash: "Squash and merge", merge: "Merge commit", rebase: "Rebase and merge" };
|
|
8
|
+
const mono = (text) => html `<span class="k-mono">${text}</span>`;
|
|
6
9
|
export function pullRequestSettingsHtml(view) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
view.
|
|
11
|
-
|
|
12
|
-
];
|
|
13
|
-
const head = html `<p class="meta">${view.name} · <span class="mono">${view.repo}</span></p>${notes}`;
|
|
10
|
+
const head = joinHtml([
|
|
11
|
+
view.problem === null ? "" : notice(view.problem),
|
|
12
|
+
view.said === null ? "" : infoLine(view.said),
|
|
13
|
+
note(html `${view.name} · ${mono(view.repo)}`),
|
|
14
|
+
]);
|
|
14
15
|
const publishing = view.publishing;
|
|
15
16
|
if (publishing.on) {
|
|
16
|
-
const status =
|
|
17
|
+
const status = settingsSection({ id: "pull-requests", title: "Pull requests", state: " · On",
|
|
18
|
+
lead: html `${mono(publishing.githubRepo)} into ${mono(publishing.base)}`,
|
|
19
|
+
body: note(`${publishing.legacy ? "Every finished build opens a pull request (set up with publish grant)." : "Complete offers “Complete and open a pull request”."} Set up by ${publishing.grantedBy}.`) });
|
|
17
20
|
if (!view.canChange)
|
|
18
|
-
return
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
+
return joinHtml([head, status]);
|
|
22
|
+
const merging = settingsSection({ id: "merging", title: "Merging",
|
|
23
|
+
body: kitSectionForm({ proof: "pull-requests", action: "/settings/pull-requests", id: "merging-form", gate: true, hidden: { repo: view.repo, act: "settings" },
|
|
24
|
+
body: joinHtml([
|
|
25
|
+
formRow("Merge method", null, select({ id: "pr-method", name: "method", label: "Merge method", current: publishing.mergeMethod,
|
|
26
|
+
items: ["squash", "merge", "rebase"].map(method => [method, `${METHOD_WORDS[method]}${method === "squash" ? " (default)" : ""}`]) })),
|
|
27
|
+
formRow("Merge when checks pass", "Off by default. When on, a pull request merges by itself once its checks pass on the completed commit. Branches are deleted after merging.", switchInput({ id: "pr-when-green", name: "when-green", label: publishing.mergeWhenGreen ? "On" : "Off", on: publishing.mergeWhenGreen, attrs: { value: "1", "aria-label": "Merge when checks pass" } }), "inline"),
|
|
28
|
+
]) }) });
|
|
29
|
+
const off = disclose("Turn off pull requests", kitSectionForm({ proof: "pull-requests", action: "/settings/pull-requests", id: "pr-off-form", hidden: { repo: view.repo, act: "off" },
|
|
30
|
+
save: { label: "Turn off", variant: "danger" }, confirm: "Turn off pull requests", body: note("Nothing more is pushed. Open pull requests stay on GitHub.") }));
|
|
31
|
+
return joinHtml([head, status, merging, off]);
|
|
21
32
|
}
|
|
22
33
|
const check = view.check;
|
|
23
34
|
if (check === null)
|
|
24
|
-
return
|
|
35
|
+
return joinHtml([head, settingsSection({ id: "pull-requests", title: "Pull requests", state: " · Off", body: note("An approver can turn on pull requests.") })]);
|
|
25
36
|
// The checks ask git and GitHub, so they run when asked, never on opening the page (docs/plans/get-side-effects.md).
|
|
26
|
-
const checkButton = (label) => postForm("/settings/pull-requests/check",
|
|
27
|
-
if (check === "unchecked")
|
|
28
|
-
return
|
|
37
|
+
const checkButton = (label, variant) => postForm("/settings/pull-requests/check", acts(btn(label, { sm: true, type: "submit", variant })), { hidden: { repo: view.repo } });
|
|
38
|
+
if (check === "unchecked") {
|
|
39
|
+
return joinHtml([head, settingsSection({ id: "pull-requests", title: "Pull requests", state: " · Off",
|
|
40
|
+
lead: "Check this project's GitHub setup to see what turning on would do.", body: checkButton("Check setup", "primary") })]);
|
|
41
|
+
}
|
|
29
42
|
if (!check.ok) {
|
|
30
|
-
return
|
|
43
|
+
return joinHtml([head, settingsSection({ id: "pull-requests", title: "Pull requests", state: " · Can’t turn on yet",
|
|
44
|
+
body: joinHtml([notice(check.message), checkButton("Check again", "outline")]) })]);
|
|
31
45
|
}
|
|
32
46
|
const plan = check.plan;
|
|
33
|
-
return
|
|
47
|
+
return joinHtml([head, settingsSection({ id: "pull-requests", title: "Pull requests", state: " · Ready to turn on",
|
|
48
|
+
body: kitSectionForm({ proof: "pull-requests-on", action: "/settings/pull-requests", id: "pr-on-form", hidden: { repo: view.repo, act: "on", github: plan.githubRepo, base: plan.base },
|
|
49
|
+
save: { label: "Turn on pull requests" }, confirm: "Turn on pull requests",
|
|
50
|
+
body: html `<ul class="k-note"><li>Pull requests open on ${mono(plan.githubRepo)} into ${mono(plan.base)}${plan.account === null ? "" : `, as ${plan.account}`}.</li>\
|
|
51
|
+
<li>Complete offers “Complete and open a pull request”, from the exact completed commit. A flow's Pull request zone can open one too.</li>\
|
|
52
|
+
<li>Merging squashes and deletes the branch. A person merges with their password; a flow merges only when its zone says so, after a person approved the card.</li></ul>` }) })]);
|
|
34
53
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.pr-settings p, .pr-settings h2 { margin: 0; }
|
|
38
|
-
.pr-settings label { display: grid; gap: .25rem; }
|
|
39
|
-
.pr-settings label input[type=password], .pr-settings select { min-height: 44px; box-sizing: border-box; width: 100%; }
|
|
40
|
-
.pr-settings .pr-check { display: flex; align-items: center; gap: .5rem; min-height: 44px; }
|
|
41
|
-
.pr-settings button { min-height: 44px; justify-self: start; }
|
|
42
|
-
.pr-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
|
|
43
|
-
@media (max-width: 480px) { .pr-settings button { justify-self: stretch; } }
|
|
44
|
-
`;
|
|
54
|
+
/** Kept for the console's style bundle (server/chrome.ts); the page itself is kit markup. */
|
|
55
|
+
export const PULL_REQUEST_SETTINGS_CSS = "";
|
package/dist/recipe-ui.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type Recipe, type RecipeDocument, type WorkflowPreview } from "./recipes.js";
|
|
2
2
|
import { type Html } from "./html.js";
|
|
3
3
|
export declare function recipeFromForm(body: URLSearchParams): RecipeDocument;
|
|
4
|
-
|
|
4
|
+
/** "One-time work" (New flow): the open project's recipes and the starters as rows, each with one "Use this"; then
|
|
5
|
+
* Create a recipe, importing a shared one, and the work recipes made lately. */
|
|
6
|
+
export declare function recipeRowsHtml(starters: Recipe[], saved: Recipe[], project: string | null, projectName: string | null, revision: number, recent: {
|
|
5
7
|
name: string;
|
|
6
8
|
href: string;
|
|
7
9
|
state: string;
|
|
@@ -16,4 +18,4 @@ export declare function recipeDefinitionPreviewHtml(preview: WorkflowPreview, re
|
|
|
16
18
|
export declare function recipeRunHtml(recipe: Recipe, repo: string, revision: number, problem?: string | null, values?: URLSearchParams, saved?: boolean): Html;
|
|
17
19
|
export declare function recipeAnswersFromForm(body: URLSearchParams): Map<string, string>;
|
|
18
20
|
export declare function recipeScript(): string;
|
|
19
|
-
export declare const RECIPE_CSS = "\n.recipe-heading { margin: .5rem 0 1.75rem; max-width: 46rem; }\n.recipe-heading h1 { font-size: clamp(1.65rem,3vw,2.5rem); letter-spacing:-.035em; margin:.35rem 0 .6rem; }\n.recipe-kicker { font-size:.7rem; text-transform:uppercase; letter-spacing:.12em; color:var(--muted-foreground); }\n.recipe-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:.8rem; margin:1rem 0 2rem; }\n.recipe-card { border:1px solid var(--border); border-radius:var(--radius); padding:1.1rem; background:var(--card); display:flex; flex-direction:column; gap:.5rem; }\n.recipe-card h3 { font-size:1.05rem; margin:0; letter-spacing:-.015em; }\n.recipe-card p { color:var(--muted-foreground); margin:.2rem 0; font-size:.85rem; line-height:1.5; }\n.recipe-card-link { margin-top:auto; padding-top:.75rem; display:flex; justify-content:space-between; }\n.recipe-editor { max-width:52rem; }.recipe-editor h2 { margin-top:0; }\n.recipe-criterion { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); padding:.8rem; margin:.8rem 0; }\n.recipe-evidence { display:flex; flex-wrap:wrap; gap:.5rem 1rem; }.recipe-evidence label { display:flex; align-items:center; gap:.4rem; font-size:.8rem; }.recipe-evidence input { width:auto; margin:0; }\n.recipe-options { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }\n.recipe-preview-grid { display:grid; grid-template-columns:minmax(0,1.7fr) minmax(16rem,1fr); gap:1rem; }\n.recipe-steps { padding-left:1.4rem; }.recipe-steps li { padding:.4rem 0 .6rem .3rem; }.recipe-steps p { margin:.3rem 0 0; font-size:.85rem; color:var(--muted-foreground); }\n.recipe-launch { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }.recipe-launch form { margin:0 0 .6rem; }\n.recipe-history a { display:flex; justify-content:space-between; gap:1rem; padding:.6rem 0; border-bottom:1px solid var(--border); }.recipe-history span { color:var(--muted-foreground); }\n.recipe-error { border-color:var(--destructive); }\n.recipe-card[hidden] { display:none; }.recipe-search { max-width:28rem; }.recipe-question { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); margin:1rem 0; padding:.8rem; min-width:0; }.recipe-run { max-width:42rem; }.recipe-run button { margin-top:1rem; }\n@media(max-width:1000px){.recipe-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }}\n@media(max-width:760px){.recipe-grid,.recipe-options,.recipe-preview-grid,.recipe-launch { grid-template-columns:1fr; }.recipe-heading { margin-bottom:1rem; }.recipe-history a { flex-direction:column; gap:.2rem; }}\n";
|
|
21
|
+
export declare const RECIPE_CSS = "\n.fl-recipe-more { display:grid; gap:12px; padding-top:12px; }.fl-recipe-more .k-disclose[open] > form, .fl-recipe-more .k-disclose[open] > div { margin-top:12px; }.fl-recipe-import { display:grid; gap:12px; margin:0; }\n.fl-recipe-recent { display:grid; }.fl-recipe-recent a { display:flex; justify-content:space-between; gap:12px; min-height:40px; align-items:center; border-bottom:1px solid var(--so-line); color:var(--so-ink); text-decoration:none; font-size:13px; }\n.recipe-heading { margin: .5rem 0 1.75rem; max-width: 46rem; }\n.recipe-heading h1 { font-size: clamp(1.65rem,3vw,2.5rem); letter-spacing:-.035em; margin:.35rem 0 .6rem; }\n.recipe-kicker { font-size:.7rem; text-transform:uppercase; letter-spacing:.12em; color:var(--muted-foreground); }\n.recipe-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:.8rem; margin:1rem 0 2rem; }\n.recipe-card { border:1px solid var(--border); border-radius:var(--radius); padding:1.1rem; background:var(--card); display:flex; flex-direction:column; gap:.5rem; }\n.recipe-card h3 { font-size:1.05rem; margin:0; letter-spacing:-.015em; }\n.recipe-card p { color:var(--muted-foreground); margin:.2rem 0; font-size:.85rem; line-height:1.5; }\n.recipe-card-link { margin-top:auto; padding-top:.75rem; display:flex; justify-content:space-between; }\n.recipe-editor { max-width:52rem; }.recipe-editor h2 { margin-top:0; }\n.recipe-criterion { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); padding:.8rem; margin:.8rem 0; }\n.recipe-evidence { display:flex; flex-wrap:wrap; gap:.5rem 1rem; }.recipe-evidence label { display:flex; align-items:center; gap:.4rem; font-size:.8rem; }.recipe-evidence input { width:auto; margin:0; }\n.recipe-options { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }\n.recipe-preview-grid { display:grid; grid-template-columns:minmax(0,1.7fr) minmax(16rem,1fr); gap:1rem; }\n.recipe-steps { padding-left:1.4rem; }.recipe-steps li { padding:.4rem 0 .6rem .3rem; }.recipe-steps p { margin:.3rem 0 0; font-size:.85rem; color:var(--muted-foreground); }\n.recipe-launch { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }.recipe-launch form { margin:0 0 .6rem; }\n.recipe-history a { display:flex; justify-content:space-between; gap:1rem; padding:.6rem 0; border-bottom:1px solid var(--border); }.recipe-history span { color:var(--muted-foreground); }\n.recipe-error { border-color:var(--destructive); }\n.recipe-card[hidden] { display:none; }.recipe-search { max-width:28rem; }.recipe-question { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); margin:1rem 0; padding:.8rem; min-width:0; }.recipe-run { max-width:42rem; }.recipe-run button { margin-top:1rem; }\n@media(max-width:1000px){.recipe-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }}\n@media(max-width:760px){.recipe-grid,.recipe-options,.recipe-preview-grid,.recipe-launch { grid-template-columns:1fr; }.recipe-heading { margin-bottom:1rem; }.recipe-history a { flex-direction:column; gap:.2rem; }}\n";
|
package/dist/recipe-ui.js
CHANGED
|
@@ -3,6 +3,8 @@ import { composerSchedule, scheduleEditorHtml, scheduleEditorScript } from "./ta
|
|
|
3
3
|
import { parseRecipe, workflowSteps, recipeScheduleWords, RecipeError, RECIPE_INPUT_LIMIT } from "./recipes.js";
|
|
4
4
|
import { ACCEPTANCE_LIMITS } from "./scope.js";
|
|
5
5
|
import { html, joinHtml, postForm, replaceMarkup } from "./html.js";
|
|
6
|
+
import { templateRow } from "./flow-gallery-ui.js";
|
|
7
|
+
import { acts, btn, count, disclose, field, group, note, textarea } from "./server/kit.js";
|
|
6
8
|
const hidden = (name, value) => html `<input type="hidden" name="${name}" value="${value}">`;
|
|
7
9
|
const evidence = [["check", "Passing tests or commands"], ["changed-path", "Changed files"], ["screenshot", "Screenshot"], ["manual-review", "Reviewer assessment"]];
|
|
8
10
|
export function recipeFromForm(body) {
|
|
@@ -53,10 +55,16 @@ export function recipeFromForm(body) {
|
|
|
53
55
|
return parseRecipe({ format: "standing-orders-recipe", version: inputs.length ? 2 : 1, ...(inputs.length ? { inputs } : {}), name: body.get("name") ?? "", description: body.get("description") ?? "", goal: body.get("goal") ?? "", outOfScope: body.get("not") || null,
|
|
54
56
|
touches: (body.get("touches") ?? "").split(/[\n,]/).map(one => one.trim()).filter(Boolean), acceptance, planning: body.get("planning") ?? "auto", deliverable: body.get("deliverable") ?? "branch", schedule: schedule.schedule, costCeilingUsd: ceiling === "" ? null : Number(ceiling) });
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/** "One-time work" (New flow): the open project's recipes and the starters as rows, each with one "Use this"; then
|
|
59
|
+
* Create a recipe, importing a shared one, and the work recipes made lately. */
|
|
60
|
+
export function recipeRowsHtml(starters, saved, project, projectName, revision, recent) {
|
|
61
|
+
const kind = (recipe) => `${recipe.document.deliverable === "report" ? "Report" : "Code change"} · ${recipe.document.schedule === null ? "runs once as a task" : "repeats on a schedule"}${recipe.document.version === 2 ? ` · ${recipe.document.inputs.length} question${recipe.document.inputs.length === 1 ? "" : "s"}` : ""}`;
|
|
62
|
+
const rowOf = (recipe, reusable) => templateRow({ name: recipe.document.name, promise: recipe.document.description, line: kind(recipe), attrs: html ` data-recipe-card data-recipe="${recipe.id}"`,
|
|
63
|
+
action: project === null ? "" : btn("Use this", { sm: true, href: `/recipes/${reusable ? "run" : "start"}?recipe=${recipe.id}&revision=${recipe.revision}`, attrs: { "aria-label": `Use ${recipe.document.name}` } }) });
|
|
64
|
+
const tools = project === null ? note("Open a project to use, save or import a recipe.")
|
|
65
|
+
: html `<p class="k-acts fl-recipe-acts">${btn("Create a recipe", { sm: true, href: "/recipes/new" })}</p>${disclose("Import a shared recipe", postForm("/recipes/import", html `${field({ id: "fl-recipe-json", label: "Recipe JSON", hint: "Paste exported recipe JSON. You review it in this project before any work is made.", control: textarea({ id: "fl-recipe-json", name: "document", rows: 6, attrs: { required: true, maxlength: 32768, spellcheck: "false" } }) })}${acts(btn("Review imported recipe", { type: "submit" }))}`, { projectRevision: String(revision), hidden: { repo: project }, attrs: { class: "fl-recipe-import" } }))}`;
|
|
66
|
+
const recentRows = recent.length === 0 ? "" : disclose(`Made from recipes lately (${recent.length})`, html `<div class="fl-recipe-recent">${recent.map(one => html `<a href="${one.href}"><span>${one.name}</span><span class="k-note">${one.state}</span></a>`)}</div>`);
|
|
67
|
+
return html `${group(projectName === null ? "One-time work" : `One-time work in ${projectName}`, count(saved.length + starters.length))}<div data-group="recipes">${saved.map(one => rowOf(one, true))}${starters.map(one => rowOf(one, false))}</div><div class="fl-recipe-more">${tools}${recentRows}</div>`;
|
|
60
68
|
}
|
|
61
69
|
function criterionHtml(index, criterion, values) {
|
|
62
70
|
const prefix = `criterion-${index}-`;
|
|
@@ -137,6 +145,8 @@ export function recipeScript() {
|
|
|
137
145
|
})();`;
|
|
138
146
|
}
|
|
139
147
|
export const RECIPE_CSS = `
|
|
148
|
+
.fl-recipe-more { display:grid; gap:12px; padding-top:12px; }.fl-recipe-more .k-disclose[open] > form, .fl-recipe-more .k-disclose[open] > div { margin-top:12px; }.fl-recipe-import { display:grid; gap:12px; margin:0; }
|
|
149
|
+
.fl-recipe-recent { display:grid; }.fl-recipe-recent a { display:flex; justify-content:space-between; gap:12px; min-height:40px; align-items:center; border-bottom:1px solid var(--so-line); color:var(--so-ink); text-decoration:none; font-size:13px; }
|
|
140
150
|
.recipe-heading { margin: .5rem 0 1.75rem; max-width: 46rem; }
|
|
141
151
|
.recipe-heading h1 { font-size: clamp(1.65rem,3vw,2.5rem); letter-spacing:-.035em; margin:.35rem 0 .6rem; }
|
|
142
152
|
.recipe-kicker { font-size:.7rem; text-transform:uppercase; letter-spacing:.12em; color:var(--muted-foreground); }
|
|
@@ -189,3 +189,6 @@ export declare class ProtectionUnreadable extends Error {
|
|
|
189
189
|
}
|
|
190
190
|
/** Whether main's protection holds, as a yes/no (release-auto.ts sealAutoGreen): null when it can't be read. */
|
|
191
191
|
export declare function readUpToDateRequired(run: Exec, gh: string, repo: string, branch?: string): Promise<boolean | null>;
|
|
192
|
+
/** The release's step record (store.ts release_step, v146) through a store opened for each entry: a failure to open or
|
|
193
|
+
* write is swallowed, so recording never stops or changes the release. */
|
|
194
|
+
export declare function releaseProgressRecorder(open: () => Store, done: (store: Store) => void): NonNullable<ReleaseAdapters["progress"]>;
|
package/dist/release-adapters.js
CHANGED
|
@@ -19,7 +19,7 @@ import { PROTECTION_MINIMUM_CHECKS, PROTECTION_REPORT_ONLY_CHECKS } from "./rele
|
|
|
19
19
|
import { activeUpdateWork } from "./desktop-update-gate.js";
|
|
20
20
|
import { BEFORE_SWAP, gateOwner } from "./release-gate.js";
|
|
21
21
|
import { findCliLinks, switchCliLinks } from "./release-links.js";
|
|
22
|
-
import { databasePath, finishReleaseOperation, requireFreshReleaseApproval, openStore, releaseAuthorityOfTask, releaseCarries, releaseOperations, renewReleaseLease, reserveRelease, runReleaseOperation, Store, releaseIssuerGone, takeReleaseLease, } from "./store.js";
|
|
22
|
+
import { databasePath, finishReleaseOperation, requireFreshReleaseApproval, openStore, releaseAuthorityOfTask, releaseCarries, releaseOperations, renewReleaseLease, reserveRelease, runReleaseOperation, Store, recordReleaseStep, releaseIssuerGone, takeReleaseLease, } from "./store.js";
|
|
23
23
|
import { acquireDeploymentLock } from "./deploy-lock.js";
|
|
24
24
|
import { autoReleaseApprovalOf, liveReleaseOf } from "./scope.js";
|
|
25
25
|
import { recheckReleaseTask, releaseCheckStanding } from "./check-failure.js";
|
|
@@ -429,7 +429,7 @@ export function releaseAdapters(options) {
|
|
|
429
429
|
// A boundary's one bounded read of GitHub's nightly: plain exec, never the release's long retries (20 s in all).
|
|
430
430
|
githubReads(run, ciGh));
|
|
431
431
|
return {
|
|
432
|
-
...(existsSync(database) ? { release } : {}),
|
|
432
|
+
...(existsSync(database) ? { release, progress: releaseProgressRecorder(() => openStore(database), store => store.close()) } : {}),
|
|
433
433
|
now: options.now ?? (() => new Date()),
|
|
434
434
|
sleep: options.sleep ?? sleep,
|
|
435
435
|
say,
|
|
@@ -1471,3 +1471,23 @@ function mainTriggered(run) {
|
|
|
1471
1471
|
return bases.length > 0 && bases.every(base => base === "main");
|
|
1472
1472
|
return false;
|
|
1473
1473
|
}
|
|
1474
|
+
/** The release's step record (store.ts release_step, v146) through a store opened for each entry: a failure to open or
|
|
1475
|
+
* write is swallowed, so recording never stops or changes the release. */
|
|
1476
|
+
export function releaseProgressRecorder(open, done) {
|
|
1477
|
+
return entry => {
|
|
1478
|
+
let store = null;
|
|
1479
|
+
try {
|
|
1480
|
+
store = open();
|
|
1481
|
+
recordReleaseStep(store, entry);
|
|
1482
|
+
}
|
|
1483
|
+
catch { /* best effort: the release goes on */ }
|
|
1484
|
+
finally {
|
|
1485
|
+
if (store !== null) {
|
|
1486
|
+
try {
|
|
1487
|
+
done(store);
|
|
1488
|
+
}
|
|
1489
|
+
catch { /* best effort */ }
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
}
|
package/dist/release-cli.js
CHANGED
|
@@ -16,7 +16,8 @@ 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 { databasePath, openStore, revokeRelease } from "./store.js";
|
|
19
|
+
import { databasePath, elapsedSeconds, latestReleaseStepTask, openStore, releaseProgressFor, revokeRelease } from "./store.js";
|
|
20
|
+
import { dur } from "./design/kit.js";
|
|
20
21
|
import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxedCarry } from "./release-carry.js";
|
|
21
22
|
import { authenticateApprover, machineActing, personProof, proveCredential } from "./scope.js";
|
|
22
23
|
import { allowAutoPublish, setReleaseApproval } from "./release-auto.js";
|
|
@@ -35,6 +36,12 @@ export const RELEASE_HELP = `toolroll release <branch> — release Toolroll itse
|
|
|
35
36
|
|
|
36
37
|
Run the same command to resume after a stop. Interrupted deployments recover their saved stage before retrying.
|
|
37
38
|
|
|
39
|
+
A branch whose name is one of this command's own words (status, nightly, carry…) is named with --branch <name>
|
|
40
|
+
or after --: \`toolroll release --branch status\`.
|
|
41
|
+
|
|
42
|
+
toolroll release status [<task>] — the steps of a release (the newest one recorded, or the release task named): each
|
|
43
|
+
step's state and how long it took, and where it stopped
|
|
44
|
+
|
|
38
45
|
toolroll release --release-gate <id> [--yes] — lift the update pause a deployment left before its swap
|
|
39
46
|
|
|
40
47
|
Shows why it is safe first: the pause is that deployment's, its journal stopped before the swap, and the
|
|
@@ -98,7 +105,7 @@ A runner's lease is returned with \`toolroll worker release <lease>\`.`;
|
|
|
98
105
|
/** A runner lease id (claim.ts mints randomUUID()). */
|
|
99
106
|
export const LEASE_SHAPED = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
100
107
|
export const LEASE_DEPRECATION = "`toolroll release <lease>` is now `toolroll worker release <lease>`; the old form goes in the next minor version.";
|
|
101
|
-
const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage", "--lane-retire"]);
|
|
108
|
+
const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage", "--lane-retire", "--branch"]);
|
|
102
109
|
const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start", "--recover-deploy", "--github"]);
|
|
103
110
|
export async function runReleaseCommand(args, write, deps = {}) {
|
|
104
111
|
// The flag, not a secret's value that happens to read "--json".
|
|
@@ -112,9 +119,23 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
112
119
|
const positional = [];
|
|
113
120
|
const limits = {};
|
|
114
121
|
let stage, laneRetire, laneAlias;
|
|
122
|
+
// `--branch <name>` (or `-- <name>`) names the branch outright, so a branch called "status", "nightly" or "carry" is
|
|
123
|
+
// still a branch and never one of this command's own words.
|
|
124
|
+
let branchFlag, literal = false, branchFlags = 0;
|
|
125
|
+
// Only what follows `--` is literal: it never changes how the words before it read.
|
|
126
|
+
const literalArgs = [];
|
|
115
127
|
let repo, tap, gate, to, request, as, token, scheduleFlag;
|
|
116
128
|
for (let i = 0; i < args.length; i += 1) {
|
|
117
129
|
const raw = args[i];
|
|
130
|
+
// After `--`, every argument is a branch name, never a flag or a subcommand.
|
|
131
|
+
if (raw === "--" && !literal) {
|
|
132
|
+
literal = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (literal) {
|
|
136
|
+
literalArgs.push(raw);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
118
139
|
if (BARE_FLAGS.has(raw))
|
|
119
140
|
continue;
|
|
120
141
|
// `--lane-alias <lane-id> <new name>`: two values, neither a flag of this command.
|
|
@@ -157,6 +178,10 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
157
178
|
stage = value;
|
|
158
179
|
else if (arg === "--lane-retire")
|
|
159
180
|
laneRetire = value;
|
|
181
|
+
else if (arg === "--branch") {
|
|
182
|
+
branchFlag = value;
|
|
183
|
+
branchFlags += 1;
|
|
184
|
+
}
|
|
160
185
|
else {
|
|
161
186
|
const match = /^([a-z]+)=([0-9]+)$/.exec(value);
|
|
162
187
|
if (match === null || !(match[1] in RELEASE_LIMITS) || Number(match[2]) < 1)
|
|
@@ -174,12 +199,12 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
174
199
|
return 0;
|
|
175
200
|
}
|
|
176
201
|
if (gate !== undefined) {
|
|
177
|
-
if (positional.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new"].includes(arg)))
|
|
202
|
+
if (positional.length > 0 || branchFlag !== undefined || literalArgs.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new"].includes(arg)))
|
|
178
203
|
return refuse("usage", "--release-gate <id> takes no branch or release options.", 2);
|
|
179
204
|
return runGateRelease(gate, bare.has("--yes"), write, json, deps.gateWorld ?? installedGateWorld());
|
|
180
205
|
}
|
|
181
206
|
if (bare.has("--recover-deploy")) {
|
|
182
|
-
if (positional.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new", "--release-gate"].includes(arg.split("=")[0])))
|
|
207
|
+
if (positional.length > 0 || branchFlag !== undefined || literalArgs.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new", "--release-gate"].includes(arg.split("=")[0])))
|
|
183
208
|
return refuse("usage", "--recover-deploy takes only --stage <dir>, --yes and --json.", 2);
|
|
184
209
|
return runRescue(stage, bare.has("--yes"), write, json, deps.rescueWorld ?? installedRescueWorld(deps.cwd ?? process.cwd()));
|
|
185
210
|
}
|
|
@@ -194,15 +219,29 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
194
219
|
return refuse("usage", "Use one of --github, --lane-alias or --lane-retire, without --schedule or --start.", 2);
|
|
195
220
|
return runGithubNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, alias: laneAlias, retire: laneRetire }, write, json, deps);
|
|
196
221
|
}
|
|
197
|
-
|
|
222
|
+
const named = branchFlag !== undefined || literalArgs.length > 0;
|
|
223
|
+
// One branch, named one way: never a guess between two.
|
|
224
|
+
if (branchFlags > 1 || literalArgs.length > 1 || (branchFlag !== undefined && (positional.length > 0 || literalArgs.length > 0)) || (literalArgs.length > 0 && positional.length > 0)) {
|
|
225
|
+
return refuse("usage", "Name the branch once: `toolroll release <branch>`, `--branch <name>` or `-- <name>`, never two of them.", 2);
|
|
226
|
+
}
|
|
227
|
+
if (branchFlag !== undefined && branchFlag === "")
|
|
228
|
+
return refuse("usage", "--branch takes a branch name.", 2);
|
|
229
|
+
// `release status [<task>]`: the steps recorded for a release. A branch named "status" is released with
|
|
230
|
+
// `--branch status` (or `-- status`).
|
|
231
|
+
if (!named && positional[0] === "status" && positional.length <= 2) {
|
|
232
|
+
if (args.some(arg => VALUE_FLAGS.has(arg.split("=")[0]) || ["--new", "--no-carry", "--start"].includes(arg)))
|
|
233
|
+
return refuse("usage", "`toolroll release status [<task>]` takes only --json.", 2);
|
|
234
|
+
return runStatusCommand(positional[1], write, json, deps);
|
|
235
|
+
}
|
|
236
|
+
if (!named && positional[0] === "nightly" && positional.length === 1)
|
|
198
237
|
return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start: bare.has("--start") }, write, json, deps);
|
|
199
|
-
if (["carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"].includes(positional[0] ?? "") && positional.length === 2) {
|
|
238
|
+
if (!named && ["carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"].includes(positional[0] ?? "") && positional.length === 2) {
|
|
200
239
|
return runAuthorityCommand(positional[0], positional[1], { to, request, as, token, repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo) }, write, json, deps);
|
|
201
240
|
}
|
|
202
|
-
if (positional.length !== 1)
|
|
241
|
+
if (!named && positional.length !== 1)
|
|
203
242
|
return refuse("usage", `Which branch? \`toolroll release <branch>\`.\n${RELEASE_HELP}`, 2);
|
|
204
|
-
const branch = positional[0];
|
|
205
|
-
if (LEASE_SHAPED.test(branch)) {
|
|
243
|
+
const branch = branchFlag ?? literalArgs[0] ?? positional[0];
|
|
244
|
+
if (!named && LEASE_SHAPED.test(branch)) {
|
|
206
245
|
const releaseOptions = args.filter(arg => VALUE_FLAGS.has(arg) || arg === "--new");
|
|
207
246
|
if (releaseOptions.length > 0)
|
|
208
247
|
return refuse("ambiguous", `${branch} looks like a lease id, but ${releaseOptions[0]} is a release option. Return a lease with \`toolroll worker release <lease>\`; a branch shaped like a lease id can't be released.`, 2);
|
|
@@ -519,3 +558,36 @@ async function runGithubNightlyCommand(flags, write, json, deps) {
|
|
|
519
558
|
store.close();
|
|
520
559
|
}
|
|
521
560
|
}
|
|
561
|
+
/** `toolroll release status [<task>]` (read-only): the release's recorded steps (store.ts release_step, v146). */
|
|
562
|
+
async function runStatusCommand(task, write, json, deps) {
|
|
563
|
+
const command = "release status";
|
|
564
|
+
const refuse = (reason, message, code) => { write(json ? checkedEnvelopeJson({ ok: false, command, reason, message }) : message); return code; };
|
|
565
|
+
const database = deps.database ?? databasePath(process.env, homedir());
|
|
566
|
+
if (!existsSync(database))
|
|
567
|
+
return refuse("no-plane", "This machine has no Toolroll database.", 1);
|
|
568
|
+
const store = openStore(database);
|
|
569
|
+
try {
|
|
570
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
571
|
+
const taskId = task ?? latestReleaseStepTask(store);
|
|
572
|
+
if (taskId === null)
|
|
573
|
+
return refuse("no-release", "No release has recorded its steps yet.", 1);
|
|
574
|
+
const progress = releaseProgressFor(store, taskId);
|
|
575
|
+
if (progress === null)
|
|
576
|
+
return refuse("no-release", `No release steps are recorded for ${taskId}.`, 1);
|
|
577
|
+
const steps = progress.steps.map(one => ({ ...one, seconds: elapsedSeconds(one.startedAt, one.endedAt, now) }));
|
|
578
|
+
if (json) {
|
|
579
|
+
write(checkedEnvelopeJson({ ok: true, command, task: taskId, release: { ...progress, steps } }));
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
const word = { running: "running", waiting: "waiting", done: "done", failed: "failed" };
|
|
583
|
+
write(`Release ${progress.version} (${progress.tag})${progress.pr === null ? "" : ` · pull request #${progress.pr}`} · task ${taskId}`);
|
|
584
|
+
for (const one of steps)
|
|
585
|
+
write(` ${one.step.padEnd(9)} ${word[one.state].padEnd(8)} ${one.seconds === null ? "–" : dur(one.seconds)}${one.detail === null ? "" : ` ${one.detail}`}`);
|
|
586
|
+
if (progress.stopped !== null)
|
|
587
|
+
write(`Stopped at ${progress.stopped.step}: ${progress.stopped.message}`);
|
|
588
|
+
return 0;
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
store.close();
|
|
592
|
+
}
|
|
593
|
+
}
|
package/dist/release.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ export declare const REQUIRED_PR_CHECKS_WORKFLOW: Readonly<Record<typeof REQUIRE
|
|
|
48
48
|
export declare const PROTECTION_REPORT_ONLY_CHECKS: readonly string[];
|
|
49
49
|
/** The checks main's protection must require at minimum: every required check that isn't report-only. */
|
|
50
50
|
export declare const PROTECTION_MINIMUM_CHECKS: readonly string[];
|
|
51
|
+
/**
|
|
52
|
+
* Pull-request checks a release never waits for or fails on, by exact name and the workflow file that defines them:
|
|
53
|
+
* youiyoux's UI drift check (src/design/KIT.md), which fails a pull request that adds design drift but says nothing
|
|
54
|
+
* about whether a build is safe to release. Only a GitHub Actions run of that name from that workflow is left out; the
|
|
55
|
+
* same name from anywhere else is judged like any other check. Never a required check.
|
|
56
|
+
*/
|
|
57
|
+
export declare const ADVISORY_PR_CHECKS: Readonly<Record<string, string>>;
|
|
51
58
|
/**
|
|
52
59
|
* Main's protection as the plane's gh reads it (release-adapters.ts readMainProtection): `effective` only when one layer
|
|
53
60
|
* (classic protection or one active ruleset) requires up-to-date branches, binds every PROTECTION_MINIMUM_CHECKS check to GitHub Actions,
|
|
@@ -393,6 +400,18 @@ export type AutoReleaseAdapter = {
|
|
|
393
400
|
export type ReleaseAdapters = {
|
|
394
401
|
/** One approval per release (release-once.md); absent on a plane that can't carry. */
|
|
395
402
|
release?: ReleaseAuthorityAdapter;
|
|
403
|
+
/** Where each step's start, end and outcome are recorded (store.ts release_step, v146) for the Release view and
|
|
404
|
+
* `toolroll release status`. Best effort: a failure here never stops or changes the release. */
|
|
405
|
+
progress?: (entry: {
|
|
406
|
+
taskId: string;
|
|
407
|
+
tag: string;
|
|
408
|
+
version: string;
|
|
409
|
+
pr: number | null;
|
|
410
|
+
step: ReleaseStep;
|
|
411
|
+
state: "running" | "done" | "failed" | "waiting";
|
|
412
|
+
at: Date;
|
|
413
|
+
detail?: string | null;
|
|
414
|
+
}) => void;
|
|
396
415
|
now(): Date;
|
|
397
416
|
sleep(ms: number): Promise<void>;
|
|
398
417
|
say(line: string): void;
|
|
@@ -731,4 +750,6 @@ export declare function checkFailedWords(input: {
|
|
|
731
750
|
}): string;
|
|
732
751
|
/** Whether GitHub's workflow-run `path` names exactly `file` (it may carry the ref it ran from, after an `@`). */
|
|
733
752
|
export declare function workflowIs(path: string | null | undefined, file: string): boolean;
|
|
753
|
+
/** How a person names this branch to `toolroll release` again. */
|
|
754
|
+
export declare function releaseBranchArg(branch: string): string;
|
|
734
755
|
export {};
|