|
1 | 1 | name: Claude Issue Triage |
2 | 2 |
|
3 | 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 |
| 4 | +# opens, when a comment lands on an issue OR PR, or when a repo member |
| 5 | +# invokes `/triage` via comment (dispatched by |
5 | 6 | # `.github/workflows/slash-command-dispatch.yml`). |
6 | 7 | # |
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. |
| 8 | +# Both issue comments and PR comments are routed to the same routine; the |
| 9 | +# payload's `is_pr` flag and `pr` block tell the routine which context it's |
| 10 | +# in so it can pick the right response (issue triage vs PR-feedback fix). |
| 11 | +# |
| 12 | +# The issue/PR body is fetched fresh and passed as *data* (fenced, size- |
| 13 | +# capped) — the routine's prompt treats anything inside the fence as |
| 14 | +# untrusted content. |
9 | 15 | # |
10 | 16 | # Required repo secrets: |
11 | 17 | # CLAUDE_ROUTINE_TRIAGE_URL — full /fire URL including routine ID |
@@ -35,14 +41,14 @@ jobs: |
35 | 41 | name: Fire triage routine |
36 | 42 | runs-on: ubuntu-latest |
37 | 43 | timeout-minutes: 3 |
38 | | - # Skip bot-opened issues. Manual path already gated by slash-dispatch |
39 | | - # permission check. |
40 | | - # Three trigger paths, each gated: |
| 44 | + # Three trigger paths, each with its own gate: |
41 | 45 | # - issues.opened/reopened → skip bot-opened issues |
42 | | - # - issue_comment.created → skip bots, skip routine self-loops |
43 | | - # ("Triaged by Claude Code" footer), |
44 | | - # skip /triage (handled by repo_dispatch), |
45 | | - # skip PR conversations (auto-fix's job) |
| 46 | + # - issue_comment.created → skip bots, skip self-loop (routine's |
| 47 | + # own comments contain "Triaged by |
| 48 | + # Claude Code"), skip /triage (handled |
| 49 | + # by slash-command-dispatch path). |
| 50 | + # Both issue and PR comments fire — the |
| 51 | + # routine receives `is_pr` to branch on. |
46 | 52 | # - repository_dispatch → always allow (slash-dispatch already |
47 | 53 | # gated by member-association check) |
48 | 54 | if: >- |
|
59 | 65 | github.event.sender.type != 'Bot' && |
60 | 66 | !startsWith(github.event.comment.body, '/triage') && |
61 | 67 | !contains(github.event.comment.body, 'Triaged by Claude Code') && |
62 | | - github.event.issue.pull_request == null |
| 68 | + !contains(github.event.comment.body, 'Fixed by Claude Code') |
63 | 69 | ) || |
64 | 70 | github.event_name == 'repository_dispatch' |
65 | 71 | defaults: |
@@ -122,9 +128,28 @@ jobs: |
122 | 128 | assoc=$(echo "$issue" | jq -r '.author_association // "NONE"') |
123 | 129 | labels=$(echo "$issue" | jq -c '[.labels[].name]') |
124 | 130 | html_url=$(echo "$issue" | jq -r '.html_url') |
| 131 | + # GitHub's issues API populates `pull_request` only when the issue |
| 132 | + # is actually a PR. When present, fetch PR-specific fields so the |
| 133 | + # routine can branch on context (head/base ref, draft status, etc.). |
| 134 | + is_pr=$(echo "$issue" | jq -r 'if .pull_request then "true" else "false" end') |
125 | 135 |
|
126 | 136 | body_safe=$(printf '%s' "$body" | tr -d '\000' | head -c 8192) |
127 | 137 |
|
| 138 | + pr_block="" |
| 139 | + if [ "$is_pr" = "true" ]; then |
| 140 | + pr=$(gh api "repos/$REPO/pulls/$ISSUE_NUMBER") |
| 141 | + pr_head=$(echo "$pr" | jq -r '.head.ref') |
| 142 | + pr_base=$(echo "$pr" | jq -r '.base.ref') |
| 143 | + pr_draft=$(echo "$pr" | jq -r '.draft') |
| 144 | + pr_state=$(echo "$pr" | jq -r '.state') |
| 145 | + pr_block=$(jq -n \ |
| 146 | + --arg head "$pr_head" \ |
| 147 | + --arg base "$pr_base" \ |
| 148 | + --arg draft "$pr_draft" \ |
| 149 | + --arg state "$pr_state" \ |
| 150 | + '{head_ref: $head, base_ref: $base, draft: ($draft == "true"), state: $state}') |
| 151 | + fi |
| 152 | +
|
128 | 153 | # For comment-driven runs, fetch the specific comment so the routine |
129 | 154 | # can act on it. The full issue body is also included so the routine |
130 | 155 | # has the original context, not just the new prose. |
@@ -159,13 +184,23 @@ jobs: |
159 | 184 | --arg comment_body "$comment_body_safe" \ |
160 | 185 | --arg comment_author "$comment_author" \ |
161 | 186 | --arg comment_assoc "$comment_assoc" \ |
| 187 | + --arg is_pr "$is_pr" \ |
| 188 | + --arg pr_block "$pr_block" \ |
162 | 189 | '{text: ( |
163 | 190 | "Event: " + $kind + "." + $action + "\n" + |
164 | 191 | "Repo: " + $repo + "\n" + |
165 | | - "Issue: #" + $num + " \"" + $title + "\"\n" + |
| 192 | + (if $is_pr == "true" then "PR" else "Issue" end) + |
| 193 | + ": #" + $num + " \"" + $title + "\"\n" + |
166 | 194 | "URL: " + $url + "\n" + |
167 | 195 | "Author: @" + $author + " (association: " + $assoc + ")\n" + |
168 | 196 | "Labels: " + ($labels | join(", ")) + "\n" + |
| 197 | + (if $is_pr == "true" then |
| 198 | + "is_pr: true\n" + |
| 199 | + "pr: " + $pr_block + "\n" + |
| 200 | + "MODE: PR-feedback. Treat new comment as actionable feedback on the PR diff. If the comment requests a fix, apply it as a follow-up commit on the PR's head branch (do not open a new PR). If the comment asks a question, answer it as a reply comment. If the comment is conversational with no action implied, post a short acknowledgement and stop.\n" |
| 201 | + else |
| 202 | + "is_pr: false\n" |
| 203 | + end) + |
169 | 204 | (if $nudge == "" then "" else $nudge + "\n" end) + |
170 | 205 | (if $comment_body == "" then "" else |
171 | 206 | "\nNew comment by @" + $comment_author + |
|
0 commit comments