-
Notifications
You must be signed in to change notification settings - Fork 32
203 lines (184 loc) · 9.27 KB
/
Copy pathauto-merge.yml
File metadata and controls
203 lines (184 loc) · 9.27 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
203
name: auto-merge
# Tier-based auto-merge for safe-tier-1 / safe-tier-2 PRs.
#
# A PR is eligible to merge iff ALL of:
# 1. State == OPEN, mergeable, not draft, not from a fork.
# 2. Has label `safe-tier-1` (or `safe-tier-2` and the operator has set
# repo variable AUTOPILOT_TIER2_ENABLED == 'true').
# The label-as-contract is enforced by .github/workflows/
# tier-label-guard.yml: anyone outside TIER_LABEL_TRUSTED_LABELERS
# who applies a tier label has it stripped immediately. By the time
# this workflow sees the label, a trusted author has signed for it.
# 3. Filename is NOT in FORBIDDEN_PATHS_REGEX (re-verified by
# `python -m scripts.lib.tier_policy validate-fork-pr`, since the
# PR head might have been updated after the label was applied).
# 4. Diff content does NOT match FORBIDDEN_DIFF_CONTENT_REGEX.
# 5. Tier-2 only: diff size <= TIER2_MAX_ADDITIONS LOC and
# <= TIER2_MAX_FILES files (re-verified at this step).
# 6. Tier-2 only: non-translation/docs PRs update CHANGES-vs-upstream.md.
#
# If all of the above pass we call `gh pr merge --auto --squash`. GitHub
# itself then waits for required status checks (per branch protection)
# to become green before merging. We do NOT poll required checks here;
# that responsibility is delegated to GitHub. The list of required
# checks per tier (TIER{1,2}_REQUIRED_CHECKS in tier-policy.config) is
# the documented contract for what branch protection should require —
# `Test Suite Summary` is the unified gate (it knows about tier-2 via
# the PR's labels and fails when Integration Tests didn't pass on a
# tier-2 PR).
#
# All numeric/regex policy values live in .github/policy/tier-policy.config.
# Anything else: the workflow comments why and stops. It never bypasses
# checks, never merges without a label, never `--admin`-overrides.
on:
pull_request:
types: [labeled, synchronize, reopened]
permissions:
pull-requests: write
contents: write
checks: read
concurrency:
group: auto-merge-tier-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
evaluate:
# Bail early for fork PRs — `gh pr merge --auto` won't work for them
# anyway (you can't enable auto-merge from outside the head repo),
# and we want one clear refusal line in the log rather than letting
# the later steps fail in confusing ways.
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout main (for tier-policy.config + scripts/lib)
# Critical: ref must be main, NOT the PR head. The policy file
# and validator module used to gate the PR are always the ones
# currently on main; a malicious PR cannot widen its own auto-
# merge rules by editing the policy or shadowing the module in
# its branch.
uses: actions/checkout@v7
with:
ref: main
fetch-depth: 1
- name: Set up Python (for scripts.lib.tier_policy)
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Evaluate and (maybe) enable auto-merge
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
TIER2_ENABLED: ${{ vars.AUTOPILOT_TIER2_ENABLED }}
run: |
set -euo pipefail
if [ ! -f .github/policy/tier-policy.config ]; then
echo "::error::missing .github/policy/tier-policy.config — auto-merge cannot run safely"
exit 1
fi
comment_and_skip() {
local reason="$1"
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "🤖 auto-merge skipped: $reason"
echo "skip #$PR_NUMBER: $reason"
exit 0
}
echo "=== evaluating PR #$PR_NUMBER ==="
META=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json state,mergeable,labels,files,headRefName,headRefOid,title,isDraft)
state=$(jq -r .state <<<"$META")
mergeable=$(jq -r .mergeable <<<"$META")
is_draft=$(jq -r .isDraft <<<"$META")
labels=$(jq -r '.labels[].name' <<<"$META" | tr '\n' ',')
if [ "$state" != "OPEN" ]; then
echo "PR state=$state, skipping"; exit 0
fi
if [ "$is_draft" = "true" ]; then
echo "PR is draft, skipping"; exit 0
fi
if [ "$mergeable" != "MERGEABLE" ]; then
echo "PR mergeable=$mergeable, skipping"; exit 0
fi
# Tier label gate. tier-label-guard.yml ensures the label was
# applied by a trusted account; if a stale safe-tier label
# somehow survives a force-push (rare — tier-label-guard.yml
# fires per-labeling, so a `synchronize` event that re-adds a
# label wouldn't re-trigger the guard), the validate step
# below catches forbidden content regardless.
if [[ ",$labels," == *",safe-tier-1,"* ]]; then
tier="safe-tier-1"
elif [[ ",$labels," == *",safe-tier-2,"* ]]; then
if [ "${TIER2_ENABLED:-false}" != "true" ]; then
echo "PR has safe-tier-2 but AUTOPILOT_TIER2_ENABLED!=true, skipping"
exit 0
fi
tier="safe-tier-2"
else
echo "PR has no safe-tier label, skipping"
exit 0
fi
echo "tier=$tier"
# ─── Run validation through the shared policy module ───────
# scripts/lib/tier_policy.py is the single source of truth.
# It owns: forbidden-path filename check, tier-2 LOC/file cap
# check, diff-content sensitive-token scan. The CLI returns
# JSON with `ok` + `category` + `reason`.
pr_json=$(mktemp)
diff_text=$(mktemp)
# `--json additions,files` mirrors what validate_fork_pr expects.
gh pr view "$PR_NUMBER" --repo "$REPO" --json additions,changedFiles,files > "$pr_json"
gh pr diff "$PR_NUMBER" --repo "$REPO" > "$diff_text"
result=$(python -m scripts.lib.tier_policy validate-fork-pr \
--tier "$tier" "$pr_json" "$diff_text")
rm -f "$pr_json" "$diff_text"
echo "validate-fork-pr result: $result"
ok=$(jq -r '.ok' <<<"$result")
category=$(jq -r '.category' <<<"$result")
reason=$(jq -r '.reason' <<<"$result")
if [ "$ok" != "true" ]; then
# Demote to needs-review and stop. tier-2 demotion strips
# both labels because the operator may have intended tier-1
# but the validator caught something requiring eyes.
gh pr edit "$PR_NUMBER" --repo "$REPO" \
--remove-label safe-tier-1 --remove-label safe-tier-2 \
--add-label needs-review || true
comment_and_skip "$reason"
fi
# ─── Tier-2 only: CHANGES-vs-upstream.md update required ───
# Tier-2 PRs that touch any non-doc, non-po file MUST also
# update CHANGES-vs-upstream.md (CLAUDE.md hard rule for
# tracking divergence). Tier-1 (.po + *.md only) is exempt.
if [ "$tier" = "safe-tier-2" ]; then
files=$(jq -r '.files[].path' <<<"$META")
has_code=0
has_changes_update=0
while IFS= read -r f; do
# Case patterns match positionally — the explicit
# CHANGES-vs-upstream.md branch MUST precede *.md or
# the wildcard wins and `has_changes_update` never
# gets set, demoting every tier-2 PR that correctly
# updates CHANGES-vs-upstream.md.
case "$f" in
CHANGES-vs-upstream.md) has_changes_update=1 ;;
*.po|*.pot) ;;
*.md|README*) ;;
*) has_code=1 ;;
esac
done <<<"$files"
if [ "$has_code" = "1" ] && [ "$has_changes_update" = "0" ]; then
comment_and_skip "tier-2 PRs with code changes must update CHANGES-vs-upstream.md to document the divergence. Add a row, push, and CI will re-evaluate."
fi
fi
# ─── Enable auto-merge ────────────────────────────────────
# We do NOT poll required checks here. GitHub waits for them
# via branch protection (validate-author + Fast Tests + Test
# Suite Summary required; the summary is tier-aware and fails
# on integration-tests failure for safe-tier-2 PRs). This is
# the workflow_run polling loop that the old version did, but
# delegated to GitHub itself — fewer places for the state
# machine to live, no more race-on-completion bugs.
#
# --auto requires the repo to have "Allow auto-merge" enabled
# in settings (it is).
echo "Enabling auto-merge for PR #$PR_NUMBER under $tier"
gh pr merge "$PR_NUMBER" --repo "$REPO" --auto --squash --delete-branch
gh pr comment "$PR_NUMBER" --repo "$REPO" \
--body "🤖 auto-merge enabled under tier \`$tier\`. GitHub will merge once required status checks complete."