@plainconceptsplatform/workflows 0.5.1 → 0.6.1
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/dist/action-validation.test.d.ts +1 -0
- package/dist/action-validation.test.js +87 -0
- package/dist/catalog-installation.d.ts +30 -0
- package/dist/catalog-installation.js +422 -0
- package/dist/catalog-installation.test.d.ts +1 -0
- package/dist/catalog-installation.test.js +485 -0
- package/dist/catalog-listing.d.ts +13 -0
- package/dist/catalog-listing.js +70 -0
- package/dist/catalog-listing.test.d.ts +1 -0
- package/dist/catalog-listing.test.js +150 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +218 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +273 -0
- package/dist/repository-inspection.d.ts +23 -0
- package/dist/repository-inspection.js +113 -0
- package/dist/repository-inspection.test.d.ts +1 -0
- package/dist/repository-inspection.test.js +77 -0
- package/dist/route-processing.d.ts +6 -0
- package/dist/route-processing.js +149 -0
- package/dist/route-processing.test.d.ts +1 -0
- package/dist/route-processing.test.js +283 -0
- package/dist/stack-defaults.d.ts +11 -0
- package/dist/stack-defaults.js +104 -0
- package/dist/stack-defaults.test.d.ts +1 -0
- package/dist/stack-defaults.test.js +266 -0
- package/dist/tui.d.ts +25 -0
- package/dist/tui.js +287 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +249 -0
- package/dist/workflow-catalog.d.ts +25 -0
- package/dist/workflow-catalog.js +55 -0
- package/dist/workflow-catalog.test.d.ts +1 -0
- package/dist/workflow-catalog.test.js +29 -0
- package/loops/actions/add-issue-labels/action.yml +2 -2
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
- package/loops/actions/classify-route/action.yml +7 -0
- package/loops/actions/classify-route/classify-route.sh +20 -5
- package/loops/actions/cleanup-artifacts/action.yml +38 -12
- package/loops/actions/identify-gate-subject/action.yml +3 -1
- package/loops/actions/remove-issue-labels/action.yml +4 -2
- package/loops/actions/validate-refine-output/validate-refine-output.sh +18 -2
- package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +300 -16
- package/loops/scripts/compile-agent-workflows.mjs +96 -1
- package/loops/scripts/merge-changelog.mjs +76 -0
- package/loops/templates/agentics/actionlint.yaml +13 -0
- package/loops/templates/agentics/agentics-checks.yml +203 -86
- package/loops/templates/ci/app-ci-dotnet-next.yml +64 -2
- package/loops/templates/ci/app-ci-node-monorepo.yml +52 -0
- package/loops/templates/opencode/opencode.ci.json +2 -2
- package/loops/templates/opencode/opencode.ci.json.md +4 -1
- package/loops/workflows/agent-apply-review.md +5 -1
- package/loops/workflows/agent-implement.md +105 -3
- package/loops/workflows/agent-merge-gate.md +139 -25
- package/loops/workflows/agent-refine.md +26 -2
- package/loops/workflows/work-router.yml +227 -51
- package/package.json +1 -1
|
@@ -91,18 +91,19 @@ on:
|
|
|
91
91
|
# moved and previously-mergeable sibling bot PRs may now conflict.
|
|
92
92
|
types: [opened, synchronize, reopened, labeled, closed]
|
|
93
93
|
|
|
94
|
-
# CI completion on a pull request.
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
# would double-fire the gate on every passing PR.
|
|
94
|
+
# CI completion on a pull request. The classifier admits only conclusion == failure with an
|
|
95
|
+
# attached pull request; a green run reaches the gate through the consumer CI's
|
|
96
|
+
# dispatch-merge-gate job and the reconcile belt, so routing success here would double-fire
|
|
97
|
+
# the gate on every passing PR.
|
|
99
98
|
#
|
|
100
|
-
#
|
|
101
|
-
# whose
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
99
|
+
# Delivery (verified 2026-09-06 in Pliny-Bot): GitHub delivers workflow_run only for CI runs
|
|
100
|
+
# whose actor is a human. A bot pull request is opened with GITHUB_TOKEN, the CI runs that
|
|
101
|
+
# follow have the bot as their actor, and no workflow_run arrives for any of them (four
|
|
102
|
+
# human-actor runs produced four router runs; three bot-actor runs produced none). For bot
|
|
103
|
+
# pull requests the consumer CI's dispatch-merge-gate job (templates/ci) and the hourly
|
|
104
|
+
# reconcile-bot-pr-runs belt are the paths. Security: workflow_run runs from the default
|
|
105
|
+
# branch with secrets, but the gate's subject job rejects any PR that is not open and
|
|
106
|
+
# bot-authored, so a fork PR's failing CI cannot start privileged work.
|
|
106
107
|
workflow_run:
|
|
107
108
|
workflows: ["App: CI"]
|
|
108
109
|
types: [completed]
|
|
@@ -118,7 +119,6 @@ on:
|
|
|
118
119
|
- cron: "43 3 * * *"
|
|
119
120
|
- cron: "0 6 * * *"
|
|
120
121
|
- cron: "17 * * * *"
|
|
121
|
-
- cron: "29 7 * * *"
|
|
122
122
|
|
|
123
123
|
workflow_dispatch:
|
|
124
124
|
inputs:
|
|
@@ -165,6 +165,11 @@ on:
|
|
|
165
165
|
description: "CI workflow run ID for merge-gate dispatch"
|
|
166
166
|
required: false
|
|
167
167
|
type: string
|
|
168
|
+
attempts_so_far:
|
|
169
|
+
description: "Failed merge-gate attempts already made against this CI verdict (filled by the router, not by humans)"
|
|
170
|
+
required: false
|
|
171
|
+
type: string
|
|
172
|
+
default: '0'
|
|
168
173
|
trigger-kind:
|
|
169
174
|
description: "Audit trigger: scheduled or manual"
|
|
170
175
|
required: false
|
|
@@ -274,6 +279,8 @@ jobs:
|
|
|
274
279
|
linked-issue: ${{ steps.route.outputs.linked-issue }}
|
|
275
280
|
ci-conclusion: ${{ steps.route.outputs.ci-conclusion }}
|
|
276
281
|
ci-run-id: ${{ steps.route.outputs.ci-run-id }}
|
|
282
|
+
merge-gate-attempts: ${{ steps.route.outputs.merge-gate-attempts }}
|
|
283
|
+
implement-attempts: ${{ steps.route.outputs.implement-attempts }}
|
|
277
284
|
refine-mode: ${{ steps.route.outputs.refine-mode }}
|
|
278
285
|
triage-mode: ${{ steps.route.outputs.triage-mode }}
|
|
279
286
|
trigger-kind: ${{ steps.route.outputs.trigger-kind }}
|
|
@@ -396,6 +403,7 @@ jobs:
|
|
|
396
403
|
permissions: write-all
|
|
397
404
|
with:
|
|
398
405
|
issue-number: ${{ needs.classify.outputs.issue-number }}
|
|
406
|
+
attempts_so_far: ${{ needs.classify.outputs.implement-attempts }}
|
|
399
407
|
secrets:
|
|
400
408
|
OPENAI_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
401
409
|
CODEX_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
@@ -474,14 +482,18 @@ jobs:
|
|
|
474
482
|
BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
475
483
|
AGENTMEMORY_SECRET: ${{ secrets.AGENTMEMORY_SECRET }}
|
|
476
484
|
|
|
477
|
-
#
|
|
478
|
-
#
|
|
485
|
+
# The merge belt is one lock for the whole repository, not one group per pull request.
|
|
486
|
+
# A batch of six overnight pull requests means every merge moves the default branch
|
|
487
|
+
# under the other five; gates running at once would each rebase onto a base another
|
|
488
|
+
# gate is about to invalidate. Serialising the belt means each gate sees a main that
|
|
489
|
+
# only changes between gates, every merge re-arms the siblings through
|
|
490
|
+
# detect-pr-conflicts's rescan, and the queue drains one pull request at a time.
|
|
479
491
|
call-merge-gate:
|
|
480
492
|
needs: classify
|
|
481
493
|
if: needs.classify.outputs.route == 'merge-gate'
|
|
482
494
|
uses: ./.github/workflows/agent-merge-gate.lock.yml
|
|
483
495
|
concurrency:
|
|
484
|
-
group:
|
|
496
|
+
group: merge-belt
|
|
485
497
|
cancel-in-progress: false
|
|
486
498
|
permissions: write-all
|
|
487
499
|
with:
|
|
@@ -489,6 +501,7 @@ jobs:
|
|
|
489
501
|
linked-issue: ${{ needs.classify.outputs.linked-issue }}
|
|
490
502
|
ci-conclusion: ${{ needs.classify.outputs.ci-conclusion }}
|
|
491
503
|
ci-run-id: ${{ needs.classify.outputs.ci-run-id }}
|
|
504
|
+
attempts_so_far: ${{ needs.classify.outputs.merge-gate-attempts }}
|
|
492
505
|
secrets:
|
|
493
506
|
OPENAI_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
494
507
|
CODEX_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
@@ -614,8 +627,41 @@ jobs:
|
|
|
614
627
|
run: |
|
|
615
628
|
set -euo pipefail
|
|
616
629
|
|
|
617
|
-
# Dispatch the Merge Gate for one pull request when it is a
|
|
618
|
-
#
|
|
630
|
+
# Dispatch the Merge Gate for one pull request when it is a red-CI open, non-draft,
|
|
631
|
+
# bot-authored pull request on this repository whose CI has concluded — the
|
|
632
|
+
# conflict case is one arm of that, because a conflicting PR never gets a fresh
|
|
633
|
+
# CI run of its own; a merge conflict with red or concluded CI is the same
|
|
634
|
+
# "needs the gate" signal. A green non-conflicting PR is left for the merge belt
|
|
635
|
+
# to pick up when its checks finish (bot-approve / reconcile).
|
|
636
|
+
# GitHub computes mergeability on demand. The first read of a pull request nobody has
|
|
637
|
+
# opened recently answers UNKNOWN through the API and null through REST, while it works
|
|
638
|
+
# the answer out in the background. Reading once therefore reports "not conflicting"
|
|
639
|
+
# for precisely the stale conflicting pull requests that need the gate. Ask until it
|
|
640
|
+
# settles.
|
|
641
|
+
mergeable_state() {
|
|
642
|
+
local pr="$1" state=UNKNOWN
|
|
643
|
+
for _ in 1 2 3 4 5 6; do
|
|
644
|
+
state=$(gh pr view "$pr" --repo "$REPO" --json mergeable --jq '.mergeable' 2>/dev/null || echo UNKNOWN)
|
|
645
|
+
[ "$state" = "UNKNOWN" ] || break
|
|
646
|
+
sleep 5
|
|
647
|
+
done
|
|
648
|
+
echo "$state"
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
# A gate already queued or running for this pull request will read the same CI verdict;
|
|
652
|
+
# a second dispatch queues behind merge-belt and the single-slot queue cancels it. Same
|
|
653
|
+
# live-run test as the reconcile belt's stale-reservation sweep, keyed on the exact
|
|
654
|
+
# title run-name gives a gate dispatch.
|
|
655
|
+
gate_is_live() {
|
|
656
|
+
local pr="$1" status live
|
|
657
|
+
for status in queued in_progress waiting pending; do
|
|
658
|
+
live=$(gh api "repos/$REPO/actions/workflows/work-router.yml/runs?status=$status&per_page=100" \
|
|
659
|
+
--jq "[.workflow_runs[] | select(.display_title == \"Working (Merge Gate): PR #${pr}\")] | length" 2>/dev/null || echo 0)
|
|
660
|
+
[ "${live:-0}" -eq 0 ] || return 0
|
|
661
|
+
done
|
|
662
|
+
return 1
|
|
663
|
+
}
|
|
664
|
+
|
|
619
665
|
maybe_dispatch() {
|
|
620
666
|
pr="$1"
|
|
621
667
|
[ -n "$pr" ] || return 0
|
|
@@ -624,21 +670,10 @@ jobs:
|
|
|
624
670
|
--jq '{state:.state, draft:.draft, bot:.user.type, head:(.head.repo.full_name // "")}' \
|
|
625
671
|
2>/dev/null || echo '')
|
|
626
672
|
[ -n "$meta" ] || return 0
|
|
627
|
-
[ "$(jq -r '.state' <<<"$meta")"
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
# GitHub computes mergeability asynchronously; poll until it is known (not null).
|
|
633
|
-
mergeable=""
|
|
634
|
-
for _ in 1 2 3 4 5 6; do
|
|
635
|
-
mergeable=$(gh api "repos/$REPO/pulls/$pr" --jq '.mergeable // "null"' 2>/dev/null || echo null)
|
|
636
|
-
[ "$mergeable" = "null" ] || break
|
|
637
|
-
sleep 5
|
|
638
|
-
done
|
|
639
|
-
# mergeable == false means a git-level conflict (mergeable_state "dirty").
|
|
640
|
-
if [ "$mergeable" != "false" ]; then
|
|
641
|
-
echo "PR #$pr mergeable=$mergeable; no conflict dispatch."
|
|
673
|
+
if [ "$(jq -r '.state' <<<"$meta")" != "open" ] \
|
|
674
|
+
|| [ "$(jq -r '.draft' <<<"$meta")" != "false" ] \
|
|
675
|
+
|| [ "$(jq -r '.bot' <<<"$meta")" != "Bot" ] \
|
|
676
|
+
|| [ "$(jq -r '.head' <<<"$meta")" != "$REPO" ]; then
|
|
642
677
|
return 0
|
|
643
678
|
fi
|
|
644
679
|
|
|
@@ -652,20 +687,88 @@ jobs:
|
|
|
652
687
|
and (.conclusion == "success" or .conclusion == "failure"))]
|
|
653
688
|
| sort_by(.created_at) | last // empty')
|
|
654
689
|
if [ -z "$ci" ]; then
|
|
655
|
-
|
|
656
|
-
|
|
690
|
+
# A conflicting pull request has no merge ref for GitHub to build, so a
|
|
691
|
+
# `pull_request` CI workflow can never run on that head: refs/pull/N/merge is a
|
|
692
|
+
# 404 for as long as the conflict lasts. Waiting for a verdict that cannot arrive
|
|
693
|
+
# deadlocks the belt, because resolving the conflict is the gate's own job. Fall
|
|
694
|
+
# back to the branch's last verdict so the gate runs; it stays on the remediation
|
|
695
|
+
# path and cannot merge on a stale conclusion.
|
|
696
|
+
head_ref=$(gh api "repos/$REPO/pulls/$pr" --jq '.head.ref')
|
|
697
|
+
mergeable_now=$(mergeable_state "$pr")
|
|
698
|
+
if [ "$mergeable_now" != "CONFLICTING" ]; then
|
|
699
|
+
echo "PR #$pr has no concluded CI run; deferring to the CI-completion route."
|
|
700
|
+
return 0
|
|
701
|
+
fi
|
|
702
|
+
ci=$(gh api "repos/$REPO/actions/runs?branch=$head_ref&per_page=100" \
|
|
703
|
+
--jq '[.workflow_runs[]
|
|
704
|
+
| select(.name == "App: CI" and .status == "completed"
|
|
705
|
+
and (.conclusion == "success" or .conclusion == "failure"))]
|
|
706
|
+
| sort_by(.created_at) | last // empty')
|
|
707
|
+
if [ -z "$ci" ]; then
|
|
708
|
+
echo "PR #$pr conflicts and has no CI verdict on any head; nothing to gate on yet."
|
|
709
|
+
return 0
|
|
710
|
+
fi
|
|
711
|
+
echo "PR #$pr conflicts, so CI cannot run on $head_sha at all; gating on the branch's last verdict."
|
|
657
712
|
fi
|
|
658
713
|
ci_conclusion=$(jq -r '.conclusion' <<<"$ci")
|
|
714
|
+
ci_finished=$(jq -r '.updated_at' <<<"$ci")
|
|
659
715
|
ci_run_id=$(jq -r '.id' <<<"$ci")
|
|
660
716
|
|
|
661
|
-
|
|
717
|
+
# GitHub computes mergeability asynchronously; poll until it is known (not null).
|
|
718
|
+
mergeable=""
|
|
719
|
+
for _ in 1 2 3 4 5 6; do
|
|
720
|
+
mergeable=$(gh api "repos/$REPO/pulls/$pr" --jq '.mergeable // "null"' 2>/dev/null || echo null)
|
|
721
|
+
[ "$mergeable" = "null" ] || break
|
|
722
|
+
sleep 5
|
|
723
|
+
done
|
|
724
|
+
|
|
725
|
+
if [ "$mergeable" != "false" ] && [ "$ci_conclusion" != "failure" ]; then
|
|
726
|
+
# Mergeable and green: the reconcile belt and the PR's own checks finish it.
|
|
727
|
+
return 0
|
|
728
|
+
fi
|
|
729
|
+
|
|
730
|
+
# A terminal verdict newer than this CI run means a gate already decided;
|
|
731
|
+
# failed attempts (the attempt marker) deliberately do not count.
|
|
732
|
+
verdict=$(gh api "repos/$REPO/issues/$pr/comments?per_page=100" \
|
|
733
|
+
--jq '[.[] | select((.body | contains("<!-- agent-merge-gate -->")) and (.body | contains("**Verdict:**"))) | .created_at] | max // empty')
|
|
734
|
+
linked_issue=$(gh pr view "$pr" --repo "$REPO" --json closingIssuesReferences --jq '.closingIssuesReferences[0].number // empty' 2>/dev/null || true)
|
|
735
|
+
if [ -n "$linked_issue" ]; then
|
|
736
|
+
issue_verdict=$(gh api "repos/$REPO/issues/$linked_issue/comments?per_page=100" \
|
|
737
|
+
--jq '[.[] | select((.body | contains("<!-- agent-merge-gate -->")) and (.body | contains("**Verdict:**"))) | .created_at] | max // empty')
|
|
738
|
+
if [ -n "$issue_verdict" ] && { [ -z "$verdict" ] || [ "$issue_verdict" \> "$verdict" ]; }; then
|
|
739
|
+
verdict="$issue_verdict"
|
|
740
|
+
fi
|
|
741
|
+
fi
|
|
742
|
+
if [ -n "$verdict" ] && [ "$verdict" \> "$ci_finished" ]; then
|
|
743
|
+
echo "PR #$pr already has a gate verdict newer than CI; skipping."
|
|
744
|
+
return 0
|
|
745
|
+
fi
|
|
746
|
+
|
|
747
|
+
attempts=$(gh api "repos/$REPO/issues/$pr/comments?per_page=100" \
|
|
748
|
+
--jq '[.[] | select(.body | contains("<!-- agent-merge-gate-attempt -->")) | select(.created_at > "'"$ci_finished"'") ] | length')
|
|
749
|
+
if [ -n "$linked_issue" ]; then
|
|
750
|
+
issue_attempts=$(gh api "repos/$REPO/issues/$linked_issue/comments?per_page=100" \
|
|
751
|
+
--jq '[.[] | select(.body | contains("<!-- agent-merge-gate-attempt -->")) | select(.created_at > "'"$ci_finished"'") ] | length')
|
|
752
|
+
attempts=$((attempts + issue_attempts))
|
|
753
|
+
fi
|
|
754
|
+
if [ "$attempts" -ge 6 ]; then
|
|
755
|
+
echo "PR #$pr already consumed $attempts gate attempts on this CI verdict, parking for review."
|
|
756
|
+
return 0
|
|
757
|
+
fi
|
|
758
|
+
if gate_is_live "$pr"; then
|
|
759
|
+
echo "PR #$pr: a merge-gate run is already live; not dispatching another."
|
|
760
|
+
return 0
|
|
761
|
+
fi
|
|
762
|
+
|
|
763
|
+
echo "PR #$pr is $([ "$mergeable" = "false" ] && echo conflicting || echo red-CI) (CI $ci_conclusion); dispatching Merge Gate (attempt $((attempts + 1)) of 6)."
|
|
662
764
|
gh api "repos/$REPO/actions/workflows/work-router.yml/dispatches" \
|
|
663
765
|
--method POST \
|
|
664
766
|
-f ref="$DEFAULT_BRANCH" \
|
|
665
767
|
-f "inputs[operation]=merge-gate" \
|
|
666
768
|
-f "inputs[pr-number]=$pr" \
|
|
667
769
|
-f "inputs[ci-conclusion]=$ci_conclusion" \
|
|
668
|
-
-f "inputs[ci-run-id]=$ci_run_id"
|
|
770
|
+
-f "inputs[ci-run-id]=$ci_run_id" \
|
|
771
|
+
-f "inputs[attempts_so_far]=$attempts"
|
|
669
772
|
}
|
|
670
773
|
|
|
671
774
|
if [ "$ACTION" = "closed" ]; then
|
|
@@ -715,6 +818,33 @@ jobs:
|
|
|
715
818
|
run: |
|
|
716
819
|
set -euo pipefail
|
|
717
820
|
|
|
821
|
+
# GitHub computes mergeability on demand. The first read of a pull request nobody has
|
|
822
|
+
# opened recently answers UNKNOWN through the API and null through REST, while it works
|
|
823
|
+
# the answer out in the background. Reading once therefore reports "not conflicting"
|
|
824
|
+
# for precisely the stale conflicting pull requests that need the gate. Ask until it
|
|
825
|
+
# settles.
|
|
826
|
+
mergeable_state() {
|
|
827
|
+
local pr="$1" state=UNKNOWN
|
|
828
|
+
for _ in 1 2 3 4 5 6; do
|
|
829
|
+
state=$(gh pr view "$pr" --repo "$REPO" --json mergeable --jq '.mergeable' 2>/dev/null || echo UNKNOWN)
|
|
830
|
+
[ "$state" = "UNKNOWN" ] || break
|
|
831
|
+
sleep 5
|
|
832
|
+
done
|
|
833
|
+
echo "$state"
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
# A gate already queued or running for this pull request will read the same CI verdict;
|
|
837
|
+
# a second dispatch queues behind merge-belt and the single-slot queue cancels it.
|
|
838
|
+
gate_is_live() {
|
|
839
|
+
local pr="$1" status live
|
|
840
|
+
for status in queued in_progress waiting pending; do
|
|
841
|
+
live=$(gh api "repos/$REPO/actions/workflows/work-router.yml/runs?status=$status&per_page=100" \
|
|
842
|
+
--jq "[.workflow_runs[] | select(.display_title == \"Working (Merge Gate): PR #${pr}\")] | length" 2>/dev/null || echo 0)
|
|
843
|
+
[ "${live:-0}" -eq 0 ] || return 0
|
|
844
|
+
done
|
|
845
|
+
return 1
|
|
846
|
+
}
|
|
847
|
+
|
|
718
848
|
# Stale reservation sweep. Labelling ten issues at once labels all ten immediately,
|
|
719
849
|
# because authorize-bot-work is keyed per issue, while implement-global lets exactly
|
|
720
850
|
# one run at a time. A queued run that is cancelled therefore leaves its issue
|
|
@@ -772,37 +902,79 @@ jobs:
|
|
|
772
902
|
)
|
|
773
903
|
] | sort_by(.created_at) | last // empty | [.id, .conclusion, .updated_at] | @tsv')
|
|
774
904
|
if [ -z "$ci" ]; then
|
|
775
|
-
|
|
905
|
+
# A conflicting pull request has no merge ref for GitHub to build, so a
|
|
906
|
+
# `pull_request` CI workflow can never run on that head: refs/pull/N/merge is a
|
|
907
|
+
# 404 for as long as the conflict lasts. Waiting for a verdict that cannot arrive
|
|
908
|
+
# deadlocks the belt, because resolving the conflict is the gate's own job. Fall
|
|
909
|
+
# back to the branch's last verdict so the gate runs; it stays on the remediation
|
|
910
|
+
# path and cannot merge on a stale conclusion.
|
|
911
|
+
mergeable_now=$(mergeable_state "$pr_number")
|
|
912
|
+
if [ "$mergeable_now" = "CONFLICTING" ]; then
|
|
913
|
+
ci=$(gh api "repos/$REPO/actions/runs?branch=$branch&per_page=100" \
|
|
914
|
+
--jq '[.workflow_runs[]
|
|
915
|
+
| select(
|
|
916
|
+
.name == "App: CI"
|
|
917
|
+
and .event == "pull_request"
|
|
918
|
+
and .status == "completed"
|
|
919
|
+
and (.conclusion == "success" or .conclusion == "failure")
|
|
920
|
+
)
|
|
921
|
+
] | sort_by(.created_at) | last // empty | [.id, .conclusion, .updated_at] | @tsv')
|
|
922
|
+
fi
|
|
923
|
+
if [ -z "$ci" ]; then
|
|
924
|
+
echo "::warning::PR #$pr_number head $head_sha has no completed App: CI run; the belt cannot gate it until CI runs."
|
|
925
|
+
continue
|
|
926
|
+
fi
|
|
927
|
+
echo "PR #$pr_number conflicts, so CI cannot run on $head_sha at all; gating on the branch's last verdict."
|
|
776
928
|
fi
|
|
777
929
|
|
|
778
930
|
IFS=$'\t' read -r ci_run_id ci_conclusion ci_finished <<<"$ci"
|
|
779
931
|
|
|
780
|
-
# A
|
|
781
|
-
#
|
|
782
|
-
#
|
|
783
|
-
#
|
|
784
|
-
|
|
785
|
-
|
|
932
|
+
# A verdict is a comment carrying BOTH the gate marker and a `**Verdict:**`
|
|
933
|
+
# line; progress comments and failed attempts deliberately carry neither
|
|
934
|
+
# matching pair, so a crashed or OOM-killed gate never masks a PR from
|
|
935
|
+
# later sweeps. A recorded verdict only counts when it is NEWER than the
|
|
936
|
+
# latest CI run; the old existence check made the gate one-shot per PR.
|
|
937
|
+
# Attempts are failed gates (the attempt marker) since this CI verdict.
|
|
938
|
+
latest_verdict=$(gh api "repos/$REPO/issues/$pr_number/comments?per_page=100" \
|
|
939
|
+
--jq '[.[] | select((.body | contains("<!-- agent-merge-gate -->")) and (.body | contains("**Verdict:**"))) | .created_at] | max // empty')
|
|
786
940
|
linked_issue=$(gh pr view "$pr_number" --repo "$REPO" --json closingIssuesReferences --jq '.closingIssuesReferences[0].number // empty' 2>/dev/null || true)
|
|
787
941
|
if [ -n "$linked_issue" ]; then
|
|
788
|
-
|
|
789
|
-
--jq '[.[] | select(.body | contains("<!-- agent-merge-gate -->")) | .created_at] | max // empty')
|
|
790
|
-
if [ -n "$
|
|
791
|
-
|
|
942
|
+
issue_verdict=$(gh api "repos/$REPO/issues/$linked_issue/comments?per_page=100" \
|
|
943
|
+
--jq '[.[] | select((.body | contains("<!-- agent-merge-gate -->")) and (.body | contains("**Verdict:**"))) | .created_at] | max // empty')
|
|
944
|
+
if [ -n "$issue_verdict" ] && { [ -z "$latest_verdict" ] || [ "$issue_verdict" \> "$latest_verdict" ]; }; then
|
|
945
|
+
latest_verdict="$issue_verdict"
|
|
792
946
|
fi
|
|
793
947
|
fi
|
|
794
|
-
if [ -n "$
|
|
795
|
-
echo "Merge gate verdict ($
|
|
948
|
+
if [ -n "$latest_verdict" ] && [ "$latest_verdict" \> "$ci_finished" ]; then
|
|
949
|
+
echo "Merge gate verdict ($latest_verdict) is newer than CI ($ci_finished) for PR #$pr_number, skipping"
|
|
950
|
+
continue
|
|
951
|
+
fi
|
|
952
|
+
|
|
953
|
+
attempts=$(gh api "repos/$REPO/issues/$pr_number/comments?per_page=100" \
|
|
954
|
+
--jq '[.[] | select(.body | contains("<!-- agent-merge-gate-attempt -->")) | select(.created_at > "'"$ci_finished"'") ] | length')
|
|
955
|
+
if [ -n "$linked_issue" ]; then
|
|
956
|
+
issue_attempts=$(gh api "repos/$REPO/issues/$linked_issue/comments?per_page=100" \
|
|
957
|
+
--jq '[.[] | select(.body | contains("<!-- agent-merge-gate-attempt -->")) | select(.created_at > "'"$ci_finished"'") ] | length')
|
|
958
|
+
attempts=$((attempts + issue_attempts))
|
|
959
|
+
fi
|
|
960
|
+
if [ "$attempts" -ge 6 ]; then
|
|
961
|
+
echo "PR #$pr_number already consumed $attempts gate attempts on this CI verdict, parking for review"
|
|
796
962
|
continue
|
|
797
963
|
fi
|
|
798
|
-
|
|
964
|
+
if gate_is_live "$pr_number"; then
|
|
965
|
+
echo "PR #$pr_number: a merge-gate run is already live; not dispatching another."
|
|
966
|
+
continue
|
|
967
|
+
fi
|
|
968
|
+
|
|
969
|
+
echo "Dispatching Merge Gate for PR #$pr_number ($ci_conclusion, run $ci_run_id, attempt $((attempts + 1)) of 6)"
|
|
799
970
|
gh api "repos/$REPO/actions/workflows/work-router.yml/dispatches" \
|
|
800
971
|
--method POST \
|
|
801
972
|
-f ref="$DEFAULT_BRANCH" \
|
|
802
973
|
-f "inputs[operation]=merge-gate" \
|
|
803
974
|
-f "inputs[pr-number]=$pr_number" \
|
|
804
975
|
-f "inputs[ci-conclusion]=$ci_conclusion" \
|
|
805
|
-
-f "inputs[ci-run-id]=$ci_run_id"
|
|
976
|
+
-f "inputs[ci-run-id]=$ci_run_id" \
|
|
977
|
+
-f "inputs[attempts_so_far]=$attempts"
|
|
806
978
|
done
|
|
807
979
|
|
|
808
980
|
audit-close:
|
|
@@ -816,6 +988,10 @@ jobs:
|
|
|
816
988
|
permissions:
|
|
817
989
|
contents: read
|
|
818
990
|
issues: write
|
|
991
|
+
# It reads the open pull requests before closing a report, so a report whose
|
|
992
|
+
# implement workers are still in review is left alone. Without this the list
|
|
993
|
+
# call answers 403 and the nightly job fails having closed nothing.
|
|
994
|
+
pull-requests: read
|
|
819
995
|
steps:
|
|
820
996
|
- name: Checkout workflow actions
|
|
821
997
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|