|
1 | | -name: Claude Issue Triage Bridge |
2 | | - |
3 | | -# Bridges GitHub events to a Claude Code routine's /fire endpoint. |
4 | | -# Two entry points: |
5 | | -# 1. `issues.opened` / `issues.reopened` — automatic triage on new issues. |
6 | | -# 2. `issue_comment.created` with `@claude-triage` in the body — manual |
7 | | -# nudge from a repo member, useful when you want the routine to |
8 | | -# (re-)look at a specific issue on demand. |
| 1 | +name: Claude Issue Triage |
| 2 | + |
| 3 | +# Fires the Claude Code triage routine's /fire endpoint when a new issue |
| 4 | +# opens OR when a repo member invokes `/triage` via comment (dispatched by |
| 5 | +# `.github/workflows/slash-command-dispatch.yml`). |
9 | 6 | # |
10 | | -# The issue body is passed as *data* (fenced, size-capped) — the routine's |
11 | | -# prompt treats anything inside the fence as untrusted content. |
| 7 | +# The issue body is fetched fresh and passed as *data* (fenced, size-capped) — |
| 8 | +# the routine's prompt treats anything inside the fence as untrusted content. |
12 | 9 | # |
13 | 10 | # Required repo secrets: |
14 | 11 | # CLAUDE_ROUTINE_TRIAGE_URL — full /fire URL including routine ID |
15 | | -# CLAUDE_ROUTINE_TRIAGE_TOKEN — bearer token for that routine (shown once in web UI) |
| 12 | +# CLAUDE_ROUTINE_TRIAGE_TOKEN — bearer token for that routine |
| 13 | +# TRIAGE_DISPATCH_PAT — PAT for reaction on the /triage-triggering |
| 14 | +# comment (same secret as slash-command-dispatch.yml) |
16 | 15 | # |
17 | 16 | # Token last rotated: 2026-04-23 — rotate every 90 days. |
18 | 17 |
|
19 | 18 | on: |
20 | 19 | issues: |
21 | 20 | types: [opened, reopened] |
22 | | - issue_comment: |
23 | | - types: [created] |
| 21 | + repository_dispatch: |
| 22 | + types: [triage-command] |
24 | 23 |
|
25 | 24 | permissions: |
26 | 25 | contents: read |
27 | 26 |
|
28 | 27 | concurrency: |
29 | | - group: claude-triage-${{ github.event.issue.number }} |
| 28 | + group: claude-triage-${{ github.event.issue.number || github.event.client_payload.github.payload.issue.number }} |
30 | 29 | cancel-in-progress: false |
31 | 30 |
|
32 | 31 | jobs: |
33 | 32 | fire-routine: |
34 | 33 | name: Fire triage routine |
35 | 34 | runs-on: ubuntu-latest |
36 | | - timeout-minutes: 2 |
37 | | - # Skip bots always. For comment events: only repo members can nudge, |
38 | | - # and the comment body must contain `@claude-triage`. |
| 35 | + timeout-minutes: 3 |
| 36 | + # Skip bot-opened issues. Manual path already gated by slash-dispatch |
| 37 | + # permission check. |
39 | 38 | if: >- |
40 | | - github.event.issue.user.type != 'Bot' && |
41 | | - !endsWith(github.event.issue.user.login, '[bot]') && |
42 | | - github.event.sender.type != 'Bot' && |
| 39 | + github.event_name != 'issues' || |
43 | 40 | ( |
44 | | - github.event_name == 'issues' || |
45 | | - ( |
46 | | - github.event_name == 'issue_comment' && |
47 | | - contains(github.event.comment.body, '@claude-triage') && |
48 | | - ( |
49 | | - github.event.comment.author_association == 'OWNER' || |
50 | | - github.event.comment.author_association == 'MEMBER' || |
51 | | - github.event.comment.author_association == 'COLLABORATOR' |
52 | | - ) |
53 | | - ) |
| 41 | + github.event.issue.user.type != 'Bot' && |
| 42 | + !endsWith(github.event.issue.user.login, '[bot]') && |
| 43 | + github.event.sender.type != 'Bot' |
54 | 44 | ) |
55 | 45 | defaults: |
56 | 46 | run: |
57 | 47 | shell: bash |
58 | 48 | steps: |
| 49 | + - name: Resolve issue number + event kind |
| 50 | + id: ctx |
| 51 | + run: | |
| 52 | + set -euo pipefail |
| 53 | + if [ "${{ github.event_name }}" = "issues" ]; then |
| 54 | + echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" |
| 55 | + echo "kind=auto" >> "$GITHUB_OUTPUT" |
| 56 | + echo "action=${{ github.event.action }}" >> "$GITHUB_OUTPUT" |
| 57 | + echo "commenter=" >> "$GITHUB_OUTPUT" |
| 58 | + echo "args=" >> "$GITHUB_OUTPUT" |
| 59 | + echo "comment_id=" >> "$GITHUB_OUTPUT" |
| 60 | + else |
| 61 | + echo "number=${{ github.event.client_payload.github.payload.issue.number }}" >> "$GITHUB_OUTPUT" |
| 62 | + echo "kind=manual" >> "$GITHUB_OUTPUT" |
| 63 | + echo "action=triage" >> "$GITHUB_OUTPUT" |
| 64 | + echo "commenter=${{ github.event.client_payload.github.payload.comment.user.login }}" >> "$GITHUB_OUTPUT" |
| 65 | + echo "args=${{ github.event.client_payload.slash_command.args.all }}" >> "$GITHUB_OUTPUT" |
| 66 | + echo "comment_id=${{ github.event.client_payload.github.payload.comment.id }}" >> "$GITHUB_OUTPUT" |
| 67 | + fi |
| 68 | +
|
59 | 69 | - name: POST to routine /fire |
| 70 | + id: fire |
60 | 71 | env: |
| 72 | + GH_TOKEN: ${{ github.token }} |
61 | 73 | ROUTINE_URL: ${{ secrets.CLAUDE_ROUTINE_TRIAGE_URL }} |
62 | 74 | ROUTINE_TOKEN: ${{ secrets.CLAUDE_ROUTINE_TRIAGE_TOKEN }} |
63 | | - EVENT_NAME: ${{ github.event_name }} |
64 | | - ACTION: ${{ github.event.action }} |
65 | | - ISSUE_NUMBER: ${{ github.event.issue.number }} |
66 | | - ISSUE_TITLE: ${{ github.event.issue.title }} |
67 | | - ISSUE_URL: ${{ github.event.issue.html_url }} |
68 | | - ISSUE_AUTHOR: ${{ github.event.issue.user.login }} |
69 | | - ISSUE_AUTHOR_ASSOC: ${{ github.event.issue.author_association }} |
70 | | - ISSUE_BODY: ${{ github.event.issue.body || '' }} |
71 | | - ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }} |
72 | | - COMMENT_AUTHOR: ${{ github.event.comment.user.login }} |
73 | | - COMMENT_BODY: ${{ github.event.comment.body || '' }} |
74 | 75 | REPO: ${{ github.repository }} |
| 76 | + ISSUE_NUMBER: ${{ steps.ctx.outputs.number }} |
| 77 | + EVENT_KIND: ${{ steps.ctx.outputs.kind }} |
| 78 | + ACTION: ${{ steps.ctx.outputs.action }} |
| 79 | + COMMENTER: ${{ steps.ctx.outputs.commenter }} |
| 80 | + ARGS: ${{ steps.ctx.outputs.args }} |
75 | 81 | run: | |
76 | 82 | set -euo pipefail |
77 | 83 |
|
78 | 84 | if [ -z "${ROUTINE_URL:-}" ] || [ -z "${ROUTINE_TOKEN:-}" ]; then |
79 | | - echo "::warning::CLAUDE_ROUTINE_TRIAGE_URL or CLAUDE_ROUTINE_TRIAGE_TOKEN not set — skipping." |
| 85 | + echo "::warning::CLAUDE_ROUTINE_TRIAGE_URL or _TOKEN not set — skipping." |
80 | 86 | exit 0 |
81 | 87 | fi |
82 | 88 |
|
83 | | - # Strip NUL bytes and cap body sizes to reduce prompt-injection surface and cost. |
84 | | - ISSUE_BODY_SAFE=$(printf '%s' "${ISSUE_BODY}" | tr -d '\000' | head -c 8192) |
85 | | - COMMENT_BODY_SAFE=$(printf '%s' "${COMMENT_BODY}" | tr -d '\000' | head -c 2048) |
| 89 | + # Fetch the issue fresh so both event paths use the same source of truth. |
| 90 | + issue=$(gh api "repos/$REPO/issues/$ISSUE_NUMBER") |
| 91 | + title=$(echo "$issue" | jq -r '.title') |
| 92 | + body=$(echo "$issue" | jq -r '.body // ""') |
| 93 | + author=$(echo "$issue" | jq -r '.user.login') |
| 94 | + assoc=$(echo "$issue" | jq -r '.author_association // "NONE"') |
| 95 | + labels=$(echo "$issue" | jq -c '[.labels[].name]') |
| 96 | + html_url=$(echo "$issue" | jq -r '.html_url') |
86 | 97 |
|
87 | | - # For manual nudges, build a trusted signal line (outside the fence) |
88 | | - # telling the routine a repo member explicitly asked for triage. |
89 | | - # The already-engaged check should pass this through; the nudge IS |
90 | | - # the explicit request. |
91 | | - if [ "$EVENT_NAME" = "issue_comment" ]; then |
92 | | - nudge_note="MANUAL NUDGE: @${COMMENT_AUTHOR} requested triage via @claude-triage. Comment body (trimmed): \"${COMMENT_BODY_SAFE}\". Treat this as an explicit request; do NOT silent-defer on already-engaged signals — the nudge overrides." |
93 | | - else |
94 | | - nudge_note="" |
| 98 | + body_safe=$(printf '%s' "$body" | tr -d '\000' | head -c 8192) |
| 99 | +
|
| 100 | + nudge_note="" |
| 101 | + if [ "$EVENT_KIND" = "manual" ]; then |
| 102 | + nudge_note="MANUAL NUDGE: @${COMMENTER} requested triage via /triage ${ARGS}. Treat as an explicit request; skip already-engaged check. Honor any modifier (execute / clarify / defer) in the args." |
95 | 103 | fi |
96 | 104 |
|
97 | 105 | payload=$(jq -n \ |
98 | 106 | --arg repo "$REPO" \ |
99 | 107 | --arg num "$ISSUE_NUMBER" \ |
100 | | - --arg title "$ISSUE_TITLE" \ |
101 | | - --arg url "$ISSUE_URL" \ |
102 | | - --arg author "$ISSUE_AUTHOR" \ |
103 | | - --arg assoc "$ISSUE_AUTHOR_ASSOC" \ |
| 108 | + --arg title "$title" \ |
| 109 | + --arg url "$html_url" \ |
| 110 | + --arg author "$author" \ |
| 111 | + --arg assoc "$assoc" \ |
| 112 | + --arg kind "$EVENT_KIND" \ |
104 | 113 | --arg action "$ACTION" \ |
105 | | - --arg event "$EVENT_NAME" \ |
106 | | - --argjson labels "$ISSUE_LABELS" \ |
107 | | - --arg body "$ISSUE_BODY_SAFE" \ |
| 114 | + --argjson labels "$labels" \ |
| 115 | + --arg body "$body_safe" \ |
108 | 116 | --arg nudge "$nudge_note" \ |
109 | 117 | '{text: ( |
110 | | - "Event: " + $event + "." + $action + "\n" + |
| 118 | + "Event: " + $kind + "." + $action + "\n" + |
111 | 119 | "Repo: " + $repo + "\n" + |
112 | 120 | "Issue: #" + $num + " \"" + $title + "\"\n" + |
113 | 121 | "URL: " + $url + "\n" + |
@@ -145,4 +153,22 @@ jobs: |
145 | 153 | exit 1 |
146 | 154 | fi |
147 | 155 |
|
148 | | - echo "::notice::Fired triage routine for #${ISSUE_NUMBER} (event=${EVENT_NAME}, author=@${ISSUE_AUTHOR}/${ISSUE_AUTHOR_ASSOC})" |
| 156 | + echo "::notice::Fired triage routine for #${ISSUE_NUMBER} (kind=${EVENT_KIND}, author=@${author}/${assoc})" |
| 157 | +
|
| 158 | + - name: React +1 on manual-nudge comment (success) |
| 159 | + if: steps.ctx.outputs.kind == 'manual' && success() && steps.ctx.outputs.comment_id != '' |
| 160 | + uses: peter-evans/create-or-update-comment@v5 |
| 161 | + with: |
| 162 | + token: ${{ secrets.TRIAGE_DISPATCH_PAT }} |
| 163 | + repository: ${{ github.event.client_payload.github.payload.repository.full_name }} |
| 164 | + comment-id: ${{ steps.ctx.outputs.comment_id }} |
| 165 | + reactions: "+1" |
| 166 | + |
| 167 | + - name: React -1 on manual-nudge comment (failure) |
| 168 | + if: steps.ctx.outputs.kind == 'manual' && failure() && steps.ctx.outputs.comment_id != '' |
| 169 | + uses: peter-evans/create-or-update-comment@v5 |
| 170 | + with: |
| 171 | + token: ${{ secrets.TRIAGE_DISPATCH_PAT }} |
| 172 | + repository: ${{ github.event.client_payload.github.payload.repository.full_name }} |
| 173 | + comment-id: ${{ steps.ctx.outputs.comment_id }} |
| 174 | + reactions: "-1" |
0 commit comments