@bridge_gpt/mcp-server 0.2.59 → 0.2.62

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.
Files changed (78) hide show
  1. package/README.md +181 -51
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/commands.generated.js +6 -6
  5. package/build/conductor/bridge-api-client.js +406 -4
  6. package/build/conductor/doctor.js +255 -53
  7. package/build/conductor/epic-runtime.js +11 -2
  8. package/build/conductor/install-doctor.js +24 -2
  9. package/build/conductor/local-merge.js +319 -26
  10. package/build/conductor/merge-execution-contract.js +86 -0
  11. package/build/conductor/merge-execution-eligibility.js +130 -0
  12. package/build/conductor/readiness-sections.js +256 -16
  13. package/build/conductor/readiness.js +203 -29
  14. package/build/conductor/recovery-operations.js +8 -2
  15. package/build/connect-bitbucket-api.js +6 -1
  16. package/build/connect-bitbucket.js +398 -88
  17. package/build/credential-store.js +172 -0
  18. package/build/docs.generated.js +2 -2
  19. package/build/doctor.js +277 -15
  20. package/build/drive-epic.js +147 -12
  21. package/build/epic-implementer-bin.js +15 -15
  22. package/build/epic-implementer-claude-hook-bin.js +1 -1
  23. package/build/epic-implementer-env.js +43 -14
  24. package/build/epic-integration-pr.js +61 -15
  25. package/build/epic-launch-profile.js +455 -0
  26. package/build/executor/ci-rerun-job.js +269 -0
  27. package/build/executor/cli.js +40 -0
  28. package/build/executor/credentials.js +28 -1
  29. package/build/executor/env.js +60 -0
  30. package/build/executor/http-client.js +45 -1
  31. package/build/executor/index.js +21 -4
  32. package/build/executor/integration-job.js +759 -0
  33. package/build/executor/job-runner.js +218 -9
  34. package/build/executor/job-types.js +12 -2
  35. package/build/executor/merge-job.js +11 -2
  36. package/build/executor/observation.js +114 -7
  37. package/build/executor/runner.js +101 -1
  38. package/build/executor/service-lifecycle.js +136 -0
  39. package/build/executor/service-unit.js +427 -9
  40. package/build/executor/types.js +48 -0
  41. package/build/index.js +479 -240
  42. package/build/install-bridge-conductor.js +599 -14
  43. package/build/install-bridge.js +448 -46
  44. package/build/mcp-install-state.js +42 -4
  45. package/build/merge-pull-request.js +103 -10
  46. package/build/merge-workflow-codeowners.js +224 -0
  47. package/build/merge-workflow-config.js +187 -0
  48. package/build/merge-workflow-drift.js +97 -0
  49. package/build/merge-workflow-install.js +638 -0
  50. package/build/merge-workflow.js +956 -0
  51. package/build/pipelines.generated.js +3 -3
  52. package/build/plane/cli.js +24 -2
  53. package/build/plane/preflight.js +68 -0
  54. package/build/plane/supervisor.js +6 -0
  55. package/build/readme.generated.js +1 -1
  56. package/build/run-unit-tests-launcher.js +5 -0
  57. package/build/setup-epic.js +263 -38
  58. package/build/version.generated.js +3 -3
  59. package/package.json +3 -3
  60. package/build/conduct-epic/bridge-client.js +0 -773
  61. package/build/conduct-epic/checkpoint-store.js +0 -542
  62. package/build/conduct-epic/cli.js +0 -3145
  63. package/build/conduct-epic/cut-protocol.js +0 -392
  64. package/build/conduct-epic/lock.js +0 -302
  65. package/build/conduct-epic/pr-state.js +0 -286
  66. package/build/conduct-epic/spawn.js +0 -113
  67. package/build/conductor/cli.js +0 -1278
  68. package/build/conductor-bundle-cli.js +0 -256
  69. package/build/direct-ticket-tools.js +0 -70
  70. package/build/executor/reconcile-overlap-governance.js +0 -129
  71. package/build/implement-epic/bridge-client.js +0 -773
  72. package/build/implement-epic/checkpoint-store.js +0 -542
  73. package/build/implement-epic/cli.js +0 -3158
  74. package/build/implement-epic/cut-protocol.js +0 -392
  75. package/build/implement-epic/lock.js +0 -302
  76. package/build/implement-epic/pr-state.js +0 -286
  77. package/build/implement-epic/spawn.js +0 -113
  78. package/build/plan-epic-conductor-eligibility.js +0 -213
