@bridge_gpt/mcp-server 0.2.38 → 0.2.39
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 +189 -14
- package/build/agent-capabilities/probe-context.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +392 -0
- package/build/agent-launchers/executor-adapter-inspection.js +163 -0
- package/build/agent-launchers/executor-adapter-registry.js +90 -0
- package/build/agent-launchers/executor-adapter.js +136 -0
- package/build/agent-registry.js +28 -0
- package/build/agents.generated.js +1 -1
- package/build/claude-login.js +85 -0
- package/build/claude-user-config-doctor.js +59 -33
- package/build/commands.generated.js +12 -11
- package/build/conduct-epic/bridge-client.js +345 -0
- package/build/conduct-epic/checkpoint-store.js +423 -0
- package/build/conduct-epic/cli.js +1732 -0
- package/build/conduct-epic/lock.js +302 -0
- package/build/conduct-epic/pr-state.js +197 -0
- package/build/conduct-epic/spawn.js +101 -0
- package/build/conductor/bridge-api-client.js +37 -2
- package/build/conductor/doctor.js +11 -1
- package/build/conductor/install-doctor.js +184 -10
- package/build/conductor-bin.js +7 -7
- package/build/credential-store.js +10 -4
- package/build/credentials-cli.js +34 -19
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +579 -88
- package/build/executor/agent-identity.js +32 -0
- package/build/executor/cli.js +50 -39
- package/build/executor/deps.js +15 -1
- package/build/executor/env.js +56 -45
- package/build/executor/index.js +9 -1
- package/build/executor/install-preflight.js +138 -0
- package/build/executor/job-errors.js +200 -0
- package/build/executor/job-runner.js +619 -268
- package/build/executor/observation.js +165 -0
- package/build/executor/permissions.js +163 -36
- package/build/executor/platform.js +54 -0
- package/build/executor/preflight.js +175 -67
- package/build/executor/process.js +39 -7
- package/build/executor/runner.js +19 -0
- package/build/executor/service-lifecycle.js +269 -0
- package/build/executor/service-unit.js +121 -12
- package/build/executor/stale-artifacts.js +70 -0
- package/build/executor/test-clock.js +188 -24
- package/build/executor/worker-command.js +22 -58
- package/build/executor/worker-log.js +82 -0
- package/build/executor/worktree-lock.js +264 -0
- package/build/index.js +527 -357
- package/build/install-bridge-conductor.js +376 -38
- package/build/install-bridge.js +414 -114
- package/build/install-doctor.js +13 -0
- package/build/install-reexec.js +5 -3
- package/build/mcp-install-state.js +130 -0
- package/build/mcp-profile.js +11 -2
- package/build/mcp-provisioning.js +15 -0
- package/build/merge-pull-request.js +562 -0
- package/build/phase-result-artifacts.js +450 -0
- package/build/pipeline-orchestrator.js +4 -0
- package/build/pipeline-utils.js +16 -0
- package/build/pipelines.generated.js +7 -7
- package/build/plane/preflight.js +18 -14
- package/build/plane/supervisor.js +8 -1
- package/build/project-root.js +34 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +36 -9
- package/build/setup-epic.js +57 -4
- package/build/sfcc/permissions.js +25 -6
- package/build/sfcc/reads-site-preference.js +6 -0
- package/build/sfcc/register.js +61 -23
- package/build/sfcc/registration-inventory.js +89 -0
- package/build/sfcc/setup-status.js +18 -34
- package/build/sfcc/tool-wrapper.js +294 -17
- package/build/sfcc/write-grants.js +33 -1
- package/build/sfcc/write-guard.js +41 -12
- package/build/sfcc/writes-custom-object-def.js +6 -2
- package/build/sfcc/writes-site-preference.js +6 -1
- package/build/sfcc/writes-system-object.js +11 -2
- package/build/sfcc/writes.js +13 -8
- package/build/start-tickets-prereqs.js +25 -15
- package/build/start-tickets.js +123 -21
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +9 -3
- package/docs/install/mcp-tool-integrations.md +54 -9
- package/docs/install/sfcc-integration.md +71 -24
- package/package.json +3 -3
- package/build/executor/worker-config-isolation.js +0 -287
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge API client for the `conduct-epic` CLI (BAPI-802).
|
|
3
|
+
*
|
|
4
|
+
* This module lives OUTSIDE `mcp_server/src/conductor/`, which is frozen v1. It
|
|
5
|
+
* imports from `conductor/bridge-api-client.ts` in the reuse-only direction the
|
|
6
|
+
* freeze allows — shared URL construction, the access type, and the timeout/
|
|
7
|
+
* sanitization fetch helpers — and adds nothing under `conductor/`. That import
|
|
8
|
+
* is recorded in `conductor/README.md`'s survivor table.
|
|
9
|
+
*
|
|
10
|
+
* Two contract properties, fixed by `conduct-epic-bridge-client` and relied on by
|
|
11
|
+
* BAPI-803, which extends this same file:
|
|
12
|
+
*
|
|
13
|
+
* - **No function throws.** Every failure — transport, timeout, HTTP status,
|
|
14
|
+
* malformed body, even a bad `baseUrl` — resolves to
|
|
15
|
+
* `{ ok: false, status, error }`. A CLI that has to wrap each call in
|
|
16
|
+
* `try/catch` gets that wrong exactly once, in the recovery path where it
|
|
17
|
+
* matters most.
|
|
18
|
+
* - **The API key never leaves the `X-API-Key` header.** It is not in a URL, a
|
|
19
|
+
* query string, a request body, or any returned error text. Unrecognized
|
|
20
|
+
* throw values collapse to a stable generic message rather than being
|
|
21
|
+
* stringified, because a stringified unknown is exactly how a credential
|
|
22
|
+
* escapes.
|
|
23
|
+
*
|
|
24
|
+
* Credentials are NOT resolved here: the caller passes an access object it
|
|
25
|
+
* already obtained from `resolveConductorBridgeApiAccess()`, which reads
|
|
26
|
+
* `BAPI_API_KEY` then the user-scoped credential store.
|
|
27
|
+
*
|
|
28
|
+
* No MCP tool is registered from this module — the `tools/list` token budget is
|
|
29
|
+
* effectively frozen.
|
|
30
|
+
*/
|
|
31
|
+
import { buildConductorJiraUrl, fetchConductorJsonPostWithTimeout, fetchConductorJsonPutWithTimeout, fetchConductorJsonWithTimeout, fetchConductorReadiness, fetchEffectiveSupervisorConfig, fetchEffectiveSupervisorSetup, fetchEpicRunState, fetchParseStatus, fetchPrReviewStatus, pollCiChecksForCommit, ConductorBridgeApiError, CONDUCTOR_FETCH_TIMEOUT_MS, } from "../conductor/bridge-api-client.js";
|
|
32
|
+
/**
|
|
33
|
+
* The one message returned for any failure whose cause cannot be described
|
|
34
|
+
* without risking the credential. Stable so callers can match on it.
|
|
35
|
+
*/
|
|
36
|
+
const GENERIC_ERROR = "Bridge API request failed";
|
|
37
|
+
/** GET headers. The key travels in this header and nowhere else. */
|
|
38
|
+
function getHeaders(access) {
|
|
39
|
+
return { "X-API-Key": access.apiKey };
|
|
40
|
+
}
|
|
41
|
+
/** POST headers. Same rule, plus the JSON content type. */
|
|
42
|
+
function postHeaders(access) {
|
|
43
|
+
return { "X-API-Key": access.apiKey, "Content-Type": "application/json" };
|
|
44
|
+
}
|
|
45
|
+
/** PUT headers. Identical to {@link postHeaders}; named for the call site. */
|
|
46
|
+
function putHeaders(access) {
|
|
47
|
+
return { "X-API-Key": access.apiKey, "Content-Type": "application/json" };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Remove the exact API key from a message that is otherwise safe to surface.
|
|
51
|
+
*
|
|
52
|
+
* The shared helpers already redact header values from their diagnostics, so
|
|
53
|
+
* this is a second, independent pass over the final string: the belt is cheap
|
|
54
|
+
* and the failure it guards against is unrecoverable. Very short keys are left
|
|
55
|
+
* alone because scrubbing a 1-3 character string would mangle ordinary prose.
|
|
56
|
+
*/
|
|
57
|
+
function scrubKey(text, apiKey) {
|
|
58
|
+
if (!apiKey || apiKey.length < 4)
|
|
59
|
+
return text;
|
|
60
|
+
return text.split(apiKey).join("[REDACTED]");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Convert any thrown value into a `{ ok: false }` result.
|
|
64
|
+
*
|
|
65
|
+
* A `ConductorBridgeApiError` carries a message the shared helper already built
|
|
66
|
+
* from the coarse kind, HTTP status, backend error code, and a bounded,
|
|
67
|
+
* secret-redacted body preview — that is what lets a 409 name the override
|
|
68
|
+
* branch the server preserved. Anything else is collapsed to
|
|
69
|
+
* {@link GENERIC_ERROR}: an unknown throw value has unknown contents, and
|
|
70
|
+
* stringifying it is how a credential or a raw response body escapes.
|
|
71
|
+
*/
|
|
72
|
+
function toFailure(err, access) {
|
|
73
|
+
if (err instanceof ConductorBridgeApiError) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
status: typeof err.status === "number" ? err.status : null,
|
|
77
|
+
error: scrubKey(err.message || GENERIC_ERROR, access.apiKey),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { ok: false, status: null, error: GENERIC_ERROR };
|
|
81
|
+
}
|
|
82
|
+
/** True when `value` is a plain (non-array, non-null) object. */
|
|
83
|
+
function isRecord(value) {
|
|
84
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
/** Read a `string | null` field, returning `undefined` when it is neither. */
|
|
87
|
+
function nullableString(value) {
|
|
88
|
+
if (value === null)
|
|
89
|
+
return null;
|
|
90
|
+
if (typeof value === "string")
|
|
91
|
+
return value;
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
/** Parse the `override` member, or `undefined` when it is malformed. */
|
|
95
|
+
function parseOverride(value) {
|
|
96
|
+
if (value === null || value === undefined)
|
|
97
|
+
return null;
|
|
98
|
+
if (!isRecord(value))
|
|
99
|
+
return undefined;
|
|
100
|
+
const original = nullableString(value["original_base_branch"]);
|
|
101
|
+
if (typeof value["repo_name"] !== "string" ||
|
|
102
|
+
original === undefined ||
|
|
103
|
+
typeof value["override_branch"] !== "string" ||
|
|
104
|
+
typeof value["created_at"] !== "string") {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
repo_name: value["repo_name"],
|
|
109
|
+
original_base_branch: original,
|
|
110
|
+
override_branch: value["override_branch"],
|
|
111
|
+
created_at: value["created_at"],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Validate a 200 body against {@link IndexBranchStatus}, returning `null` when it
|
|
116
|
+
* does not match.
|
|
117
|
+
*
|
|
118
|
+
* A body that parsed as JSON but is not this shape is treated as a failure
|
|
119
|
+
* rather than cast through: `changed` and `current_base_branch` drive
|
|
120
|
+
* destructive branch decisions in the caller, and a missing `changed` read as
|
|
121
|
+
* `undefined` would silently mean "nothing happened".
|
|
122
|
+
*/
|
|
123
|
+
function parseStatus(body) {
|
|
124
|
+
if (!isRecord(body))
|
|
125
|
+
return null;
|
|
126
|
+
const current = nullableString(body["current_base_branch"]);
|
|
127
|
+
const override = parseOverride(body["override"]);
|
|
128
|
+
if (typeof body["repo_name"] !== "string" ||
|
|
129
|
+
current === undefined ||
|
|
130
|
+
override === undefined ||
|
|
131
|
+
typeof body["changed"] !== "boolean") {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
repo_name: body["repo_name"],
|
|
136
|
+
current_base_branch: current,
|
|
137
|
+
override,
|
|
138
|
+
changed: body["changed"],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** Wrap a parsed body into a success result, or a generic failure if malformed. */
|
|
142
|
+
function toStatusResult(body) {
|
|
143
|
+
const status = parseStatus(body);
|
|
144
|
+
if (status === null) {
|
|
145
|
+
return { ok: false, status: null, error: GENERIC_ERROR };
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, value: status };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* `POST /jira/index-branch/repoint` — point the indexed base branch at `branch`.
|
|
151
|
+
*
|
|
152
|
+
* A repeat call with the same branch resolves `{ ok: true }` with
|
|
153
|
+
* `value.changed === false`. A call for a *different* branch while an override is
|
|
154
|
+
* active resolves `{ ok: false, status: 409 }` with an error naming the override
|
|
155
|
+
* the server preserved — it is not an exception, because a conflict is an
|
|
156
|
+
* expected outcome the CLI must report and continue from.
|
|
157
|
+
*/
|
|
158
|
+
export async function repointIndexBranch(access, input, fetchImpl = globalThis.fetch) {
|
|
159
|
+
try {
|
|
160
|
+
// `buildConductorJiraUrl` already appends the `/jira` segment.
|
|
161
|
+
const url = buildConductorJiraUrl(access.baseUrl, "/index-branch/repoint");
|
|
162
|
+
const body = await fetchConductorJsonPostWithTimeout(url, postHeaders(access), JSON.stringify({ repo_name: access.repoName, branch: input.branch }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
163
|
+
return toStatusResult(body);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
return toFailure(err, access);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* `POST /jira/index-branch/restore` — write the stored original branch back and
|
|
171
|
+
* drop the override.
|
|
172
|
+
*
|
|
173
|
+
* Idempotent: with no active override the call resolves `{ ok: true }` with
|
|
174
|
+
* `value.changed === false` and `value.override === null`. That matters because
|
|
175
|
+
* restore is the recovery path — it must be safe to run after a crash whose
|
|
176
|
+
* position is unknown.
|
|
177
|
+
*/
|
|
178
|
+
export async function restoreIndexBranch(access, fetchImpl = globalThis.fetch) {
|
|
179
|
+
try {
|
|
180
|
+
const url = buildConductorJiraUrl(access.baseUrl, "/index-branch/restore");
|
|
181
|
+
const body = await fetchConductorJsonPostWithTimeout(url, postHeaders(access), JSON.stringify({ repo_name: access.repoName }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
182
|
+
return toStatusResult(body);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return toFailure(err, access);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* `GET /jira/index-branch?repo_name=` — report the current indexed branch and any
|
|
190
|
+
* active override.
|
|
191
|
+
*
|
|
192
|
+
* This is how a stale override left behind by a crashed loop becomes visible;
|
|
193
|
+
* `value.changed` is always `false` because a read changes nothing.
|
|
194
|
+
*/
|
|
195
|
+
export async function getIndexBranch(access, fetchImpl = globalThis.fetch) {
|
|
196
|
+
try {
|
|
197
|
+
const url = buildConductorJiraUrl(access.baseUrl, "/index-branch", {
|
|
198
|
+
repo_name: access.repoName,
|
|
199
|
+
});
|
|
200
|
+
const body = await fetchConductorJsonWithTimeout(url, getHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
201
|
+
return toStatusResult(body);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
return toFailure(err, access);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Run `operation` and convert any thrown value into a sanitized failure result.
|
|
209
|
+
*
|
|
210
|
+
* Every +803 read is this shape, so the try/catch lives in one place: a helper
|
|
211
|
+
* added later cannot forget it and reintroduce a throwing client.
|
|
212
|
+
*/
|
|
213
|
+
async function wrap(access, operation) {
|
|
214
|
+
try {
|
|
215
|
+
return { ok: true, value: await operation() };
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
return toFailure(err, access);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* `PUT /jira/epic-runs/supervisor-config/defaults/?repo_name=` — replace the
|
|
223
|
+
* project-default supervisor configuration.
|
|
224
|
+
*
|
|
225
|
+
* The complete {@link EpicSupervisorConfigRequest} is sent. `init` builds it by
|
|
226
|
+
* reading the effective config and flipping exactly one field, so enabling
|
|
227
|
+
* auto-merge never silently reverts another operator setting.
|
|
228
|
+
*/
|
|
229
|
+
export async function putSupervisorConfigDefaults(access, config, fetchImpl = globalThis.fetch) {
|
|
230
|
+
return wrap(access, () => {
|
|
231
|
+
const url = buildConductorJiraUrl(access.baseUrl, "/epic-runs/supervisor-config/defaults/", { repo_name: access.repoName });
|
|
232
|
+
return fetchConductorJsonPutWithTimeout(url, putHeaders(access), JSON.stringify(config), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/** `GET /jira/epic-runs/supervisor-config/defaults/` — the effective config. */
|
|
236
|
+
export async function getEffectiveSupervisorConfig(access, fetchImpl = globalThis.fetch) {
|
|
237
|
+
return wrap(access, () => fetchEffectiveSupervisorConfig(access, undefined, fetchImpl));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* `GET /jira/epic-runs/supervisor-setup/defaults/` — the effective setup.
|
|
241
|
+
*
|
|
242
|
+
* Returned verbatim, `done_gate_config` included and un-normalized: it is the
|
|
243
|
+
* raw operator-authored JSONB that `parseDoneGateConfig` is the sole authority
|
|
244
|
+
* on, and normalizing it here would fork that vocabulary.
|
|
245
|
+
*/
|
|
246
|
+
export async function getEffectiveSupervisorSetup(access, fetchImpl = globalThis.fetch) {
|
|
247
|
+
return wrap(access, () => fetchEffectiveSupervisorSetup(access, undefined, fetchImpl));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* `GET /jira/epic-runs/conductor-readiness?repo_name=` — readiness facts.
|
|
251
|
+
*
|
|
252
|
+
* `init` reads three of them: `supervisor.auto_merge_enabled`,
|
|
253
|
+
* `supervisor.required_checks_count`, and `supervisor.required_checks_empty`.
|
|
254
|
+
* The underlying fetcher validates the body fail-closed, so a 200 whose body is
|
|
255
|
+
* missing `required_checks_empty` becomes a failure here rather than an
|
|
256
|
+
* apparently-healthy report.
|
|
257
|
+
*/
|
|
258
|
+
export async function getConductorReadiness(access, fetchImpl = globalThis.fetch) {
|
|
259
|
+
return wrap(access, () => fetchConductorReadiness(access, fetchImpl));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* `POST /jira/resolve-ci-checks` — discover and classify the repository's CI
|
|
263
|
+
* checks, and warm the `poll-ci-checks` cache as a side effect.
|
|
264
|
+
*
|
|
265
|
+
* `commitRef` is optional and forwarded only when supplied. The response is the
|
|
266
|
+
* server's soft envelope, preserved verbatim inside `value`.
|
|
267
|
+
*/
|
|
268
|
+
export async function resolveCiChecks(access, commitRef, fetchImpl = globalThis.fetch) {
|
|
269
|
+
return wrap(access, () => {
|
|
270
|
+
const url = buildConductorJiraUrl(access.baseUrl, "/resolve-ci-checks");
|
|
271
|
+
const payload = { repo_name: access.repoName };
|
|
272
|
+
if (typeof commitRef === "string" && commitRef.trim().length > 0) {
|
|
273
|
+
payload.commit_ref = commitRef;
|
|
274
|
+
}
|
|
275
|
+
return fetchConductorJsonPostWithTimeout(url, postHeaders(access), JSON.stringify(payload), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* `GET /jira/poll-ci-checks?repo_name=&commit_ref=` — CI status for one SHA.
|
|
280
|
+
*
|
|
281
|
+
* Until `resolve-ci-checks` has run once for the repository this answers 200
|
|
282
|
+
* with `{ available: false, action: "Call resolve-ci-checks first…" }`. That is
|
|
283
|
+
* a successful result carrying an instruction, not an error, and it stays in
|
|
284
|
+
* `value` so `status` can act on it.
|
|
285
|
+
*/
|
|
286
|
+
export async function pollCiChecks(access, commitRef, fetchImpl = globalThis.fetch) {
|
|
287
|
+
return wrap(access, () => pollCiChecksForCommit(access, commitRef, fetchImpl));
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* `GET /vcs/pull-requests/{n}/reviews/status?repo_name=` — normalized review
|
|
291
|
+
* state. Root-mounted, NOT under `/jira`.
|
|
292
|
+
*
|
|
293
|
+
* `prNumber` is validated by the wrapped fetcher before any request is issued,
|
|
294
|
+
* so a zero/negative/non-integer number fails without a network round trip.
|
|
295
|
+
*/
|
|
296
|
+
export async function getPrReviewStatus(access, prNumber, fetchImpl = globalThis.fetch) {
|
|
297
|
+
return wrap(access, () => fetchPrReviewStatus(access, prNumber, fetchImpl));
|
|
298
|
+
}
|
|
299
|
+
/** `GET /jira/parse-status?repo_name=` — the durable parse-run status. */
|
|
300
|
+
export async function getParseStatus(access, fetchImpl = globalThis.fetch) {
|
|
301
|
+
return wrap(access, () => fetchParseStatus(access, fetchImpl));
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* `GET /jira/epic-runs/runs/{epicKey}/state?repo_name=` — the durable epic-run
|
|
305
|
+
* state.
|
|
306
|
+
*
|
|
307
|
+
* A pure READ. `conduct-epic` never creates an `epic_run`; `init` calls this
|
|
308
|
+
* only to refuse when the server is already conducting the same epic through
|
|
309
|
+
* the v2 reconciler, which would otherwise drive the same tickets in parallel.
|
|
310
|
+
*/
|
|
311
|
+
export async function getEpicRunState(access, epicKey, fetchImpl = globalThis.fetch) {
|
|
312
|
+
return wrap(access, () => fetchEpicRunState(access, epicKey, fetchImpl));
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* `GET /jira/config-field/base_branch?repo_name=` — the repository's configured
|
|
316
|
+
* base branch.
|
|
317
|
+
*
|
|
318
|
+
* Normalized defensively: an absent `value`, an explicit `null`, and a
|
|
319
|
+
* blank string all become `base_branch: null`, which the caller reads as "not
|
|
320
|
+
* configured" and falls through to `main`. A body that is not an object at all
|
|
321
|
+
* is a failure — silently treating an unparseable response as "not configured"
|
|
322
|
+
* would provision the epic branch from the wrong base.
|
|
323
|
+
*/
|
|
324
|
+
export async function getConfigFieldBaseBranch(access, fetchImpl = globalThis.fetch) {
|
|
325
|
+
const result = await wrap(access, () => {
|
|
326
|
+
const url = buildConductorJiraUrl(access.baseUrl, "/config-field/base_branch", {
|
|
327
|
+
repo_name: access.repoName,
|
|
328
|
+
});
|
|
329
|
+
return fetchConductorJsonWithTimeout(url, getHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
330
|
+
});
|
|
331
|
+
if (!result.ok)
|
|
332
|
+
return result;
|
|
333
|
+
if (!isRecord(result.value)) {
|
|
334
|
+
return { ok: false, status: null, error: GENERIC_ERROR };
|
|
335
|
+
}
|
|
336
|
+
const raw = result.value["value"];
|
|
337
|
+
if (raw === null || raw === undefined) {
|
|
338
|
+
return { ok: true, value: { base_branch: null } };
|
|
339
|
+
}
|
|
340
|
+
if (typeof raw !== "string") {
|
|
341
|
+
return { ok: false, status: null, error: GENERIC_ERROR };
|
|
342
|
+
}
|
|
343
|
+
const trimmed = raw.trim();
|
|
344
|
+
return { ok: true, value: { base_branch: trimmed.length === 0 ? null : trimmed } };
|
|
345
|
+
}
|