-
Notifications
You must be signed in to change notification settings - Fork 358
176 lines (169 loc) · 6.89 KB
/
Copy pathauto-label.yml
File metadata and controls
176 lines (169 loc) · 6.89 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
# Auto-label issues and PRs.
#
# Two passes per item:
# 1. LLM classification (Claude Haiku) over title + body for engine::*,
# area::*, runtime::*, component, and type labels. Runs once per
# newly opened issue/PR.
# 2. Deterministic priority::warm / priority::hot from community
# engagement counts (commenters + supporting reactions, excluding
# maintainers). Refreshed on every new comment (engagement event)
# and via a daily cron (to pick up reaction-only changes, which
# don't fire webhook events).
#
# *** Labels applied by this workflow are ADVISORY ONLY. ***
# Do not use them as inputs to CI gates, release gates, security
# decisions, or any policy check — the model sees untrusted user-supplied
# title/body content and the classification is best-effort.
#
# Add-only for human / LLM labels — never removes a human-applied label.
# The bot may transition between its own priority labels (drops warm
# when promoting to hot) so the two never coexist.
#
# Priority is sticky: once warm or hot is applied, it is NEVER demoted
# or removed if engagement later drops back below the threshold. The
# label reflects peak engagement; demotion, if ever needed, is manual.
name: Auto-label issues and PRs
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
issue_comment:
types: [created]
schedule:
# Daily at 06:00 UTC — catches priority transitions driven by
# reactions, which don't fire webhook events.
- cron: "0 6 * * *"
workflow_dispatch:
inputs:
target:
description: "Issue/PR number to label (leave blank to backfill all unlabeled open items)"
required: false
type: string
dry_run:
description: "Log decisions without applying labels (recommended for spot-testing)"
required: false
type: boolean
default: true
mode:
description: "all = LLM classification + priority refresh. priority-only = skip the LLM (no Anthropic call)."
required: false
type: choice
options:
- all
- priority-only
default: all
permissions:
contents: read
issues: write
# Required for labels on PRs. GITHUB_TOKEN's label-write scope is
# checked per resource type — `gh issue edit --add-label` against a
# PR number routes through the issues API but the permission check is
# against the underlying pull_request resource.
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
# Skip workflow-bot comments to avoid self-triggering loops.
if: github.event_name != 'issue_comment' || github.event.comment.user.type != 'Bot'
concurrency:
group: auto-label-${{ github.event.issue.number || github.event.pull_request.number || inputs.target || github.event_name }}
cancel-in-progress: false
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 1
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Resolve items, dry-run, and mode
id: resolve
env:
TARGET_INPUT: ${{ inputs.target }}
DRY_RUN_INPUT: ${{ inputs.dry_run }}
MODE_INPUT: ${{ inputs.mode }}
run: |
set -euo pipefail
case "${{ github.event_name }}" in
issues)
echo "items=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
echo "dry_run=false" >> "$GITHUB_OUTPUT"
echo "mode=all" >> "$GITHUB_OUTPUT"
;;
pull_request_target)
echo "items=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
echo "dry_run=false" >> "$GITHUB_OUTPUT"
echo "mode=all" >> "$GITHUB_OUTPUT"
;;
issue_comment)
# Engagement event — refresh priority labels only. The LLM
# already classified this item when it was opened; running
# it again on every comment would burn tokens with no new
# signal.
echo "items=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
echo "dry_run=false" >> "$GITHUB_OUTPUT"
echo "mode=priority-only" >> "$GITHUB_OUTPUT"
;;
schedule)
# Daily sweep: re-evaluate priority across every open item
# to pick up reaction-only changes since the last run.
ITEMS=$(gh api --paginate \
"/repos/${{ github.repository }}/issues?state=open&per_page=100" \
--jq '.[].number' | xargs)
echo "items=$ITEMS" >> "$GITHUB_OUTPUT"
echo "dry_run=false" >> "$GITHUB_OUTPUT"
echo "mode=priority-only" >> "$GITHUB_OUTPUT"
;;
workflow_dispatch)
if [ -n "$TARGET_INPUT" ]; then
if ! [[ "$TARGET_INPUT" =~ ^[0-9]+$ ]]; then
echo "::error::target must be a positive integer (got: $TARGET_INPUT)"
exit 1
fi
echo "items=$TARGET_INPUT" >> "$GITHUB_OUTPUT"
else
# Backfill: every open issue + PR missing both engine::*
# and area::*. /repos/.../issues includes PRs because PRs
# are issues in the API model.
ITEMS=$(gh api --paginate \
"/repos/${{ github.repository }}/issues?state=open&per_page=100" \
--jq '.[] | select((.labels | map(.name) | any(startswith("engine::") or startswith("area::"))) | not) | .number' \
| xargs)
echo "items=$ITEMS" >> "$GITHUB_OUTPUT"
fi
echo "dry_run=$DRY_RUN_INPUT" >> "$GITHUB_OUTPUT"
echo "mode=$MODE_INPUT" >> "$GITHUB_OUTPUT"
;;
esac
- name: Run auto-labeler (LLM + priority)
if: steps.resolve.outputs.mode == 'all'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
set -euo pipefail
ITEMS="${{ steps.resolve.outputs.items }}"
if [ -z "$ITEMS" ]; then
echo "No items to label."
exit 0
fi
if [ "${{ steps.resolve.outputs.dry_run }}" = "true" ]; then
python .github/scripts/auto_label.py $ITEMS --dry-run
else
python .github/scripts/auto_label.py $ITEMS
fi
- name: Run auto-labeler (priority only)
if: steps.resolve.outputs.mode == 'priority-only'
run: |
set -euo pipefail
ITEMS="${{ steps.resolve.outputs.items }}"
if [ -z "$ITEMS" ]; then
echo "No items to refresh."
exit 0
fi
if [ "${{ steps.resolve.outputs.dry_run }}" = "true" ]; then
python .github/scripts/auto_label.py $ITEMS --priority-only --dry-run
else
python .github/scripts/auto_label.py $ITEMS --priority-only
fi