Repository navigation
307 lines (301 loc) · 18.4 KB
/
Copy pathpr-area-label.yml
File metadata and controls
307 lines (301 loc) · 18.4 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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
name: PR Area Label (reusable)
# Reusable agentic PR area-label classifier. Two responsibilities, one taxonomy that lives
# in the CONSUMER repo (default .github/area-labels.yml):
#
# sync-labels — keeps the consumer's `area:*` labels standardized. Runs on push to the
# consumer's default branch (the caller filters to the taxonomy path) or a
# manual full-sync, and create-or-updates every label from the YAML with
# `gh label create --force` — both the classified `labels[]` and the optional
# `sub_labels[]`. Edit the YAML, never the GitHub UI — the UI drifts, this
# heals. Needs no secret.
#
# label-pr — classifies each PR into exactly one `area:*` label. An LLM does the
# classification, but it is given NO tools and NO token: PR metadata is
# passed to the Anthropic Messages API as data inside <pr_data> tags, the
# reply is constrained by a JSON-schema enum to the taxonomy's own names, and
# a deterministic step applies the result with targeted `area:*` add/remove
# ops (never a full-set PUT). So untrusted PR text can never steer a
# privileged tool call — the worst a malicious PR earns itself is a (valid)
# area label. The taxonomy is read from the PR's BASE ref, never its head, so
# a PR cannot rewrite the rules that classify it.
#
# The taxonomy's optional `sub_labels[]` ride ALONGSIDE that one area: `area:*`
# names carrying `paths:` globs, matched against the PR's changed files. No
# model sees them — a subsystem inside another area's service is a path
# question, not a judgement call — and matches are exempt from the one-area
# cleanup, which retires them again once a PR stops matching. Their names are
# held to the same `area:*` shape and must be disjoint from `labels[]`, so
# every write this workflow makes stays inside the `area:*` namespace.
#
# The classifier logic and scripts load from THIS repo at the pinned `workflows_ref` — never
# from the classified PR's checkout (no PR code is checked out at all). See scripts/area-label/.
#
# WHY A REUSABLE. This design was first shipped in a private caller repo as a repo-local
# workflow; it moved here so every caller shares ONE hardened implementation and keeps only
# a thin caller + its own taxonomy (the risk-grader pattern).
# Per-repo framing (the domain-vs-path judgement calls) lives in the taxonomy's optional
# top-level `repo_context:` key, so nothing repo-specific is baked into this file.
#
# PERMISSIONS the caller must grant (union of both jobs — GitHub validates every nested job's
# declared permissions against the caller at STARTUP, before any `if:` runs):
# contents: read # sync reads the taxonomy from the checkout; label-pr reads none
# issues: write # sync-labels: labels are an issues API surface
# pull-requests: write # label-pr: apply the label
# A short grant fails the whole run with an opaque "workflow file issue" and zero jobs.
#
# SECRET: ANTHROPIC_API_KEY (optional). label-pr needs it; if unset it fails soft (warns,
# touches nothing). sync-labels needs no secret. Comfy-Org provides it org-wide.
on:
workflow_call:
inputs:
workflows_ref:
description: >-
Full 40-hex commit SHA of THIS repo to load the classifier scripts from. Pin to
the SAME SHA as the caller's `uses:` — a floating ref would run new scripts under an
old workflow. Required; validated to be a full lowercase SHA before checkout.
type: string
required: true
taxonomy_path:
description: Path of the consumer repo's area-label taxonomy YAML.
type: string
required: false
default: .github/area-labels.yml
model:
description: Anthropic model id for classification.
type: string
required: false
default: claude-opus-4-8
pr_number:
description: >-
For a manual dispatch, the PR to (re)classify. Leave empty on a push/pull_request
run. A dispatch WITH a number classifies; WITHOUT one runs a full label sync.
type: string
required: false
default: ''
dry_run:
description: Classify and log the decision without applying the label.
type: boolean
required: false
default: false
secrets:
ANTHROPIC_API_KEY:
description: Anthropic API key for the classifier. Omit to run sync-only; label-pr fails soft.
required: false
permissions: {}
concurrency:
# One classification per PR; newer commits cancel the in-flight run.
group: pr-area-label-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}
cancel-in-progress: true
jobs:
sync-labels:
# Sync on push to the consumer's default branch (the caller filters to the taxonomy
# path), or a manual dispatch with no PR number.
if: >-
github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && inputs.pr_number == '' && inputs.dry_run != true)
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read # checkout the taxonomy file
issues: write # labels are an issues API surface
steps:
- name: Require a pinned workflows_ref
# workflows_ref runs THIS repo's scripts in a job holding the caller's write token +
# the ANTHROPIC secret, so it is a code-execution trust boundary. GitHub does NOT
# enforce `required: true` for workflow_call inputs (an omitted input arrives as '' and
# checkout silently takes the default branch), so validate on TWO axes before checkout:
# SHAPE — a full 40-hex lowercase SHA; rejects everything mutable (branch, tag,
# refs/pull/N/head), which could skew between jobs mid-run.
# ANCESTRY — the SHA is reachable from THIS repo's upstream main. Shape proves the
# value is immutable, not WHICH repo authored it: a fork of this PUBLIC repo
# shares the object store and GitHub serves a fork PR's head objects from
# this repo's URL unauthenticated, so a fork-authored 40-hex SHA is just as
# well-shaped and would otherwise run with the caller's token. Only merged
# upstream history may be pinned; no opt-out (an opt-out would be set by the
# pin-bump PR the check distrusts). Fails CLOSED on infra errors. Mirrors
# pr-risk.yml's guard. See BE-5546.
env:
WORKFLOWS_REF: ${{ inputs.workflows_ref }}
run: |
set -euo pipefail
# actions/checkout trims `ref`, so strip whitespace before the -z test. Echo only a
# sanitized, bounded form so a multi-line value can't forge ::workflow commands:: in
# this PUBLIC log.
REF="$(printf '%s' "$WORKFLOWS_REF" | tr -d '[:space:]')"
if [ -z "$REF" ]; then
echo "::error::workflows_ref is required; pin it to the same commit SHA as the uses: line (see the caller doc)"
exit 1
fi
safe_ref=$(printf '%s' "$REF" | tr -c 'A-Za-z0-9._/-' '?'); safe_ref=${safe_ref:0:64}
# Length + character-class, not `grep -Eq '^..$'`: grep anchors per LINE, so a
# multi-line value carrying one SHA-shaped line would pass. Lowercase only — the value
# is handed to checkout verbatim and everything that emits a SHA emits lowercase.
if [ "${#REF}" -ne 40 ] || [ -n "${REF//[0-9a-f]/}" ]; then
echo "::error::workflows_ref must be a full 40-hex lowercase commit SHA of Comfy-Org/github-workflows (got '${safe_ref}'). Branches, tags and PR-head refs are mutable and are rejected before checkout."
exit 1
fi
# ANCESTRY. The upstream URL is a LITERAL: inside a reusable workflow no github context
# names this repo, so a variable would be an alias a fork could point at itself. Two
# bounded fetches (--filter=blob:none — no file content): depth-1 proves the SHA is in
# this repo's object network; the second brings main to walk down to the pin. The fetch
# retries (a transient github.com error must not red the fleet); the verdict does not.
scratch=$(mktemp -d) || { echo "::error::ancestry check for workflows_ref (${safe_ref}) could not allocate scratch space; failing closed."; exit 1; }
trap 'rm -rf "$scratch"' EXIT
git init -q "$scratch" || { echo "::error::ancestry check for workflows_ref (${safe_ref}) could not init a scratch repo; failing closed."; exit 1; }
fetch_upstream() {
for attempt in 1 2 3; do
if timeout 40 git -C "$scratch" -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=30 \
fetch --quiet --no-tags --filter=blob:none "$@"; then
return 0
fi
[ "$attempt" -lt 3 ] && sleep $((attempt * 5))
done
return 1
}
if ! fetch_upstream --depth=1 https://github.com/Comfy-Org/github-workflows "$REF"; then
echo "::error::workflows_ref (${safe_ref}) could not be fetched from Comfy-Org/github-workflows after 3 attempts; failing closed."
exit 1
fi
if ! fetch_upstream https://github.com/Comfy-Org/github-workflows +refs/heads/main:refs/heads/upstream-main; then
echo "::error::main of Comfy-Org/github-workflows could not be read after 3 attempts, so workflows_ref (${safe_ref}) cannot be shown merged upstream; failing closed."
exit 1
fi
# --is-ancestor has THREE outcomes: 0 yes, 1 no, >1 git could not answer (infra fault).
# Don't report an infra fault as a hostile pin; both still fail closed, only the message differs.
ancestry_rc=0
GIT_NO_LAZY_FETCH=1 git -C "$scratch" merge-base --is-ancestor "$REF" upstream-main || ancestry_rc=$?
if [ "$ancestry_rc" -gt 1 ]; then
echo "::error::the ancestry test for workflows_ref (${safe_ref}) could not COMPLETE (git error, not a verdict); failing closed."
exit 1
fi
if [ "$ancestry_rc" -ne 0 ]; then
echo "::error::workflows_ref (${safe_ref}) is not an ancestor of Comfy-Org/github-workflows main. A well-shaped SHA can still be fork-authored; only merged upstream commits may be pinned. If this is a genuine just-merged pin, re-run."
exit 1
fi
- name: Checkout consumer repo (taxonomy)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
- name: Checkout classifier scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Comfy-Org/github-workflows
ref: ${{ inputs.workflows_ref }}
path: .area-label-tools
persist-credentials: false
- name: Standardize area:* labels from taxonomy
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAXONOMY_FILE: ${{ inputs.taxonomy_path }}
run: bash .area-label-tools/scripts/area-label/sync-labels.sh
label-pr:
# Only classify trusted PRs. Fork PRs run without a writable token and without the
# ANTHROPIC secret; Dependabot PRs run without Actions secrets — both would fail rather
# than label, so skip them here. workflow_dispatch with a PR number is the writable
# manual fallback. Guarding inside the reusable keeps the caller a bare `uses:`.
if: >-
(github.event_name == 'pull_request' &&
github.event.pull_request.user.login != 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.pr_number != '')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read # scripts checkout only; PR code is never checked out
pull-requests: write # apply the label
steps:
- name: Require a pinned workflows_ref
# workflows_ref runs THIS repo's scripts in a job holding the caller's write token +
# the ANTHROPIC secret, so it is a code-execution trust boundary. GitHub does NOT
# enforce `required: true` for workflow_call inputs (an omitted input arrives as '' and
# checkout silently takes the default branch), so validate on TWO axes before checkout:
# SHAPE — a full 40-hex lowercase SHA; rejects everything mutable (branch, tag,
# refs/pull/N/head), which could skew between jobs mid-run.
# ANCESTRY — the SHA is reachable from THIS repo's upstream main. Shape proves the
# value is immutable, not WHICH repo authored it: a fork of this PUBLIC repo
# shares the object store and GitHub serves a fork PR's head objects from
# this repo's URL unauthenticated, so a fork-authored 40-hex SHA is just as
# well-shaped and would otherwise run with the caller's token. Only merged
# upstream history may be pinned; no opt-out (an opt-out would be set by the
# pin-bump PR the check distrusts). Fails CLOSED on infra errors. Mirrors
# pr-risk.yml's guard. See BE-5546.
env:
WORKFLOWS_REF: ${{ inputs.workflows_ref }}
run: |
set -euo pipefail
# actions/checkout trims `ref`, so strip whitespace before the -z test. Echo only a
# sanitized, bounded form so a multi-line value can't forge ::workflow commands:: in
# this PUBLIC log.
REF="$(printf '%s' "$WORKFLOWS_REF" | tr -d '[:space:]')"
if [ -z "$REF" ]; then
echo "::error::workflows_ref is required; pin it to the same commit SHA as the uses: line (see the caller doc)"
exit 1
fi
safe_ref=$(printf '%s' "$REF" | tr -c 'A-Za-z0-9._/-' '?'); safe_ref=${safe_ref:0:64}
# Length + character-class, not `grep -Eq '^..$'`: grep anchors per LINE, so a
# multi-line value carrying one SHA-shaped line would pass. Lowercase only — the value
# is handed to checkout verbatim and everything that emits a SHA emits lowercase.
if [ "${#REF}" -ne 40 ] || [ -n "${REF//[0-9a-f]/}" ]; then
echo "::error::workflows_ref must be a full 40-hex lowercase commit SHA of Comfy-Org/github-workflows (got '${safe_ref}'). Branches, tags and PR-head refs are mutable and are rejected before checkout."
exit 1
fi
# ANCESTRY. The upstream URL is a LITERAL: inside a reusable workflow no github context
# names this repo, so a variable would be an alias a fork could point at itself. Two
# bounded fetches (--filter=blob:none — no file content): depth-1 proves the SHA is in
# this repo's object network; the second brings main to walk down to the pin. The fetch
# retries (a transient github.com error must not red the fleet); the verdict does not.
scratch=$(mktemp -d) || { echo "::error::ancestry check for workflows_ref (${safe_ref}) could not allocate scratch space; failing closed."; exit 1; }
trap 'rm -rf "$scratch"' EXIT
git init -q "$scratch" || { echo "::error::ancestry check for workflows_ref (${safe_ref}) could not init a scratch repo; failing closed."; exit 1; }
fetch_upstream() {
for attempt in 1 2 3; do
if timeout 40 git -C "$scratch" -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=30 \
fetch --quiet --no-tags --filter=blob:none "$@"; then
return 0
fi
[ "$attempt" -lt 3 ] && sleep $((attempt * 5))
done
return 1
}
if ! fetch_upstream --depth=1 https://github.com/Comfy-Org/github-workflows "$REF"; then
echo "::error::workflows_ref (${safe_ref}) could not be fetched from Comfy-Org/github-workflows after 3 attempts; failing closed."
exit 1
fi
if ! fetch_upstream https://github.com/Comfy-Org/github-workflows +refs/heads/main:refs/heads/upstream-main; then
echo "::error::main of Comfy-Org/github-workflows could not be read after 3 attempts, so workflows_ref (${safe_ref}) cannot be shown merged upstream; failing closed."
exit 1
fi
# --is-ancestor has THREE outcomes: 0 yes, 1 no, >1 git could not answer (infra fault).
# Don't report an infra fault as a hostile pin; both still fail closed, only the message differs.
ancestry_rc=0
GIT_NO_LAZY_FETCH=1 git -C "$scratch" merge-base --is-ancestor "$REF" upstream-main || ancestry_rc=$?
if [ "$ancestry_rc" -gt 1 ]; then
echo "::error::the ancestry test for workflows_ref (${safe_ref}) could not COMPLETE (git error, not a verdict); failing closed."
exit 1
fi
if [ "$ancestry_rc" -ne 0 ]; then
echo "::error::workflows_ref (${safe_ref}) is not an ancestor of Comfy-Org/github-workflows main. A well-shaped SHA can still be fork-authored; only merged upstream commits may be pinned. If this is a genuine just-merged pin, re-run."
exit 1
fi
- name: Checkout classifier scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Comfy-Org/github-workflows
ref: ${{ inputs.workflows_ref }}
path: .area-label-tools
persist-credentials: false
- name: Classify and apply the area label
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
# Trust the taxonomy from the base branch, never the PR's own copy. For a manual
# dispatch there is no PR base, so read it from the ref the run is on.
BASE_REF: ${{ github.event.pull_request.base.sha || github.sha }}
TAXONOMY_PATH: ${{ inputs.taxonomy_path }}
MODEL: ${{ inputs.model }}
DRY_RUN: ${{ inputs.dry_run }}
run: bash .area-label-tools/scripts/area-label/classify-and-apply.sh