-
-
Notifications
You must be signed in to change notification settings - Fork 0
202 lines (194 loc) · 8.21 KB
/
Copy pathpr-gate.yml
File metadata and controls
202 lines (194 loc) · 8.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
name: pr-gate
# Server-side mirror of the local pre-commit hook's enforcement.
# Closes Web UI bypass paths for branch-scope and base-branch rules.
#
# Triggers on every pull_request event (no branch filter) so a PR with
# the wrong base — e.g. culture/germany -> main bypassing culture/release —
# is also caught here. The base-branch-check job decides if the head/base
# pair is even allowed.
#
# Adding a check: define a new job below and run a tests/validate_pr_*.py
# helper. Keep CI logic in Python (testable) and yaml as orchestration only.
#
# The report job turns a rejection into instruction: it posts the
# validators' prescriptive remedy as a PR comment, writes it to the job
# summary, and fails its own check when routing is broken. The remedy
# then reaches an agent that opened the PR via the GitHub API whether it
# reads the check conclusion, the run log, the job summary, or the PR
# comment -- four channels, because the failure has to be self-fixable.
#
# Note on the validation stamp: this workflow used to include a third job
# (stamp-tree-hash-check) that verified .validation-stamp equalled the
# tree hash of HEAD. That gate was dropped because API-driven PR pushes
# (via the GitHub MCP / web UI) cannot run the local pre-commit hook to
# refresh the stamp, which created persistent friction. The remaining
# protection is the cultures - Validation stamp gate in validate.yml
# (file existence check, regions-only) plus every actual validator job in
# validate.yml. Branch protection still requires those checks before merge.
on:
pull_request:
permissions:
contents: read
jobs:
base-branch-check:
name: pr-gate - Allowed base for head
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Verify PR base matches head's allowed target
run: |
python3 tests/validate_pr_base.py \
"${{ github.head_ref }}" \
"${{ github.base_ref }}"
branch-scope-check:
name: pr-gate - Diff within branch scope
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Need history back to the merge base so `base...HEAD` works.
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Verify diff stays inside head branch's allowed paths
run: |
python3 tests/validate_pr_scope.py \
"${{ github.head_ref }}" \
"origin/${{ github.base_ref }}"
brownfield-check:
name: pr-gate - Brownfield migration ("if you touch, you migrate")
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Need history back to the merge base so `base...HEAD` works,
# same reason as branch-scope-check.
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install pyyaml
- name: Verify touched files are on the khai-lexicon schema
run: |
python3 tests/validate_pr_brownfield.py \
"origin/${{ github.base_ref }}"
report:
name: pr-gate - Routing remedy
needs: [base-branch-check, branch-scope-check]
# Always runs for same-repo PRs: on failure it posts the prescriptive fix
# as a PR comment (so an agent that opened the PR via the API still sees
# it); on success it marks any earlier remedy comment resolved. Forked PRs
# get a read-only token, so they are skipped -- they still get the check
# failures and the step summary.
if: always() && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build routing remedy
id: build
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
run: |
# Capture both validators WITHOUT aborting. GitHub runs `run:`
# blocks under `set -e`, so a failing command substitution
# (var=$(... exit 1)) kills the step -- exactly the
# routing-rejected case this job exists to report. Drop `set -e`
# around the captures; keep the recorded exit codes.
set +e
base_out=$(python3 tests/validate_pr_base.py "$HEAD_REF" "$BASE_REF" 2>&1)
base_rc=$?
scope_out=$(python3 tests/validate_pr_scope.py "$HEAD_REF" "origin/$BASE_REF" 2>&1)
scope_rc=$?
set -e
if [ "$base_rc" -eq 0 ] && [ "$scope_rc" -eq 0 ]; then
echo "status=pass" >> "$GITHUB_OUTPUT"
else
echo "status=fail" >> "$GITHUB_OUTPUT"
fi
{
echo "## pr-gate routing"
echo
echo "Branch routing is decided by the *operation*, not the file types a"
echo "change happens to touch. A failure here is fixed by re-routing the"
echo "PR (branch kind / base), never by editing validators or workflows."
echo
echo "### Allowed base for head"
echo '```'
echo "$base_out"
echo '```'
echo "### Diff within branch scope"
echo '```'
echo "$scope_out"
echo '```'
echo
echo "Pre-flight next time, before opening the PR:"
echo '```'
echo "python tests/branch_scope.py check-pr --head <head> --base <base>"
echo '```'
echo "(or \`advise --op <operation>\` before \`git checkout -b\`)."
echo "A failure says whether it is fixable in place -- a wrong base"
echo "(retarget the PR) or a wrong head (close and re-open: a PR's"
echo "head branch cannot be changed)."
} > remedy.md
# On a routing FAILURE, surface the remedy in the run log and
# the job summary too -- actionable from the Actions UI and the
# API, not only from the PR comment.
if [ "$base_rc" -ne 0 ] || [ "$scope_rc" -ne 0 ]; then
cat remedy.md
cat remedy.md >> "$GITHUB_STEP_SUMMARY"
fi
- name: Post or update PR comment
uses: actions/github-script@v7
env:
STATUS: ${{ steps.build.outputs.status }}
with:
script: |
const fs = require('fs');
const marker = '<!-- pr-gate-remedy -->';
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (process.env.STATUS === 'pass') {
if (existing) {
await github.rest.issues.updateComment({
owner, repo, comment_id: existing.id,
body: marker + '\n## pr-gate routing\n\n✅ Routing checks pass.',
});
}
return;
}
const body = marker + '\n' + fs.readFileSync('remedy.md', 'utf8');
if (existing) {
await github.rest.issues.updateComment({
owner, repo, comment_id: existing.id, body,
});
} else {
await github.rest.issues.createComment({
owner, repo, issue_number, body,
});
}
# The remedy is in the run log, the job summary, and the PR comment
# by now -- this step makes "pr-gate - Routing remedy" itself go red
# so a routing failure cannot be missed by a glance at the check list.
- name: Fail when routing is broken
if: steps.build.outputs.status == 'fail'
run: |
echo "::error title=pr-gate routing rejected this PR::Branch/base routing is invalid. The prescriptive fix is in this job summary, the run log above, and a PR comment. Re-route the PR (branch kind / base) -- do not edit validators or workflows."
exit 1