@bridge_gpt/mcp-server 0.2.58 → 0.2.60
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 +231 -113
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +7 -8
- package/build/conductor/bridge-api-client.js +202 -2
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +279 -72
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +21 -12
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +28 -6
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +61 -0
- package/build/conductor/merge-execution-eligibility.js +129 -0
- package/build/conductor/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +166 -9
- package/build/conductor/readiness.js +97 -16
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +64 -6
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +16 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +199 -303
- package/build/drive-epic.js +88 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +184 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +31 -0
- package/build/executor/credentials.js +11 -0
- package/build/executor/env.js +79 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/index.js +21 -4
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +127 -1
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +46 -5
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +5 -0
- package/build/index.js +320 -342
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +208 -15
- package/build/install-bridge.js +555 -37
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-install-state.js +42 -4
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +118 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +133 -0
- package/build/merge-workflow-drift.js +66 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +572 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +10 -10
- package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/cli.js +24 -2
- package/build/plane/member-roster.js +5 -5
- package/build/plane/preflight.js +68 -0
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +171 -17
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +475 -91
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +4 -4
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +2 -2
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
- /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
|
@@ -10,6 +10,12 @@ export function adaptBridgeEntryForHostTarget(entry, target) {
|
|
|
10
10
|
args: [...entry.args],
|
|
11
11
|
env: { ...entry.env },
|
|
12
12
|
};
|
|
13
|
+
// The marker belongs to the installed physical registration, never the
|
|
14
|
+
// portable logical entry or committed .bridge/config. Replacing an entry
|
|
15
|
+
// refreshes it; removing the registration removes it automatically.
|
|
16
|
+
delete physical.env.BAPI_MCP_HOST_FAMILY;
|
|
17
|
+
if (target.routingHostFamily)
|
|
18
|
+
physical.env.BAPI_MCP_HOST_FAMILY = target.routingHostFamily;
|
|
13
19
|
if (adapter.transportType !== undefined)
|
|
14
20
|
physical.type = adapter.transportType;
|
|
15
21
|
if (adapter.extraEntryKeys)
|
|
@@ -67,6 +67,7 @@ export function hostAdapterForTarget(target) {
|
|
|
67
67
|
export const MCP_HOST_TARGETS = {
|
|
68
68
|
"claude-code": {
|
|
69
69
|
id: "claude-code",
|
|
70
|
+
routingHostFamily: "claude_code",
|
|
70
71
|
label: "Claude Code",
|
|
71
72
|
scope: "project",
|
|
72
73
|
relPath: ".mcp.json",
|
|
@@ -106,6 +107,7 @@ export const MCP_HOST_TARGETS = {
|
|
|
106
107
|
},
|
|
107
108
|
codex: {
|
|
108
109
|
id: "codex",
|
|
110
|
+
routingHostFamily: "codex",
|
|
109
111
|
label: "OpenAI Codex",
|
|
110
112
|
scope: "global",
|
|
111
113
|
absPathResolver: (homedir) => joinPath(homedir, ".codex/config.toml"),
|
|
@@ -64,12 +64,46 @@ function normalizePlatforms(ids) {
|
|
|
64
64
|
const wanted = new Set(ids);
|
|
65
65
|
return HOST_PLATFORM_ORDER.filter((id) => wanted.has(id));
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Is this a plain, repository-relative POSIX path safe to record?
|
|
69
|
+
*
|
|
70
|
+
* BAPI-1136. The install manifest is read back by `doctor` and by the installer
|
|
71
|
+
* itself to decide which files Bridge manages. A recorded absolute path, a `..`
|
|
72
|
+
* escape, or a Windows separator therefore names a file OUTSIDE the project that
|
|
73
|
+
* a later tool would treat as project-owned. The install-state file's own path is
|
|
74
|
+
* excluded for the same reason — recording it would make the manifest claim
|
|
75
|
+
* itself as a managed artifact.
|
|
76
|
+
*/
|
|
77
|
+
export function isRecordableProjectPath(relPath) {
|
|
78
|
+
if (typeof relPath !== "string" || relPath.length === 0)
|
|
79
|
+
return false;
|
|
80
|
+
if (relPath.includes("\\"))
|
|
81
|
+
return false;
|
|
82
|
+
if (relPath.startsWith("/"))
|
|
83
|
+
return false;
|
|
84
|
+
if (/^[A-Za-z]:/.test(relPath))
|
|
85
|
+
return false;
|
|
86
|
+
if (relPath === MCP_INSTALL_STATE_RELPATH)
|
|
87
|
+
return false;
|
|
88
|
+
const segments = relPath.split("/");
|
|
89
|
+
if (segments.some((seg) => seg.length === 0 || seg === "." || seg === ".."))
|
|
90
|
+
return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Dedupe + stably order relative project config paths, DROPPING any path that is
|
|
95
|
+
* not recordable.
|
|
96
|
+
*
|
|
97
|
+
* BAPI-1136 widened this from a non-empty-string filter. The filter runs on the
|
|
98
|
+
* read side too, so a malformed path written by an older build (or by hand) is
|
|
99
|
+
* dropped rather than faithfully reserialized — a normalizer that preserves
|
|
100
|
+
* every value it was handed turns one bad write into a permanent one.
|
|
101
|
+
*/
|
|
68
102
|
function normalizeProjectPaths(paths) {
|
|
69
103
|
const seen = new Set();
|
|
70
104
|
const out = [];
|
|
71
105
|
for (const p of paths) {
|
|
72
|
-
if (
|
|
106
|
+
if (isRecordableProjectPath(p) && !seen.has(p)) {
|
|
73
107
|
seen.add(p);
|
|
74
108
|
out.push(p);
|
|
75
109
|
}
|
|
@@ -337,8 +371,12 @@ async function persistMcpInstallState(cwd, state, finalPath, deps) {
|
|
|
337
371
|
* has no field for one, and the serializer emits only schema-approved keys.
|
|
338
372
|
*/
|
|
339
373
|
export async function recordInstalledProjectArtifact(cwd, relPath, deps) {
|
|
340
|
-
if (
|
|
341
|
-
return {
|
|
374
|
+
if (!isRecordableProjectPath(relPath)) {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
error: "artifact path must be a repository-relative POSIX path with no absolute prefix, " +
|
|
378
|
+
"traversal segment, empty segment, or backslash, and must not be the install-state file itself",
|
|
379
|
+
};
|
|
342
380
|
}
|
|
343
381
|
const existing = await readMcpInstallState(cwd, deps);
|
|
344
382
|
const priorPlatforms = existing.status === "valid" ? existing.state.selectedPlatforms : [];
|
package/build/mcp-invoke.js
CHANGED
|
@@ -306,21 +306,21 @@ export async function resolveBapiInvocation(projectRoot, deps) {
|
|
|
306
306
|
BAPI_PROJECT_ROOT: projectRoot,
|
|
307
307
|
BAPI_REPO_NAME: repoName,
|
|
308
308
|
};
|
|
309
|
-
// BAPI-527: `
|
|
309
|
+
// BAPI-527: `EPIC_IMPLEMENTER_NODE_PATH` is NON-SECRET operational metadata (the
|
|
310
310
|
// captured conductor Node path used by the worker-ledger CLI subprocess).
|
|
311
311
|
// `buildChildEnv` already inherits the full parent env, so this re-affirms the
|
|
312
312
|
// pass-through explicitly and ONLY when present — it is a non-secret allowlist
|
|
313
313
|
// entry, never a credential. It is emphatically NOT the MCP server's re-exec
|
|
314
314
|
// command: the spawned command stays `deps.execPath` + `[scriptPath]` below.
|
|
315
|
-
const conductorNodePath = deps.env.
|
|
315
|
+
const conductorNodePath = deps.env.EPIC_IMPLEMENTER_NODE_PATH;
|
|
316
316
|
if (typeof conductorNodePath === "string" && conductorNodePath.trim().length > 0) {
|
|
317
|
-
overlay.
|
|
317
|
+
overlay.EPIC_IMPLEMENTER_NODE_PATH = conductorNodePath;
|
|
318
318
|
}
|
|
319
319
|
// BAPI-727: overlay the endpoint ONLY when `--base-url` was supplied, mirroring
|
|
320
|
-
// the conditional `
|
|
320
|
+
// the conditional `EPIC_IMPLEMENTER_NODE_PATH` overlay directly above. The absent case
|
|
321
321
|
// must add, delete, normalize, and override nothing — `buildChildEnv` already
|
|
322
322
|
// inherits the parent's `BAPI_BASE_URL`, and the server keeps its own production
|
|
323
|
-
// default for callers that were never given a URL. Like `
|
|
323
|
+
// default for callers that were never given a URL. Like `EPIC_IMPLEMENTER_NODE_PATH`,
|
|
324
324
|
// this is non-secret routing metadata, not a credential.
|
|
325
325
|
if (typeof deps.baseUrl === "string" && deps.baseUrl.length > 0) {
|
|
326
326
|
overlay.BAPI_BASE_URL = deps.baseUrl;
|
package/build/mcp-profile.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
const VALID_GROUPS = new Set([
|
|
17
17
|
"core",
|
|
18
|
-
"
|
|
18
|
+
"epic_implementer",
|
|
19
19
|
"estimation",
|
|
20
20
|
"pipeline-authoring",
|
|
21
21
|
"sfcc",
|
|
@@ -49,7 +49,7 @@ export function resolveProfiles(raw) {
|
|
|
49
49
|
// rather than an implicit widening of an existing user's surface.
|
|
50
50
|
return new Set([
|
|
51
51
|
"core",
|
|
52
|
-
"
|
|
52
|
+
"epic_implementer",
|
|
53
53
|
"estimation",
|
|
54
54
|
"pipeline-authoring",
|
|
55
55
|
"sfcc",
|
|
@@ -67,7 +67,7 @@ export function serverNameForMcpTarget(target) {
|
|
|
67
67
|
* node executable is a single substitutable field), and the npm-channel fallback
|
|
68
68
|
* as `{ command: "npx", args: ["-y", "<spec>", "mcp-invoke", …] }`.
|
|
69
69
|
*
|
|
70
|
-
* BAPI-527: `.mcp.json` remains intentionally ENV-FREE. `
|
|
70
|
+
* BAPI-527: `.mcp.json` remains intentionally ENV-FREE. `EPIC_IMPLEMENTER_NODE_PATH`
|
|
71
71
|
* (the captured conductor Node path for the worker-ledger CLI subprocess) is NOT
|
|
72
72
|
* written here — it is inherited at runtime from the worker SHELL env that
|
|
73
73
|
* `start-tickets` injects at the spawn boundary, never stored as a registration
|
|
@@ -77,7 +77,7 @@ export function serverNameForMcpTarget(target) {
|
|
|
77
77
|
*
|
|
78
78
|
* BAPI-724: the executor caller's `invocation` is resolved BEFORE spawn from its
|
|
79
79
|
* OWN running process (`resolveMcpShimInvocationForRuntime`, absolute-build-path
|
|
80
|
-
* form preferred) and never depends on a worker-injected `
|
|
80
|
+
* form preferred) and never depends on a worker-injected `EPIC_IMPLEMENTER_NODE_PATH`
|
|
81
81
|
* or any other worker-shell env — the executor worker environment carries no
|
|
82
82
|
* such key at all (see `executor/env.ts`).
|
|
83
83
|
*
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* server file is invoked (`node <absPath>/index.js` vs `npx @…`). It does NOT
|
|
18
18
|
* change WHICH Node binary runs that file — the `nodeExecutable` field is kept as
|
|
19
19
|
* a single substitutable value (never string-concatenated into the command) so
|
|
20
|
-
* the later Ticket 3 (P2) work can point it at `
|
|
20
|
+
* the later Ticket 3 (P2) work can point it at `EPIC_IMPLEMENTER_NODE_PATH` without
|
|
21
21
|
* re-plumbing argv construction.
|
|
22
22
|
*/
|
|
23
23
|
import path from "node:path";
|
|
@@ -48,7 +48,7 @@ export function isTransientNpxEntryPath(entryPath) {
|
|
|
48
48
|
* Build the concrete `{ command, args }` launch pair for a target + worktree from
|
|
49
49
|
* a structured invocation. Pure — no filesystem or process access. The
|
|
50
50
|
* `nodeExecutable` is returned as the standalone `command`, never concatenated
|
|
51
|
-
* with the server entry path, so `
|
|
51
|
+
* with the server entry path, so `EPIC_IMPLEMENTER_NODE_PATH` can substitute it later.
|
|
52
52
|
*/
|
|
53
53
|
export function buildMcpShimCommand(invocation, target, absoluteWorktreePath) {
|
|
54
54
|
if (invocation.form === "absolute-build-path") {
|
|
@@ -141,7 +141,7 @@ function basenameAnySep(filePath) {
|
|
|
141
141
|
* NOTE (scope boundary): this ticket changes WHICH server file is invoked, not
|
|
142
142
|
* WHICH Node runtime runs it. `nodeExecutable` defaults to `"node"` (NOT
|
|
143
143
|
* `process.execPath`) to preserve today's worker runtime selection; Ticket 3 (P2)
|
|
144
|
-
* can later inject `
|
|
144
|
+
* can later inject `EPIC_IMPLEMENTER_NODE_PATH` into this single field.
|
|
145
145
|
*/
|
|
146
146
|
export function resolveMcpShimInvocationForRuntime(deps) {
|
|
147
147
|
const nodeExecutable = deps.nodeExecutable ?? "node";
|
|
@@ -78,7 +78,7 @@ import { DEFAULT_GATE_NAME, REQUIRED_CI_CHECKS_GREEN, REVIEW_STATE, normalizeChe
|
|
|
78
78
|
import { MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON, VERDICTLESS_DISPOSITION_FAIL_OPEN, VERDICTLESS_DISPOSITION_PARK, } from "./conductor/git-ci-types.js";
|
|
79
79
|
import { evaluateReviewCondition, normalizeReviewSnapshot, parseDoneGateConfig, } from "./conductor/done-gate.js";
|
|
80
80
|
import { buildGateIdentity, makeMergeActionKey } from "./conductor/merge-identity.js";
|
|
81
|
-
import { resolveLocalMergeMethod, runApprovedLocalMerge, } from "./conductor/local-merge.js";
|
|
81
|
+
import { resolveLocalMergeMethod, runApprovedLocalMerge, resolveLocalMergeCredential, LOCAL_MERGE_CREDENTIAL_ENV_KEYS, LOCAL_MERGE_CREDENTIAL_MISSING_REASONS, } from "./conductor/local-merge.js";
|
|
82
82
|
/**
|
|
83
83
|
* The exact remediation for a `dry_run`. Fixed text, because a caller that has to
|
|
84
84
|
* infer WHICH setting to flip from a reason code guesses wrong.
|
|
@@ -102,6 +102,19 @@ const CI_NOT_GREEN_REASON = "ci_not_green";
|
|
|
102
102
|
* moves an unreadable, unsupported, or malformed review configuration.
|
|
103
103
|
*/
|
|
104
104
|
const REVIEW_NOT_APPROVED_REASON = "review_not_approved";
|
|
105
|
+
/**
|
|
106
|
+
* BAPI-1234 — the server's migration-graph merge guard. Both are named here so
|
|
107
|
+
* the mapping is explicit rather than an accident of the default branch, and
|
|
108
|
+
* both are `needs_human`: repeating the call against the same base and head
|
|
109
|
+
* reproduces the same refusal. `migration_graph_invalid` means a human must
|
|
110
|
+
* renumber the PR's own migration; `migration_graph_unreadable` means the graph
|
|
111
|
+
* could not be proven either way. The exact reason is returned unchanged so the
|
|
112
|
+
* caller can tell them apart; no server-supplied text is added.
|
|
113
|
+
*/
|
|
114
|
+
const MIGRATION_GRAPH_REFUSAL_REASONS = new Set([
|
|
115
|
+
"migration_graph_invalid",
|
|
116
|
+
"migration_graph_unreadable",
|
|
117
|
+
]);
|
|
105
118
|
// ---------------------------------------------------------------------------
|
|
106
119
|
// Envelope construction
|
|
107
120
|
// ---------------------------------------------------------------------------
|
|
@@ -560,6 +573,8 @@ function retryHintForFailure(reason, ciSummary) {
|
|
|
560
573
|
}
|
|
561
574
|
if (reason === REVIEW_NOT_APPROVED_REASON)
|
|
562
575
|
return "retry_later";
|
|
576
|
+
if (MIGRATION_GRAPH_REFUSAL_REASONS.has(reason))
|
|
577
|
+
return "needs_human";
|
|
563
578
|
return "needs_human";
|
|
564
579
|
}
|
|
565
580
|
/**
|
|
@@ -614,22 +629,86 @@ function interpretMergeResponse(body, expectedHeadSha, prNumber) {
|
|
|
614
629
|
// ---------------------------------------------------------------------------
|
|
615
630
|
/** The non-terminal status the server returns when it approves a local merge. */
|
|
616
631
|
const LOCAL_APPROVAL_STATUS = "approved_for_local_execution";
|
|
617
|
-
const DEFAULT_MERGE_EXECUTION = "local";
|
|
618
|
-
/**
|
|
632
|
+
export const DEFAULT_MERGE_EXECUTION = "local";
|
|
633
|
+
/**
|
|
634
|
+
* The ONE place the legacy repository vocabulary is reconciled onto the typed
|
|
635
|
+
* policy vocabulary (BAPI-1133, A2). Exhaustive and pure.
|
|
636
|
+
*
|
|
637
|
+
* `local -> local` is the identity case. `server -> machine_token`, NOT
|
|
638
|
+
* `github_actions`: legacy `server` means the existing direct credentialed
|
|
639
|
+
* provider-API write through the App-mediated merge path
|
|
640
|
+
* (`executeApprovedLocalMerge`'s sibling branch on the server), so its typed
|
|
641
|
+
* equivalent is the direct-credential declaration. `github_actions` represents
|
|
642
|
+
* workflow-DISPATCH intent, which the legacy `server` behavior never performed
|
|
643
|
+
* and so can never be inferred from it. `server` is therefore structurally
|
|
644
|
+
* impossible as a {@link TypedMergeExecutionMode} — this function is the only
|
|
645
|
+
* place that mapping exists, so no other consumer can reimplement it
|
|
646
|
+
* differently.
|
|
647
|
+
*/
|
|
648
|
+
export function mapLegacyMergeExecutionModeToPolicyMode(mode) {
|
|
649
|
+
switch (mode) {
|
|
650
|
+
case "local":
|
|
651
|
+
return "local";
|
|
652
|
+
case "server":
|
|
653
|
+
return "machine_token";
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Which configured credential a resolved legacy mode reads (BAPI-1136).
|
|
658
|
+
*
|
|
659
|
+
* Routed through {@link mapLegacyMergeExecutionModeToPolicyMode} rather than
|
|
660
|
+
* switching on the legacy value again, so there remains exactly ONE place where
|
|
661
|
+
* legacy `server` becomes typed `machine_token`. Typed `github_actions` never
|
|
662
|
+
* reaches this local path — that mode dispatches the customer's workflow and
|
|
663
|
+
* performs no local subprocess — so it falls back to the local key rather than
|
|
664
|
+
* inventing a third credential.
|
|
665
|
+
*/
|
|
666
|
+
export function resolveLocalCredentialMode(mode) {
|
|
667
|
+
const typed = mapLegacyMergeExecutionModeToPolicyMode(mode);
|
|
668
|
+
return typed === "machine_token" ? "machine_token" : "local";
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Operator-fixable hints for every preflight and credential refusal.
|
|
672
|
+
*
|
|
673
|
+
* BAPI-1136 removed the `gh auth login` remediation outright. A merge no longer
|
|
674
|
+
* authenticates as whoever is logged into `gh`, so telling an operator to log in
|
|
675
|
+
* would send them to fix a thing that is not consulted — the most expensive kind
|
|
676
|
+
* of wrong hint, because following it appears reasonable and changes nothing.
|
|
677
|
+
*
|
|
678
|
+
* Each hint names the CONFIGURATION KEY to set, never where or how a secret
|
|
679
|
+
* manager should materialize the value: that is the operator's own infrastructure
|
|
680
|
+
* decision, and prescribing one would be guidance this tool cannot verify.
|
|
681
|
+
*/
|
|
619
682
|
const LOCAL_GH_HINTS = {
|
|
620
683
|
local_gh_unavailable: "Install the GitHub CLI (`gh`) on the machine running this MCP server — local merges execute there.",
|
|
621
|
-
|
|
684
|
+
local_merge_credential_missing: `Set ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local} in the environment this MCP server is launched with. ` +
|
|
685
|
+
"It is a configured local scoped credential; Bridge never stores it and there is no ambient-login fallback.",
|
|
686
|
+
machine_merge_credential_missing: `Set ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} in the environment this MCP server is launched with. ` +
|
|
687
|
+
"It is your machine user's credential; Bridge never stores it and there is no ambient-login fallback.",
|
|
688
|
+
local_merge_credential_invalid: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local} was rejected by GitHub. Rotate it and relaunch this MCP server.`,
|
|
689
|
+
machine_merge_credential_invalid: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} was rejected by GitHub. Rotate it and relaunch this MCP server.`,
|
|
690
|
+
local_merge_credential_unpermitted: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local} authenticates but cannot write to this repository. Grant it merge permission.`,
|
|
691
|
+
machine_merge_credential_unpermitted: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} authenticates but cannot write to this repository. Grant the machine user merge permission.`,
|
|
622
692
|
};
|
|
623
693
|
/**
|
|
624
|
-
* Read the repository's merge execution mode.
|
|
694
|
+
* Read the repository's LEGACY merge execution mode.
|
|
625
695
|
*
|
|
626
696
|
* Mode is server-side configuration, never a tool argument: an agent must not be
|
|
627
697
|
* able to choose who performs a privileged write. An unreadable or unrecognized
|
|
628
698
|
* value falls back to `local`, which is the mode that works on an installation
|
|
629
699
|
* without `contents: write` — defaulting to the broken mode on a transient
|
|
630
|
-
* config read failure would park a perfectly mergeable pull request.
|
|
700
|
+
* config read failure would park a perfectly mergeable pull request. Total: every
|
|
701
|
+
* failure mode (network error, non-2xx, malformed body, missing/unknown
|
|
702
|
+
* `merge_execution`) resolves to {@link DEFAULT_MERGE_EXECUTION} rather than
|
|
703
|
+
* throwing.
|
|
704
|
+
*
|
|
705
|
+
* Returns only the legacy vocabulary — `local | server` — never a typed mode.
|
|
706
|
+
* `setup-epic.ts` maps this result through
|
|
707
|
+
* {@link mapLegacyMergeExecutionModeToPolicyMode} before composing it into a
|
|
708
|
+
* `RunPolicy` declaration; `mergePullRequestHandler` sends it straight to the
|
|
709
|
+
* existing merge route unchanged.
|
|
631
710
|
*/
|
|
632
|
-
async function resolveMergeExecutionMode(deps) {
|
|
711
|
+
export async function resolveMergeExecutionMode(deps) {
|
|
633
712
|
try {
|
|
634
713
|
const resp = await (deps.fetchImpl ?? fetch)(deps.buildGetUrl("/epic-runs/supervisor-config/defaults/", { repo_name: deps.repoName }), { headers: await deps.getHeaders() });
|
|
635
714
|
if (!resp.ok)
|
|
@@ -703,7 +782,7 @@ async function reportLocalCompletion(deps, prNumber, body) {
|
|
|
703
782
|
* left to time out; and a refusal here is always `needs_human`, because every
|
|
704
783
|
* local failure this can produce is an operator fix, not a wait.
|
|
705
784
|
*/
|
|
706
|
-
async function executeApprovedLocalMerge(deps, approval, prNumber, expectedHeadSha, actionKey) {
|
|
785
|
+
async function executeApprovedLocalMerge(deps, approval, prNumber, expectedHeadSha, actionKey, credentialMode) {
|
|
707
786
|
if (localApprovalMismatch(approval, prNumber, expectedHeadSha, actionKey)) {
|
|
708
787
|
return envelope(false, "refused", "local_approval_mismatch", "needs_human", expectedHeadSha, prNumber);
|
|
709
788
|
}
|
|
@@ -715,10 +794,34 @@ async function executeApprovedLocalMerge(deps, approval, prNumber, expectedHeadS
|
|
|
715
794
|
gate: { name: DEFAULT_GATE_NAME },
|
|
716
795
|
action_key: actionKey,
|
|
717
796
|
};
|
|
797
|
+
// BAPI-1136. The credential is resolved HERE, before the executor is called,
|
|
798
|
+
// so an unconfigured mode refuses without spawning a privileged subprocess —
|
|
799
|
+
// and so the refusal names which key to set rather than surfacing whatever
|
|
800
|
+
// `gh` says when it has no identity.
|
|
801
|
+
const credential = resolveLocalMergeCredential(deps.runtimeEnv ?? process.env, credentialMode);
|
|
802
|
+
if (credential === null) {
|
|
803
|
+
const reason = LOCAL_MERGE_CREDENTIAL_MISSING_REASONS[credentialMode];
|
|
804
|
+
// The lease is still closed: an attempt that cannot run is an attempt that
|
|
805
|
+
// is over, and leaving the ledger row open would make the reconciler wait
|
|
806
|
+
// out a lease for a merge nobody is going to perform.
|
|
807
|
+
await reportLocalCompletion(deps, prNumber, {
|
|
808
|
+
repo_name: deps.repoName,
|
|
809
|
+
action_key: actionKey,
|
|
810
|
+
expected_head_sha: expectedHeadSha,
|
|
811
|
+
result: "failed",
|
|
812
|
+
reason,
|
|
813
|
+
});
|
|
814
|
+
const missingHint = LOCAL_GH_HINTS[reason];
|
|
815
|
+
return envelope(false, "refused", reason, "needs_human", expectedHeadSha, prNumber, {
|
|
816
|
+
...(missingHint ? { hint: missingHint } : {}),
|
|
817
|
+
});
|
|
818
|
+
}
|
|
718
819
|
let local;
|
|
719
820
|
try {
|
|
720
821
|
const runLocal = deps.runLocalMerge ?? runApprovedLocalMerge;
|
|
721
|
-
|
|
822
|
+
// Exactly the resolved credential — never `{ env: process.env }`, which is
|
|
823
|
+
// how the ambient identity used to reach the subprocess.
|
|
824
|
+
local = await runLocal(request, { method }, { credential });
|
|
722
825
|
}
|
|
723
826
|
catch {
|
|
724
827
|
// The executor itself threw. Treat it exactly like a reported failure: close
|
|
@@ -876,7 +979,12 @@ export async function mergePullRequestHandler(deps, args) {
|
|
|
876
979
|
if (executionMode === "local" &&
|
|
877
980
|
isPlainObject(mergeJson) &&
|
|
878
981
|
mergeJson.status === LOCAL_APPROVAL_STATUS) {
|
|
879
|
-
return text(withReviewWaiver(await executeApprovedLocalMerge(deps, mergeJson, prNumber, expectedHeadSha, actionKey
|
|
982
|
+
return text(withReviewWaiver(await executeApprovedLocalMerge(deps, mergeJson, prNumber, expectedHeadSha, actionKey,
|
|
983
|
+
// The AUTHORITATIVE typed axis decides which credential is read.
|
|
984
|
+
// Legacy `server` maps to typed `machine_token` through the one
|
|
985
|
+
// existing mapping function; nothing here re-derives that, and no
|
|
986
|
+
// tool argument can influence it.
|
|
987
|
+
resolveLocalCredentialMode(executionMode)), reviewWaiver));
|
|
880
988
|
}
|
|
881
989
|
return text(withReviewWaiver(interpretMergeResponse(mergeJson, expectedHeadSha, prNumber), reviewWaiver));
|
|
882
990
|
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer-owned review artifacts for the merge workflow (BAPI-1136).
|
|
3
|
+
*
|
|
4
|
+
* The merge workflow is only as safe as the review gate on its path. This module
|
|
5
|
+
* renders the two artifacts that make that gate possible, and it renders NOTHING
|
|
6
|
+
* that enforces it — enforcement is a repository setting only the customer can
|
|
7
|
+
* apply.
|
|
8
|
+
*
|
|
9
|
+
* ## Why a fragment and not an edit to `.github/CODEOWNERS`
|
|
10
|
+
*
|
|
11
|
+
* A repository's CODEOWNERS file is live policy. An installer that rewrote it
|
|
12
|
+
* could reassign ownership of unrelated paths, drop a rule whose syntax it
|
|
13
|
+
* misparsed, or resolve a conflict nobody asked it to resolve — and it would do
|
|
14
|
+
* so inside a pull request whose reviewer is reading a workflow file, not a
|
|
15
|
+
* policy diff. So this emits a self-contained FRAGMENT at a dedicated path and
|
|
16
|
+
* tells the customer to merge it themselves. The fragment is real, reviewable
|
|
17
|
+
* CODEOWNERS syntax; it simply is not yet in force.
|
|
18
|
+
*
|
|
19
|
+
* ## Why the guidance is plain text
|
|
20
|
+
*
|
|
21
|
+
* It is read in a pull-request diff, in a terminal, and by a screen reader. So:
|
|
22
|
+
* descriptive headings, explicit "Required:" labels, short paragraphs, stable
|
|
23
|
+
* ordering, and no code block that needs horizontal scrolling.
|
|
24
|
+
*
|
|
25
|
+
* ## Secret discipline
|
|
26
|
+
*
|
|
27
|
+
* Neither renderer accepts, embeds, or invites a secret VALUE. The Bridge API
|
|
28
|
+
* key is named only — `BAPI_API_KEY` — and every command shown is one that
|
|
29
|
+
* cannot take a credential on argv.
|
|
30
|
+
*/
|
|
31
|
+
import { MERGE_WORKFLOW_EXPECTED_VERSION, MERGE_WORKFLOW_PATH, } from "./conductor/merge-execution-contract.js";
|
|
32
|
+
import { MERGE_WORKFLOW_API_KEY_SECRET_NAME, MERGE_WORKFLOW_BASE_URL_VARIABLE_NAME, } from "./merge-workflow-config.js";
|
|
33
|
+
/** Repo-relative path of the generated CODEOWNERS fragment. */
|
|
34
|
+
export const MERGE_WORKFLOW_CODEOWNERS_RELPATH = ".bridge/merge-workflow/CODEOWNERS.fragment";
|
|
35
|
+
/** Repo-relative path of the generated repository-rule guidance. */
|
|
36
|
+
export const MERGE_WORKFLOW_GUIDANCE_RELPATH = ".bridge/merge-workflow/REPOSITORY-RULES.md";
|
|
37
|
+
/**
|
|
38
|
+
* Every repo-relative path this installer may write, in the order a reader
|
|
39
|
+
* should see them. Exported as the single manifest so the preflight, the
|
|
40
|
+
* consent summary, the commit, and the install-state record cannot disagree
|
|
41
|
+
* about what "the managed artifacts" are.
|
|
42
|
+
*/
|
|
43
|
+
export const MERGE_WORKFLOW_MANAGED_RELPATHS = [
|
|
44
|
+
MERGE_WORKFLOW_PATH,
|
|
45
|
+
MERGE_WORKFLOW_CODEOWNERS_RELPATH,
|
|
46
|
+
MERGE_WORKFLOW_GUIDANCE_RELPATH,
|
|
47
|
+
];
|
|
48
|
+
/** Thrown when a code-owner value fails validation. Carries no rendered output. */
|
|
49
|
+
export class MergeWorkflowCodeOwnerError extends Error {
|
|
50
|
+
constructor(message) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "MergeWorkflowCodeOwnerError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A GitHub code owner: `@user` or `@org/team`.
|
|
57
|
+
*
|
|
58
|
+
* Deliberately rejects whitespace, quotes, `#` (which would comment out the rest
|
|
59
|
+
* of the CODEOWNERS line), `$`, `{`, `}`, and any second `/`. The pattern is an
|
|
60
|
+
* allowlist rather than a denylist so a character nobody thought about is
|
|
61
|
+
* rejected rather than rendered.
|
|
62
|
+
*/
|
|
63
|
+
const CODE_OWNER_PATTERN = /^@[A-Za-z0-9][A-Za-z0-9-]*(\/[A-Za-z0-9][A-Za-z0-9._-]*)?$/;
|
|
64
|
+
const MAX_CODE_OWNER_LENGTH = 100;
|
|
65
|
+
/**
|
|
66
|
+
* Values that parse as a code owner but are obviously a placeholder someone
|
|
67
|
+
* pasted from documentation. Accepting one would open a pull request assigning
|
|
68
|
+
* review of a write-enabled workflow to an account that does not exist, which
|
|
69
|
+
* reads as protected while protecting nothing.
|
|
70
|
+
*/
|
|
71
|
+
const PLACEHOLDER_CODE_OWNERS = new Set([
|
|
72
|
+
"@org/team",
|
|
73
|
+
"@owner/team",
|
|
74
|
+
"@your-org/your-team",
|
|
75
|
+
"@user",
|
|
76
|
+
"@username",
|
|
77
|
+
"@your-username",
|
|
78
|
+
"@example",
|
|
79
|
+
"@example/team",
|
|
80
|
+
]);
|
|
81
|
+
/**
|
|
82
|
+
* Validate a single code owner.
|
|
83
|
+
*
|
|
84
|
+
* Runs BEFORE any filesystem or git mutation — an invalid owner must cost
|
|
85
|
+
* nothing, and must never leave a half-written branch behind.
|
|
86
|
+
*
|
|
87
|
+
* @throws MergeWorkflowCodeOwnerError naming the problem, never echoing a value
|
|
88
|
+
* longer than the bound above.
|
|
89
|
+
*/
|
|
90
|
+
export function validateMergeWorkflowCodeOwner(value) {
|
|
91
|
+
if (typeof value !== "string") {
|
|
92
|
+
throw new MergeWorkflowCodeOwnerError("--codeowner requires a value");
|
|
93
|
+
}
|
|
94
|
+
const trimmed = value.trim();
|
|
95
|
+
if (trimmed.length === 0) {
|
|
96
|
+
throw new MergeWorkflowCodeOwnerError("--codeowner requires a non-empty value");
|
|
97
|
+
}
|
|
98
|
+
if (trimmed.length > MAX_CODE_OWNER_LENGTH) {
|
|
99
|
+
throw new MergeWorkflowCodeOwnerError("--codeowner value is too long");
|
|
100
|
+
}
|
|
101
|
+
if (/\s/.test(trimmed)) {
|
|
102
|
+
throw new MergeWorkflowCodeOwnerError("--codeowner takes exactly one owner; supply a single @user or @org/team");
|
|
103
|
+
}
|
|
104
|
+
if (!trimmed.startsWith("@")) {
|
|
105
|
+
throw new MergeWorkflowCodeOwnerError("--codeowner must start with @ (e.g. @org/team or @user)");
|
|
106
|
+
}
|
|
107
|
+
if (!CODE_OWNER_PATTERN.test(trimmed)) {
|
|
108
|
+
throw new MergeWorkflowCodeOwnerError("--codeowner must be a GitHub @user or @org/team handle");
|
|
109
|
+
}
|
|
110
|
+
if (PLACEHOLDER_CODE_OWNERS.has(trimmed.toLowerCase())) {
|
|
111
|
+
throw new MergeWorkflowCodeOwnerError("--codeowner looks like a documentation placeholder; supply the real reviewing team or user");
|
|
112
|
+
}
|
|
113
|
+
return trimmed;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Render the CODEOWNERS fragment for the merge-workflow path.
|
|
117
|
+
*
|
|
118
|
+
* Deterministic for a given owner, newline-terminated, and covering exactly one
|
|
119
|
+
* path. It claims no other file.
|
|
120
|
+
*/
|
|
121
|
+
export function renderMergeWorkflowCodeownersFragment(codeOwner) {
|
|
122
|
+
const owner = validateMergeWorkflowCodeOwner(codeOwner);
|
|
123
|
+
return [
|
|
124
|
+
"# Bridge-managed CODEOWNERS fragment for the Epic Implementer merge workflow.",
|
|
125
|
+
"#",
|
|
126
|
+
"# This file is NOT a CODEOWNERS file. GitHub reads CODEOWNERS only from",
|
|
127
|
+
"# .github/CODEOWNERS, CODEOWNERS, or docs/CODEOWNERS, so nothing here is in",
|
|
128
|
+
"# force until you copy the rule below into the file your repository actually",
|
|
129
|
+
"# uses. It is generated separately so an installer never rewrites live review",
|
|
130
|
+
"# policy on your behalf.",
|
|
131
|
+
"#",
|
|
132
|
+
"# The rule exists because " + MERGE_WORKFLOW_PATH + " is the one file in this",
|
|
133
|
+
"# repository that can perform a privileged merge. Whoever can change it can",
|
|
134
|
+
"# change what merges.",
|
|
135
|
+
"",
|
|
136
|
+
MERGE_WORKFLOW_PATH + " " + owner,
|
|
137
|
+
"",
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Render the repository-rule guidance document.
|
|
142
|
+
*
|
|
143
|
+
* Deterministic for a given owner. States, in order: what was generated, what
|
|
144
|
+
* the customer must do, and what is NOT protected until they do it.
|
|
145
|
+
*/
|
|
146
|
+
export function renderMergeWorkflowGuidance(codeOwner) {
|
|
147
|
+
const owner = validateMergeWorkflowCodeOwner(codeOwner);
|
|
148
|
+
return [
|
|
149
|
+
"# Epic Implementer merge workflow — required repository setup",
|
|
150
|
+
"",
|
|
151
|
+
"Bridge generated three files in this pull request. Merging the pull request",
|
|
152
|
+
"installs them. It does not protect them: two of the three steps below are",
|
|
153
|
+
"repository settings that only you can apply.",
|
|
154
|
+
"",
|
|
155
|
+
"## What was generated",
|
|
156
|
+
"",
|
|
157
|
+
"- `" + MERGE_WORKFLOW_PATH + "` — the merge workflow itself, contract version " +
|
|
158
|
+
String(MERGE_WORKFLOW_EXPECTED_VERSION) + ".",
|
|
159
|
+
"- `" + MERGE_WORKFLOW_CODEOWNERS_RELPATH + "` — a CODEOWNERS rule for that path.",
|
|
160
|
+
"- `" + MERGE_WORKFLOW_GUIDANCE_RELPATH + "` — this document.",
|
|
161
|
+
"",
|
|
162
|
+
"## Required: create the repository secret",
|
|
163
|
+
"",
|
|
164
|
+
"Required. The workflow authenticates to Bridge with a repository-level Actions",
|
|
165
|
+
"secret named `" + MERGE_WORKFLOW_API_KEY_SECRET_NAME + "`, holding a Bridge API",
|
|
166
|
+
"key for this repository.",
|
|
167
|
+
"",
|
|
168
|
+
"Create it through GitHub's own secret interface — repository Settings, then",
|
|
169
|
+
"Secrets and variables, then Actions. Bridge never sees, transmits, stores, or",
|
|
170
|
+
"writes the value: the installer that opened this pull request has no flag, no",
|
|
171
|
+
"prompt, and no file field for a credential, and the value must never be",
|
|
172
|
+
"committed, printed, or pasted into a project file.",
|
|
173
|
+
"",
|
|
174
|
+
"Optional: set a repository Actions VARIABLE named",
|
|
175
|
+
"`" + MERGE_WORKFLOW_BASE_URL_VARIABLE_NAME + "` if this repository talks to a",
|
|
176
|
+
"Bridge deployment other than the default. A variable, not a secret, and not a",
|
|
177
|
+
"workflow input — the base URL decides which server authorizes a merge, so it",
|
|
178
|
+
"is repository configuration rather than something a caller supplies per run.",
|
|
179
|
+
"",
|
|
180
|
+
"## Required: merge the CODEOWNERS rule into your active CODEOWNERS file",
|
|
181
|
+
"",
|
|
182
|
+
"Required. GitHub reads code owners only from `.github/CODEOWNERS`,",
|
|
183
|
+
"`CODEOWNERS`, or `docs/CODEOWNERS`. The generated fragment sits at",
|
|
184
|
+
"`" + MERGE_WORKFLOW_CODEOWNERS_RELPATH + "` and enforces nothing where it is.",
|
|
185
|
+
"",
|
|
186
|
+
"Copy its single rule into the CODEOWNERS file your repository actually uses,",
|
|
187
|
+
"keeping your existing rules intact. The rule is:",
|
|
188
|
+
"",
|
|
189
|
+
"```",
|
|
190
|
+
MERGE_WORKFLOW_PATH + " " + owner,
|
|
191
|
+
"```",
|
|
192
|
+
"",
|
|
193
|
+
"CODEOWNERS is last-match-wins, so place it after any broader rule that also",
|
|
194
|
+
"matches `.github/workflows/`.",
|
|
195
|
+
"",
|
|
196
|
+
"## Required: add the default-branch repository rule",
|
|
197
|
+
"",
|
|
198
|
+
"Required. A CODEOWNERS entry requests a reviewer. It does not require one.",
|
|
199
|
+
"Until you add a rule, anyone who can push to the default branch can rewrite",
|
|
200
|
+
"the merge workflow.",
|
|
201
|
+
"",
|
|
202
|
+
"In repository Settings, under Rules, add a ruleset targeting the default",
|
|
203
|
+
"branch that requires a pull request before merging and requires review from",
|
|
204
|
+
"code owners. Restricting who can bypass the ruleset matters as much as the",
|
|
205
|
+
"rule itself.",
|
|
206
|
+
"",
|
|
207
|
+
"## What is not protected until you finish",
|
|
208
|
+
"",
|
|
209
|
+
"Enforcement is absent until the repository rule above is in place. Between",
|
|
210
|
+
"merging this pull request and applying that rule, the workflow is installed",
|
|
211
|
+
"and reviewable but its path is not review-gated.",
|
|
212
|
+
"",
|
|
213
|
+
"## If something fails later",
|
|
214
|
+
"",
|
|
215
|
+
"Bridge refuses to dispatch a merge when it cannot detect the workflow, or when",
|
|
216
|
+
"the workflow's contract version is not the one Bridge understands. Escalate",
|
|
217
|
+
"both to a repository administrator — they are the only person who can check",
|
|
218
|
+
"that the workflow is present on the default branch, that Actions is enabled",
|
|
219
|
+
"for this repository, and that the file has not been hand-edited. Re-running",
|
|
220
|
+
"the installer reports which of the four states the file is in and never",
|
|
221
|
+
"overwrites a file it did not write.",
|
|
222
|
+
"",
|
|
223
|
+
].join("\n");
|
|
224
|
+
}
|