Skip to content

Commit a014cd5

Browse files
committed
Reconcile pull request labels from the default branch
Replace the fork-approval-gated dispatcher with frequent trusted scheduled reconciliation and stable hourly recovery shards. Revalidate each open pull request and head SHA immediately before applying labels without checking out pull request code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 512eb347-ec89-4250-8bf1-87048974b01d
1 parent 862b71a commit a014cd5

2 files changed

Lines changed: 136 additions & 108 deletions

File tree

.github/workflows/labeler-apply.yml

Lines changed: 0 additions & 100 deletions
This file was deleted.

.github/workflows/labeler.yml

Lines changed: 136 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,144 @@
11
name: "Pull Request Labeler"
22

33
on:
4-
pull_request:
5-
types: [opened, synchronize, reopened]
4+
schedule:
5+
# Reconcile recently updated PRs promptly, including unapproved forks and
6+
# conflicted PRs for which pull_request workflows do not run.
7+
- cron: "7,22,37,52 * * * *"
8+
# Reconcile one stable shard of all open PRs each hour to recover from
9+
# delayed or missed scheduled runs.
10+
- cron: "12 * * * *"
11+
workflow_dispatch:
12+
inputs:
13+
pr_number:
14+
description: "Open pull request number to reconcile"
15+
required: true
16+
type: string
617

7-
permissions:
8-
contents: read
9-
pull-requests: read
18+
permissions: {}
19+
20+
concurrency:
21+
group: pull-request-labeler
22+
cancel-in-progress: false
1023

1124
jobs:
12-
request-labels:
25+
triage:
26+
if: github.ref_name == github.event.repository.default_branch
1327
runs-on: ubuntu-latest
28+
timeout-minutes: 30
29+
permissions:
30+
contents: read
31+
pull-requests: write
1432
steps:
15-
- name: Request privileged labeling
16-
run: echo "Labels are applied by the workflow_run writer after re-validating the pull request."
33+
- uses: actions/checkout@v5
34+
with:
35+
persist-credentials: false
36+
sparse-checkout: .github/labeler.yml
37+
sparse-checkout-cone-mode: false
38+
39+
- name: Collect pull requests to reconcile
40+
id: collect
41+
env:
42+
GH_TOKEN: ${{ github.token }}
43+
REPO: ${{ github.repository }}
44+
EVENT_NAME: ${{ github.event_name }}
45+
SCHEDULE: ${{ github.event.schedule }}
46+
REQUESTED_PR: ${{ inputs.pr_number }}
47+
run: |
48+
set -euo pipefail
49+
50+
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
51+
if [[ ! "$REQUESTED_PR" =~ ^[1-9][0-9]*$ ]]; then
52+
echo "Invalid pull request number: $REQUESTED_PR"
53+
exit 1
54+
fi
55+
56+
pr_json=$(gh api "repos/$REPO/pulls/$REQUESTED_PR")
57+
candidates=$(jq -c '[{
58+
number: .number,
59+
head_sha: .head.sha
60+
}]' <<<"$pr_json")
61+
else
62+
pulls_json=$(gh api --paginate \
63+
"repos/$REPO/pulls?state=open&sort=updated&direction=desc&per_page=100" |
64+
jq -cs 'add')
65+
66+
if [ "$SCHEDULE" = "12 * * * *" ]; then
67+
shard=$(( ($(date -u +%s) / 3600) % 6 ))
68+
candidates=$(jq -c --argjson shard "$shard" \
69+
'[.[] | select((.number % 6) == $shard) | {
70+
number: .number,
71+
head_sha: .head.sha
72+
}]' <<<"$pulls_json")
73+
else
74+
cutoff=$(date -u -d "1 hour ago" "+%Y-%m-%dT%H:%M:%SZ")
75+
# Hourly shards reconcile any candidates beyond this API budget.
76+
candidates=$(jq -c --arg cutoff "$cutoff" \
77+
'[.[] | select(.updated_at >= $cutoff) | {
78+
number: .number,
79+
head_sha: .head.sha
80+
}][0:100]' <<<"$pulls_json")
81+
fi
82+
fi
83+
84+
echo "Collected $(jq 'length' <<<"$candidates") pull request(s)."
85+
{
86+
echo "candidates<<EOF"
87+
echo "$candidates"
88+
echo "EOF"
89+
} >> "$GITHUB_OUTPUT"
90+
91+
- name: Validate pull request state
92+
id: validate
93+
env:
94+
GH_TOKEN: ${{ github.token }}
95+
REPO: ${{ github.repository }}
96+
CANDIDATES: ${{ steps.collect.outputs.candidates }}
97+
run: |
98+
set -euo pipefail
99+
100+
valid_numbers=()
101+
while IFS=$'\t' read -r pr_number expected_sha; do
102+
if [[ ! "$pr_number" =~ ^[1-9][0-9]*$ ]] ||
103+
[[ ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then
104+
echo "Skipping malformed pull request candidate."
105+
continue
106+
fi
107+
108+
if ! pr_json=$(gh api "repos/$REPO/pulls/$pr_number"); then
109+
echo "Pull request #$pr_number could not be fetched; skipping."
110+
continue
111+
fi
112+
113+
if ! jq -e \
114+
--arg repo "$REPO" \
115+
--arg sha "$expected_sha" \
116+
'.state == "open" and
117+
.base.repo.full_name == $repo and
118+
.head.sha == $sha and
119+
(.head.repo.full_name | type == "string")' \
120+
>/dev/null <<<"$pr_json"; then
121+
echo "Pull request #$pr_number changed or is no longer open; skipping."
122+
continue
123+
fi
124+
125+
valid_numbers+=("$pr_number")
126+
done < <(jq -r '.[] | [.number, .head_sha] | @tsv' <<<"$CANDIDATES")
127+
128+
if [ "${#valid_numbers[@]}" -eq 0 ]; then
129+
echo "has_prs=false" >> "$GITHUB_OUTPUT"
130+
exit 0
131+
fi
132+
133+
{
134+
echo "has_prs=true"
135+
echo "pr_numbers<<EOF"
136+
printf '%s\n' "${valid_numbers[@]}"
137+
echo "EOF"
138+
} >> "$GITHUB_OUTPUT"
139+
140+
- uses: actions/labeler@v4
141+
if: steps.validate.outputs.has_prs == 'true'
142+
with:
143+
repo-token: "${{ github.token }}"
144+
pr-number: ${{ steps.validate.outputs.pr_numbers }}

0 commit comments

Comments
 (0)