@bridge_gpt/mcp-server 0.2.68 → 0.2.70
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/README.md +226 -320
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +131 -120
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +68 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +115 -4
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1214 -465
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +629 -350
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +360 -20
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BAPI-1506 — read-only launch-need probes for the `drive-epic` create preview.
|
|
3
|
+
*
|
|
4
|
+
* Two probes live here: whether every self-hosted label set behind the
|
|
5
|
+
* repository's required checks has an online runner, and how far the epic
|
|
6
|
+
* branch is behind its base. Both are bookkeeping reads (design principle 5):
|
|
7
|
+
* every subprocess has a bounded timeout, and every failure, timeout, missing
|
|
8
|
+
* tool, or unreadable output becomes `unverifiable`. Nothing here can refuse a
|
|
9
|
+
* launch, and nothing from a subprocess, a workflow file, or a path reaches the
|
|
10
|
+
* returned facts — only closed classifications and bounded counts.
|
|
11
|
+
*/
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
14
|
+
import { validateBranchName } from "../base-ref.js";
|
|
15
|
+
import { PLANE_FRESHNESS_TIMEOUT_MS } from "../plane/plane-freshness.js";
|
|
16
|
+
/** One deadline for the whole runner probe, however many pages it reads. */
|
|
17
|
+
export const LAUNCH_RUNNER_PROBE_DEADLINE_MS = PLANE_FRESHNESS_TIMEOUT_MS;
|
|
18
|
+
/** Bound on each git read the behind-base probe runs. */
|
|
19
|
+
export const LAUNCH_BRANCH_PROBE_TIMEOUT_MS = PLANE_FRESHNESS_TIMEOUT_MS;
|
|
20
|
+
const RUNNER_PAGE_SIZE = 100;
|
|
21
|
+
const RUNNER_MAX_PAGES = 10;
|
|
22
|
+
const WORKFLOW_MAX_FILES = 200;
|
|
23
|
+
const WORKFLOW_MAX_BYTES = 512 * 1024;
|
|
24
|
+
export function createWorkflowSource() {
|
|
25
|
+
return {
|
|
26
|
+
list: (dir) => readdir(dir),
|
|
27
|
+
read: async (file) => {
|
|
28
|
+
const entry = await stat(file);
|
|
29
|
+
if (!entry.isFile() || entry.size > WORKFLOW_MAX_BYTES)
|
|
30
|
+
throw new Error("workflow unreadable");
|
|
31
|
+
return readFile(file, "utf8");
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function stripComment(value) {
|
|
36
|
+
// A `#` starts a comment only at the start or after whitespace, outside quotes.
|
|
37
|
+
let quote = null;
|
|
38
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
39
|
+
const ch = value[i];
|
|
40
|
+
if (quote !== null) {
|
|
41
|
+
if (ch === quote)
|
|
42
|
+
quote = null;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === "'" || ch === '"')
|
|
46
|
+
quote = ch;
|
|
47
|
+
else if (ch === "#" && (i === 0 || /\s/.test(value[i - 1] ?? "")))
|
|
48
|
+
return value.slice(0, i);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function unquote(value) {
|
|
53
|
+
const v = value.trim();
|
|
54
|
+
if (v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))) {
|
|
55
|
+
return v.slice(1, -1);
|
|
56
|
+
}
|
|
57
|
+
if (/^["']/.test(v) || /["']$/.test(v))
|
|
58
|
+
return null;
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
function plainScalar(raw) {
|
|
62
|
+
const value = stripComment(raw).trim();
|
|
63
|
+
if (value === "" || value.includes("${{") || /^[|>&*!{[]/.test(value))
|
|
64
|
+
return null;
|
|
65
|
+
const text = unquote(value);
|
|
66
|
+
return text === null || text === "" ? null : text;
|
|
67
|
+
}
|
|
68
|
+
function runsOnLabels(raw) {
|
|
69
|
+
const value = stripComment(raw).trim();
|
|
70
|
+
if (value === "" || value.includes("${{"))
|
|
71
|
+
return null;
|
|
72
|
+
if (value.startsWith("[")) {
|
|
73
|
+
if (!value.endsWith("]"))
|
|
74
|
+
return null;
|
|
75
|
+
const inner = value.slice(1, -1).trim();
|
|
76
|
+
if (inner === "")
|
|
77
|
+
return null;
|
|
78
|
+
const labels = [];
|
|
79
|
+
for (const part of inner.split(",")) {
|
|
80
|
+
const label = unquote(part);
|
|
81
|
+
if (label === null || label === "" || /[[\]{}:]/.test(label))
|
|
82
|
+
return null;
|
|
83
|
+
labels.push(label);
|
|
84
|
+
}
|
|
85
|
+
return labels;
|
|
86
|
+
}
|
|
87
|
+
const label = plainScalar(value);
|
|
88
|
+
return label === null ? null : [label];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Read the jobs out of one workflow file. A deliberately small reader: it
|
|
92
|
+
* understands only the top-level `jobs:` mapping and, inside each job, the
|
|
93
|
+
* `name:` and `runs-on:` keys at the job's own indent. Anything it cannot read
|
|
94
|
+
* plainly is reported as unreadable rather than guessed.
|
|
95
|
+
*/
|
|
96
|
+
export function parseWorkflowJobs(text) {
|
|
97
|
+
const lines = text.split(/\r?\n/);
|
|
98
|
+
const jobs = [];
|
|
99
|
+
let inJobs = false;
|
|
100
|
+
let jobIndent = null;
|
|
101
|
+
let current = null;
|
|
102
|
+
const flush = () => {
|
|
103
|
+
if (current !== null) {
|
|
104
|
+
jobs.push({ id: current.id, name: current.name, nameUnreadable: current.nameUnreadable, runsOn: current.runsOn });
|
|
105
|
+
}
|
|
106
|
+
current = null;
|
|
107
|
+
};
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
if (line.trim() === "" || line.trimStart().startsWith("#"))
|
|
110
|
+
continue;
|
|
111
|
+
const indent = line.length - line.trimStart().length;
|
|
112
|
+
if (indent === 0) {
|
|
113
|
+
flush();
|
|
114
|
+
inJobs = /^jobs:\s*(#.*)?$/.test(line);
|
|
115
|
+
jobIndent = null;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!inJobs)
|
|
119
|
+
continue;
|
|
120
|
+
if (jobIndent === null)
|
|
121
|
+
jobIndent = indent;
|
|
122
|
+
if (indent < jobIndent)
|
|
123
|
+
continue;
|
|
124
|
+
if (indent === jobIndent) {
|
|
125
|
+
flush();
|
|
126
|
+
const match = /^\s*(["']?)([A-Za-z_][A-Za-z0-9_-]*)\1:\s*(#.*)?$/.exec(line);
|
|
127
|
+
if (match !== null) {
|
|
128
|
+
current = { id: match[2], name: null, nameUnreadable: false, runsOn: null, propIndent: null };
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (current === null)
|
|
133
|
+
continue;
|
|
134
|
+
const job = current;
|
|
135
|
+
if (job.propIndent === null)
|
|
136
|
+
job.propIndent = indent;
|
|
137
|
+
if (indent !== job.propIndent)
|
|
138
|
+
continue;
|
|
139
|
+
const key = /^\s*([A-Za-z_-]+):(.*)$/.exec(line);
|
|
140
|
+
if (key === null)
|
|
141
|
+
continue;
|
|
142
|
+
if (key[1] === "name") {
|
|
143
|
+
const name = plainScalar(key[2] ?? "");
|
|
144
|
+
if (name === null)
|
|
145
|
+
job.nameUnreadable = true;
|
|
146
|
+
else
|
|
147
|
+
job.name = name;
|
|
148
|
+
}
|
|
149
|
+
else if (key[1] === "runs-on") {
|
|
150
|
+
job.runsOn = runsOnLabels(key[2] ?? "");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
flush();
|
|
154
|
+
return jobs;
|
|
155
|
+
}
|
|
156
|
+
function parseRunnerPage(stdout) {
|
|
157
|
+
let body;
|
|
158
|
+
try {
|
|
159
|
+
body = JSON.parse(stdout);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
165
|
+
return null;
|
|
166
|
+
const { total_count: total, runners } = body;
|
|
167
|
+
if (typeof total !== "number" || !Number.isSafeInteger(total) || total < 0 || !Array.isArray(runners))
|
|
168
|
+
return null;
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const runner of runners) {
|
|
171
|
+
if (runner === null || typeof runner !== "object")
|
|
172
|
+
return null;
|
|
173
|
+
const { status, labels } = runner;
|
|
174
|
+
if (typeof status !== "string" || !Array.isArray(labels))
|
|
175
|
+
return null;
|
|
176
|
+
const names = new Set();
|
|
177
|
+
for (const label of labels) {
|
|
178
|
+
const name = label?.name;
|
|
179
|
+
if (typeof name !== "string" || name === "")
|
|
180
|
+
return null;
|
|
181
|
+
names.add(name.toLowerCase());
|
|
182
|
+
}
|
|
183
|
+
out.push({ online: status === "online", labels: names });
|
|
184
|
+
}
|
|
185
|
+
return { total, runners: out };
|
|
186
|
+
}
|
|
187
|
+
const PROVIDER_REPOSITORY = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
|
|
188
|
+
/**
|
|
189
|
+
* Whether every self-hosted label set behind the required checks has an
|
|
190
|
+
* online runner. Reads only the jobs behind `gate_profile.provider_checks`.
|
|
191
|
+
* NEVER throws.
|
|
192
|
+
*/
|
|
193
|
+
export async function inspectRequiredSelfHostedRunners(input) {
|
|
194
|
+
try {
|
|
195
|
+
return await probeRunners(input);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return { state: "unverifiable", reason: "probe_failed" };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function probeRunners(input) {
|
|
202
|
+
const now = input.now ?? Date.now;
|
|
203
|
+
const deadline = now() + LAUNCH_RUNNER_PROBE_DEADLINE_MS;
|
|
204
|
+
const profile = input.gateProfile;
|
|
205
|
+
if (profile === null || profile === undefined || profile.read_failed || !Array.isArray(profile.provider_checks)) {
|
|
206
|
+
return { state: "unverifiable", reason: "no_gate_profile" };
|
|
207
|
+
}
|
|
208
|
+
const required = profile.provider_checks
|
|
209
|
+
.map((check) => check.name)
|
|
210
|
+
.filter((name) => typeof name === "string" && name !== "");
|
|
211
|
+
if (required.length !== profile.provider_checks.length) {
|
|
212
|
+
return { state: "unverifiable", reason: "no_gate_profile" };
|
|
213
|
+
}
|
|
214
|
+
if (required.length === 0)
|
|
215
|
+
return { state: "not_applicable" };
|
|
216
|
+
// Every workflow job, from both `.yml` and `.yaml` files.
|
|
217
|
+
const dir = path.join(input.repoRoot, ".github", "workflows");
|
|
218
|
+
let files;
|
|
219
|
+
try {
|
|
220
|
+
files = (await input.workflows.list(dir)).filter((file) => /\.ya?ml$/.test(file)).sort();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return { state: "unverifiable", reason: "workflows_unreadable" };
|
|
224
|
+
}
|
|
225
|
+
if (files.length > WORKFLOW_MAX_FILES)
|
|
226
|
+
return { state: "unverifiable", reason: "workflows_unreadable" };
|
|
227
|
+
const jobs = [];
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
try {
|
|
230
|
+
jobs.push(...parseWorkflowJobs(await input.workflows.read(path.join(dir, file))));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return { state: "unverifiable", reason: "workflows_unreadable" };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Map each required check to exactly one job: its literal name, else its id.
|
|
237
|
+
const labelSets = new Map();
|
|
238
|
+
for (const check of new Set(required)) {
|
|
239
|
+
const matches = jobs.filter((job) => (job.name ?? (job.nameUnreadable ? null : job.id)) === check);
|
|
240
|
+
if (matches.length !== 1) {
|
|
241
|
+
return { state: "unverifiable", reason: "check_not_mapped" };
|
|
242
|
+
}
|
|
243
|
+
const runsOn = matches[0]?.runsOn ?? null;
|
|
244
|
+
if (runsOn === null)
|
|
245
|
+
return { state: "unverifiable", reason: "runs_on_unreadable" };
|
|
246
|
+
const lowered = runsOn.map((label) => label.toLowerCase());
|
|
247
|
+
if (!lowered.includes("self-hosted"))
|
|
248
|
+
continue;
|
|
249
|
+
const key = [...new Set(lowered)].sort().join("\u0000");
|
|
250
|
+
labelSets.set(key, [...new Set(lowered)]);
|
|
251
|
+
}
|
|
252
|
+
if (labelSets.size === 0)
|
|
253
|
+
return { state: "not_applicable" };
|
|
254
|
+
const repository = input.providerRepository;
|
|
255
|
+
if (typeof repository !== "string" || !PROVIDER_REPOSITORY.test(repository)) {
|
|
256
|
+
return { state: "unverifiable", reason: "no_provider_repository" };
|
|
257
|
+
}
|
|
258
|
+
// A complete runner listing, page by page, inside the one deadline.
|
|
259
|
+
const runners = [];
|
|
260
|
+
let total = null;
|
|
261
|
+
for (let page = 1; page <= RUNNER_MAX_PAGES; page += 1) {
|
|
262
|
+
const remaining = deadline - now();
|
|
263
|
+
if (remaining <= 0)
|
|
264
|
+
return { state: "unverifiable", reason: "runner_list_unavailable" };
|
|
265
|
+
const result = await input.execFile("gh", ["api", `repos/${repository}/actions/runners?per_page=${RUNNER_PAGE_SIZE}&page=${page}`], { cwd: input.repoRoot, timeoutMs: remaining, env: { GH_PROMPT_DISABLED: "1" } });
|
|
266
|
+
if (!result.ok)
|
|
267
|
+
return { state: "unverifiable", reason: "runner_list_unavailable" };
|
|
268
|
+
const parsed = parseRunnerPage(result.stdout);
|
|
269
|
+
if (parsed === null)
|
|
270
|
+
return { state: "unverifiable", reason: "runner_list_malformed" };
|
|
271
|
+
if (total === null)
|
|
272
|
+
total = parsed.total;
|
|
273
|
+
else if (total !== parsed.total)
|
|
274
|
+
return { state: "unverifiable", reason: "runner_list_malformed" };
|
|
275
|
+
runners.push(...parsed.runners);
|
|
276
|
+
if (runners.length >= total)
|
|
277
|
+
break;
|
|
278
|
+
if (parsed.runners.length === 0)
|
|
279
|
+
return { state: "unverifiable", reason: "runner_list_malformed" };
|
|
280
|
+
}
|
|
281
|
+
if (total === null || runners.length !== total)
|
|
282
|
+
return { state: "unverifiable", reason: "runner_list_malformed" };
|
|
283
|
+
let unsatisfied = 0;
|
|
284
|
+
for (const labels of labelSets.values()) {
|
|
285
|
+
const served = runners.some((runner) => runner.online && labels.every((label) => runner.labels.has(label)));
|
|
286
|
+
if (!served)
|
|
287
|
+
unsatisfied += 1;
|
|
288
|
+
}
|
|
289
|
+
return unsatisfied > 0
|
|
290
|
+
? { state: "present", unsatisfied_label_sets: unsatisfied, label_sets: labelSets.size }
|
|
291
|
+
: { state: "absent", label_sets: labelSets.size };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* How many commits `origin/<branch>` is behind `origin/<base>`. Information
|
|
295
|
+
* only: integration's `sync_base` merges the base into the epic branch. Uses
|
|
296
|
+
* the remote-tracking refs the preview already has; it fetches nothing.
|
|
297
|
+
* NEVER throws.
|
|
298
|
+
*/
|
|
299
|
+
export async function inspectEpicBranchBehindBase(input) {
|
|
300
|
+
try {
|
|
301
|
+
const { branch, baseBranch } = input;
|
|
302
|
+
if (branch === null || branch === "")
|
|
303
|
+
return { state: "not_applicable", reason: "no_epic_branch" };
|
|
304
|
+
if (baseBranch === null || baseBranch === "")
|
|
305
|
+
return { state: "unverifiable", reason: "no_base_branch" };
|
|
306
|
+
if (validateBranchName(branch) !== null || validateBranchName(baseBranch) !== null) {
|
|
307
|
+
return { state: "unverifiable", reason: "invalid_branch_name" };
|
|
308
|
+
}
|
|
309
|
+
if (branch === baseBranch)
|
|
310
|
+
return { state: "not_applicable", reason: "no_epic_branch" };
|
|
311
|
+
const git = (args) => input.execFile("git", args, {
|
|
312
|
+
cwd: input.repoRoot,
|
|
313
|
+
timeoutMs: LAUNCH_BRANCH_PROBE_TIMEOUT_MS,
|
|
314
|
+
env: { GIT_TERMINAL_PROMPT: "0" },
|
|
315
|
+
});
|
|
316
|
+
// `for-each-ref` exits 0 with no output for a ref that does not exist, so a
|
|
317
|
+
// missing branch is told apart from a git failure.
|
|
318
|
+
const ref = `refs/remotes/origin/${branch}`;
|
|
319
|
+
const listed = await git(["for-each-ref", "--format=%(refname)", ref]);
|
|
320
|
+
if (!listed.ok)
|
|
321
|
+
return { state: "unverifiable", reason: "git_failed" };
|
|
322
|
+
if (!listed.stdout.split(/\r?\n/).some((line) => line.trim() === ref)) {
|
|
323
|
+
return { state: "not_applicable", reason: "branch_not_on_origin" };
|
|
324
|
+
}
|
|
325
|
+
const counted = await git(["rev-list", "--count", `refs/remotes/origin/${branch}..refs/remotes/origin/${baseBranch}`]);
|
|
326
|
+
if (!counted.ok)
|
|
327
|
+
return { state: "unverifiable", reason: "git_failed" };
|
|
328
|
+
const text = counted.stdout.trim();
|
|
329
|
+
if (!/^\d{1,9}$/.test(text))
|
|
330
|
+
return { state: "unverifiable", reason: "git_failed" };
|
|
331
|
+
const behind = Number.parseInt(text, 10);
|
|
332
|
+
return behind === 0 ? { state: "absent" } : { state: "present", behind };
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return { state: "unverifiable", reason: "git_failed" };
|
|
336
|
+
}
|
|
337
|
+
}
|