Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions .github/workflows/claude-issue-triage.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
name: Claude Issue Triage

# Fires the Claude Code triage routine's /fire endpoint when a new issue
# opens OR when a repo member invokes `/triage` via comment (dispatched by
# opens, when a comment lands on an issue OR PR, or when a repo member
# invokes `/triage` via comment (dispatched by
# `.github/workflows/slash-command-dispatch.yml`).
#
# The issue body is fetched fresh and passed as *data* (fenced, size-capped) —
# the routine's prompt treats anything inside the fence as untrusted content.
# Both issue comments and PR comments are routed to the same routine; the
# payload's `is_pr` flag and `pr` block tell the routine which context it's
# in so it can pick the right response (issue triage vs PR-feedback fix).
#
# The issue/PR body is fetched fresh and passed as *data* (fenced, size-
# capped) — the routine's prompt treats anything inside the fence as
# untrusted content.
#
# Required repo secrets:
# CLAUDE_ROUTINE_TRIAGE_URL — full /fire URL including routine ID
Expand Down Expand Up @@ -35,14 +41,14 @@ jobs:
name: Fire triage routine
runs-on: ubuntu-latest
timeout-minutes: 3
# Skip bot-opened issues. Manual path already gated by slash-dispatch
# permission check.
# Three trigger paths, each gated:
# Three trigger paths, each with its own gate:
# - issues.opened/reopened → skip bot-opened issues
# - issue_comment.created → skip bots, skip routine self-loops
# ("Triaged by Claude Code" footer),
# skip /triage (handled by repo_dispatch),
# skip PR conversations (auto-fix's job)
# - issue_comment.created → skip bots, skip self-loop (routine's
# own comments contain "Triaged by
# Claude Code"), skip /triage (handled
# by slash-command-dispatch path).
# Both issue and PR comments fire — the
# routine receives `is_pr` to branch on.
# - repository_dispatch → always allow (slash-dispatch already
# gated by member-association check)
if: >-
Expand All @@ -59,7 +65,7 @@ jobs:
github.event.sender.type != 'Bot' &&
!startsWith(github.event.comment.body, '/triage') &&
!contains(github.event.comment.body, 'Triaged by Claude Code') &&
github.event.issue.pull_request == null
!contains(github.event.comment.body, 'Fixed by Claude Code')
) ||
github.event_name == 'repository_dispatch'
defaults:
Expand Down Expand Up @@ -122,9 +128,28 @@ jobs:
assoc=$(echo "$issue" | jq -r '.author_association // "NONE"')
labels=$(echo "$issue" | jq -c '[.labels[].name]')
html_url=$(echo "$issue" | jq -r '.html_url')
# GitHub's issues API populates `pull_request` only when the issue
# is actually a PR. When present, fetch PR-specific fields so the
# routine can branch on context (head/base ref, draft status, etc.).
is_pr=$(echo "$issue" | jq -r 'if .pull_request then "true" else "false" end')

body_safe=$(printf '%s' "$body" | tr -d '\000' | head -c 8192)

pr_block=""
if [ "$is_pr" = "true" ]; then
pr=$(gh api "repos/$REPO/pulls/$ISSUE_NUMBER")
pr_head=$(echo "$pr" | jq -r '.head.ref')
pr_base=$(echo "$pr" | jq -r '.base.ref')
pr_draft=$(echo "$pr" | jq -r '.draft')
pr_state=$(echo "$pr" | jq -r '.state')
pr_block=$(jq -n \
--arg head "$pr_head" \
--arg base "$pr_base" \
--arg draft "$pr_draft" \
--arg state "$pr_state" \
'{head_ref: $head, base_ref: $base, draft: ($draft == "true"), state: $state}')
fi

# For comment-driven runs, fetch the specific comment so the routine
# can act on it. The full issue body is also included so the routine
# has the original context, not just the new prose.
Expand Down Expand Up @@ -159,13 +184,23 @@ jobs:
--arg comment_body "$comment_body_safe" \
--arg comment_author "$comment_author" \
--arg comment_assoc "$comment_assoc" \
--arg is_pr "$is_pr" \
--arg pr_block "$pr_block" \
'{text: (
"Event: " + $kind + "." + $action + "\n" +
"Repo: " + $repo + "\n" +
"Issue: #" + $num + " \"" + $title + "\"\n" +
(if $is_pr == "true" then "PR" else "Issue" end) +
": #" + $num + " \"" + $title + "\"\n" +
"URL: " + $url + "\n" +
"Author: @" + $author + " (association: " + $assoc + ")\n" +
"Labels: " + ($labels | join(", ")) + "\n" +
(if $is_pr == "true" then
"is_pr: true\n" +
"pr: " + $pr_block + "\n" +
"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"
else
"is_pr: false\n"
end) +
(if $nudge == "" then "" else $nudge + "\n" end) +
(if $comment_body == "" then "" else
"\nNew comment by @" + $comment_author +
Expand Down
Loading