Skip to content

Commit f74e2a2

Browse files
committed
(fix): address review findings on Claude workflows
1 parent fb452d2 commit f74e2a2

4 files changed

Lines changed: 119 additions & 42 deletions

File tree

.github/actions/classify-complexity/action.yml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,30 @@ runs:
6767
--dangerously-skip-permissions \
6868
--output-format json \
6969
--json-schema "$SCHEMA" \
70-
"$CLASSIFY_PROMPT" 2>/dev/null)
70+
"$CLASSIFY_PROMPT")
71+
STATUS=$?
7172
72-
CLASSIFICATION=$(printf '%s' "$RAW" | jq -r '.result | fromjson? | .classification // empty' 2>/dev/null)
73+
if [[ $STATUS -ne 0 ]]; then
74+
echo "claude CLI exited $STATUS — routing to opus."
75+
echo "CLI output: $RAW"
76+
fi
77+
78+
# With --json-schema the structured payload lands at .structured_output as a
79+
# parsed object. Older builds (or runs without schema) put the response at
80+
# .result as either a plain string or a stringified JSON. Try both so the
81+
# action keeps working across CLI versions.
82+
CLASSIFICATION=$(printf '%s' "$RAW" | jq -r '
83+
(.structured_output.classification // "") as $s
84+
| if $s != "" then $s
85+
else ((.result // "") | fromjson? | .classification // "")
86+
end
87+
' 2>/dev/null)
88+
89+
# Last-ditch fallback: plain .result that happens to be the literal word.
90+
if [[ -z "$CLASSIFICATION" ]]; then
91+
CLASSIFICATION=$(printf '%s' "$RAW" | jq -r '.result // empty' 2>/dev/null \
92+
| tr -d '[:space:]' | tr '[:upper:]' '[:lower:]' | head -c 20)
93+
fi
7394
7495
echo "Haiku classification: '$CLASSIFICATION'"
7596

.github/workflows/claude-comments.yml

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
11
name: Claude Code
22

3+
# Only triggers that carry comment bodies worth @claude-scanning.
4+
# pull_request_review is excluded: it fires on every review submit (even bare
5+
# approvals), so the @claude filter would show a "Skipped" check in the PR jobs
6+
# list every time. Inline review comments still fire through
7+
# pull_request_review_comment, so we don't lose the typical reviewer flow.
38
on:
49
issue_comment:
510
types: [created]
611
pull_request_review_comment:
712
types: [created]
813
issues:
914
types: [opened, assigned]
10-
pull_request_review:
11-
types: [submitted]
1215

16+
# For PR review events (where head.ref is available), share the branch queue
17+
# with claude-improvement and claude-healing so we never push concurrently to
18+
# the same head. For issue_comment / issues events, fall back to a per-issue
19+
# key — can't resolve the branch at concurrency-evaluation time without an API
20+
# call, but at least successive @claude comments on one PR/issue serialize.
1321
concurrency:
14-
group: claude-comments-${{ github.event.issue.number || github.event.pull_request.number }}-${{ github.event.comment.id || github.event.review.id || github.run_id }}
22+
group: claude-pr-${{ github.event.pull_request.head.ref || github.event.issue.number || github.run_id }}
1523
cancel-in-progress: false
1624

1725
jobs:
1826
claude:
27+
# Gate on author association: Claude runs with --dangerously-skip-permissions
28+
# and contents: write, so user-controlled prompt content from untrusted
29+
# commenters would be a prompt-injection vector. Only trusted associations
30+
# (repo owner, org member, collaborator) can invoke @claude.
1931
if: |
20-
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
21-
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
22-
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
23-
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
32+
(
33+
github.event.comment.author_association == 'OWNER' ||
34+
github.event.comment.author_association == 'MEMBER' ||
35+
github.event.comment.author_association == 'COLLABORATOR' ||
36+
github.event.issue.author_association == 'OWNER' ||
37+
github.event.issue.author_association == 'MEMBER' ||
38+
github.event.issue.author_association == 'COLLABORATOR'
39+
) && (
40+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
41+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
42+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
43+
)
2444
runs-on: ubuntu-latest
2545
permissions:
2646
contents: write
@@ -29,9 +49,32 @@ jobs:
2949
id-token: write
3050
actions: read
3151
steps:
52+
- name: Resolve PR head ref
53+
id: ref
54+
env:
55+
GH_TOKEN: ${{ github.token }}
56+
REPO: ${{ github.repository }}
57+
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
58+
ISSUE_PR_URL: ${{ github.event.issue.pull_request.url }}
59+
ISSUE_NUMBER: ${{ github.event.issue.number }}
60+
run: |
61+
# pull_request_review* events expose head.ref directly.
62+
# issue_comment events on a PR expose issue.pull_request.url; resolve
63+
# the branch via gh. Plain issues have neither → stay on default branch.
64+
if [[ -n "$PR_HEAD_REF" ]]; then
65+
ref="$PR_HEAD_REF"
66+
elif [[ -n "$ISSUE_PR_URL" ]]; then
67+
ref=$(gh pr view "$ISSUE_NUMBER" --repo "$REPO" --json headRefName --jq .headRefName)
68+
else
69+
ref=""
70+
fi
71+
echo "ref=$ref" >> "$GITHUB_OUTPUT"
72+
echo "Resolved checkout ref: '${ref:-<default branch>}'"
73+
3274
- name: Checkout repository
33-
uses: actions/checkout@v6
75+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3476
with:
77+
ref: ${{ steps.ref.outputs.ref }}
3578
fetch-depth: 20
3679

3780
- name: Classify task complexity with Haiku
@@ -48,11 +91,11 @@ jobs:
4891
TITLE: ${{ github.event.issue.title }}
4992
5093
REQUEST:
51-
${{ github.event.comment.body || github.event.review.body || github.event.issue.body }}
94+
${{ github.event.comment.body || github.event.issue.body }}
5295
5396
- name: Run Claude Code
5497
id: claude
55-
uses: anthropics/claude-code-action@v1
98+
uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104
5699
with:
57100
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
58101
plugin_marketplaces: |

.github/workflows/claude-healing.yml

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
name: Claude CI Watcher
1+
name: Claude CI Healing
22

33
on:
44
workflow_run:
5-
workflows: [CI]
5+
workflows: [Tests]
66
types: [completed]
77

8-
# Shared with claude-review: both mutate the PR branch (commit + push), so they
9-
# must not run simultaneously. Keying on head branch serializes reviewer and
10-
# watcher runs targeting the same PR into a single queue.
8+
# Shared with claude-improvement and claude-comments: all three mutate the PR
9+
# branch (commit + push), so they must not run simultaneously. Keying on head
10+
# branch serializes runs targeting the same PR into a single queue.
1111
concurrency:
1212
group: claude-pr-${{ github.event.workflow_run.head_branch }}
1313
cancel-in-progress: false
1414

1515
jobs:
1616
fix-failures:
17-
# Only fire on PR failures (not push-to-main) and skip fork PRs.
17+
# Only fire on PR failures (not push-to-main) and skip fork PRs. Blocking
18+
# fork PRs also mitigates prompt injection via CI logs — only collaborators
19+
# with push access can introduce branch content that would feed the prompt.
1820
if: >
1921
github.event.workflow_run.conclusion == 'failure' &&
2022
github.event.workflow_run.event == 'pull_request' &&
@@ -47,10 +49,10 @@ jobs:
4749
4850
- name: Checkout PR branch
4951
if: steps.pr.outputs.skip != 'true'
50-
uses: actions/checkout@v6
52+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
5153
with:
5254
ref: ${{ github.event.workflow_run.head_branch }}
53-
fetch-depth: 10
55+
fetch-depth: 0
5456

5557
- name: Get failure logs
5658
if: steps.pr.outputs.skip != 'true'
@@ -88,7 +90,7 @@ jobs:
8890
8991
- name: Claude fixes CI failures
9092
if: steps.pr.outputs.skip != 'true'
91-
uses: anthropics/claude-code-action@v1
93+
uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104
9294
env:
9395
PR_NUMBER: ${{ steps.pr.outputs.number }}
9496
FAILED_RUN_ID: ${{ github.event.workflow_run.id }}
@@ -112,15 +114,16 @@ jobs:
112114
The failure logs are at /tmp/ci-failure-logs.txt. Read them first.
113115
114116
## Coordination with the code reviewer
115-
The `claude-review` workflow also pushes to this PR branch. We share a
116-
concurrency group (branch-keyed), so only one of us runs at a time — but
117-
the reviewer may have pushed fix commits between the failing CI run and
118-
this job starting. Before STEP 6, `git fetch origin` and
119-
`git pull --rebase origin $HEAD_BRANCH`. If rebase conflicts, resolve them
120-
(prefer the reviewer's changes unless they directly contradict your CI fix),
121-
then continue. After rebasing, re-check whether the CI failure is still
122-
reproducible — the reviewer may have already fixed it, in which case post
123-
a comment saying so and STOP without pushing.
117+
The `claude-improvement` and `claude-comments` workflows also push to
118+
this PR branch. We share a concurrency group (branch-keyed), so only one
119+
of us runs at a time — but the other may have pushed fix commits between
120+
the failing CI run and this job starting. Before STEP 6,
121+
`git fetch origin` and `git pull --rebase origin $HEAD_BRANCH`. If rebase
122+
conflicts, resolve them (prefer the other workflow's changes unless they
123+
directly contradict your CI fix), then continue. After rebasing, re-check
124+
whether the CI failure is still reproducible — the other workflow may have
125+
already fixed it, in which case post a comment saying so and STOP without
126+
pushing.
124127
125128
Your job:
126129
1. Read the failure logs to identify the root cause (compile error, test failure, lint violation, etc.)

.github/workflows/claude-improvement.yml

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,26 @@ on:
44
pull_request:
55
types: [opened, synchronize, ready_for_review, reopened]
66

7-
# Shared with claude-watcher: both mutate the PR branch (commit + push), so they
8-
# must not run simultaneously. Keying on head branch serializes reviewer and
9-
# watcher runs targeting the same PR into a single queue.
7+
# Shared with claude-healing and claude-comments: all three mutate the PR branch
8+
# (commit + push), so they must not run simultaneously. Keying on head branch
9+
# serializes runs targeting the same PR into a single queue.
1010
concurrency:
1111
group: claude-pr-${{ github.event.pull_request.head.ref }}
1212
cancel-in-progress: false
1313

1414
jobs:
1515
claude-review:
1616
runs-on: ubuntu-latest
17-
if: github.event.pull_request.head.repo.full_name == github.repository
17+
# Defense in depth against prompt injection via PR title/body: require the
18+
# PR author to be a trusted association in addition to the head being
19+
# internal to this repo (which already blocks forks).
20+
if: >
21+
github.event.pull_request.head.repo.full_name == github.repository &&
22+
(
23+
github.event.pull_request.author_association == 'OWNER' ||
24+
github.event.pull_request.author_association == 'MEMBER' ||
25+
github.event.pull_request.author_association == 'COLLABORATOR'
26+
)
1827
permissions:
1928
contents: write
2029
pull-requests: write
@@ -23,7 +32,7 @@ jobs:
2332

2433
steps:
2534
- name: Checkout PR branch
26-
uses: actions/checkout@v6
35+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2736
with:
2837
ref: ${{ github.event.pull_request.head.ref }}
2938
fetch-depth: 0
@@ -71,7 +80,7 @@ jobs:
7180
7281
- name: Run Claude Code Review
7382
id: claude-review
74-
uses: anthropics/claude-code-action@v1
83+
uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104
7584
env:
7685
PR_NUMBER: ${{ github.event.pull_request.number }}
7786
REPO: ${{ github.repository }}
@@ -95,12 +104,13 @@ jobs:
95104
You are reviewing and fixing PR #$PR_NUMBER in $REPO.
96105
97106
## Coordination with the CI watcher
98-
The `claude-watcher` workflow also pushes to this PR branch when CI fails.
99-
We share a concurrency group (branch-keyed), so only one of us runs at a time —
100-
but the OTHER may have pushed between the last event and this job starting.
101-
Before committing in STEP 5, `git fetch origin` and `git pull --rebase origin <branch>`.
102-
If rebase conflicts, resolve them (prefer the other side's changes unless they
103-
contradict a finding you're fixing), then continue.
107+
The `claude-healing` and `claude-comments` workflows also push to this PR
108+
branch. We share a concurrency group (branch-keyed), so only one of us runs
109+
at a time — but the OTHER may have pushed between the last event and this
110+
job starting. Before committing in STEP 5, `git fetch origin` and
111+
`git pull --rebase origin <branch>`. If rebase conflicts, resolve them
112+
(prefer the other side's changes unless they contradict a finding you're
113+
fixing), then continue.
104114
105115
## STEP 1 — Analyze
106116
Run `/code-review:code-review` for all CRITICAL/HIGH/MEDIUM findings. Skip low/nits.

0 commit comments

Comments
 (0)