@@ -0,0 +1,956 @@
1
+ /**
2
+ * The Epic Implementer merge-workflow renderer (BAPI-1136, A2 slice 4).
3
+ *
4
+ * This module renders the ONE GitHub Actions workflow that performs the
5
+ * child-pull-request merge on the customer's own infrastructure, with the
6
+ * ephemeral `GITHUB_TOKEN` supplying the `contents: write` scope Bridge
7
+ * deliberately does not hold. It is the customer-side WRITE ACTOR of the A2
8
+ * "infra floors & write authority" epic.
9
+ *
10
+ * ## Why a TypeScript renderer and not a bundled YAML asset
11
+ *
12
+ * The ticket text asked for a template asset copied by
13
+ * `mcp_server/scripts/bundle-assets.js`. The review rejected that in favour of
14
+ * the repository's own established precedent (decision E-19):
15
+ * `claude-review-workflow.ts` already renders a workflow as compiled TypeScript,
16
+ * `tsc` already ships it in the published package, and a second mechanism for a
17
+ * single template would add a `*.generated.ts` that must never be hand-edited
18
+ * for no benefit. So this file is the workflow's ONLY byte source — nothing in
19
+ * `bundle-assets.js` changes.
20
+ *
21
+ * ## Why the renderer takes no parameters
22
+ *
23
+ * {@link renderEpicImplementerMergeWorkflow} is input-free and deterministic.
24
+ * The claude-review renderer interpolates org-specific literals because its
25
+ * template genuinely varies per customer; this one does not. Every
26
+ * customer-specific value the merge actuator needs — the Bridge API key, the
27
+ * base URL, the attempt's action key and lease — arrives at RUN time through a
28
+ * repository secret, a repository variable, or a dispatch input. If a customer
29
+ * value could alter the canonical bytes, then "the workflow on the default
30
+ * branch" would no longer be one reviewable artifact with one identity, and the
31
+ * byte-pin that makes drift detectable would be meaningless.
32
+ *
33
+ * ## The security shape, in one paragraph
34
+ *
35
+ * `workflow_dispatch` runs the workflow definition from the TRUSTED DEFAULT
36
+ * BRANCH, so a pull request that edits this file cannot change the version that
37
+ * actually runs against it. The two dispatch inputs are a wake-up and a lease
38
+ * correlator; they authorize nothing. Authorization is the durable, SHA-bound
39
+ * merge command the job fetches from Bridge, presenting the repository's own
40
+ * API-key secret and the per-attempt lease secret. Nothing is ever checked out,
41
+ * no third-party action runs, the merge carries an expected-head-SHA
42
+ * precondition, a fork-origin head is refused, and a base that is the repository
43
+ * default branch is refused outright.
44
+ *
45
+ * ## Two jobs, one managed file (contract version 2, BAPI-1259)
46
+ *
47
+ * The `operation` dispatch input selects exactly one job. `child_merge` (the
48
+ * default, and the only value Bridge's child-merge dispatch ever implies) runs
49
+ * the unchanged `merge` job. `integration_action` runs the `integration` job,
50
+ * which performs ONE server-issued integration action — merging the base into
51
+ * the exact run-bound epic branch (`sync_base`), or taking the ONE matching
52
+ * integration pull request out of draft (`mark_ready`) — fetched from Bridge's
53
+ * dedicated integration-action contract, never from the merge-command contract.
54
+ * The integration job has no merge-pull-request step at all: no path in this
55
+ * file merges an integration pull request into the default branch.
56
+ *
57
+ * ## Secret discipline
58
+ *
59
+ * The rendered script never enables shell tracing, never echoes either dispatch
60
+ * input, never prints a response body, and never writes a credential or the
61
+ * lease to a workflow output, a step summary, or `$GITHUB_ENV`. The API key and
62
+ * the lease reach `curl` through a `umask 077` config file and a request body
63
+ * that `jq` builds FROM THE ENVIRONMENT, so neither is ever an argv element
64
+ * visible to another process in the runner; both files are removed by an `EXIT`
65
+ * trap. `gh` authenticates from `GH_TOKEN` in the environment for the same
66
+ * reason.
67
+ */
68
+ import { MERGE_WORKFLOW_EXPECTED_VERSION, MERGE_WORKFLOW_PATH, MERGE_WORKFLOW_VERSION_MARKER_KEY, } from "./conductor/merge-execution-contract.js";
69
+ import { CHILD_MERGE_WORKFLOW_OPERATION, INTEGRATION_ACTION_FETCH_PATH, INTEGRATION_ACTION_PROTOCOL_VERSION, INTEGRATION_ACTION_REPORT_PATH, INTEGRATION_ACTION_WORKFLOW_OPERATION, MERGE_COMMAND_FETCH_PATH, MERGE_WORKFLOW_API_KEY_SECRET_NAME, MERGE_WORKFLOW_BASE_URL_VARIABLE_NAME, MERGE_WORKFLOW_DEFAULT_BASE_URL, } from "./merge-workflow-config.js";
70
+ /**
71
+ * Every refusal the rendered workflow can emit, as a closed vocabulary.
72
+ *
73
+ * A refusal is always one of these tokens — never raw `curl`, `jq`, or `gh`
74
+ * output, and never an HTTP body. Raw subprocess text is unbounded and can echo
75
+ * a URL, a header, or provider detail into a log that is world-readable on a
76
+ * public repository.
77
+ *
78
+ * Exported so the static safety tests can assert the rendered template mentions
79
+ * exactly these and nothing else.
80
+ */
81
+ export const MERGE_WORKFLOW_REFUSAL_REASONS = [
82
+ /** The repository-level Bridge API key secret is absent or empty. */
83
+ "bridge_api_key_secret_missing",
84
+ /** The durable merge command could not be fetched (includes the 404 the absent sibling child returns). */
85
+ "merge_command_unavailable",
86
+ /** The command was fetched but is unusable: missing fields, or an execution mode this actuator does not perform. */
87
+ "merge_command_rejected",
88
+ /** The command names a repository or action key that is not this workflow's live context. */
89
+ "repository_identity_mismatch",
90
+ /** The repository default branch could not be read, so the base guard cannot be applied. */
91
+ "default_branch_unresolved",
92
+ /** The pull request could not be read from live provider state. */
93
+ "pr_read_failed",
94
+ /** The pull request is not open. */
95
+ "pr_not_open",
96
+ /** The head is a fork or an otherwise unverifiable cross-repository ref. */
97
+ "fork_origin_denied",
98
+ /** The pull request targets the repository default branch. Never retried. */
99
+ "base_is_default_branch",
100
+ /** The live head moved away from the command's expected head SHA. Never retried. */
101
+ "head_sha_drift",
102
+ /** `gh pr merge` failed and the read-back did not show the merge landed. */
103
+ "gh_merge_failed",
104
+ /** The merge attempt's outcome could not be classified from the read-back. */
105
+ "merge_outcome_ambiguous",
106
+ /** The merge landed but its completion report was not accepted; the reconciler recovers. */
107
+ "completion_unreported",
108
+ ];
109
+ /**
110
+ * Every refusal the rendered `integration` job can emit (BAPI-1259), as a
111
+ * closed vocabulary. Kept separate from {@link MERGE_WORKFLOW_REFUSAL_REASONS}
112
+ * so neither job can emit the other's refusal by accident.
113
+ */
114
+ export const INTEGRATION_WORKFLOW_REFUSAL_REASONS = [
115
+ /** The repository-level Bridge API key secret is absent or empty. */
116
+ "bridge_api_key_secret_missing",
117
+ /** Bridge did not return the integration action under this dispatch's lease. */
118
+ "integration_action_unavailable",
119
+ /** The fetched action is unusable here: wrong version, actor, operation, or shape. */
120
+ "integration_action_rejected",
121
+ /** The action names a repository or action id that is not this workflow's live context. */
122
+ "repository_identity_mismatch",
123
+ /** The repository default branch could not be read, so the branch guard cannot be applied. */
124
+ "default_branch_unresolved",
125
+ /** The action's epic branch IS the repository default branch. Never retried. */
126
+ "epic_branch_is_default_branch",
127
+ ];
128
+ /**
129
+ * The closed outcome tokens the `integration` job may report. Mirrors the
130
+ * server's `INTEGRATION_ACTION_OUTCOMES`; the job reports FACTS only.
131
+ */
132
+ export const INTEGRATION_WORKFLOW_REPORTED_OUTCOMES = [
133
+ "applied",
134
+ "already_satisfied",
135
+ "conflict",
136
+ "authority_unavailable",
137
+ "authority_lost",
138
+ "head_drift",
139
+ "pr_ambiguous",
140
+ "indeterminate",
141
+ "failed",
142
+ ];
143
+ /**
144
+ * Build a GitHub Actions expression without writing `${` in TypeScript source.
145
+ *
146
+ * The rendered lines below are ordinary double-quoted strings, not template
147
+ * literals, so this is belt-and-braces: it keeps every expression marker in one
148
+ * place and makes a grep for "which expressions does this template evaluate"
149
+ * answerable.
150
+ */
151
+ const gh = (expr) => "${{ " + expr + " }}";
152
+ const SECRET_EXPR = gh(`secrets.${MERGE_WORKFLOW_API_KEY_SECRET_NAME}`);
153
+ const BASE_URL_EXPR = gh(`vars.${MERGE_WORKFLOW_BASE_URL_VARIABLE_NAME} || '${MERGE_WORKFLOW_DEFAULT_BASE_URL}'`);
154
+ /**
155
+ * Render the canonical merge workflow.
156
+ *
157
+ * Deterministic and input-free: two calls in the same build return byte-identical
158
+ * output, and no customer value can alter a single byte. The result is
159
+ * newline-terminated.
160
+ */
161
+ export function renderEpicImplementerMergeWorkflow() {
162
+ const lines = [
163
+ "# ===========================================================================",
164
+ "# Epic Implementer merge actuator — MANAGED FILE",
165
+ "#",
166
+ "# Generated by Bridge (`" + MERGE_WORKFLOW_PATH + "`). Do not hand-edit: the",
167
+ "# installer compares this file byte for byte against the version it ships and",
168
+ "# refuses to touch a modified or unrecognized copy.",
169
+ "#",
170
+ "# WHAT THIS WORKFLOW IS",
171
+ "#",
172
+ "# It performs the GitHub merge for an Epic Implementer child pull request that",
173
+ "# Bridge has already approved. Bridge's own GitHub App installation is",
174
+ "# `contents: read` and cannot merge; the ephemeral GITHUB_TOKEN issued to this",
175
+ "# job can. That is the entire reason this file exists, and it is why the file",
176
+ "# is worth a careful human review before it is merged.",
177
+ "#",
178
+ "# TRUSTED-BRANCH EXECUTION",
179
+ "#",
180
+ "# `workflow_dispatch` ALWAYS runs the workflow definition that is on the",
181
+ "# repository's DEFAULT BRANCH. A pull request that edits this file therefore",
182
+ "# cannot change the version of this workflow that runs against it — the copy",
183
+ "# under review is inert until it is merged into the default branch. Protect",
184
+ "# this path with a code-owner review rule so that merge is a deliberate act.",
185
+ "#",
186
+ "# THE TWO DISPATCH INPUTS AUTHORIZE NOTHING",
187
+ "#",
188
+ "# `action_key` is a wake-up: it names which approved attempt this run is for.",
189
+ "# `lease_owner` is lease-correlation data: it proves to Bridge that this run is",
190
+ "# the live attempt. Neither grants permission to merge anything, neither is",
191
+ "# trusted as the source of what to merge, and neither is ever echoed to the",
192
+ "# log, a step output, a job summary, or $GITHUB_ENV. Everything this job acts",
193
+ "# on comes from the durable, SHA-bound merge command it fetches from Bridge and",
194
+ "# then re-validates against live provider state.",
195
+ "#",
196
+ "# WHAT IT NEVER DOES",
197
+ "#",
198
+ "# * It never checks out the repository, and never runs pull-request content.",
199
+ "# * It uses no `uses:` step and no third-party action — only runner-provided",
200
+ "# bash, curl, jq, and the GitHub CLI.",
201
+ "# * It never merges into the repository default branch.",
202
+ "# * It never merges a fork-origin or otherwise unverifiable head.",
203
+ "# * It never merges a head that has moved from the approved SHA.",
204
+ "#",
205
+ "# THE INTEGRATION JOB (contract version 2)",
206
+ "#",
207
+ "# Dispatched with `operation: integration_action`, the `integration` job runs",
208
+ "# instead of `merge`. It performs one integration action Bridge issued for an",
209
+ "# epic run: merge the base into the run's exact `epic/` branch, or take that",
210
+ "# epic's ONE integration pull request out of draft. It fetches the action from",
211
+ "# Bridge's dedicated integration-action contract, re-validates the lease and",
212
+ "# the epic head immediately before writing, and reports only facts. It never",
213
+ "# merges a pull request, never writes the default branch, and never force-pushes.",
214
+ "#",
215
+ "# A push or a ready-for-review event made with this job's GITHUB_TOKEN does not",
216
+ "# start other workflows (GitHub's recursion guard). If your required checks",
217
+ "# must re-run after a base synchronization, prefer a local or machine-token",
218
+ "# executor for integration actions; otherwise Bridge blocks the run with a",
219
+ "# specific reason once its final-observation window lapses.",
220
+ "#",
221
+ "# RESIDUAL RACE (base retargeting)",
222
+ "#",
223
+ "# Base selection is not atomic with the provider merge. This job validates the",
224
+ "# base before merging, and Bridge's completion path re-reads the pull request",
225
+ "# and re-applies the same base guard afterwards. A base retargeted inside that",
226
+ "# window is detected on the completion side and refused there, which leaves the",
227
+ "# attempt recorded as refused rather than silently accepted. Recovering from a",
228
+ "# detected retarget race is a repository-administrator action, not a retry.",
229
+ "# ===========================================================================",
230
+ "",
231
+ "name: Epic Implementer merge",
232
+ "",
233
+ "# The machine-readable contract version. Bridge's eligibility probe requires",
234
+ "# EXACT equality with the version it understands; a newer file is a workflow it",
235
+ "# has never been taught to reason about and is refused rather than dispatched.",
236
+ "#",
237
+ "# WHY IT LIVES UNDER `env:` AND NOT AT TOP LEVEL. GitHub Actions validates a",
238
+ "# workflow against a CLOSED top-level schema — name, run-name, on, permissions,",
239
+ "# env, defaults, concurrency, jobs. An unknown top-level key is not a tolerated",
240
+ "# extension: it makes the whole file refuse to load with \"Invalid workflow",
241
+ "# file\", which would leave a permanently red annotation on the customer's",
242
+ "# default branch AND never register the `workflow_dispatch` trigger, so the",
243
+ "# actuator could not perform a single merge. The failure would also be QUIET:",
244
+ "# Bridge parses this file with PyYAML, which reads an unknown top-level key",
245
+ "# happily, so the eligibility probe would report the repository ready and the",
246
+ "# breakage would surface only as a dispatch error. Bridge's own",
247
+ "# `_resolve_version_marker` reads the marker from the top level OR from `env`,",
248
+ "# and declaring it in exactly one of them — this one — is both schema-legal and",
249
+ "# unambiguous. Declaring it in BOTH is what makes a document unreadable.",
250
+ "env:",
251
+ " " + MERGE_WORKFLOW_VERSION_MARKER_KEY + ": " + String(MERGE_WORKFLOW_EXPECTED_VERSION),
252
+ "",
253
+ "on:",
254
+ " workflow_dispatch:",
255
+ " inputs:",
256
+ " action_key:",
257
+ " description: >-",
258
+ " Durable merge-action key naming the approved attempt. A wake-up and a",
259
+ " correlator only; it authorizes nothing by itself.",
260
+ " required: true",
261
+ " type: string",
262
+ " lease_owner:",
263
+ " description: >-",
264
+ " Per-attempt lease secret proving this run is the live attempt. Never",
265
+ " printed, never echoed, never written to an output.",
266
+ " required: true",
267
+ " type: string",
268
+ " operation:",
269
+ " description: >-",
270
+ " Which job runs. `" + CHILD_MERGE_WORKFLOW_OPERATION + "` (the default) merges an approved",
271
+ " child pull request; `" + INTEGRATION_ACTION_WORKFLOW_OPERATION + "` performs one integration",
272
+ " action. Selects a job only; the work itself is fetched from Bridge.",
273
+ " required: false",
274
+ " default: " + CHILD_MERGE_WORKFLOW_OPERATION,
275
+ " type: string",
276
+ "",
277
+ "# Exactly the two scopes the merge needs, granted once at workflow level. No",
278
+ "# job overrides these, and no other permission is requested.",
279
+ "permissions:",
280
+ " contents: write",
281
+ " pull-requests: write",
282
+ "",
283
+ "# Grouped on the non-secret action key so two dispatches for the SAME attempt",
284
+ "# queue instead of racing. `cancel-in-progress: false` because cancelling a run",
285
+ "# that may already have merged would destroy the evidence. This is a courtesy,",
286
+ "# not a control: Bridge's single-use lease remains the authoritative guard",
287
+ "# against a second actuation.",
288
+ "concurrency:",
289
+ " group: epic-implementer-merge-" + gh("inputs.action_key"),
290
+ " cancel-in-progress: false",
291
+ "",
292
+ "jobs:",
293
+ " merge:",
294
+ " name: Merge the approved Epic Implementer pull request",
295
+ // A job-level `if:` is an expression without the `${{ }}` wrapper.
296
+ " if: inputs.operation == '" + CHILD_MERGE_WORKFLOW_OPERATION + "'",
297
+ " runs-on: ubuntu-latest",
298
+ " timeout-minutes: 10",
299
+ " env:",
300
+ " BAPI_BASE_URL: " + BASE_URL_EXPR,
301
+ " BAPI_API_KEY: " + SECRET_EXPR,
302
+ " ACTION_KEY: " + gh("inputs.action_key"),
303
+ " LEASE_OWNER: " + gh("inputs.lease_owner"),
304
+ " REPO_FULL_NAME: " + gh("github.repository"),
305
+ " GH_TOKEN: " + gh("github.token"),
306
+ " GH_PROMPT_DISABLED: '1'",
307
+ " GH_NO_UPDATE_NOTIFIER: '1'",
308
+ " steps:",
309
+ " # ------------------------------------------------------------------",
310
+ " # 1. Fail before anything else when the repository secret is absent.",
311
+ " # Reported as a named reason so an operator sees the fix, not a",
312
+ " # 404 from an unauthenticated call three steps later.",
313
+ " # ------------------------------------------------------------------",
314
+ " - name: Require the Bridge API key secret",
315
+ " shell: bash",
316
+ " run: |",
317
+ " set -uo pipefail",
318
+ " set +x",
319
+ ' if [ -z "${BAPI_API_KEY:-}" ]; then',
320
+ ' echo "REFUSAL_REASON=bridge_api_key_secret_missing" >> "$GITHUB_ENV"',
321
+ ' echo "::error::Repository secret ' +
322
+ MERGE_WORKFLOW_API_KEY_SECRET_NAME +
323
+ ' is not set. Create it with a Bridge API key for this repository."',
324
+ " exit 1",
325
+ " fi",
326
+ "",
327
+ " # ------------------------------------------------------------------",
328
+ " # 2. Fetch the durable, SHA-bound merge command. This is the ONLY",
329
+ " # authorization in the whole job. The API key reaches curl through a",
330
+ " # 0600 config file and the lease through a jq-built body, so neither",
331
+ " # is ever an argv element; both files die with the step.",
332
+ " #",
333
+ " # A 404 here is the EXPECTED state until the Bridge-side durable",
334
+ " # command store ships. The job refuses without retrying and without",
335
+ " # touching the pull request, so this actuator is inert rather than",
336
+ " # dangerous while that sibling is outstanding.",
337
+ " # ------------------------------------------------------------------",
338
+ " - name: Fetch the durable merge command",
339
+ " shell: bash",
340
+ " run: |",
341
+ " set -uo pipefail",
342
+ " set +x",
343
+ " umask 077",
344
+ ' curlrc="${RUNNER_TEMP}/bapi-merge.curlrc"',
345
+ ' reqbody="${RUNNER_TEMP}/bapi-merge-request.json"',
346
+ ' respbody="${RUNNER_TEMP}/bapi-merge-command.json"',
347
+ " trap 'rm -f \"$curlrc\" \"$reqbody\" \"$respbody\"' EXIT",
348
+ " # Both reads are from the ENVIRONMENT, never from argv or a here-string.",
349
+ " jq -rn 'env.BAPI_API_KEY | \"header = \\\"X-API-Key: \\(.)\\\"\"' > \"$curlrc\"",
350
+ " jq -n '{repo_name: (env.ACTION_KEY | split(\":\") | .[1] // \"\"), action_key: env.ACTION_KEY, lease_owner: env.LEASE_OWNER}' > \"$reqbody\"",
351
+ " # --retry 0: every refusal this endpoint returns is deterministic, and",
352
+ " # retrying a lease-bound POST risks a second billable attempt.",
353
+ " code=\"$(curl --silent --show-error --no-progress-meter --retry 0 \\",
354
+ " --max-time 30 \\",
355
+ ' --config "$curlrc" \\',
356
+ " --header 'Content-Type: application/json' \\",
357
+ " --request POST \\",
358
+ ' --data-binary "@$reqbody" \\',
359
+ ' --output "$respbody" \\',
360
+ " --write-out '%{http_code}' \\",
361
+ ' "${BAPI_BASE_URL%/}' +
362
+ MERGE_COMMAND_FETCH_PATH +
363
+ '" || echo "000")"',
364
+ ' if [ "$code" != "200" ]; then',
365
+ ' echo "REFUSAL_REASON=merge_command_unavailable" >> "$GITHUB_ENV"',
366
+ ' echo "::error::Bridge did not return a durable merge command (HTTP $code)."',
367
+ " exit 1",
368
+ " fi",
369
+ " # Only non-secret projection fields are lifted into the job env. The",
370
+ " # response body itself is never printed and never survives this step.",
371
+ ' cmd_action_key="$(jq -r \'.action_key // ""\' "$respbody")"',
372
+ ' cmd_repo_name="$(jq -r \'.repo_name // ""\' "$respbody")"',
373
+ ' cmd_repository="$(jq -r \'.github_repository // ""\' "$respbody")"',
374
+ ' lease_expires_at="$(jq -r \'.lease_expires_at // ""\' "$respbody")"',
375
+ ' pr_number="$(jq -r \'.pr_number // ""\' "$respbody")"',
376
+ ' expected_head_sha="$(jq -r \'.expected_head_sha // ""\' "$respbody")"',
377
+ ' merge_method="$(jq -r \'.merge_method // ""\' "$respbody")"',
378
+ ' execution="$(jq -r \'.execution // ""\' "$respbody")"',
379
+ ' case "$merge_method" in',
380
+ " squash|merge|rebase) ;;",
381
+ " *)",
382
+ ' echo "REFUSAL_REASON=merge_command_rejected" >> "$GITHUB_ENV"',
383
+ ' echo "::error::The merge command names an unsupported merge method."',
384
+ " exit 1",
385
+ " ;;",
386
+ " esac",
387
+ ' if [ "$execution" != "github_actions" ]; then',
388
+ ' echo "REFUSAL_REASON=merge_command_rejected" >> "$GITHUB_ENV"',
389
+ ' echo "::error::The merge command was not issued for GitHub Actions execution."',
390
+ " exit 1",
391
+ " fi",
392
+ ' case "$pr_number" in',
393
+ " ''|*[!0-9]*)",
394
+ ' echo "REFUSAL_REASON=merge_command_rejected" >> "$GITHUB_ENV"',
395
+ ' echo "::error::The merge command carries no usable pull request number."',
396
+ " exit 1",
397
+ " ;;",
398
+ " esac",
399
+ ' if [ "${#expected_head_sha}" -ne 40 ]; then',
400
+ ' echo "REFUSAL_REASON=merge_command_rejected" >> "$GITHUB_ENV"',
401
+ ' echo "::error::The merge command carries no usable expected head SHA."',
402
+ " exit 1",
403
+ " fi",
404
+ " # An already-expired lease cannot be closed, so merging on it would",
405
+ " # perform a real write that no completion report could record. Refuse",
406
+ " # before the first provider read rather than after the merge.",
407
+ ' if [ -n "$lease_expires_at" ]; then',
408
+ ' expiry_epoch="$(date -u -d "$lease_expires_at" +%s 2>/dev/null || echo "")"',
409
+ ' if [ -n "$expiry_epoch" ] && [ "$expiry_epoch" -le "$(date -u +%s)" ]; then',
410
+ ' echo "REFUSAL_REASON=merge_command_rejected" >> "$GITHUB_ENV"',
411
+ ' echo "::error::The merge command lease had already expired when this run started."',
412
+ " exit 1",
413
+ " fi",
414
+ " fi",
415
+ " # The live workflow context is authoritative. A command minted for a",
416
+ " # different repository, or for a different attempt, is refused here —",
417
+ " # before a single provider read is spent on it. Both repository",
418
+ " # identities are checked: the provider one against github.repository,",
419
+ " # and Bridge's own against the name carried inside the action key, so a",
420
+ " # command whose two namespaces disagree cannot actuate either.",
421
+ ' key_repo_name="$(jq -rn \'env.ACTION_KEY | split(":") | .[1] // ""\')"',
422
+ ' if [ "$cmd_repository" != "$REPO_FULL_NAME" ] \\',
423
+ ' || [ "$cmd_action_key" != "$ACTION_KEY" ] \\',
424
+ ' || [ "$cmd_repo_name" != "$key_repo_name" ]; then',
425
+ ' echo "REFUSAL_REASON=repository_identity_mismatch" >> "$GITHUB_ENV"',
426
+ ' echo "::error::The merge command does not match this repository and attempt."',
427
+ " exit 1",
428
+ " fi",
429
+ ' {',
430
+ ' echo "PR_NUMBER=$pr_number"',
431
+ ' echo "EXPECTED_HEAD_SHA=$expected_head_sha"',
432
+ ' echo "MERGE_METHOD=$merge_method"',
433
+ ' } >> "$GITHUB_ENV"',
434
+ "",
435
+ " # ------------------------------------------------------------------",
436
+ " # 3. Read live provider state and apply every guard, in the order a",
437
+ " # cheap deterministic refusal comes before an expensive one. No ref",
438
+ " # is checked out at any point.",
439
+ " # ------------------------------------------------------------------",
440
+ " - name: Resolve the repository default branch",
441
+ " shell: bash",
442
+ " run: |",
443
+ " set -uo pipefail",
444
+ " set +x",
445
+ ' default_branch="$(gh api "repos/$REPO_FULL_NAME" --jq \'.default_branch // ""\' 2>/dev/null || echo "")"',
446
+ ' if [ -z "$default_branch" ]; then',
447
+ ' echo "REFUSAL_REASON=default_branch_unresolved" >> "$GITHUB_ENV"',
448
+ ' echo "::error::The repository default branch could not be read, so the base guard cannot be applied."',
449
+ " exit 1",
450
+ " fi",
451
+ ' echo "DEFAULT_BRANCH=$default_branch" >> "$GITHUB_ENV"',
452
+ "",
453
+ " - name: Guard the pull request before merging",
454
+ " shell: bash",
455
+ " run: |",
456
+ " set -uo pipefail",
457
+ " set +x",
458
+ " pr_json=\"$(gh pr view \"$PR_NUMBER\" --repo \"$REPO_FULL_NAME\" \\",
459
+ " --json state,baseRefName,headRefOid,isCrossRepository,headRepositoryOwner,headRepository \\",
460
+ ' 2>/dev/null || echo "")"',
461
+ ' if [ -z "$pr_json" ]; then',
462
+ ' echo "REFUSAL_REASON=pr_read_failed" >> "$GITHUB_ENV"',
463
+ ' echo "::error::Pull request #$PR_NUMBER could not be read."',
464
+ " exit 1",
465
+ " fi",
466
+ ' state="$(printf \'%s\' "$pr_json" | jq -r \'.state // ""\')"',
467
+ ' base_ref="$(printf \'%s\' "$pr_json" | jq -r \'.baseRefName // ""\')"',
468
+ ' head_sha="$(printf \'%s\' "$pr_json" | jq -r \'.headRefOid // ""\')"',
469
+ // BAPI-1257: NOT `.isCrossRepository // true`. jq's `//` yields its
470
+ // right-hand side when the left is null OR FALSE, so that spelling mapped a
471
+ // legitimate `false` (a same-repository pull request — every epic child)
472
+ // onto `true` and refused the merge as fork-origin. The lane could not merge
473
+ // anything. An explicit null test keeps the intended fail-closed default for
474
+ // an ABSENT field while reading a present `false` as itself.
475
+ ' cross_repo="$(printf \'%s\' "$pr_json" | jq -r \'if .isCrossRepository == null then true else .isCrossRepository end\')"',
476
+ ' head_repo="$(printf \'%s\' "$pr_json" | jq -r \'((.headRepositoryOwner.login // "") + "/" + (.headRepository.name // ""))\')"',
477
+ ' if [ "$state" != "OPEN" ]; then',
478
+ ' echo "REFUSAL_REASON=pr_not_open" >> "$GITHUB_ENV"',
479
+ ' echo "::error::Pull request #$PR_NUMBER is not open."',
480
+ " exit 1",
481
+ " fi",
482
+ " # A fork head, or a head whose repository cannot be proven to be this",
483
+ " # one, is refused. `isCrossRepository` defaults to true above so an",
484
+ " # absent field fails closed rather than reading as same-repo.",
485
+ ' if [ "$cross_repo" != "false" ] || [ "$head_repo" != "$REPO_FULL_NAME" ]; then',
486
+ ' echo "REFUSAL_REASON=fork_origin_denied" >> "$GITHUB_ENV"',
487
+ ' echo "::error::Pull request #$PR_NUMBER has a fork-origin or unverifiable head."',
488
+ " exit 1",
489
+ " fi",
490
+ " # The epic's AC-5 hard denial. Never retried: a default-branch base is",
491
+ " # a decision that has to change, not a condition that can clear.",
492
+ ' if [ "$base_ref" = "$DEFAULT_BRANCH" ]; then',
493
+ ' echo "REFUSAL_REASON=base_is_default_branch" >> "$GITHUB_ENV"',
494
+ ' echo "::error::Pull request #$PR_NUMBER targets the repository default branch; this actuator never merges into it."',
495
+ " exit 1",
496
+ " fi",
497
+ " # Head drift. Also never retried — the approval was bound to a SHA.",
498
+ ' if [ "$head_sha" != "$EXPECTED_HEAD_SHA" ]; then',
499
+ ' echo "REFUSAL_REASON=head_sha_drift" >> "$GITHUB_ENV"',
500
+ ' echo "::error::Pull request #$PR_NUMBER moved away from the approved head commit."',
501
+ " exit 1",
502
+ " fi",
503
+ "",
504
+ " # ------------------------------------------------------------------",
505
+ " # 4. The one privileged write. `--match-head-commit` re-asserts the",
506
+ " # expected head at the provider, so the guard above and the merge",
507
+ " # itself cannot disagree across the gap between them. The token",
508
+ " # reaches gh through GH_TOKEN in the environment, never argv.",
509
+ " # ------------------------------------------------------------------",
510
+ " - name: Merge the pull request",
511
+ " shell: bash",
512
+ " run: |",
513
+ " set -uo pipefail",
514
+ " set +x",
515
+ " if gh pr merge \"$PR_NUMBER\" --repo \"$REPO_FULL_NAME\" \\",
516
+ ' "--$MERGE_METHOD" --match-head-commit "$EXPECTED_HEAD_SHA" >/dev/null 2>&1; then',
517
+ ' echo "MERGE_ATTEMPT=accepted" >> "$GITHUB_ENV"',
518
+ " else",
519
+ ' echo "MERGE_ATTEMPT=rejected" >> "$GITHUB_ENV"',
520
+ " fi",
521
+ "",
522
+ " # ------------------------------------------------------------------",
523
+ " # 5. Classify from a FRESH read rather than from the merge command's own",
524
+ " # exit status. A slow-but-successful merge that gh reported as a",
525
+ " # failure would otherwise be recorded as a failure and invite a",
526
+ " # second attempt at a pull request that is already merged.",
527
+ " # ------------------------------------------------------------------",
528
+ " - name: Classify the merge outcome",
529
+ " shell: bash",
530
+ " run: |",
531
+ " set -uo pipefail",
532
+ " set +x",
533
+ " pr_json=\"$(gh pr view \"$PR_NUMBER\" --repo \"$REPO_FULL_NAME\" \\",
534
+ ' --json state,mergeCommit,headRefOid 2>/dev/null || echo "")"',
535
+ ' if [ -z "$pr_json" ]; then',
536
+ ' echo "REFUSAL_REASON=merge_outcome_ambiguous" >> "$GITHUB_ENV"',
537
+ ' echo "::error::Pull request #$PR_NUMBER could not be re-read after the merge attempt."',
538
+ " exit 1",
539
+ " fi",
540
+ ' state="$(printf \'%s\' "$pr_json" | jq -r \'.state // ""\')"',
541
+ ' merge_sha="$(printf \'%s\' "$pr_json" | jq -r \'.mergeCommit.oid // ""\')"',
542
+ ' if [ "$state" = "MERGED" ]; then',
543
+ ' {',
544
+ ' echo "MERGE_RESULT=merged"',
545
+ ' echo "MERGE_SHA=$merge_sha"',
546
+ ' } >> "$GITHUB_ENV"',
547
+ " exit 0",
548
+ " fi",
549
+ ' if [ "${MERGE_ATTEMPT:-}" = "rejected" ]; then',
550
+ ' echo "REFUSAL_REASON=gh_merge_failed" >> "$GITHUB_ENV"',
551
+ ' echo "::error::The merge was refused by GitHub and pull request #$PR_NUMBER is still open."',
552
+ " exit 1",
553
+ " fi",
554
+ ' echo "REFUSAL_REASON=merge_outcome_ambiguous" >> "$GITHUB_ENV"',
555
+ ' echo "::error::The merge attempt for pull request #$PR_NUMBER could not be classified."',
556
+ " exit 1",
557
+ "",
558
+ " # ------------------------------------------------------------------",
559
+ " # 6. Report the final result. This runs only AFTER the merge/read-back",
560
+ " # sequence — it reports what happened and decides nothing. The lease",
561
+ " # secret travels in the completion contract's own `lease_owner` field",
562
+ " # and appears nowhere else.",
563
+ " # ------------------------------------------------------------------",
564
+ " - name: Report the merge result to Bridge",
565
+ " shell: bash",
566
+ " run: |",
567
+ " set -uo pipefail",
568
+ " set +x",
569
+ " umask 077",
570
+ ' curlrc="${RUNNER_TEMP}/bapi-complete.curlrc"',
571
+ ' reqbody="${RUNNER_TEMP}/bapi-complete.json"',
572
+ " trap 'rm -f \"$curlrc\" \"$reqbody\"' EXIT",
573
+ " jq -rn 'env.BAPI_API_KEY | \"header = \\\"X-API-Key: \\(.)\\\"\"' > \"$curlrc\"",
574
+ " jq -n '{repo_name: (env.ACTION_KEY | split(\":\") | .[1] // \"\"), action_key: env.ACTION_KEY, expected_head_sha: env.EXPECTED_HEAD_SHA, result: \"merged\", merge_sha: env.MERGE_SHA, lease_owner: env.LEASE_OWNER}' > \"$reqbody\"",
575
+ " code=\"$(curl --silent --show-error --no-progress-meter --retry 0 \\",
576
+ " --max-time 30 \\",
577
+ ' --config "$curlrc" \\',
578
+ " --header 'Content-Type: application/json' \\",
579
+ " --request POST \\",
580
+ ' --data-binary "@$reqbody" \\',
581
+ " --output /dev/null \\",
582
+ " --write-out '%{http_code}' \\",
583
+ ' "${BAPI_BASE_URL%/}/vcs/pull-requests/$PR_NUMBER/merge/complete" || echo "000")"',
584
+ ' if [ "$code" = "200" ]; then',
585
+ ' echo "Reported the merge of pull request #$PR_NUMBER."',
586
+ " exit 0",
587
+ " fi",
588
+ " # A merged pull request whose report was not accepted is an UNREPORTED",
589
+ " # outcome, not an unmerged one. Bridge's reconciler recovers it, with",
590
+ " # the lease's own expiry as the backstop; a repository administrator",
591
+ " # only needs to intervene if that recovery does not close the row.",
592
+ ' echo "::error::The merge of pull request #$PR_NUMBER landed but its completion report was not accepted (HTTP $code); Bridge will reconcile it."',
593
+ ' echo "REFUSAL_REASON=completion_unreported" >> "$GITHUB_ENV"',
594
+ " exit 1",
595
+ "",
596
+ " # ------------------------------------------------------------------",
597
+ " # 7. Bounded failure report. Attempted for every denial that resolved",
598
+ " # enough identity to name the attempt — which means the command",
599
+ " # fetch succeeded. A failure BEFORE that point has no pull request",
600
+ " # number, so the completion route cannot be addressed at all; the",
601
+ " # lease expiry is the reconciler's backstop for that window.",
602
+ " #",
603
+ " # A 409 here is EXPECTED for a default-branch base or a drifted head:",
604
+ " # Bridge's completion path re-applies both guards above the failure",
605
+ " # branch and refuses. It is never retried.",
606
+ " # ------------------------------------------------------------------",
607
+ " - name: Report a denial to Bridge",
608
+ " if: failure()",
609
+ " shell: bash",
610
+ " run: |",
611
+ " set -uo pipefail",
612
+ " set +x",
613
+ ' if [ -z "${PR_NUMBER:-}" ] || [ -z "${EXPECTED_HEAD_SHA:-}" ]; then',
614
+ ' echo "::warning::Not enough resolved identity to report this denial; Bridge reconciles it when the lease expires."',
615
+ " exit 0",
616
+ " fi",
617
+ ' if [ "${REFUSAL_REASON:-}" = "completion_unreported" ]; then',
618
+ ' echo "::warning::The merge landed; no failure report is appropriate."',
619
+ " exit 0",
620
+ " fi",
621
+ " umask 077",
622
+ ' curlrc="${RUNNER_TEMP}/bapi-denial.curlrc"',
623
+ ' reqbody="${RUNNER_TEMP}/bapi-denial.json"',
624
+ " trap 'rm -f \"$curlrc\" \"$reqbody\"' EXIT",
625
+ " jq -rn 'env.BAPI_API_KEY | \"header = \\\"X-API-Key: \\(.)\\\"\"' > \"$curlrc\"",
626
+ " jq -n '{repo_name: (env.ACTION_KEY | split(\":\") | .[1] // \"\"), action_key: env.ACTION_KEY, expected_head_sha: env.EXPECTED_HEAD_SHA, result: \"failed\", reason: (env.REFUSAL_REASON // \"merge_outcome_ambiguous\"), lease_owner: env.LEASE_OWNER}' > \"$reqbody\"",
627
+ " code=\"$(curl --silent --show-error --no-progress-meter --retry 0 \\",
628
+ " --max-time 30 \\",
629
+ ' --config "$curlrc" \\',
630
+ " --header 'Content-Type: application/json' \\",
631
+ " --request POST \\",
632
+ ' --data-binary "@$reqbody" \\',
633
+ " --output /dev/null \\",
634
+ " --write-out '%{http_code}' \\",
635
+ ' "${BAPI_BASE_URL%/}/vcs/pull-requests/$PR_NUMBER/merge/complete" || echo "000")"',
636
+ ' case "$code" in',
637
+ " 200)",
638
+ ' echo "Reported denial ${REFUSAL_REASON:-unknown} for pull request #$PR_NUMBER."',
639
+ " ;;",
640
+ " 409)",
641
+ ' echo "::warning::Bridge refused the denial report as already settled (HTTP 409). This is expected for a default-branch base or a drifted head and is not retried."',
642
+ " ;;",
643
+ " *)",
644
+ ' echo "::warning::The denial report was not accepted (HTTP $code); Bridge reconciles this attempt when the lease expires. Escalate to a repository administrator if it does not clear."',
645
+ " ;;",
646
+ " esac",
647
+ "",
648
+ "",
649
+ " # =========================================================================",
650
+ " # The integration job (contract version 2, BAPI-1259). Runs ONLY for",
651
+ " # `operation: " + INTEGRATION_ACTION_WORKFLOW_OPERATION + "`. It performs one server-issued integration action and",
652
+ " # reports facts. It contains no pull-request merge step.",
653
+ " # =========================================================================",
654
+ " integration:",
655
+ " name: Perform one Epic Implementer integration action",
656
+ " if: inputs.operation == '" + INTEGRATION_ACTION_WORKFLOW_OPERATION + "'",
657
+ " runs-on: ubuntu-latest",
658
+ " timeout-minutes: 10",
659
+ " env:",
660
+ " BAPI_BASE_URL: " + BASE_URL_EXPR,
661
+ " BAPI_API_KEY: " + SECRET_EXPR,
662
+ " ACTION_ID: " + gh("inputs.action_key"),
663
+ " CLAIM_TOKEN: " + gh("inputs.lease_owner"),
664
+ " REPO_FULL_NAME: " + gh("github.repository"),
665
+ " GH_TOKEN: " + gh("github.token"),
666
+ " GH_PROMPT_DISABLED: '1'",
667
+ " GH_NO_UPDATE_NOTIFIER: '1'",
668
+ " steps:",
669
+ " - name: Require the Bridge API key secret",
670
+ " shell: bash",
671
+ " run: |",
672
+ " set -uo pipefail",
673
+ " set +x",
674
+ " if [ -z \"${BAPI_API_KEY:-}\" ]; then",
675
+ " echo \"REFUSAL_REASON=bridge_api_key_secret_missing\" >> \"$GITHUB_ENV\"",
676
+ " echo \"::error::Repository secret " + MERGE_WORKFLOW_API_KEY_SECRET_NAME + " is not set. Create it with a Bridge API key for this repository.\"",
677
+ " exit 1",
678
+ " fi",
679
+ "",
680
+ " # ------------------------------------------------------------------",
681
+ " # Fetch the action. This is the ONLY source of what to do: the",
682
+ " # operation, the exact epic branch, and the expected heads. The claim",
683
+ " # token reaches Bridge in a jq-built body and the key through a 0600",
684
+ " # curl config file; neither is ever an argv element.",
685
+ " # ------------------------------------------------------------------",
686
+ " - name: Fetch the integration action",
687
+ " shell: bash",
688
+ " run: |",
689
+ " set -uo pipefail",
690
+ " set +x",
691
+ " umask 077",
692
+ " curlrc=\"${RUNNER_TEMP}/bapi-integ.curlrc\"",
693
+ " reqbody=\"${RUNNER_TEMP}/bapi-integ-request.json\"",
694
+ " respbody=\"${RUNNER_TEMP}/bapi-integ-action.json\"",
695
+ " trap 'rm -f \"$curlrc\" \"$reqbody\" \"$respbody\"' EXIT",
696
+ " jq -rn 'env.BAPI_API_KEY | \"header = \\\"X-API-Key: \\(.)\\\"\"' > \"$curlrc\"",
697
+ " jq -n '{protocol_version: " + String(INTEGRATION_ACTION_PROTOCOL_VERSION) + ", repo_name: (env.ACTION_ID | split(\":\") | .[1] // \"\"), action_id: env.ACTION_ID, claim_token: env.CLAIM_TOKEN}' > \"$reqbody\"",
698
+ " code=\"$(curl --silent --show-error --no-progress-meter --retry 0 \\",
699
+ " --max-time 30 \\",
700
+ " --config \"$curlrc\" \\",
701
+ " --header 'Content-Type: application/json' \\",
702
+ " --request POST \\",
703
+ " --data-binary \"@$reqbody\" \\",
704
+ " --output \"$respbody\" \\",
705
+ " --write-out '%{http_code}' \\",
706
+ " \"${BAPI_BASE_URL%/}" + INTEGRATION_ACTION_FETCH_PATH + "\" || echo \"000\")\"",
707
+ " if [ \"$code\" != \"200\" ]; then",
708
+ " echo \"REFUSAL_REASON=integration_action_unavailable\" >> \"$GITHUB_ENV\"",
709
+ " echo \"::error::Bridge did not return the integration action for this dispatch (HTTP $code).\"",
710
+ " exit 1",
711
+ " fi",
712
+ " version=\"$(jq -r '.protocol_version // \"\"' \"$respbody\")\"",
713
+ " action_id=\"$(jq -r '.action_id // \"\"' \"$respbody\")\"",
714
+ " actor=\"$(jq -r '.actor // \"\"' \"$respbody\")\"",
715
+ " operation=\"$(jq -r '.operation // \"\"' \"$respbody\")\"",
716
+ " repo_name=\"$(jq -r '.repo_name // \"\"' \"$respbody\")\"",
717
+ " provider_repository=\"$(jq -r '.provider_repository // \"\"' \"$respbody\")\"",
718
+ " epic_key=\"$(jq -r '.epic_key // \"\"' \"$respbody\")\"",
719
+ " epic_branch=\"$(jq -r '.epic_branch // \"\"' \"$respbody\")\"",
720
+ " base_branch=\"$(jq -r '.base_branch // \"\"' \"$respbody\")\"",
721
+ " expected_epic_head_sha=\"$(jq -r '.expected_epic_head_sha // \"\"' \"$respbody\")\"",
722
+ " expected_base_sha=\"$(jq -r '.expected_base_sha // \"\"' \"$respbody\")\"",
723
+ " pr_number=\"$(jq -r '.pr_number // \"\"' \"$respbody\")\"",
724
+ " key_repo_name=\"$(jq -rn 'env.ACTION_ID | split(\":\") | .[1] // \"\"')\"",
725
+ " if [ \"$provider_repository\" != \"$REPO_FULL_NAME\" ] \\",
726
+ " || [ \"$action_id\" != \"$ACTION_ID\" ] \\",
727
+ " || [ \"$repo_name\" != \"$key_repo_name\" ]; then",
728
+ " echo \"REFUSAL_REASON=repository_identity_mismatch\" >> \"$GITHUB_ENV\"",
729
+ " echo \"::error::The integration action does not match this repository and dispatch.\"",
730
+ " exit 1",
731
+ " fi",
732
+ " # From here on the action is identified, so a refusal is reportable.",
733
+ " echo \"ACTION_FETCHED=true\" >> \"$GITHUB_ENV\"",
734
+ " if [ \"$version\" != \"" + String(INTEGRATION_ACTION_PROTOCOL_VERSION) + "\" ] || [ \"$actor\" != \"github_actions\" ]; then",
735
+ " echo \"REFUSAL_REASON=integration_action_rejected\" >> \"$GITHUB_ENV\"",
736
+ " echo \"::error::The integration action uses a protocol version or actor this workflow does not perform.\"",
737
+ " exit 1",
738
+ " fi",
739
+ " case \"$operation\" in",
740
+ " sync_base|mark_ready) ;;",
741
+ " *)",
742
+ " echo \"REFUSAL_REASON=integration_action_rejected\" >> \"$GITHUB_ENV\"",
743
+ " echo \"::error::The integration action names an operation this workflow does not perform.\"",
744
+ " exit 1",
745
+ " ;;",
746
+ " esac",
747
+ " case \"$epic_branch\" in",
748
+ " epic/*) ;;",
749
+ " *)",
750
+ " echo \"REFUSAL_REASON=integration_action_rejected\" >> \"$GITHUB_ENV\"",
751
+ " echo \"::error::The integration action does not name an epic branch.\"",
752
+ " exit 1",
753
+ " ;;",
754
+ " esac",
755
+ " if ! [[ \"$expected_epic_head_sha\" =~ ^[0-9a-f]{40}$ ]] \\",
756
+ " || ! [[ \"$expected_base_sha\" =~ ^[0-9a-f]{40}$ ]] \\",
757
+ " || [ -z \"$base_branch\" ] \\",
758
+ " || ! [[ \"$pr_number\" =~ ^[0-9]*$ ]]; then",
759
+ " echo \"REFUSAL_REASON=integration_action_rejected\" >> \"$GITHUB_ENV\"",
760
+ " echo \"::error::The integration action carries unusable branch or commit fields.\"",
761
+ " exit 1",
762
+ " fi",
763
+ " {",
764
+ " echo \"OPERATION=$operation\"",
765
+ " echo \"EPIC_KEY=$epic_key\"",
766
+ " echo \"EPIC_BRANCH=$epic_branch\"",
767
+ " echo \"BASE_BRANCH=$base_branch\"",
768
+ " echo \"EXPECTED_EPIC_HEAD_SHA=$expected_epic_head_sha\"",
769
+ " echo \"EXPECTED_BASE_SHA=$expected_base_sha\"",
770
+ " echo \"ACTION_PR_NUMBER=$pr_number\"",
771
+ " } >> \"$GITHUB_ENV\"",
772
+ "",
773
+ " - name: Guard the epic branch",
774
+ " shell: bash",
775
+ " run: |",
776
+ " set -uo pipefail",
777
+ " set +x",
778
+ " default_branch=\"$(gh api \"repos/$REPO_FULL_NAME\" --jq '.default_branch // \"\"' 2>/dev/null || echo \"\")\"",
779
+ " if [ -z \"$default_branch\" ]; then",
780
+ " echo \"REFUSAL_REASON=default_branch_unresolved\" >> \"$GITHUB_ENV\"",
781
+ " echo \"::error::The repository default branch could not be read, so the branch guard cannot be applied.\"",
782
+ " exit 1",
783
+ " fi",
784
+ " # Hard denial, never retried: an integration action writes only the",
785
+ " # run-bound epic branch, never the default branch.",
786
+ " if [ \"$EPIC_BRANCH\" = \"$default_branch\" ]; then",
787
+ " echo \"REFUSAL_REASON=epic_branch_is_default_branch\" >> \"$GITHUB_ENV\"",
788
+ " echo \"::error::The integration action names the repository default branch; this workflow never writes it.\"",
789
+ " exit 1",
790
+ " fi",
791
+ "",
792
+ " # ------------------------------------------------------------------",
793
+ " # Revalidate IMMEDIATELY before writing: the lease and policy generation",
794
+ " # (a fresh fetch must still succeed) and the live epic head (it must",
795
+ " # still be the expected one). A mismatch is reported as a fact and",
796
+ " # nothing is written.",
797
+ " # ------------------------------------------------------------------",
798
+ " - name: Revalidate authority and the epic head",
799
+ " shell: bash",
800
+ " run: |",
801
+ " set -uo pipefail",
802
+ " set +x",
803
+ " umask 077",
804
+ " curlrc=\"${RUNNER_TEMP}/bapi-integ-revalidate.curlrc\"",
805
+ " reqbody=\"${RUNNER_TEMP}/bapi-integ-revalidate.json\"",
806
+ " trap 'rm -f \"$curlrc\" \"$reqbody\"' EXIT",
807
+ " jq -rn 'env.BAPI_API_KEY | \"header = \\\"X-API-Key: \\(.)\\\"\"' > \"$curlrc\"",
808
+ " jq -n '{protocol_version: " + String(INTEGRATION_ACTION_PROTOCOL_VERSION) + ", repo_name: (env.ACTION_ID | split(\":\") | .[1] // \"\"), action_id: env.ACTION_ID, claim_token: env.CLAIM_TOKEN}' > \"$reqbody\"",
809
+ " code=\"$(curl --silent --show-error --no-progress-meter --retry 0 \\",
810
+ " --max-time 30 \\",
811
+ " --config \"$curlrc\" \\",
812
+ " --header 'Content-Type: application/json' \\",
813
+ " --request POST \\",
814
+ " --data-binary \"@$reqbody\" \\",
815
+ " --output /dev/null \\",
816
+ " --write-out '%{http_code}' \\",
817
+ " \"${BAPI_BASE_URL%/}" + INTEGRATION_ACTION_FETCH_PATH + "\" || echo \"000\")\"",
818
+ " if [ \"$code\" != \"200\" ]; then",
819
+ " { echo \"OUTCOME=authority_lost\"; echo \"SKIP_WRITE=true\"; } >> \"$GITHUB_ENV\"",
820
+ " echo \"::warning::The integration action's lease is no longer held (HTTP $code); nothing was written.\"",
821
+ " exit 0",
822
+ " fi",
823
+ " live_head=\"$(gh api \"repos/$REPO_FULL_NAME/branches/$EPIC_BRANCH\" --jq '.commit.sha // \"\"' 2>/dev/null || echo \"\")\"",
824
+ " if [ \"$live_head\" != \"$EXPECTED_EPIC_HEAD_SHA\" ]; then",
825
+ " { echo \"OUTCOME=head_drift\"; echo \"SKIP_WRITE=true\"; } >> \"$GITHUB_ENV\"",
826
+ " echo \"::warning::The epic branch moved away from the expected head; nothing was written.\"",
827
+ " exit 0",
828
+ " fi",
829
+ "",
830
+ " # ------------------------------------------------------------------",
831
+ " # sync_base: merge the EXACT expected base SHA into the run-bound epic",
832
+ " # branch through the provider's merge API. No checkout, no force. The",
833
+ " # outcome is read from the HTTP status and, when that is unknown, from",
834
+ " # a read-back of the branch — never guessed, and never retried blindly.",
835
+ " # ------------------------------------------------------------------",
836
+ " - name: Merge the base into the epic branch",
837
+ " if: env.OPERATION == 'sync_base' && env.SKIP_WRITE != 'true'",
838
+ " shell: bash",
839
+ " run: |",
840
+ " set -uo pipefail",
841
+ " set +x",
842
+ " message=\"Merge $BASE_BRANCH into $EPIC_BRANCH ($EPIC_KEY integration preparation)\"",
843
+ " out=\"$(gh api --include --method POST \"repos/$REPO_FULL_NAME/merges\" \\",
844
+ " -f base=\"$EPIC_BRANCH\" -f head=\"$EXPECTED_BASE_SHA\" -f commit_message=\"$message\" \\",
845
+ " 2>/dev/null || true)\"",
846
+ " http=\"$(printf '%s\\n' \"$out\" | head -n 1 | awk '{print $2}')\"",
847
+ " body=\"$(printf '%s\\n' \"$out\" | sed '1,/^\\r*$/d')\"",
848
+ " parents_of() {",
849
+ " gh api \"repos/$REPO_FULL_NAME/commits/$1\" --jq '[.parents[].sha] | join(\" \")' 2>/dev/null || echo \"\"",
850
+ " }",
851
+ " outcome=\"indeterminate\"",
852
+ " result=\"\"",
853
+ " case \"$http\" in",
854
+ " 201)",
855
+ " result=\"$(printf '%s' \"$body\" | jq -r '.sha // \"\"' 2>/dev/null || echo \"\")\"",
856
+ " if [ -n \"$result\" ] && [ \"$(parents_of \"$result\")\" = \"$EXPECTED_EPIC_HEAD_SHA $EXPECTED_BASE_SHA\" ]; then",
857
+ " outcome=\"applied\"",
858
+ " else",
859
+ " outcome=\"head_drift\"",
860
+ " fi",
861
+ " ;;",
862
+ " 204) outcome=\"already_satisfied\"; result=\"$EXPECTED_EPIC_HEAD_SHA\" ;;",
863
+ " 409) outcome=\"conflict\" ;;",
864
+ " 401|403|404) outcome=\"authority_unavailable\" ;;",
865
+ " *)",
866
+ " # Lost response: read the branch back and match it to THIS action.",
867
+ " live_head=\"$(gh api \"repos/$REPO_FULL_NAME/branches/$EPIC_BRANCH\" --jq '.commit.sha // \"\"' 2>/dev/null || echo \"\")\"",
868
+ " if [ -n \"$live_head\" ] && [ \"$live_head\" != \"$EXPECTED_EPIC_HEAD_SHA\" ] \\",
869
+ " && [ \"$(parents_of \"$live_head\")\" = \"$EXPECTED_EPIC_HEAD_SHA $EXPECTED_BASE_SHA\" ]; then",
870
+ " outcome=\"applied\"; result=\"$live_head\"",
871
+ " fi",
872
+ " ;;",
873
+ " esac",
874
+ " { echo \"OUTCOME=$outcome\"; echo \"RESULT_SHA=$result\"; } >> \"$GITHUB_ENV\"",
875
+ "",
876
+ " # ------------------------------------------------------------------",
877
+ " # mark_ready: find the ONE open pull request from the epic branch to the",
878
+ " # base branch (creating it only when absence is conclusive), confirm its",
879
+ " # head is the expected epic head, and take it out of draft. There is no",
880
+ " # merge command here.",
881
+ " # ------------------------------------------------------------------",
882
+ " - name: Take the integration pull request out of draft",
883
+ " if: env.OPERATION == 'mark_ready' && env.SKIP_WRITE != 'true'",
884
+ " shell: bash",
885
+ " run: |",
886
+ " set -uo pipefail",
887
+ " set +x",
888
+ " owner=\"${REPO_FULL_NAME%%/*}\"",
889
+ " list_prs() {",
890
+ " gh api \"repos/$REPO_FULL_NAME/pulls?state=open&per_page=100&head=$owner:$EPIC_BRANCH&base=$BASE_BRANCH\" 2>/dev/null || echo \"\"",
891
+ " }",
892
+ " report() { { echo \"OUTCOME=$1\"; echo \"RESULT_PR=${2:-}\"; } >> \"$GITHUB_ENV\"; exit 0; }",
893
+ " prs=\"$(list_prs)\"",
894
+ " count=\"$(printf '%s' \"$prs\" | jq 'if type == \"array\" then length else -1 end' 2>/dev/null || echo -1)\"",
895
+ " if [ \"$count\" = \"0\" ]; then",
896
+ " gh api --method POST \"repos/$REPO_FULL_NAME/pulls\" \\",
897
+ " -f title=\"$EPIC_KEY: epic integration branch\" -f head=\"$EPIC_BRANCH\" \\",
898
+ " -f base=\"$BASE_BRANCH\" -F draft=false \\",
899
+ " -f body=\"Integration pull request for $EPIC_KEY, opened by the Epic Implementer. A human reviews and merges it.\" \\",
900
+ " >/dev/null 2>&1 || true",
901
+ " # Confirm by probing, never by trusting the create response: a lost",
902
+ " # response must not become a second pull request.",
903
+ " prs=\"$(list_prs)\"",
904
+ " count=\"$(printf '%s' \"$prs\" | jq 'if type == \"array\" then length else -1 end' 2>/dev/null || echo -1)\"",
905
+ " fi",
906
+ " if [ \"$count\" = \"-1\" ] || [ \"$count\" = \"0\" ]; then report indeterminate; fi",
907
+ " if [ \"$count\" != \"1\" ]; then report pr_ambiguous; fi",
908
+ " number=\"$(printf '%s' \"$prs\" | jq -r '.[0].number // \"\"')\"",
909
+ " head_sha=\"$(printf '%s' \"$prs\" | jq -r '.[0].head.sha // \"\"')\"",
910
+ " draft=\"$(printf '%s' \"$prs\" | jq -r 'if .[0].draft == true then \"true\" else \"false\" end')\"",
911
+ " if [ \"$head_sha\" != \"$EXPECTED_EPIC_HEAD_SHA\" ]; then report head_drift \"$number\"; fi",
912
+ " if [ \"$draft\" != \"true\" ]; then report already_satisfied \"$number\"; fi",
913
+ " gh pr ready \"$number\" --repo \"$REPO_FULL_NAME\" >/dev/null 2>&1 || true",
914
+ " after=\"$(gh api \"repos/$REPO_FULL_NAME/pulls/$number\" --jq 'if .draft == false then \"ready\" else \"draft\" end' 2>/dev/null || echo \"\")\"",
915
+ " case \"$after\" in",
916
+ " ready) report applied \"$number\" ;;",
917
+ " draft) report failed \"$number\" ;;",
918
+ " *) report indeterminate \"$number\" ;;",
919
+ " esac",
920
+ "",
921
+ " # ------------------------------------------------------------------",
922
+ " # Report facts. Runs whenever the action was identified, including after",
923
+ " # a refusal. Bridge records the report and decides nothing from it: the",
924
+ " # reconciler re-observes the repository before completing any run.",
925
+ " # ------------------------------------------------------------------",
926
+ " - name: Report the integration action facts to Bridge",
927
+ " if: always() && env.ACTION_FETCHED == 'true'",
928
+ " shell: bash",
929
+ " run: |",
930
+ " set -uo pipefail",
931
+ " set +x",
932
+ " umask 077",
933
+ " curlrc=\"${RUNNER_TEMP}/bapi-integ-report.curlrc\"",
934
+ " reqbody=\"${RUNNER_TEMP}/bapi-integ-report.json\"",
935
+ " trap 'rm -f \"$curlrc\" \"$reqbody\"' EXIT",
936
+ " jq -rn 'env.BAPI_API_KEY | \"header = \\\"X-API-Key: \\(.)\\\"\"' > \"$curlrc\"",
937
+ " jq -n '{protocol_version: " + String(INTEGRATION_ACTION_PROTOCOL_VERSION) + ", repo_name: (env.ACTION_ID | split(\":\") | .[1] // \"\"), action_id: env.ACTION_ID, claim_token: env.CLAIM_TOKEN, outcome: (if (env.REFUSAL_REASON // \"\") != \"\" then \"failed\" else (env.OUTCOME // \"indeterminate\") end), result_epic_head_sha: (if (env.RESULT_SHA // \"\") == \"\" then null else env.RESULT_SHA end), pr_number: (if (env.RESULT_PR // \"\") == \"\" then null else (env.RESULT_PR | tonumber) end), detail_code: (if (env.REFUSAL_REASON // \"\") == \"\" then null else env.REFUSAL_REASON end)}' > \"$reqbody\"",
938
+ " code=\"$(curl --silent --show-error --no-progress-meter --retry 0 \\",
939
+ " --max-time 30 \\",
940
+ " --config \"$curlrc\" \\",
941
+ " --header 'Content-Type: application/json' \\",
942
+ " --request POST \\",
943
+ " --data-binary \"@$reqbody\" \\",
944
+ " --output /dev/null \\",
945
+ " --write-out '%{http_code}' \\",
946
+ " \"${BAPI_BASE_URL%/}" + INTEGRATION_ACTION_REPORT_PATH + "\" || echo \"000\")\"",
947
+ " if [ \"$code\" = \"200\" ]; then",
948
+ " echo \"Reported the integration action facts.\"",
949
+ " exit 0",
950
+ " fi",
951
+ " echo \"::warning::The integration action report was not accepted (HTTP $code); Bridge re-observes the repository and expires the action on its own.\"",
952
+ " exit 0",
953
+ "",
954
+ ];
955
+ return lines.join("\n");
956
+ }