ramstack 0.1.0
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/LICENSE +21 -0
- package/README.md +44 -0
- package/agents/comment-sicko.md +32 -0
- package/agents/poteto-agent.md +9 -0
- package/cli/rstack.mjs +539 -0
- package/package.json +29 -0
- package/skills/architect/README.md +25 -0
- package/skills/architect/SKILL.md +83 -0
- package/skills/architect/agents/openai.yaml +5 -0
- package/skills/architect/references/design-red-flags.md +33 -0
- package/skills/architect/references/rationale-template.md +35 -0
- package/skills/architect/references/runner-prompt.md +20 -0
- package/skills/arena/README.md +21 -0
- package/skills/arena/SKILL.md +71 -0
- package/skills/arena/agents/openai.yaml +5 -0
- package/skills/automate-me/README.md +21 -0
- package/skills/automate-me/SKILL.md +109 -0
- package/skills/automate-me/agents/openai.yaml +5 -0
- package/skills/blast-radius/README.md +21 -0
- package/skills/blast-radius/SKILL.md +50 -0
- package/skills/blast-radius/agents/openai.yaml +5 -0
- package/skills/bro/README.md +16 -0
- package/skills/bro/SKILL.md +7 -0
- package/skills/bro/agents/openai.yaml +5 -0
- package/skills/create-verification-skill/README.md +22 -0
- package/skills/create-verification-skill/SKILL.md +44 -0
- package/skills/create-verification-skill/agents/openai.yaml +5 -0
- package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/skills/figure-it-out/README.md +22 -0
- package/skills/figure-it-out/SKILL.md +55 -0
- package/skills/figure-it-out/agents/openai.yaml +5 -0
- package/skills/how/README.md +25 -0
- package/skills/how/SKILL.md +134 -0
- package/skills/how/agents/openai.yaml +3 -0
- package/skills/how/references/critic-prompt.md +59 -0
- package/skills/how/references/critique-rubric.md +58 -0
- package/skills/how/references/explainer-prompt.md +55 -0
- package/skills/how/references/explorer-prompt.md +52 -0
- package/skills/interrogate/README.md +21 -0
- package/skills/interrogate/SKILL.md +112 -0
- package/skills/interrogate/agents/openai.yaml +5 -0
- package/skills/interrogate/references/code-quality-review.md +47 -0
- package/skills/interrogate/references/lead-judgment.md +58 -0
- package/skills/interrogate/references/reviewer-prompt.md +72 -0
- package/skills/interrogate/references/rubric.md +77 -0
- package/skills/maintain-verification-skill/README.md +21 -0
- package/skills/maintain-verification-skill/SKILL.md +39 -0
- package/skills/maintain-verification-skill/agents/openai.yaml +5 -0
- package/skills/no-comments/README.md +22 -0
- package/skills/no-comments/SKILL.md +24 -0
- package/skills/no-comments/agents/openai.yaml +5 -0
- package/skills/poteto-mode/README.md +26 -0
- package/skills/poteto-mode/SKILL.md +140 -0
- package/skills/poteto-mode/agents/openai.yaml +5 -0
- package/skills/poteto-mode/playbooks/authoring-a-skill.md +13 -0
- package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
- package/skills/poteto-mode/playbooks/babysit.md +27 -0
- package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
- package/skills/poteto-mode/playbooks/eval.md +27 -0
- package/skills/poteto-mode/playbooks/feature.md +21 -0
- package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +3 -0
- package/skills/poteto-mode/playbooks/opening-a-pr.md +11 -0
- package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
- package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
- package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
- package/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/skills/poteto-mode/playbooks/session-pickup.md +13 -0
- package/skills/poteto-mode/playbooks/shipping.md +20 -0
- package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
- package/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/skills/poteto-mode/references/plan.md +105 -0
- package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
- package/skills/poteto-mode/scripts/bun.lock +67 -0
- package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
- package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
- package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
- package/skills/poteto-mode/scripts/package.json +16 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
- package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
- package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
- package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
- package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
- package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
- package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
- package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
- package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
- package/skills/poteto-mode/scripts/worktree-audit.sh +86 -0
- package/skills/principle-boundary-discipline/README.md +17 -0
- package/skills/principle-boundary-discipline/SKILL.md +34 -0
- package/skills/principle-boundary-discipline/agents/openai.yaml +5 -0
- package/skills/principle-build-the-lever/README.md +17 -0
- package/skills/principle-build-the-lever/SKILL.md +23 -0
- package/skills/principle-build-the-lever/agents/openai.yaml +5 -0
- package/skills/principle-encode-lessons-in-structure/README.md +17 -0
- package/skills/principle-encode-lessons-in-structure/SKILL.md +31 -0
- package/skills/principle-encode-lessons-in-structure/agents/openai.yaml +5 -0
- package/skills/principle-exhaust-the-design-space/README.md +17 -0
- package/skills/principle-exhaust-the-design-space/SKILL.md +21 -0
- package/skills/principle-exhaust-the-design-space/agents/openai.yaml +5 -0
- package/skills/principle-experience-first/README.md +17 -0
- package/skills/principle-experience-first/SKILL.md +19 -0
- package/skills/principle-experience-first/agents/openai.yaml +5 -0
- package/skills/principle-fix-root-causes/README.md +17 -0
- package/skills/principle-fix-root-causes/SKILL.md +23 -0
- package/skills/principle-fix-root-causes/agents/openai.yaml +5 -0
- package/skills/principle-foundational-thinking/README.md +17 -0
- package/skills/principle-foundational-thinking/SKILL.md +21 -0
- package/skills/principle-foundational-thinking/agents/openai.yaml +5 -0
- package/skills/principle-guard-the-context-window/README.md +17 -0
- package/skills/principle-guard-the-context-window/SKILL.md +17 -0
- package/skills/principle-guard-the-context-window/agents/openai.yaml +5 -0
- package/skills/principle-laziness-protocol/README.md +17 -0
- package/skills/principle-laziness-protocol/SKILL.md +18 -0
- package/skills/principle-laziness-protocol/agents/openai.yaml +5 -0
- package/skills/principle-make-operations-idempotent/README.md +17 -0
- package/skills/principle-make-operations-idempotent/SKILL.md +24 -0
- package/skills/principle-make-operations-idempotent/agents/openai.yaml +5 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/README.md +17 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +22 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/agents/openai.yaml +5 -0
- package/skills/principle-minimize-reader-load/README.md +17 -0
- package/skills/principle-minimize-reader-load/SKILL.md +23 -0
- package/skills/principle-minimize-reader-load/agents/openai.yaml +5 -0
- package/skills/principle-model-the-domain/README.md +17 -0
- package/skills/principle-model-the-domain/SKILL.md +26 -0
- package/skills/principle-model-the-domain/agents/openai.yaml +5 -0
- package/skills/principle-never-block-on-the-human/README.md +16 -0
- package/skills/principle-never-block-on-the-human/SKILL.md +23 -0
- package/skills/principle-never-block-on-the-human/agents/openai.yaml +5 -0
- package/skills/principle-outcome-oriented-execution/README.md +16 -0
- package/skills/principle-outcome-oriented-execution/SKILL.md +22 -0
- package/skills/principle-outcome-oriented-execution/agents/openai.yaml +5 -0
- package/skills/principle-prove-it-works/README.md +18 -0
- package/skills/principle-prove-it-works/SKILL.md +33 -0
- package/skills/principle-prove-it-works/agents/openai.yaml +5 -0
- package/skills/principle-redesign-from-first-principles/README.md +16 -0
- package/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
- package/skills/principle-redesign-from-first-principles/agents/openai.yaml +5 -0
- package/skills/principle-separate-before-serializing-shared-state/README.md +16 -0
- package/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
- package/skills/principle-separate-before-serializing-shared-state/agents/openai.yaml +5 -0
- package/skills/principle-sequence-verifiable-units/README.md +18 -0
- package/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/skills/principle-sequence-verifiable-units/agents/openai.yaml +5 -0
- package/skills/principle-subtract-before-you-add/README.md +17 -0
- package/skills/principle-subtract-before-you-add/SKILL.md +22 -0
- package/skills/principle-subtract-before-you-add/agents/openai.yaml +5 -0
- package/skills/principle-type-system-discipline/README.md +19 -0
- package/skills/principle-type-system-discipline/SKILL.md +31 -0
- package/skills/principle-type-system-discipline/agents/openai.yaml +5 -0
- package/skills/recall/README.md +23 -0
- package/skills/recall/SKILL.md +35 -0
- package/skills/recall/agents/openai.yaml +5 -0
- package/skills/reflect/README.md +24 -0
- package/skills/reflect/SKILL.md +77 -0
- package/skills/reflect/agents/openai.yaml +5 -0
- package/skills/reflect/references/divergent-reviewer.md +43 -0
- package/skills/reflect/references/judgment-reviewer.md +42 -0
- package/skills/reflect/references/synthesizer.md +56 -0
- package/skills/reflect/references/tooling-reviewer.md +57 -0
- package/skills/setup-rstack/README.md +25 -0
- package/skills/setup-rstack/SKILL.md +81 -0
- package/skills/setup-rstack/agents/openai.yaml +3 -0
- package/skills/show-me-your-work/README.md +23 -0
- package/skills/show-me-your-work/SKILL.md +82 -0
- package/skills/show-me-your-work/agents/openai.yaml +5 -0
- package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/skills/show-me-your-work/scripts/log.sh +40 -0
- package/skills/swarm/README.md +21 -0
- package/skills/swarm/SKILL.md +46 -0
- package/skills/swarm/agents/openai.yaml +5 -0
- package/skills/tdd/README.md +21 -0
- package/skills/tdd/SKILL.md +44 -0
- package/skills/tdd/agents/openai.yaml +5 -0
- package/skills/teach/README.md +22 -0
- package/skills/teach/SKILL.md +21 -0
- package/skills/teach/agents/openai.yaml +5 -0
- package/skills/technical-writing/README.md +23 -0
- package/skills/technical-writing/SKILL.md +130 -0
- package/skills/technical-writing/agents/openai.yaml +5 -0
- package/skills/typescript-best-practices/README.md +22 -0
- package/skills/typescript-best-practices/SKILL.md +28 -0
- package/skills/typescript-best-practices/agents/openai.yaml +3 -0
- package/skills/typescript-best-practices/references/patterns.md +292 -0
- package/skills/unslop/README.md +21 -0
- package/skills/unslop/SKILL.md +80 -0
- package/skills/unslop/agents/openai.yaml +3 -0
- package/skills/why/README.md +26 -0
- package/skills/why/SKILL.md +229 -0
- package/skills/why/agents/openai.yaml +3 -0
- package/skills/why/references/epistemics.md +144 -0
- package/skills/why/references/investigator-prompt.md +103 -0
- package/skills/why/references/source-playbook.md +17 -0
- package/skills/why/references/sources/code-archaeology.md +88 -0
- package/skills/why/references/sources/databricks.md +70 -0
- package/skills/why/references/sources/datadog.md +99 -0
- package/skills/why/references/sources/incident-postmortem.md +15 -0
- package/skills/why/references/sources/linear.md +48 -0
- package/skills/why/references/sources/notion.md +55 -0
- package/skills/why/references/sources/sentry.md +100 -0
- package/skills/why/references/sources/slack.md +54 -0
- package/skills/why/references/synthesizer-prompt.md +135 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
import { WatcherQueryError, resolveChecks } from "./github.ts";
|
|
2
|
+
import type * as T from "./types.ts";
|
|
3
|
+
import { nonEmpty } from "./types.ts";
|
|
4
|
+
export function assessGitHubMerge(args: {
|
|
5
|
+
readonly mergeStateStatus: T.MergeStateStatus;
|
|
6
|
+
readonly headRollupState: T.RollupState;
|
|
7
|
+
}): T.GitHubMergeAssessment {
|
|
8
|
+
if (args.mergeStateStatus === "BLOCKED") {
|
|
9
|
+
if (args.headRollupState === "ERROR" || args.headRollupState === "FAILURE")
|
|
10
|
+
return {
|
|
11
|
+
kind: "refused",
|
|
12
|
+
mergeStateStatus: args.mergeStateStatus,
|
|
13
|
+
headRollupState: args.headRollupState,
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
kind: "allowed",
|
|
17
|
+
basis: "rollup",
|
|
18
|
+
mergeStateStatus: args.mergeStateStatus,
|
|
19
|
+
headRollupState: args.headRollupState,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
kind: "allowed",
|
|
24
|
+
basis: "merge-state",
|
|
25
|
+
mergeStateStatus: args.mergeStateStatus,
|
|
26
|
+
headRollupState: args.headRollupState,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function mergeAssessment(
|
|
30
|
+
reader: T.GitHubReader,
|
|
31
|
+
facts: T.PullRequestFacts
|
|
32
|
+
) {
|
|
33
|
+
const commits = await reader.commitRollups(facts.context);
|
|
34
|
+
const headRollupState =
|
|
35
|
+
facts.headRefOid === null
|
|
36
|
+
? null
|
|
37
|
+
: (commits.find((commit) => commit.oid === facts.headRefOid)?.state ??
|
|
38
|
+
null);
|
|
39
|
+
return {
|
|
40
|
+
hadPreviousPassingCi: commits.some(
|
|
41
|
+
(commit) => commit.oid !== facts.headRefOid && commit.state === "SUCCESS"
|
|
42
|
+
),
|
|
43
|
+
github: assessGitHubMerge({
|
|
44
|
+
mergeStateStatus: facts.mergeStateStatus,
|
|
45
|
+
headRollupState,
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const AUTOMATION_TOKENS = [
|
|
50
|
+
"bugbot",
|
|
51
|
+
"security review",
|
|
52
|
+
"pr review automation",
|
|
53
|
+
"review automation",
|
|
54
|
+
] as const;
|
|
55
|
+
export async function readSnapshot(args: {
|
|
56
|
+
readonly reader: T.GitHubReader;
|
|
57
|
+
readonly context: T.PrContext;
|
|
58
|
+
readonly pendingHistory: "include" | "omit";
|
|
59
|
+
readonly allowDraft: boolean;
|
|
60
|
+
}): Promise<T.PrSnapshot> {
|
|
61
|
+
const facts = await args.reader.pullRequest(args.context);
|
|
62
|
+
if (facts.state === "MERGED" || facts.mergedAt !== null)
|
|
63
|
+
return { kind: "merged", context: args.context, facts };
|
|
64
|
+
if (facts.state === "CLOSED")
|
|
65
|
+
return { kind: "closed", context: args.context, facts };
|
|
66
|
+
const threads = await args.reader.reviewThreads(args.context);
|
|
67
|
+
const checks = await resolveChecks(args.reader, args.context);
|
|
68
|
+
const failed = nonEmpty(
|
|
69
|
+
checks.checks.filter(
|
|
70
|
+
(check): check is T.FailedCheck => check.kind === "failed"
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
const pending = nonEmpty(
|
|
74
|
+
checks.checks.filter(
|
|
75
|
+
(check): check is T.PendingCheck => check.kind === "pending"
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
let ci: T.CiState;
|
|
79
|
+
if (failed === null && pending !== null && args.pendingHistory === "omit")
|
|
80
|
+
ci = {
|
|
81
|
+
kind: "ci-pending",
|
|
82
|
+
source: checks.source,
|
|
83
|
+
all: checks.checks,
|
|
84
|
+
failed: [],
|
|
85
|
+
pending,
|
|
86
|
+
hadPreviousPassingCi: false,
|
|
87
|
+
};
|
|
88
|
+
else {
|
|
89
|
+
const merge = await mergeAssessment(args.reader, facts);
|
|
90
|
+
const base = {
|
|
91
|
+
source: checks.source,
|
|
92
|
+
all: checks.checks,
|
|
93
|
+
hadPreviousPassingCi: merge.hadPreviousPassingCi,
|
|
94
|
+
};
|
|
95
|
+
if (failed !== null)
|
|
96
|
+
ci = {
|
|
97
|
+
...base,
|
|
98
|
+
kind: "ci-failing",
|
|
99
|
+
failed,
|
|
100
|
+
pending: pending ?? [],
|
|
101
|
+
github: merge.github,
|
|
102
|
+
};
|
|
103
|
+
else if (merge.github.kind === "refused")
|
|
104
|
+
ci = {
|
|
105
|
+
...base,
|
|
106
|
+
kind: "ci-github-rejected",
|
|
107
|
+
failed: [],
|
|
108
|
+
pending: pending ?? [],
|
|
109
|
+
github: merge.github,
|
|
110
|
+
};
|
|
111
|
+
else if (pending !== null)
|
|
112
|
+
ci = { ...base, kind: "ci-pending", failed: [], pending };
|
|
113
|
+
else
|
|
114
|
+
ci = {
|
|
115
|
+
...base,
|
|
116
|
+
kind: "ci-clean",
|
|
117
|
+
failed: [],
|
|
118
|
+
pending: [],
|
|
119
|
+
github: merge.github,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
kind: "open",
|
|
124
|
+
context: args.context,
|
|
125
|
+
facts,
|
|
126
|
+
threads,
|
|
127
|
+
ci,
|
|
128
|
+
reviewAutomationRunning: checks.checks.some(
|
|
129
|
+
(check) =>
|
|
130
|
+
check.kind === "pending" &&
|
|
131
|
+
AUTOMATION_TOKENS.some((token) =>
|
|
132
|
+
check.name.toLowerCase().includes(token)
|
|
133
|
+
)
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const conflictBlocker = (row: T.PrSnapshot): T.MergeBlocker | null =>
|
|
138
|
+
row.kind === "open" &&
|
|
139
|
+
(row.facts.mergeable === "CONFLICTING" ||
|
|
140
|
+
row.facts.mergeStateStatus === "DIRTY" ||
|
|
141
|
+
row.facts.mergeStateStatus === "CONFLICTING")
|
|
142
|
+
? { kind: "merge-conflicts", pr: row.context, facts: row.facts }
|
|
143
|
+
: null;
|
|
144
|
+
function threadBlocker(row: T.PrSnapshot): T.MergeBlocker | null {
|
|
145
|
+
if (row.kind !== "open") return null;
|
|
146
|
+
const threads = nonEmpty(row.threads);
|
|
147
|
+
return threads === null
|
|
148
|
+
? null
|
|
149
|
+
: { kind: "review-threads", pr: row.context, threads };
|
|
150
|
+
}
|
|
151
|
+
const ciBlocker = (row: T.PrSnapshot): T.MergeBlocker | null =>
|
|
152
|
+
row.kind === "open" &&
|
|
153
|
+
(row.ci.kind === "ci-failing" || row.ci.kind === "ci-github-rejected")
|
|
154
|
+
? { kind: "failing-checks", pr: row.context, ci: row.ci }
|
|
155
|
+
: null;
|
|
156
|
+
function gateReason(
|
|
157
|
+
row: T.PrSnapshot,
|
|
158
|
+
allowDraft: boolean
|
|
159
|
+
): T.MergeGateReason | null {
|
|
160
|
+
if (row.kind === "merged") return null;
|
|
161
|
+
if (row.kind === "closed") return "closed-without-merge";
|
|
162
|
+
if (row.facts.isDraft && !allowDraft) return "draft-pr";
|
|
163
|
+
return row.facts.reviewDecision === "CHANGES_REQUESTED"
|
|
164
|
+
? "changes-requested"
|
|
165
|
+
: null;
|
|
166
|
+
}
|
|
167
|
+
function gateBlocker(
|
|
168
|
+
row: T.PrSnapshot,
|
|
169
|
+
allowDraft: boolean
|
|
170
|
+
): T.MergeBlocker | null {
|
|
171
|
+
const reason = gateReason(row, allowDraft);
|
|
172
|
+
return reason === null ||
|
|
173
|
+
(reason === "draft-pr" &&
|
|
174
|
+
row.kind === "open" &&
|
|
175
|
+
row.ci.kind === "ci-pending")
|
|
176
|
+
? null
|
|
177
|
+
: { kind: "merge-gate", pr: row.context, reason };
|
|
178
|
+
}
|
|
179
|
+
function readyContribution(
|
|
180
|
+
row: T.PrSnapshot,
|
|
181
|
+
allowDraft: boolean
|
|
182
|
+
): T.ReadyPr | T.MergedPr | null {
|
|
183
|
+
if (row.kind === "merged")
|
|
184
|
+
return {
|
|
185
|
+
kind: "merged-pr",
|
|
186
|
+
context: row.context,
|
|
187
|
+
mergedAt: row.facts.mergedAt,
|
|
188
|
+
};
|
|
189
|
+
if (
|
|
190
|
+
row.kind !== "open" ||
|
|
191
|
+
row.ci.kind !== "ci-clean" ||
|
|
192
|
+
row.threads.length !== 0 ||
|
|
193
|
+
conflictBlocker(row) !== null ||
|
|
194
|
+
gateReason(row, allowDraft) !== null
|
|
195
|
+
)
|
|
196
|
+
return null;
|
|
197
|
+
const reviewDecision = row.facts.reviewDecision;
|
|
198
|
+
if (reviewDecision === "CHANGES_REQUESTED") return null;
|
|
199
|
+
return {
|
|
200
|
+
kind: "ready-pr",
|
|
201
|
+
context: row.context,
|
|
202
|
+
proof: {
|
|
203
|
+
mergeability: "clear",
|
|
204
|
+
threads: [],
|
|
205
|
+
ci: row.ci,
|
|
206
|
+
gate: {
|
|
207
|
+
state: "OPEN",
|
|
208
|
+
reviewDecision,
|
|
209
|
+
draft: row.facts.isDraft ? "draft-allowed" : "not-draft",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export function classifyPr(
|
|
215
|
+
row: T.PrSnapshot,
|
|
216
|
+
allowDraft = false
|
|
217
|
+
): T.PrDecision {
|
|
218
|
+
for (const blocker of [
|
|
219
|
+
conflictBlocker(row),
|
|
220
|
+
threadBlocker(row),
|
|
221
|
+
ciBlocker(row),
|
|
222
|
+
gateBlocker(row, allowDraft),
|
|
223
|
+
])
|
|
224
|
+
if (blocker !== null) return { kind: "blocker", blocker };
|
|
225
|
+
if (row.kind === "open" && row.ci.kind === "ci-pending")
|
|
226
|
+
return { kind: "waiting", frontier: row.context, pending: row.ci.pending };
|
|
227
|
+
const ready = readyContribution(row, allowDraft);
|
|
228
|
+
if (ready === null) throw new Error("snapshot has no classified decision");
|
|
229
|
+
return ready.kind === "merged-pr"
|
|
230
|
+
? { kind: "merged", pr: ready }
|
|
231
|
+
: { kind: "ready", pr: ready };
|
|
232
|
+
}
|
|
233
|
+
export function selectTierMajorStackDecision(
|
|
234
|
+
rows: T.NonEmpty<T.PrSnapshot>,
|
|
235
|
+
allowDraft = false
|
|
236
|
+
): T.StackDecision {
|
|
237
|
+
for (const tier of [conflictBlocker, threadBlocker, ciBlocker])
|
|
238
|
+
for (const row of rows) {
|
|
239
|
+
const blocker = tier(row);
|
|
240
|
+
if (blocker !== null) return { kind: "blocker", blocker };
|
|
241
|
+
}
|
|
242
|
+
for (const row of rows) {
|
|
243
|
+
const blocker = gateBlocker(row, allowDraft);
|
|
244
|
+
if (blocker !== null) return { kind: "blocker", blocker };
|
|
245
|
+
}
|
|
246
|
+
for (const row of rows)
|
|
247
|
+
if (row.kind === "open" && row.ci.kind === "ci-pending")
|
|
248
|
+
return {
|
|
249
|
+
kind: "waiting",
|
|
250
|
+
frontier: row.context,
|
|
251
|
+
pending: row.ci.pending,
|
|
252
|
+
};
|
|
253
|
+
const prs = nonEmpty(
|
|
254
|
+
rows
|
|
255
|
+
.map((row) => readyContribution(row, allowDraft))
|
|
256
|
+
.filter((row): row is T.ReadyPr | T.MergedPr => row !== null)
|
|
257
|
+
);
|
|
258
|
+
if (prs === null || prs.length !== rows.length)
|
|
259
|
+
throw new Error("stack has no classified decision");
|
|
260
|
+
return { kind: "clear", prs };
|
|
261
|
+
}
|
|
262
|
+
export const queryBackoffSeconds = (
|
|
263
|
+
interval: number,
|
|
264
|
+
failures: number
|
|
265
|
+
): number => Math.min(Math.max(interval, 60) * 2 ** (failures - 1), 300);
|
|
266
|
+
interface Envelope<M extends T.WatchMode> {
|
|
267
|
+
readonly schemaVersion: 1;
|
|
268
|
+
readonly sequence: number;
|
|
269
|
+
readonly observedAt: string;
|
|
270
|
+
readonly mode: M;
|
|
271
|
+
}
|
|
272
|
+
type Payload<V> = V extends unknown
|
|
273
|
+
? Omit<V, keyof Envelope<T.WatchMode>>
|
|
274
|
+
: never;
|
|
275
|
+
type VerdictPayload = Payload<T.WatcherVerdict>;
|
|
276
|
+
export interface VerdictStamp<M extends T.WatchMode = T.WatchMode> {
|
|
277
|
+
<const P extends VerdictPayload>(payload: P): Envelope<M> & P;
|
|
278
|
+
<const P extends VerdictPayload, M2 extends T.WatchMode>(
|
|
279
|
+
payload: P,
|
|
280
|
+
mode: M2
|
|
281
|
+
): Envelope<M2> & P;
|
|
282
|
+
}
|
|
283
|
+
export function verdictFactory<M extends T.WatchMode>(
|
|
284
|
+
clock: WatchClock,
|
|
285
|
+
mode: M
|
|
286
|
+
): VerdictStamp<M> {
|
|
287
|
+
let sequence = 0;
|
|
288
|
+
function stamp<const P extends VerdictPayload>(payload: P): Envelope<M> & P;
|
|
289
|
+
function stamp<const P extends VerdictPayload, M2 extends T.WatchMode>(
|
|
290
|
+
payload: P,
|
|
291
|
+
mode: M2
|
|
292
|
+
): Envelope<M2> & P;
|
|
293
|
+
function stamp<const P extends VerdictPayload>(
|
|
294
|
+
payload: P,
|
|
295
|
+
override?: T.WatchMode
|
|
296
|
+
): Envelope<T.WatchMode> & P {
|
|
297
|
+
return {
|
|
298
|
+
schemaVersion: 1,
|
|
299
|
+
sequence: (sequence += 1),
|
|
300
|
+
observedAt: clock.observedAt(),
|
|
301
|
+
mode: override ?? mode,
|
|
302
|
+
...payload,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return stamp;
|
|
306
|
+
}
|
|
307
|
+
function blockerVerdict(
|
|
308
|
+
stamp: VerdictStamp,
|
|
309
|
+
blocker: T.MergeBlocker
|
|
310
|
+
): T.BlockerVerdict {
|
|
311
|
+
switch (blocker.kind) {
|
|
312
|
+
case "merge-conflicts":
|
|
313
|
+
return stamp({ kind: "BLOCKER", terminal: true, exitCode: 2, blocker });
|
|
314
|
+
case "review-threads":
|
|
315
|
+
return stamp({ kind: "BLOCKER", terminal: true, exitCode: 3, blocker });
|
|
316
|
+
case "failing-checks":
|
|
317
|
+
return stamp({ kind: "BLOCKER", terminal: true, exitCode: 4, blocker });
|
|
318
|
+
case "merge-gate":
|
|
319
|
+
return stamp({ kind: "BLOCKER", terminal: true, exitCode: 6, blocker });
|
|
320
|
+
default: {
|
|
321
|
+
const exhaustive: never = blocker;
|
|
322
|
+
return exhaustive;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export function statusQueryVerdict(
|
|
327
|
+
stamp: VerdictStamp,
|
|
328
|
+
failures: number,
|
|
329
|
+
failure: T.QueryFailure
|
|
330
|
+
): T.BlockerVerdict {
|
|
331
|
+
return stamp({
|
|
332
|
+
kind: "BLOCKER",
|
|
333
|
+
terminal: true,
|
|
334
|
+
exitCode: 7,
|
|
335
|
+
blocker: { kind: "status-query", failures, failure },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
export interface WatchClock {
|
|
339
|
+
now(): number;
|
|
340
|
+
observedAt(): string;
|
|
341
|
+
sleep(seconds: number): Promise<void>;
|
|
342
|
+
}
|
|
343
|
+
export interface RunDependencies {
|
|
344
|
+
readonly reader: T.GitHubReader;
|
|
345
|
+
readonly clock: WatchClock;
|
|
346
|
+
readonly emit: (verdict: T.ProgressVerdict) => void;
|
|
347
|
+
}
|
|
348
|
+
const deadlinePassed = (
|
|
349
|
+
started: number,
|
|
350
|
+
options: T.PollingOptions,
|
|
351
|
+
now: number
|
|
352
|
+
): boolean => options.timeout > 0 && now - started >= options.timeout;
|
|
353
|
+
type StepResult<V> =
|
|
354
|
+
| { readonly kind: "terminal"; readonly verdict: V }
|
|
355
|
+
| {
|
|
356
|
+
readonly kind: "sleep";
|
|
357
|
+
readonly seconds: number;
|
|
358
|
+
readonly onDeadline?: () => V;
|
|
359
|
+
}
|
|
360
|
+
| { readonly kind: "continue" };
|
|
361
|
+
async function pollUntilTerminal<V>(args: {
|
|
362
|
+
readonly dependencies: RunDependencies;
|
|
363
|
+
readonly options: T.PollingOptions;
|
|
364
|
+
readonly stamp: VerdictStamp;
|
|
365
|
+
readonly step: () => Promise<StepResult<V>>;
|
|
366
|
+
}): Promise<V | T.BlockerVerdict | T.TimeoutVerdict> {
|
|
367
|
+
let failures = 0;
|
|
368
|
+
const started = args.dependencies.clock.now();
|
|
369
|
+
while (true) {
|
|
370
|
+
let result: StepResult<V>;
|
|
371
|
+
try {
|
|
372
|
+
result = await args.step();
|
|
373
|
+
failures = 0;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (!(error instanceof WatcherQueryError)) throw error;
|
|
376
|
+
failures += 1;
|
|
377
|
+
if (!error.failure.retryable || failures >= args.options.maxQueryErrors)
|
|
378
|
+
return statusQueryVerdict(args.stamp, failures, error.failure);
|
|
379
|
+
const retryInSeconds = queryBackoffSeconds(
|
|
380
|
+
args.options.interval,
|
|
381
|
+
failures
|
|
382
|
+
);
|
|
383
|
+
args.dependencies.emit(
|
|
384
|
+
args.stamp({
|
|
385
|
+
kind: "RETRY",
|
|
386
|
+
terminal: false,
|
|
387
|
+
failure: error.failure,
|
|
388
|
+
consecutiveFailures: failures,
|
|
389
|
+
retryInSeconds,
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
if (deadlinePassed(started, args.options, args.dependencies.clock.now()))
|
|
393
|
+
return args.stamp({
|
|
394
|
+
kind: "TIMEOUT",
|
|
395
|
+
terminal: true,
|
|
396
|
+
exitCode: 5,
|
|
397
|
+
reason: { kind: "status-unavailable", failure: error.failure },
|
|
398
|
+
});
|
|
399
|
+
await args.dependencies.clock.sleep(retryInSeconds);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (result.kind === "terminal") return result.verdict;
|
|
403
|
+
if (result.kind === "sleep") {
|
|
404
|
+
if (
|
|
405
|
+
result.onDeadline !== undefined &&
|
|
406
|
+
deadlinePassed(started, args.options, args.dependencies.clock.now())
|
|
407
|
+
)
|
|
408
|
+
return result.onDeadline();
|
|
409
|
+
await args.dependencies.clock.sleep(result.seconds);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
export async function runSimple(args: {
|
|
414
|
+
readonly dependencies: RunDependencies;
|
|
415
|
+
readonly contexts: T.NonEmpty<T.PrContext>;
|
|
416
|
+
readonly mode: T.WatchMode;
|
|
417
|
+
readonly statusOnly: boolean;
|
|
418
|
+
readonly options: T.PollingOptions;
|
|
419
|
+
}): Promise<T.TerminalVerdict> {
|
|
420
|
+
const stamp = verdictFactory(args.dependencies.clock, args.mode);
|
|
421
|
+
const step = async (): Promise<StepResult<T.TerminalVerdict>> => {
|
|
422
|
+
const rows: T.PrSnapshot[] = [];
|
|
423
|
+
for (const context of args.contexts)
|
|
424
|
+
rows.push(
|
|
425
|
+
await readSnapshot({
|
|
426
|
+
reader: args.dependencies.reader,
|
|
427
|
+
context,
|
|
428
|
+
pendingHistory: "include",
|
|
429
|
+
allowDraft: args.options.allowDraft,
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
const complete = nonEmpty(rows);
|
|
433
|
+
if (complete === null) throw new Error("watch context cannot be empty");
|
|
434
|
+
if (args.statusOnly)
|
|
435
|
+
return {
|
|
436
|
+
kind: "terminal",
|
|
437
|
+
verdict: stamp({
|
|
438
|
+
kind: "STATUS",
|
|
439
|
+
terminal: true,
|
|
440
|
+
exitCode: 0,
|
|
441
|
+
reason: "status-only",
|
|
442
|
+
rows: complete,
|
|
443
|
+
}),
|
|
444
|
+
};
|
|
445
|
+
if (args.mode === "queued-stack")
|
|
446
|
+
throw new Error("queued-stack requires status-only in the simple runner");
|
|
447
|
+
if (args.mode === "stack")
|
|
448
|
+
args.dependencies.emit(
|
|
449
|
+
stamp(
|
|
450
|
+
{ kind: "STATUS", terminal: false, reason: "poll", rows: complete },
|
|
451
|
+
args.mode
|
|
452
|
+
)
|
|
453
|
+
);
|
|
454
|
+
const decision =
|
|
455
|
+
args.mode === "single"
|
|
456
|
+
? classifyPr(complete[0], args.options.allowDraft)
|
|
457
|
+
: selectTierMajorStackDecision(complete, args.options.allowDraft);
|
|
458
|
+
if (decision.kind === "blocker")
|
|
459
|
+
return {
|
|
460
|
+
kind: "terminal",
|
|
461
|
+
verdict: blockerVerdict(stamp, decision.blocker),
|
|
462
|
+
};
|
|
463
|
+
if (decision.kind === "ready" || decision.kind === "merged")
|
|
464
|
+
return {
|
|
465
|
+
kind: "terminal",
|
|
466
|
+
verdict: stamp(
|
|
467
|
+
{
|
|
468
|
+
kind: "READY",
|
|
469
|
+
terminal: true,
|
|
470
|
+
exitCode: 0,
|
|
471
|
+
scope: { kind: "single", pr: decision.pr },
|
|
472
|
+
},
|
|
473
|
+
args.mode
|
|
474
|
+
),
|
|
475
|
+
};
|
|
476
|
+
if (decision.kind === "clear")
|
|
477
|
+
return {
|
|
478
|
+
kind: "terminal",
|
|
479
|
+
verdict: stamp(
|
|
480
|
+
{
|
|
481
|
+
kind: "READY",
|
|
482
|
+
terminal: true,
|
|
483
|
+
exitCode: 0,
|
|
484
|
+
scope: { kind: "stack", prs: decision.prs },
|
|
485
|
+
},
|
|
486
|
+
args.mode
|
|
487
|
+
),
|
|
488
|
+
};
|
|
489
|
+
args.dependencies.emit(
|
|
490
|
+
stamp({
|
|
491
|
+
kind: "WAITING",
|
|
492
|
+
terminal: false,
|
|
493
|
+
frontier: decision.frontier,
|
|
494
|
+
reason: { kind: "pending-checks", pending: decision.pending },
|
|
495
|
+
})
|
|
496
|
+
);
|
|
497
|
+
return {
|
|
498
|
+
kind: "sleep",
|
|
499
|
+
seconds: args.options.interval,
|
|
500
|
+
onDeadline: () =>
|
|
501
|
+
stamp({
|
|
502
|
+
kind: "TIMEOUT",
|
|
503
|
+
terminal: true,
|
|
504
|
+
exitCode: 5,
|
|
505
|
+
reason: { kind: "pending-checks", pending: decision.pending },
|
|
506
|
+
}),
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
return pollUntilTerminal({
|
|
510
|
+
dependencies: args.dependencies,
|
|
511
|
+
options: args.options,
|
|
512
|
+
stamp,
|
|
513
|
+
step,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
export type QueueWork =
|
|
517
|
+
| {
|
|
518
|
+
readonly kind: "whole-stack-sweep";
|
|
519
|
+
readonly remaining: T.NonEmpty<T.PrContext>;
|
|
520
|
+
}
|
|
521
|
+
| { readonly kind: "frontier-poll"; readonly frontier: T.PrContext };
|
|
522
|
+
export interface QueueState {
|
|
523
|
+
readonly queue: T.NonEmpty<T.PrContext>;
|
|
524
|
+
readonly snapshots: ReadonlyMap<T.PrNumber, T.PrSnapshot>;
|
|
525
|
+
readonly work: QueueWork | null;
|
|
526
|
+
readonly nextSweepAt: number;
|
|
527
|
+
readonly frontier: T.PrContext | null;
|
|
528
|
+
readonly lastWaitKey: string | null;
|
|
529
|
+
readonly startedAt: number;
|
|
530
|
+
}
|
|
531
|
+
export const createQueueState = (
|
|
532
|
+
queue: T.NonEmpty<T.PrContext>,
|
|
533
|
+
now: number
|
|
534
|
+
): QueueState => ({
|
|
535
|
+
queue,
|
|
536
|
+
snapshots: new Map(),
|
|
537
|
+
work: { kind: "whole-stack-sweep", remaining: queue },
|
|
538
|
+
nextSweepAt: now,
|
|
539
|
+
frontier: null,
|
|
540
|
+
lastWaitKey: null,
|
|
541
|
+
startedAt: now,
|
|
542
|
+
});
|
|
543
|
+
const orderedRows = (state: QueueState): T.PrSnapshot[] =>
|
|
544
|
+
state.queue.flatMap((context) => {
|
|
545
|
+
const row = state.snapshots.get(context.number);
|
|
546
|
+
return row === undefined ? [] : [row];
|
|
547
|
+
});
|
|
548
|
+
const activeRows = (state: QueueState): T.PrSnapshot[] =>
|
|
549
|
+
orderedRows(state).filter((row) => row.kind !== "merged");
|
|
550
|
+
export function planQueue(state: QueueState, now: number): QueueState {
|
|
551
|
+
if (state.work !== null) return state;
|
|
552
|
+
if (state.snapshots.size === 0 || now >= state.nextSweepAt) {
|
|
553
|
+
const remaining = nonEmpty(
|
|
554
|
+
state.queue.filter(
|
|
555
|
+
(context) => state.snapshots.get(context.number)?.kind !== "merged"
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
if (remaining !== null)
|
|
559
|
+
return { ...state, work: { kind: "whole-stack-sweep", remaining } };
|
|
560
|
+
}
|
|
561
|
+
const frontier = activeRows(state)[0]?.context;
|
|
562
|
+
return frontier === undefined
|
|
563
|
+
? state
|
|
564
|
+
: { ...state, work: { kind: "frontier-poll", frontier } };
|
|
565
|
+
}
|
|
566
|
+
export interface QueueSnapshotResult {
|
|
567
|
+
readonly state: QueueState;
|
|
568
|
+
readonly completedSweepRows: T.NonEmpty<T.PrSnapshot> | null;
|
|
569
|
+
}
|
|
570
|
+
export function applyQueueSnapshot(
|
|
571
|
+
state: QueueState,
|
|
572
|
+
snapshot: T.PrSnapshot,
|
|
573
|
+
now: number,
|
|
574
|
+
options: T.PollingOptions
|
|
575
|
+
): QueueSnapshotResult {
|
|
576
|
+
if (state.work === null) throw new Error("queue has no read in flight");
|
|
577
|
+
const snapshots = new Map(state.snapshots);
|
|
578
|
+
snapshots.set(snapshot.context.number, snapshot);
|
|
579
|
+
const base = { ...state, snapshots };
|
|
580
|
+
if (state.work.kind === "frontier-poll")
|
|
581
|
+
return { state: { ...base, work: null }, completedSweepRows: null };
|
|
582
|
+
const [head, ...tail] = state.work.remaining;
|
|
583
|
+
if (head.number !== snapshot.context.number)
|
|
584
|
+
throw new Error("snapshot does not match sweep head");
|
|
585
|
+
const remaining = nonEmpty(tail);
|
|
586
|
+
if (remaining !== null)
|
|
587
|
+
return {
|
|
588
|
+
state: { ...base, work: { kind: "whole-stack-sweep", remaining } },
|
|
589
|
+
completedSweepRows: null,
|
|
590
|
+
};
|
|
591
|
+
const rows = nonEmpty(
|
|
592
|
+
state.queue.flatMap((context) => {
|
|
593
|
+
const row = snapshots.get(context.number);
|
|
594
|
+
return row === undefined ? [] : [row];
|
|
595
|
+
})
|
|
596
|
+
);
|
|
597
|
+
if (rows === null || rows.length !== state.queue.length)
|
|
598
|
+
throw new Error("sweep completed without every snapshot");
|
|
599
|
+
return {
|
|
600
|
+
state: { ...base, work: null, nextSweepAt: now + options.sweepInterval },
|
|
601
|
+
completedSweepRows: rows,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
export type QueueEvaluation =
|
|
605
|
+
| {
|
|
606
|
+
readonly kind: "complete";
|
|
607
|
+
readonly state: QueueState;
|
|
608
|
+
readonly merged: T.NonEmpty<T.MergedPr>;
|
|
609
|
+
}
|
|
610
|
+
| {
|
|
611
|
+
readonly kind: "blocker";
|
|
612
|
+
readonly state: QueueState;
|
|
613
|
+
readonly blocker: T.MergeBlocker;
|
|
614
|
+
}
|
|
615
|
+
| {
|
|
616
|
+
readonly kind: "advance";
|
|
617
|
+
readonly state: QueueState;
|
|
618
|
+
readonly merged: T.PrContext;
|
|
619
|
+
readonly frontier: T.PrContext;
|
|
620
|
+
readonly remaining: number;
|
|
621
|
+
}
|
|
622
|
+
| {
|
|
623
|
+
readonly kind: "timeout";
|
|
624
|
+
readonly state: QueueState;
|
|
625
|
+
readonly frontier: T.PrContext;
|
|
626
|
+
readonly unmergedCount: number;
|
|
627
|
+
}
|
|
628
|
+
| {
|
|
629
|
+
readonly kind: "waiting";
|
|
630
|
+
readonly state: QueueState;
|
|
631
|
+
readonly frontier: T.PrContext;
|
|
632
|
+
readonly reason:
|
|
633
|
+
| {
|
|
634
|
+
readonly kind: "pending-checks";
|
|
635
|
+
readonly pending: T.NonEmpty<T.PendingCheck>;
|
|
636
|
+
}
|
|
637
|
+
| { readonly kind: "merge-queue"; readonly unmergedCount: number };
|
|
638
|
+
readonly emit: boolean;
|
|
639
|
+
};
|
|
640
|
+
export function evaluateQueue(
|
|
641
|
+
state: QueueState,
|
|
642
|
+
now: number,
|
|
643
|
+
options: T.PollingOptions
|
|
644
|
+
): QueueEvaluation {
|
|
645
|
+
const active = activeRows(state);
|
|
646
|
+
if (active.length === 0) {
|
|
647
|
+
const merged = nonEmpty(
|
|
648
|
+
orderedRows(state).flatMap((row) =>
|
|
649
|
+
row.kind === "merged"
|
|
650
|
+
? [
|
|
651
|
+
{
|
|
652
|
+
kind: "merged-pr" as const,
|
|
653
|
+
context: row.context,
|
|
654
|
+
mergedAt: row.facts.mergedAt,
|
|
655
|
+
},
|
|
656
|
+
]
|
|
657
|
+
: []
|
|
658
|
+
)
|
|
659
|
+
);
|
|
660
|
+
if (merged === null) throw new Error("empty queue cannot complete");
|
|
661
|
+
return { kind: "complete", state, merged };
|
|
662
|
+
}
|
|
663
|
+
const rows = nonEmpty(active);
|
|
664
|
+
if (rows === null) throw new Error("active queue cannot be empty");
|
|
665
|
+
const decision = selectTierMajorStackDecision(rows, options.allowDraft);
|
|
666
|
+
if (decision.kind === "blocker")
|
|
667
|
+
return { kind: "blocker", state, blocker: decision.blocker };
|
|
668
|
+
const frontier = rows[0].context;
|
|
669
|
+
if (state.frontier !== null && state.frontier.number !== frontier.number)
|
|
670
|
+
return {
|
|
671
|
+
kind: "advance",
|
|
672
|
+
state: { ...state, frontier, lastWaitKey: null },
|
|
673
|
+
merged: state.frontier,
|
|
674
|
+
frontier,
|
|
675
|
+
remaining: active.length,
|
|
676
|
+
};
|
|
677
|
+
if (deadlinePassed(state.startedAt, options, now))
|
|
678
|
+
return {
|
|
679
|
+
kind: "timeout",
|
|
680
|
+
state: { ...state, frontier },
|
|
681
|
+
frontier,
|
|
682
|
+
unmergedCount: active.length,
|
|
683
|
+
};
|
|
684
|
+
const row = rows[0];
|
|
685
|
+
const pending =
|
|
686
|
+
row.kind === "open" && row.ci.kind === "ci-pending" ? row.ci.pending : null;
|
|
687
|
+
const reason =
|
|
688
|
+
pending === null
|
|
689
|
+
? ({ kind: "merge-queue", unmergedCount: active.length } as const)
|
|
690
|
+
: ({ kind: "pending-checks", pending } as const);
|
|
691
|
+
const key =
|
|
692
|
+
reason.kind === "pending-checks"
|
|
693
|
+
? `pending:${frontier.number}:${reason.pending.length}`
|
|
694
|
+
: `queue:${frontier.number}:${reason.unmergedCount}`;
|
|
695
|
+
return {
|
|
696
|
+
kind: "waiting",
|
|
697
|
+
state: { ...state, frontier, lastWaitKey: key },
|
|
698
|
+
frontier,
|
|
699
|
+
reason,
|
|
700
|
+
emit: state.lastWaitKey !== key,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
export async function runQueued(args: {
|
|
704
|
+
readonly dependencies: RunDependencies;
|
|
705
|
+
readonly contexts: T.NonEmpty<T.PrContext>;
|
|
706
|
+
readonly options: T.PollingOptions;
|
|
707
|
+
}): Promise<T.QueueTerminalVerdict> {
|
|
708
|
+
let state = createQueueState(args.contexts, args.dependencies.clock.now());
|
|
709
|
+
const stamp = verdictFactory(args.dependencies.clock, "queued-stack");
|
|
710
|
+
args.dependencies.emit(
|
|
711
|
+
stamp({ kind: "QUEUE", terminal: false, queue: args.contexts })
|
|
712
|
+
);
|
|
713
|
+
const step = async (): Promise<StepResult<T.QueueTerminalVerdict>> => {
|
|
714
|
+
state = planQueue(state, args.dependencies.clock.now());
|
|
715
|
+
if (state.work === null) {
|
|
716
|
+
const complete = evaluateQueue(
|
|
717
|
+
state,
|
|
718
|
+
args.dependencies.clock.now(),
|
|
719
|
+
args.options
|
|
720
|
+
);
|
|
721
|
+
if (complete.kind !== "complete")
|
|
722
|
+
throw new Error("queue has no work while active");
|
|
723
|
+
return {
|
|
724
|
+
kind: "terminal",
|
|
725
|
+
verdict: stamp({
|
|
726
|
+
kind: "COMPLETE",
|
|
727
|
+
terminal: true,
|
|
728
|
+
exitCode: 0,
|
|
729
|
+
queue: state.queue,
|
|
730
|
+
merged: complete.merged,
|
|
731
|
+
}),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const context =
|
|
735
|
+
state.work.kind === "whole-stack-sweep"
|
|
736
|
+
? state.work.remaining[0]
|
|
737
|
+
: state.work.frontier;
|
|
738
|
+
const snapshot = await readSnapshot({
|
|
739
|
+
reader: args.dependencies.reader,
|
|
740
|
+
context,
|
|
741
|
+
pendingHistory: "omit",
|
|
742
|
+
allowDraft: args.options.allowDraft,
|
|
743
|
+
});
|
|
744
|
+
const applied = applyQueueSnapshot(
|
|
745
|
+
state,
|
|
746
|
+
snapshot,
|
|
747
|
+
args.dependencies.clock.now(),
|
|
748
|
+
args.options
|
|
749
|
+
);
|
|
750
|
+
state = applied.state;
|
|
751
|
+
if (applied.completedSweepRows !== null)
|
|
752
|
+
args.dependencies.emit(
|
|
753
|
+
stamp({
|
|
754
|
+
kind: "STATUS",
|
|
755
|
+
terminal: false,
|
|
756
|
+
reason: "whole-stack-sweep",
|
|
757
|
+
rows: applied.completedSweepRows,
|
|
758
|
+
})
|
|
759
|
+
);
|
|
760
|
+
if (state.work !== null) return { kind: "continue" };
|
|
761
|
+
const evaluation = evaluateQueue(
|
|
762
|
+
state,
|
|
763
|
+
args.dependencies.clock.now(),
|
|
764
|
+
args.options
|
|
765
|
+
);
|
|
766
|
+
state = evaluation.state;
|
|
767
|
+
switch (evaluation.kind) {
|
|
768
|
+
case "complete":
|
|
769
|
+
return {
|
|
770
|
+
kind: "terminal",
|
|
771
|
+
verdict: stamp({
|
|
772
|
+
kind: "COMPLETE",
|
|
773
|
+
terminal: true,
|
|
774
|
+
exitCode: 0,
|
|
775
|
+
queue: state.queue,
|
|
776
|
+
merged: evaluation.merged,
|
|
777
|
+
}),
|
|
778
|
+
};
|
|
779
|
+
case "blocker":
|
|
780
|
+
return {
|
|
781
|
+
kind: "terminal",
|
|
782
|
+
verdict: blockerVerdict(stamp, evaluation.blocker),
|
|
783
|
+
};
|
|
784
|
+
case "advance":
|
|
785
|
+
args.dependencies.emit(
|
|
786
|
+
stamp({
|
|
787
|
+
kind: "ADVANCE",
|
|
788
|
+
terminal: false,
|
|
789
|
+
merged: evaluation.merged,
|
|
790
|
+
frontier: evaluation.frontier,
|
|
791
|
+
remaining: evaluation.remaining,
|
|
792
|
+
})
|
|
793
|
+
);
|
|
794
|
+
return { kind: "continue" };
|
|
795
|
+
case "timeout":
|
|
796
|
+
return {
|
|
797
|
+
kind: "terminal",
|
|
798
|
+
verdict: stamp({
|
|
799
|
+
kind: "TIMEOUT",
|
|
800
|
+
terminal: true,
|
|
801
|
+
exitCode: 5,
|
|
802
|
+
reason: {
|
|
803
|
+
kind: "queued-stack",
|
|
804
|
+
frontier: evaluation.frontier,
|
|
805
|
+
unmergedCount: evaluation.unmergedCount,
|
|
806
|
+
},
|
|
807
|
+
}),
|
|
808
|
+
};
|
|
809
|
+
case "waiting":
|
|
810
|
+
if (evaluation.emit)
|
|
811
|
+
args.dependencies.emit(
|
|
812
|
+
stamp({
|
|
813
|
+
kind: "WAITING",
|
|
814
|
+
terminal: false,
|
|
815
|
+
frontier: evaluation.frontier,
|
|
816
|
+
reason: evaluation.reason,
|
|
817
|
+
})
|
|
818
|
+
);
|
|
819
|
+
return { kind: "sleep", seconds: args.options.interval };
|
|
820
|
+
default: {
|
|
821
|
+
const exhaustive: never = evaluation;
|
|
822
|
+
return exhaustive;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
return pollUntilTerminal({
|
|
827
|
+
dependencies: args.dependencies,
|
|
828
|
+
options: args.options,
|
|
829
|
+
stamp,
|
|
830
|
+
step,
|
|
831
|
+
});
|
|
832
|
+
}
|