@bridge_gpt/mcp-server 0.2.60 → 0.2.65
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 +58 -11
- package/build/agent-launchers/claude-executor-adapter.js +134 -3
- package/build/agent-launchers/executor-adapter.js +54 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/commands.generated.js +8 -7
- package/build/conductor/bridge-api-client.js +678 -14
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +324 -0
- package/build/conductor/epic-implementer-cli.js +142 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/merge-execution-contract.js +30 -5
- package/build/conductor/merge-execution-eligibility.js +10 -9
- package/build/conductor/readiness-sections.js +104 -20
- package/build/conductor/readiness.js +129 -25
- package/build/conductor/recovery-cli.js +354 -13
- package/build/conductor/recovery-operations.js +251 -5
- package/build/conductor/repair-contract.js +8 -0
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +194 -0
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +161 -7
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1318 -111
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +35 -29
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +464 -0
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +48 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +286 -0
- package/build/executor/credentials.js +19 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +6 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +117 -1
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-errors.js +48 -0
- package/build/executor/job-runner.js +1386 -35
- package/build/executor/job-types.js +10 -2
- package/build/executor/observation.js +114 -7
- package/build/executor/preflight.js +99 -0
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +262 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +246 -1
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +55 -0
- package/build/executor/worker-command.js +49 -13
- package/build/executor/worker-finalization.js +88 -11
- package/build/executor/worker-guard-hook.js +29 -4
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +772 -954
- package/build/install-bridge-conductor.js +622 -40
- package/build/install-bridge.js +92 -16
- package/build/merge-pull-request.js +1 -1
- package/build/merge-workflow-config.js +54 -0
- package/build/merge-workflow-drift.js +35 -4
- package/build/merge-workflow.js +386 -2
- package/build/pipelines.generated.js +9 -8
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/supervisor.js +148 -0
- package/build/readme.generated.js +1 -1
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +130 -48
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/docs/CONDUCTOR.md +116 -2
- package/package.json +2 -1
package/build/merge-workflow.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|