forked from google/adk-recipes
-
Notifications
You must be signed in to change notification settings - Fork 0
130 lines (115 loc) · 5.34 KB
/
Copy pathai-issue-scheduled-triage.yml
File metadata and controls
130 lines (115 loc) · 5.34 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
name: '✨ AI Scheduled Issue Triage'
# Hourly sweep that hands untriaged open issues to
# `_ai-issue-triage-core.yml` — a safety net for anything the on-open
# automated-triage workflow missed (an issue opened during CI downtime, a
# failed run, a label stripped later).
#
# Everything about how the triage is performed lives in that core; this
# workflow only decides WHICH issues to triage.
on:
schedule:
- cron: '0 * * * *' # Runs every hour
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}'
# Deliberately false. This is a periodic sweep, not a per-issue reaction:
# a newer tick does not obsolete an older one. With `true`, an overrunning
# sweep (or a manual dispatch landing near the hour) was killed by the next
# cron tick, potentially after labelling only part of its batch.
cancel-in-progress: false
defaults:
run:
shell: 'bash'
# Declared per job rather than here. At workflow level every job inherits
# these — including any added later that only needs to read — which is what
# zizmor's `excessive-permissions` audit flags. Same shape as go-format.yml
# and recipe-canary.yml.
permissions: {}
jobs:
# Split from the triage job so the "is there anything to do?" check is
# expressed ONCE, as that job's `if`. It also keeps this workflow cheap:
# an ordinary hourly run with nothing to triage makes one API call and
# stops — no auth, no agy install, no model call.
find:
timeout-minutes: 5
runs-on: 'ubuntu-latest'
permissions:
issues: 'read'
outputs:
issues: '${{ steps.find_issues.outputs.issues_to_triage }}'
steps:
- name: 'Find untriaged issues'
id: 'find_issues'
env:
# GITHUB_REPOSITORY and GITHUB_OUTPUT are auto-supplied by the
# runner — do NOT redeclare them here. A previous incarnation of
# this step set `GITHUB_OUTPUT: '${{ github.output }}'`, which is
# not a valid Actions context, resolves to an empty string, and
# silently broke every `>> "${GITHUB_OUTPUT}"` write.
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
GH_REPO: '${{ github.repository }}'
run: |-
set -euo pipefail
# Every issue is inlined into the prompt WITH ITS BODY, so an
# unbounded batch produces an enormous prompt and invites the 429s
# we have already seen from the model provider. The hourly cadence
# drains any remainder, and a small batch also limits how much is
# lost when one run fails partway.
MAX_BATCH=15
MAX_BODY_CHARS=4000
# Give the on-open workflow first refusal. Without a floor, an
# issue opened moments before the hour is still unlabelled when
# this sweep queries, so both workflows triage it at once and can
# land two different labels on it.
CUTOFF="$(date -u -d '20 minutes ago' +%Y-%m-%dT%H:%M:%SZ)"
echo "🔍 Finding issues untriaged as of ${CUTOFF}..."
# "Untriaged" means the issue carries NO labels at all.
#
# An earlier revision defined it as "no kind/* and no priority/*
# label", copying the taxonomy the original prompt assumed. This
# repository has no such labels — its scheme is flat (`bug`,
# `enhancement`, `python`, ...) — so that definition matched every
# open issue, and the sweep would have re-triaged all of them every
# hour forever, never converging.
#
# Trade-off, accepted deliberately: an issue carrying only an
# incidental label (`good first issue`, `duplicate`) counts as
# triaged and is skipped. A definition that never terminates is far
# worse than one that occasionally skips.
ALL_ISSUES="$(gh issue list --state open --limit 200 \
--json number,title,body,labels,createdAt)"
CANDIDATES="$(echo "${ALL_ISSUES}" | jq -c \
--arg cutoff "${CUTOFF}" \
--argjson maxbody "${MAX_BODY_CHARS}" '
[ .[]
| select(.createdAt < $cutoff)
| select(.labels | length == 0)
| {number, title, body: ((.body // "")[0:$maxbody])}
]')"
TOTAL="$(echo "${CANDIDATES}" | jq 'length')"
ISSUES="$(echo "${CANDIDATES}" | jq -c --argjson n "${MAX_BATCH}" '.[0:$n]')"
COUNT="$(echo "${ISSUES}" | jq 'length')"
echo "issues_to_triage=${ISSUES}" >> "${GITHUB_OUTPUT}"
if [[ "${TOTAL}" -gt "${COUNT}" ]]; then
echo "::notice::${TOTAL} issues need triage; taking ${COUNT} this run, the rest follow next hour."
fi
echo "✅ Triaging ${COUNT} of ${TOTAL} issues. 🎯"
triage:
needs: 'find'
if: |-
${{ needs.find.outputs.issues != '[]' && needs.find.outputs.issues != '' }}
permissions:
contents: 'read'
id-token: 'write'
issues: 'write'
uses: './.github/workflows/_ai-issue-triage-core.yml'
with:
issues: '${{ needs.find.outputs.issues }}'
print_timeout: '8m'
# A whole batch coming back unclassifiable is the signature of a broken
# run, not of genuinely ambiguous issues — fail loudly rather than
# report a silent no-op as success.
fail_when_none_applied: true
app_id: '${{ vars.APP_ID }}'
secrets:
APP_PRIVATE_KEY: '${{ secrets.APP_PRIVATE_KEY }}'