@bridge_gpt/mcp-server 0.2.60 → 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.
@@ -131,3 +131,57 @@ export const MERGE_COMMAND_FETCH_RESPONSE_FIELDS = [
131
131
  "execution",
132
132
  "lease_expires_at",
133
133
  ];
134
+ /**
135
+ * The integration-action actor contract the rendered workflow's
136
+ * `integration` job consumes (BAPI-1259).
137
+ *
138
+ * A DEDICATED contract, deliberately not an extension of the merge-command
139
+ * fetch above: an integration action is never a child merge, and a workflow
140
+ * that could interpret one as the other would be one refactor away from
141
+ * merging an integration pull request. The routes are mounted under
142
+ * {@link INTEGRATION_ACTION_PATH_PREFIX} in `api/routes/__init__.py`.
143
+ *
144
+ * The dispatch inputs only wake and correlate: `action_key` carries the
145
+ * server-minted action id (`integ:{repo}:{hex}`) and `lease_owner` carries the
146
+ * server-minted per-claim token. Everything the job acts on — the operation,
147
+ * the exact epic branch, the expected heads — comes from the fetch response.
148
+ */
149
+ export const INTEGRATION_ACTION_PATH_PREFIX = "/epic-integration-actions";
150
+ /** Re-read the approved work under the dispatched lease. */
151
+ export const INTEGRATION_ACTION_FETCH_PATH = INTEGRATION_ACTION_PATH_PREFIX + "/fetch";
152
+ /** Report bounded facts. Returns an acknowledgement only. */
153
+ export const INTEGRATION_ACTION_REPORT_PATH = INTEGRATION_ACTION_PATH_PREFIX + "/report";
154
+ /** The one protocol version the rendered job speaks (exact equality server-side). */
155
+ export const INTEGRATION_ACTION_PROTOCOL_VERSION = 1;
156
+ /** The dispatch `operation` value that selects the integration job. */
157
+ export const INTEGRATION_ACTION_WORKFLOW_OPERATION = "integration_action";
158
+ /** The dispatch `operation` default that selects the child-merge job. */
159
+ export const CHILD_MERGE_WORKFLOW_OPERATION = "child_merge";
160
+ /**
161
+ * The operations the WORKFLOW may perform. `correct` is absent on purpose: a
162
+ * correction needs a worker, which only a local or durable executor runs, so the
163
+ * server never issues one to the workflow and the job refuses one if it arrives.
164
+ */
165
+ export const INTEGRATION_WORKFLOW_OPERATIONS = ["sync_base", "mark_ready"];
166
+ /** The request fields of the fetch, in the contract's own spelling. */
167
+ export const INTEGRATION_ACTION_FETCH_REQUEST_FIELDS = [
168
+ "protocol_version",
169
+ "repo_name",
170
+ "action_id",
171
+ "claim_token",
172
+ ];
173
+ /** The response fields the rendered job reads from the fetch. */
174
+ export const INTEGRATION_ACTION_FETCH_RESPONSE_FIELDS = [
175
+ "protocol_version",
176
+ "action_id",
177
+ "operation",
178
+ "actor",
179
+ "repo_name",
180
+ "provider_repository",
181
+ "epic_key",
182
+ "epic_branch",
183
+ "base_branch",
184
+ "expected_epic_head_sha",
185
+ "expected_base_sha",
186
+ "pr_number",
187
+ ];
@@ -33,11 +33,42 @@ export function canonicalMergeWorkflowSha() {
33
33
  * Every Bridge-published historical rendering of the merge workflow, by Git blob
34
34
  * SHA, with the contract version it declared.
35
35
  *
36
- * Empty at contract version 1: there is no earlier published version to upgrade
37
- * FROM. A future contract version adds its predecessor's SHA here, and only
38
- * then can {@link classifyMergeWorkflowContent} return `managed_version_older`.
36
+ * A future contract version adds its predecessor's SHA here, and only then can
37
+ * {@link classifyMergeWorkflowContent} return `managed_version_older`.
38
+ *
39
+ * ## Why a version-1 entry exists without a version bump (BAPI-1257)
40
+ *
41
+ * The first entry is a rendering that was published AS version 1 and then
42
+ * corrected WITHIN version 1. Its fork-origin guard read
43
+ * `jq -r '.isCrossRepository // true'`, and jq's `//` yields its right-hand side
44
+ * when the left is null OR FALSE — so a same-repository pull request, which
45
+ * every epic child is, read as `true` and the merge was refused as fork-origin.
46
+ * That rendering could never merge anything.
47
+ *
48
+ * The marker version is deliberately NOT bumped: the wire contract, the dispatch
49
+ * inputs and the command-fetch protocol are all unchanged, and
50
+ * `MERGE_WORKFLOW_EXPECTED_VERSION` is the server's deploy-ordering check, not a
51
+ * byte checksum. But without this entry a repository holding that rendering
52
+ * classifies as `modified_or_unrecognized`, and the installer's preflight hard
53
+ * refuses to touch a workflow Bridge does not recognise — leaving no automated
54
+ * path off a copy that cannot merge. Registering the superseded SHA restores it:
55
+ * the copy classifies `managed_version_older` and the installer opens its
56
+ * ordinary upgrade pull request.
57
+ *
58
+ * The SHA is the Git blob SHA of the complete superseded rendering, which is
59
+ * what {@link canonicalMergeWorkflowSha} computes for the current one.
60
+ *
61
+ * ## The version-1 entry added by BAPI-1259
62
+ *
63
+ * `0d44c814…` is the last version-1 rendering (the one BAPI-1257 corrected).
64
+ * Contract version 2 adds the `operation` input and the integration job, so a
65
+ * repository holding that rendering is an ordinary managed older version and
66
+ * the installer opens its upgrade pull request.
39
67
  */
40
- export const MANAGED_MERGE_WORKFLOW_HISTORY = new Map();
68
+ export const MANAGED_MERGE_WORKFLOW_HISTORY = new Map([
69
+ ["3d4956c9447223c37075069352cdd9aae2d561dc", 1],
70
+ ["0d44c81436005beb3c314de1b98eaf972ef3b4db", 1],
71
+ ]);
41
72
  /**
42
73
  * Classify an installed copy against the canonical bytes.
43
74
  *
@@ -42,6 +42,18 @@
42
42
  * precondition, a fork-origin head is refused, and a base that is the repository
43
43
  * default branch is refused outright.
44
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
+ *
45
57
  * ## Secret discipline
46
58
  *
47
59
  * The rendered script never enables shell tracing, never echoes either dispatch
@@ -54,7 +66,7 @@
54
66
  * reason.
55
67
  */
56
68
  import { MERGE_WORKFLOW_EXPECTED_VERSION, MERGE_WORKFLOW_PATH, MERGE_WORKFLOW_VERSION_MARKER_KEY, } from "./conductor/merge-execution-contract.js";
57
- import { 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";
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";
58
70
  /**
59
71
  * Every refusal the rendered workflow can emit, as a closed vocabulary.
60
72
  *
@@ -94,6 +106,40 @@ export const MERGE_WORKFLOW_REFUSAL_REASONS = [
94
106
  /** The merge landed but its completion report was not accepted; the reconciler recovers. */
95
107
  "completion_unreported",
96
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
+ ];
97
143
  /**
98
144
  * Build a GitHub Actions expression without writing `${` in TypeScript source.
99
145
  *
@@ -156,6 +202,22 @@ export function renderEpicImplementerMergeWorkflow() {
156
202
  "# * It never merges a fork-origin or otherwise unverifiable head.",
157
203
  "# * It never merges a head that has moved from the approved SHA.",
158
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
+ "#",
159
221
  "# RESIDUAL RACE (base retargeting)",
160
222
  "#",
161
223
  "# Base selection is not atomic with the provider merge. This job validates the",
@@ -203,6 +265,14 @@ export function renderEpicImplementerMergeWorkflow() {
203
265
  " printed, never echoed, never written to an output.",
204
266
  " required: true",
205
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",
206
276
  "",
207
277
  "# Exactly the two scopes the merge needs, granted once at workflow level. No",
208
278
  "# job overrides these, and no other permission is requested.",
@@ -222,6 +292,8 @@ export function renderEpicImplementerMergeWorkflow() {
222
292
  "jobs:",
223
293
  " merge:",
224
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 + "'",
225
297
  " runs-on: ubuntu-latest",
226
298
  " timeout-minutes: 10",
227
299
  " env:",
@@ -394,7 +466,13 @@ export function renderEpicImplementerMergeWorkflow() {
394
466
  ' state="$(printf \'%s\' "$pr_json" | jq -r \'.state // ""\')"',
395
467
  ' base_ref="$(printf \'%s\' "$pr_json" | jq -r \'.baseRefName // ""\')"',
396
468
  ' head_sha="$(printf \'%s\' "$pr_json" | jq -r \'.headRefOid // ""\')"',
397
- ' cross_repo="$(printf \'%s\' "$pr_json" | jq -r \'.isCrossRepository // true\')"',
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\')"',
398
476
  ' head_repo="$(printf \'%s\' "$pr_json" | jq -r \'((.headRepositoryOwner.login // "") + "/" + (.headRepository.name // ""))\')"',
399
477
  ' if [ "$state" != "OPEN" ]; then',
400
478
  ' echo "REFUSAL_REASON=pr_not_open" >> "$GITHUB_ENV"',
@@ -567,6 +645,312 @@ export function renderEpicImplementerMergeWorkflow() {
567
645
  " ;;",
568
646
  " esac",
569
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
+ "",
570
954
  ];
571
955
  return lines.join("\n");
572
956
  }