From 20c90b4b6389d02c792400b55e7f5304b28b8a3c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 23 Apr 2026 14:41:25 +1200 Subject: [PATCH 1/9] (feat): add classify-complexity composite action --- .../actions/classify-complexity/action.yml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/actions/classify-complexity/action.yml diff --git a/.github/actions/classify-complexity/action.yml b/.github/actions/classify-complexity/action.yml new file mode 100644 index 000000000..6fe1ee082 --- /dev/null +++ b/.github/actions/classify-complexity/action.yml @@ -0,0 +1,88 @@ +name: Classify task complexity with Haiku +description: > + Ask Claude Haiku to classify a task as 'simple' or 'complex' and pick a model + pair (primary + fallback). Favours opus — sonnet is only chosen when Haiku + explicitly says 'simple'; any other output routes to opus. + +inputs: + oauth-token: + description: Claude Code OAuth token. + required: true + prompt: + description: > + Full classification prompt. Must instruct Haiku to reply with EXACTLY + one lowercase word — 'simple' or 'complex' — and nothing else. + required: true + +outputs: + model: + description: Selected primary model. + value: ${{ steps.classify.outputs.model }} + fallback: + description: Fallback model. + value: ${{ steps.classify.outputs.fallback }} + classification: + description: Raw classification string ('simple', 'complex', or fallback on empty/garbage). + value: ${{ steps.classify.outputs.classification }} + +runs: + using: composite + steps: + - name: Install Claude CLI + shell: bash + run: | + if ! command -v claude >/dev/null 2>&1; then + curl -fsSL https://claude.ai/install.sh | bash + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + fi + + - name: Run Haiku classification + id: classify + shell: bash + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.oauth-token }} + CLASSIFY_PROMPT: ${{ inputs.prompt }} + run: | + set +e + + # Force the model to emit a JSON object matching this schema — removes + # any chance of leading/trailing prose, punctuation, or casing drift. + SCHEMA='{ + "type": "object", + "properties": { + "classification": { + "type": "string", + "enum": ["simple", "complex"] + } + }, + "required": ["classification"], + "additionalProperties": false + }' + + RAW=$(claude -p \ + --model claude-haiku-4-5 \ + --bare \ + --max-turns 1 \ + --tools "" \ + --dangerously-skip-permissions \ + --output-format json \ + --json-schema "$SCHEMA" \ + "$CLASSIFY_PROMPT" 2>/dev/null) + + CLASSIFICATION=$(printf '%s' "$RAW" | jq -r '.result | fromjson? | .classification // empty' 2>/dev/null) + + echo "Haiku classification: '$CLASSIFICATION'" + + # Favour opus, sonnet only when haiku explicitly says simple. + if [[ "$CLASSIFICATION" == "simple" ]]; then + MODEL="claude-sonnet-4-6" + FALLBACK="claude-opus-4-7" + else + MODEL="claude-opus-4-7" + FALLBACK="claude-sonnet-4-6" + fi + + echo "classification=$CLASSIFICATION" >> "$GITHUB_OUTPUT" + echo "model=$MODEL" >> "$GITHUB_OUTPUT" + echo "fallback=$FALLBACK" >> "$GITHUB_OUTPUT" + echo "Selected primary=$MODEL, fallback=$FALLBACK" From 564ac1ac92b23cecc8666f7923e4d2c5b643fa93 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 23 Apr 2026 14:41:28 +1200 Subject: [PATCH 2/9] (feat): add Claude PR automation workflows --- .github/workflows/claude-comments.yml | 70 ++++++++++ .github/workflows/claude-healing.yml | 140 +++++++++++++++++++ .github/workflows/claude-improvement.yml | 171 +++++++++++++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 .github/workflows/claude-comments.yml create mode 100644 .github/workflows/claude-healing.yml create mode 100644 .github/workflows/claude-improvement.yml diff --git a/.github/workflows/claude-comments.yml b/.github/workflows/claude-comments.yml new file mode 100644 index 000000000..64ca15a4c --- /dev/null +++ b/.github/workflows/claude-comments.yml @@ -0,0 +1,70 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +concurrency: + group: claude-comments-${{ github.event.issue.number || github.event.pull_request.number }}-${{ github.event.comment.id || github.event.review.id || github.run_id }} + cancel-in-progress: false + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: self-hosted + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 20 + + - name: Classify task complexity with Haiku + id: classify + uses: ./.github/actions/classify-complexity + with: + oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + Classify this coding task's complexity. Reply with EXACTLY one lowercase word and nothing else: 'simple' or 'complex'. + + simple = typo fix, docs tweak, one-file obvious bug, rename, trivial refactor within a single function + complex = multi-file change, new feature, architecture or API change, deep debugging, performance work, anything with unclear scope or touching more than ~3 files + + TITLE: ${{ github.event.issue.title }} + + REQUEST: + ${{ github.event.comment.body || github.event.review.body || github.event.issue.body }} + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: | + https://github.com/anthropics/claude-code.git + https://github.com/abnegate/claudes.git + plugins: 'skills@claudes' + additional_permissions: | + actions: read + use_sticky_comment: true + use_commit_signing: true + exclude_comments_by_actor: 'dependabot[bot],renovate[bot]' + claude_args: | + --model ${{ steps.classify.outputs.model }} + --fallback-model ${{ steps.classify.outputs.fallback }} + --dangerously-skip-permissions diff --git a/.github/workflows/claude-healing.yml b/.github/workflows/claude-healing.yml new file mode 100644 index 000000000..857f133b3 --- /dev/null +++ b/.github/workflows/claude-healing.yml @@ -0,0 +1,140 @@ +name: Claude CI Watcher + +on: + workflow_run: + workflows: [CI] + types: [completed] + +# Shared with claude-review: both mutate the PR branch (commit + push), so they +# must not run simultaneously. Keying on head branch serializes reviewer and +# watcher runs targeting the same PR into a single queue. +concurrency: + group: claude-pr-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false + +jobs: + fix-failures: + # Only fire on PR failures (not push-to-main) and skip fork PRs. + if: > + github.event.workflow_run.conclusion == 'failure' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.head_repository.full_name == github.repository + runs-on: self-hosted + permissions: + contents: write + pull-requests: write + issues: read + actions: read + checks: read + id-token: write + + steps: + - name: Find the PR + id: pr + env: + GH_TOKEN: ${{ github.token }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + run: | + pr_number=$(gh pr list --repo "$GITHUB_REPOSITORY" \ + --head "$HEAD_BRANCH" --state open --json number --jq '.[0].number') + if [ -z "$pr_number" ]; then + echo "No open PR found — skipping." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "number=$pr_number" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout PR branch + if: steps.pr.outputs.skip != 'true' + uses: actions/checkout@v6 + with: + ref: ${{ github.event.workflow_run.head_branch }} + fetch-depth: 10 + + - name: Get failure logs + if: steps.pr.outputs.skip != 'true' + env: + GH_TOKEN: ${{ github.token }} + RUN_ID: ${{ github.event.workflow_run.id }} + run: | + gh run view "$RUN_ID" --log-failed 2>&1 | tail -200 > /tmp/ci-failure-logs.txt + echo "Captured $(wc -l < /tmp/ci-failure-logs.txt) lines of failure logs" + + - name: Extract failure log excerpt + if: steps.pr.outputs.skip != 'true' + id: excerpt + run: | + { + echo 'log<<__EOF__' + tail -120 /tmp/ci-failure-logs.txt + echo '__EOF__' + } >> "$GITHUB_OUTPUT" + + - name: Classify failure complexity with Haiku + if: steps.pr.outputs.skip != 'true' + id: classify + uses: ./.github/actions/classify-complexity + with: + oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + Classify this CI failure's fix complexity. Reply with EXACTLY one lowercase word and nothing else: 'simple' or 'complex'. + + simple = lint violation, formatting, obvious typo, missing import, trivial test assertion fix, single-file obvious mistake + complex = compile error with unclear cause, real test failure exposing a bug, flaky infrastructure, multi-file breakage, anything with unclear root cause or scope + + FAILURE LOGS (tail): + ${{ steps.excerpt.outputs.log }} + + - name: Claude fixes CI failures + if: steps.pr.outputs.skip != 'true' + uses: anthropics/claude-code-action@v1 + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + FAILED_RUN_ID: ${{ github.event.workflow_run.id }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: | + https://github.com/anthropics/claude-code.git + https://github.com/abnegate/claudes.git + plugins: 'skills@claudes' + use_sticky_comment: 'false' + use_commit_signing: 'true' + show_full_output: 'true' + claude_args: | + --model ${{ steps.classify.outputs.model }} + --fallback-model ${{ steps.classify.outputs.fallback }} + --dangerously-skip-permissions + prompt: | + CI failed on PR #$PR_NUMBER (run $FAILED_RUN_ID). + + The failure logs are at /tmp/ci-failure-logs.txt. Read them first. + + ## Coordination with the code reviewer + The `claude-review` workflow also pushes to this PR branch. We share a + concurrency group (branch-keyed), so only one of us runs at a time — but + the reviewer may have pushed fix commits between the failing CI run and + this job starting. Before STEP 6, `git fetch origin` and + `git pull --rebase origin $HEAD_BRANCH`. If rebase conflicts, resolve them + (prefer the reviewer's changes unless they directly contradict your CI fix), + then continue. After rebasing, re-check whether the CI failure is still + reproducible — the reviewer may have already fixed it, in which case post + a comment saying so and STOP without pushing. + + Your job: + 1. Read the failure logs to identify the root cause (compile error, test failure, lint violation, etc.) + 2. Read the project's CLAUDE.md for build/test/lint commands and conventions + 3. Fix the issue directly in the codebase + 4. Verify your fix by re-running the failed step (use whatever build system the project uses) + 5. Post a brief PR comment explaining what failed and what you fixed: + `gh pr comment $PR_NUMBER --repo $GITHUB_REPOSITORY --body "..."` + 6. `git fetch origin && git pull --rebase origin $HEAD_BRANCH`, then commit with + message: `(fix): CI — ` + 7. Push to the PR branch + + Rules: + - Only fix the CI failure. Don't refactor, clean up, or improve other code. + - If the failure is a flaky test (passes on retry), just re-run it and comment that it was flaky. + - If you can't determine the root cause, post a comment asking for help instead of guessing. + - Never push to main. Only push to the PR head branch. diff --git a/.github/workflows/claude-improvement.yml b/.github/workflows/claude-improvement.yml new file mode 100644 index 000000000..165b41f15 --- /dev/null +++ b/.github/workflows/claude-improvement.yml @@ -0,0 +1,171 @@ +name: Claude Code Improvements + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + +# Shared with claude-watcher: both mutate the PR branch (commit + push), so they +# must not run simultaneously. Keying on head branch serializes reviewer and +# watcher runs targeting the same PR into a single queue. +concurrency: + group: claude-pr-${{ github.event.pull_request.head.ref }} + cancel-in-progress: false + +jobs: + claude-review: + runs-on: self-hosted + if: github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: write + pull-requests: write + issues: read + id-token: write + + steps: + - name: Checkout PR branch + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + + - name: Compute PR diff stats + id: diff + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + git fetch --no-tags origin "+refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF" + merge_base=$(git merge-base "origin/$BASE_REF" "$HEAD_SHA") + changed_files=$(git diff --name-only "$merge_base..$HEAD_SHA" | wc -l | tr -d ' ') + changed_lines=$(git diff --shortstat "$merge_base..$HEAD_SHA" | awk '{ ins=0; del=0; for (i=1;i<=NF;i++) { if ($i ~ /insertion/) ins=$(i-1); if ($i ~ /deletion/) del=$(i-1) } print ins + del }') + changed_lines=${changed_lines:-0} + + { + echo "files=$changed_files" + echo "lines=$changed_lines" + echo 'file_list<<__EOF__' + git diff --name-only "$merge_base..$HEAD_SHA" | head -50 + echo '__EOF__' + } >> "$GITHUB_OUTPUT" + + - name: Classify PR complexity with Haiku + id: classify + uses: ./.github/actions/classify-complexity + with: + oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + Classify this pull request's review complexity. Reply with EXACTLY one lowercase word and nothing else: 'simple' or 'complex'. + + simple = typo fix, docs tweak, one-file obvious bug, rename, trivial refactor within a single function, small test-only change + complex = multi-file change, new feature, architecture or API change, deep debugging, performance work, security-sensitive code, anything with unclear scope or touching more than ~3 files + + TITLE: ${{ github.event.pull_request.title }} + + STATS: ${{ steps.diff.outputs.files }} files, ${{ steps.diff.outputs.lines }} lines changed + + FILES: + ${{ steps.diff.outputs.file_list }} + + DESCRIPTION: + ${{ github.event.pull_request.body }} + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: | + https://github.com/anthropics/claude-code.git + https://github.com/abnegate/claudes.git + plugins: | + code-review@claude-code-plugins + skills@claudes + use_sticky_comment: 'false' + use_commit_signing: 'true' + show_full_output: 'true' + claude_args: | + --model ${{ steps.classify.outputs.model }} + --fallback-model ${{ steps.classify.outputs.fallback }} + --dangerously-skip-permissions + prompt: | + You are reviewing and fixing PR #$PR_NUMBER in $REPO. + + ## Coordination with the CI watcher + The `claude-watcher` workflow also pushes to this PR branch when CI fails. + We share a concurrency group (branch-keyed), so only one of us runs at a time — + but the OTHER may have pushed between the last event and this job starting. + Before committing in STEP 5, `git fetch origin` and `git pull --rebase origin `. + If rebase conflicts, resolve them (prefer the other side's changes unless they + contradict a finding you're fixing), then continue. + + ## STEP 1 — Analyze + Run `/code-review:code-review` for all CRITICAL/HIGH/MEDIUM findings. Skip low/nits. + + ## STEP 2 — Post inline review + Build /tmp/review.json with this structure and post it: + ```json + { + "event": "COMMENT", + "body": "## Code Review\n\n**N finding(s)**\n\nSee inline comments. Fixes incoming.", + "comments": [ + {"path": "file.php", "line": 42, "body": "**[SEVERITY]** ...\n\nExplanation + fix."} + ] + } + ``` + Post: `gh api repos/$REPO/pulls/$PR_NUMBER/reviews --input /tmp/review.json` + + If zero findings: post "No critical/high/medium findings." and STOP. + + ## STEP 3 — Fix in parallel via isolated worktree subagents + For MAXIMUM speed, launch one Agent per finding using worktree isolation. + Findings in different files run in TRUE parallel — launch them ALL in one message. + Findings in the SAME file go to the SAME agent to avoid conflicts. + + Each agent prompt must be self-contained: + - Include the finding: severity, file path, line numbers, what's wrong, how to fix + - Tell it to verify the fix compiles (read CLAUDE.md for the build command) + - Tell it NOT to touch other files or make unrelated changes + + Example — 3 findings in 3 files, all launched at once: + Agent({description: "Fix 1", isolation: "worktree", prompt: "Fix [HIGH] ... in file.php line 42 ..."}) + Agent({description: "Fix 2", isolation: "worktree", prompt: "Fix [MEDIUM] ... in other.php line 99 ..."}) + Agent({description: "Fix 3", isolation: "worktree", prompt: "Fix [MEDIUM] ... in third.php line 7 ..."}) + + ## STEP 4 — Consolidate + After all agents finish, apply their changes to the main checkout: + - Each worktree agent returns the files it changed + - Cherry-pick or manually apply each agent's diff to the working tree + - If two agents touched the same file, merge carefully + - Verify final result compiles + + ## STEP 5 — Commit and push + - `git fetch origin && git pull --rebase origin $HEAD_BRANCH` to absorb any + commits the watcher (or the PR author) pushed while this job was queued + - Capture the pre-commit tip: `BEFORE_SHA=$(git rev-parse HEAD)` + - Stage only fix files + - Commit: `(fix): address review findings — X HIGH, Y MEDIUM` + - Body: one bullet per finding + - Push to PR branch + - Capture the post-push tip: `AFTER_SHA=$(git rev-parse HEAD)` + + ## STEP 6 — Post summary comment with compare link + After a successful push, add a follow-up comment linking to a compare view + of everything this run added, so reviewers can see exactly what changed: + + COMPARE_URL="https://github.com/$REPO/compare/$BEFORE_SHA...$AFTER_SHA" + gh pr comment $PR_NUMBER --repo $REPO --body "Fixes pushed: $COMPARE_URL + + " + + If BEFORE_SHA equals AFTER_SHA (nothing was actually pushed — e.g. all + fixes were no-ops after rebase), skip this step. + + Rules: + - Do NOT skip findings. + - Maximize parallelism — launch as many worktree agents as there are independent file groups. + - Each agent prompt must be fully self-contained (it has no context from this conversation). + - Never push to main. From fbd1669468f87f005e50793a09b9e7a96e21b5de Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 23 Apr 2026 14:47:00 +1200 Subject: [PATCH 3/9] (chore): run Claude workflows on ubuntu-latest --- .github/workflows/claude-comments.yml | 2 +- .github/workflows/claude-healing.yml | 2 +- .github/workflows/claude-improvement.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/claude-comments.yml b/.github/workflows/claude-comments.yml index 64ca15a4c..d2eb77d4a 100644 --- a/.github/workflows/claude-comments.yml +++ b/.github/workflows/claude-comments.yml @@ -21,7 +21,7 @@ jobs: (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: self-hosted + runs-on: ubuntu-latest permissions: contents: write pull-requests: write diff --git a/.github/workflows/claude-healing.yml b/.github/workflows/claude-healing.yml index 857f133b3..c5413be0e 100644 --- a/.github/workflows/claude-healing.yml +++ b/.github/workflows/claude-healing.yml @@ -19,7 +19,7 @@ jobs: github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.head_repository.full_name == github.repository - runs-on: self-hosted + runs-on: ubuntu-latest permissions: contents: write pull-requests: write diff --git a/.github/workflows/claude-improvement.yml b/.github/workflows/claude-improvement.yml index 165b41f15..d721a325e 100644 --- a/.github/workflows/claude-improvement.yml +++ b/.github/workflows/claude-improvement.yml @@ -13,7 +13,7 @@ concurrency: jobs: claude-review: - runs-on: self-hosted + runs-on: ubuntu-latest if: github.event.pull_request.head.repo.full_name == github.repository permissions: contents: write From fb452d2d1cfa8f5167cb0e4e3b8bb749b99e3322 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 23 Apr 2026 15:11:55 +1200 Subject: [PATCH 4/9] (chore): pin third-party actions to SHAs --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/linter.yml | 2 +- .github/workflows/tests.yml | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 161d9cebd..485040712 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 7148b95b7..ca49ca5c6 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 386d728b6..b12075662 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,13 +16,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Build Docker Image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@1104d471370f9806843c095c1db02b5a90c5f8b6 # v3.3.1 with: context: . push: false @@ -33,7 +33,7 @@ jobs: outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar - name: Cache Docker Image - uses: actions/cache@v3 + uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -45,10 +45,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -88,10 +88,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar From f74e2a26086bd952ca031216afe5abfcc8bab127 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 23 Apr 2026 15:12:00 +1200 Subject: [PATCH 5/9] (fix): address review findings on Claude workflows --- .../actions/classify-complexity/action.yml | 25 +++++++- .github/workflows/claude-comments.yml | 63 ++++++++++++++++--- .github/workflows/claude-healing.yml | 39 ++++++------ .github/workflows/claude-improvement.yml | 34 ++++++---- 4 files changed, 119 insertions(+), 42 deletions(-) diff --git a/.github/actions/classify-complexity/action.yml b/.github/actions/classify-complexity/action.yml index 6fe1ee082..ec8c00c10 100644 --- a/.github/actions/classify-complexity/action.yml +++ b/.github/actions/classify-complexity/action.yml @@ -67,9 +67,30 @@ runs: --dangerously-skip-permissions \ --output-format json \ --json-schema "$SCHEMA" \ - "$CLASSIFY_PROMPT" 2>/dev/null) + "$CLASSIFY_PROMPT") + STATUS=$? - CLASSIFICATION=$(printf '%s' "$RAW" | jq -r '.result | fromjson? | .classification // empty' 2>/dev/null) + if [[ $STATUS -ne 0 ]]; then + echo "claude CLI exited $STATUS — routing to opus." + echo "CLI output: $RAW" + fi + + # With --json-schema the structured payload lands at .structured_output as a + # parsed object. Older builds (or runs without schema) put the response at + # .result as either a plain string or a stringified JSON. Try both so the + # action keeps working across CLI versions. + CLASSIFICATION=$(printf '%s' "$RAW" | jq -r ' + (.structured_output.classification // "") as $s + | if $s != "" then $s + else ((.result // "") | fromjson? | .classification // "") + end + ' 2>/dev/null) + + # Last-ditch fallback: plain .result that happens to be the literal word. + if [[ -z "$CLASSIFICATION" ]]; then + CLASSIFICATION=$(printf '%s' "$RAW" | jq -r '.result // empty' 2>/dev/null \ + | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]' | head -c 20) + fi echo "Haiku classification: '$CLASSIFICATION'" diff --git a/.github/workflows/claude-comments.yml b/.github/workflows/claude-comments.yml index d2eb77d4a..353622ec2 100644 --- a/.github/workflows/claude-comments.yml +++ b/.github/workflows/claude-comments.yml @@ -1,5 +1,10 @@ name: Claude Code +# Only triggers that carry comment bodies worth @claude-scanning. +# pull_request_review is excluded: it fires on every review submit (even bare +# approvals), so the @claude filter would show a "Skipped" check in the PR jobs +# list every time. Inline review comments still fire through +# pull_request_review_comment, so we don't lose the typical reviewer flow. on: issue_comment: types: [created] @@ -7,20 +12,35 @@ on: types: [created] issues: types: [opened, assigned] - pull_request_review: - types: [submitted] +# For PR review events (where head.ref is available), share the branch queue +# with claude-improvement and claude-healing so we never push concurrently to +# the same head. For issue_comment / issues events, fall back to a per-issue +# key — can't resolve the branch at concurrency-evaluation time without an API +# call, but at least successive @claude comments on one PR/issue serialize. concurrency: - group: claude-comments-${{ github.event.issue.number || github.event.pull_request.number }}-${{ github.event.comment.id || github.event.review.id || github.run_id }} + group: claude-pr-${{ github.event.pull_request.head.ref || github.event.issue.number || github.run_id }} cancel-in-progress: false jobs: claude: + # Gate on author association: Claude runs with --dangerously-skip-permissions + # and contents: write, so user-controlled prompt content from untrusted + # commenters would be a prompt-injection vector. Only trusted associations + # (repo owner, org member, collaborator) can invoke @claude. if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' || + github.event.issue.author_association == 'OWNER' || + github.event.issue.author_association == 'MEMBER' || + github.event.issue.author_association == 'COLLABORATOR' + ) && ( + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + ) runs-on: ubuntu-latest permissions: contents: write @@ -29,9 +49,32 @@ jobs: id-token: write actions: read steps: + - name: Resolve PR head ref + id: ref + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + ISSUE_PR_URL: ${{ github.event.issue.pull_request.url }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + # pull_request_review* events expose head.ref directly. + # issue_comment events on a PR expose issue.pull_request.url; resolve + # the branch via gh. Plain issues have neither → stay on default branch. + if [[ -n "$PR_HEAD_REF" ]]; then + ref="$PR_HEAD_REF" + elif [[ -n "$ISSUE_PR_URL" ]]; then + ref=$(gh pr view "$ISSUE_NUMBER" --repo "$REPO" --json headRefName --jq .headRefName) + else + ref="" + fi + echo "ref=$ref" >> "$GITHUB_OUTPUT" + echo "Resolved checkout ref: '${ref:-}'" + - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: ${{ steps.ref.outputs.ref }} fetch-depth: 20 - name: Classify task complexity with Haiku @@ -48,11 +91,11 @@ jobs: TITLE: ${{ github.event.issue.title }} REQUEST: - ${{ github.event.comment.body || github.event.review.body || github.event.issue.body }} + ${{ github.event.comment.body || github.event.issue.body }} - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} plugin_marketplaces: | diff --git a/.github/workflows/claude-healing.yml b/.github/workflows/claude-healing.yml index c5413be0e..408bdb5a5 100644 --- a/.github/workflows/claude-healing.yml +++ b/.github/workflows/claude-healing.yml @@ -1,20 +1,22 @@ -name: Claude CI Watcher +name: Claude CI Healing on: workflow_run: - workflows: [CI] + workflows: [Tests] types: [completed] -# Shared with claude-review: both mutate the PR branch (commit + push), so they -# must not run simultaneously. Keying on head branch serializes reviewer and -# watcher runs targeting the same PR into a single queue. +# Shared with claude-improvement and claude-comments: all three mutate the PR +# branch (commit + push), so they must not run simultaneously. Keying on head +# branch serializes runs targeting the same PR into a single queue. concurrency: group: claude-pr-${{ github.event.workflow_run.head_branch }} cancel-in-progress: false jobs: fix-failures: - # Only fire on PR failures (not push-to-main) and skip fork PRs. + # Only fire on PR failures (not push-to-main) and skip fork PRs. Blocking + # fork PRs also mitigates prompt injection via CI logs — only collaborators + # with push access can introduce branch content that would feed the prompt. if: > github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' && @@ -47,10 +49,10 @@ jobs: - name: Checkout PR branch if: steps.pr.outputs.skip != 'true' - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.workflow_run.head_branch }} - fetch-depth: 10 + fetch-depth: 0 - name: Get failure logs if: steps.pr.outputs.skip != 'true' @@ -88,7 +90,7 @@ jobs: - name: Claude fixes CI failures if: steps.pr.outputs.skip != 'true' - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104 env: PR_NUMBER: ${{ steps.pr.outputs.number }} FAILED_RUN_ID: ${{ github.event.workflow_run.id }} @@ -112,15 +114,16 @@ jobs: The failure logs are at /tmp/ci-failure-logs.txt. Read them first. ## Coordination with the code reviewer - The `claude-review` workflow also pushes to this PR branch. We share a - concurrency group (branch-keyed), so only one of us runs at a time — but - the reviewer may have pushed fix commits between the failing CI run and - this job starting. Before STEP 6, `git fetch origin` and - `git pull --rebase origin $HEAD_BRANCH`. If rebase conflicts, resolve them - (prefer the reviewer's changes unless they directly contradict your CI fix), - then continue. After rebasing, re-check whether the CI failure is still - reproducible — the reviewer may have already fixed it, in which case post - a comment saying so and STOP without pushing. + The `claude-improvement` and `claude-comments` workflows also push to + this PR branch. We share a concurrency group (branch-keyed), so only one + of us runs at a time — but the other may have pushed fix commits between + the failing CI run and this job starting. Before STEP 6, + `git fetch origin` and `git pull --rebase origin $HEAD_BRANCH`. If rebase + conflicts, resolve them (prefer the other workflow's changes unless they + directly contradict your CI fix), then continue. After rebasing, re-check + whether the CI failure is still reproducible — the other workflow may have + already fixed it, in which case post a comment saying so and STOP without + pushing. Your job: 1. Read the failure logs to identify the root cause (compile error, test failure, lint violation, etc.) diff --git a/.github/workflows/claude-improvement.yml b/.github/workflows/claude-improvement.yml index d721a325e..b5b9aa931 100644 --- a/.github/workflows/claude-improvement.yml +++ b/.github/workflows/claude-improvement.yml @@ -4,9 +4,9 @@ on: pull_request: types: [opened, synchronize, ready_for_review, reopened] -# Shared with claude-watcher: both mutate the PR branch (commit + push), so they -# must not run simultaneously. Keying on head branch serializes reviewer and -# watcher runs targeting the same PR into a single queue. +# Shared with claude-healing and claude-comments: all three mutate the PR branch +# (commit + push), so they must not run simultaneously. Keying on head branch +# serializes runs targeting the same PR into a single queue. concurrency: group: claude-pr-${{ github.event.pull_request.head.ref }} cancel-in-progress: false @@ -14,7 +14,16 @@ concurrency: jobs: claude-review: runs-on: ubuntu-latest - if: github.event.pull_request.head.repo.full_name == github.repository + # Defense in depth against prompt injection via PR title/body: require the + # PR author to be a trusted association in addition to the head being + # internal to this repo (which already blocks forks). + if: > + github.event.pull_request.head.repo.full_name == github.repository && + ( + github.event.pull_request.author_association == 'OWNER' || + github.event.pull_request.author_association == 'MEMBER' || + github.event.pull_request.author_association == 'COLLABORATOR' + ) permissions: contents: write pull-requests: write @@ -23,7 +32,7 @@ jobs: steps: - name: Checkout PR branch - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.ref }} fetch-depth: 0 @@ -71,7 +80,7 @@ jobs: - name: Run Claude Code Review id: claude-review - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104 env: PR_NUMBER: ${{ github.event.pull_request.number }} REPO: ${{ github.repository }} @@ -95,12 +104,13 @@ jobs: You are reviewing and fixing PR #$PR_NUMBER in $REPO. ## Coordination with the CI watcher - The `claude-watcher` workflow also pushes to this PR branch when CI fails. - We share a concurrency group (branch-keyed), so only one of us runs at a time — - but the OTHER may have pushed between the last event and this job starting. - Before committing in STEP 5, `git fetch origin` and `git pull --rebase origin `. - If rebase conflicts, resolve them (prefer the other side's changes unless they - contradict a finding you're fixing), then continue. + The `claude-healing` and `claude-comments` workflows also push to this PR + branch. We share a concurrency group (branch-keyed), so only one of us runs + at a time — but the OTHER may have pushed between the last event and this + job starting. Before committing in STEP 5, `git fetch origin` and + `git pull --rebase origin `. If rebase conflicts, resolve them + (prefer the other side's changes unless they contradict a finding you're + fixing), then continue. ## STEP 1 — Analyze Run `/code-review:code-review` for all CRITICAL/HIGH/MEDIUM findings. Skip low/nits. From 8f90adea3582fcb0f3b33b30aafe9974491c8cc2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 23 Apr 2026 18:02:46 +1200 Subject: [PATCH 6/9] (refactor): replace Claude workflows with claude-pr-owner reusable workflow --- .../actions/classify-complexity/action.yml | 109 ----------- .github/workflows/claude-comments.yml | 113 ----------- .github/workflows/claude-healing.yml | 143 -------------- .github/workflows/claude-improvement.yml | 181 ------------------ .github/workflows/claude.yml | 27 +++ 5 files changed, 27 insertions(+), 546 deletions(-) delete mode 100644 .github/actions/classify-complexity/action.yml delete mode 100644 .github/workflows/claude-comments.yml delete mode 100644 .github/workflows/claude-healing.yml delete mode 100644 .github/workflows/claude-improvement.yml create mode 100644 .github/workflows/claude.yml diff --git a/.github/actions/classify-complexity/action.yml b/.github/actions/classify-complexity/action.yml deleted file mode 100644 index ec8c00c10..000000000 --- a/.github/actions/classify-complexity/action.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: Classify task complexity with Haiku -description: > - Ask Claude Haiku to classify a task as 'simple' or 'complex' and pick a model - pair (primary + fallback). Favours opus — sonnet is only chosen when Haiku - explicitly says 'simple'; any other output routes to opus. - -inputs: - oauth-token: - description: Claude Code OAuth token. - required: true - prompt: - description: > - Full classification prompt. Must instruct Haiku to reply with EXACTLY - one lowercase word — 'simple' or 'complex' — and nothing else. - required: true - -outputs: - model: - description: Selected primary model. - value: ${{ steps.classify.outputs.model }} - fallback: - description: Fallback model. - value: ${{ steps.classify.outputs.fallback }} - classification: - description: Raw classification string ('simple', 'complex', or fallback on empty/garbage). - value: ${{ steps.classify.outputs.classification }} - -runs: - using: composite - steps: - - name: Install Claude CLI - shell: bash - run: | - if ! command -v claude >/dev/null 2>&1; then - curl -fsSL https://claude.ai/install.sh | bash - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - fi - - - name: Run Haiku classification - id: classify - shell: bash - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.oauth-token }} - CLASSIFY_PROMPT: ${{ inputs.prompt }} - run: | - set +e - - # Force the model to emit a JSON object matching this schema — removes - # any chance of leading/trailing prose, punctuation, or casing drift. - SCHEMA='{ - "type": "object", - "properties": { - "classification": { - "type": "string", - "enum": ["simple", "complex"] - } - }, - "required": ["classification"], - "additionalProperties": false - }' - - RAW=$(claude -p \ - --model claude-haiku-4-5 \ - --bare \ - --max-turns 1 \ - --tools "" \ - --dangerously-skip-permissions \ - --output-format json \ - --json-schema "$SCHEMA" \ - "$CLASSIFY_PROMPT") - STATUS=$? - - if [[ $STATUS -ne 0 ]]; then - echo "claude CLI exited $STATUS — routing to opus." - echo "CLI output: $RAW" - fi - - # With --json-schema the structured payload lands at .structured_output as a - # parsed object. Older builds (or runs without schema) put the response at - # .result as either a plain string or a stringified JSON. Try both so the - # action keeps working across CLI versions. - CLASSIFICATION=$(printf '%s' "$RAW" | jq -r ' - (.structured_output.classification // "") as $s - | if $s != "" then $s - else ((.result // "") | fromjson? | .classification // "") - end - ' 2>/dev/null) - - # Last-ditch fallback: plain .result that happens to be the literal word. - if [[ -z "$CLASSIFICATION" ]]; then - CLASSIFICATION=$(printf '%s' "$RAW" | jq -r '.result // empty' 2>/dev/null \ - | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]' | head -c 20) - fi - - echo "Haiku classification: '$CLASSIFICATION'" - - # Favour opus, sonnet only when haiku explicitly says simple. - if [[ "$CLASSIFICATION" == "simple" ]]; then - MODEL="claude-sonnet-4-6" - FALLBACK="claude-opus-4-7" - else - MODEL="claude-opus-4-7" - FALLBACK="claude-sonnet-4-6" - fi - - echo "classification=$CLASSIFICATION" >> "$GITHUB_OUTPUT" - echo "model=$MODEL" >> "$GITHUB_OUTPUT" - echo "fallback=$FALLBACK" >> "$GITHUB_OUTPUT" - echo "Selected primary=$MODEL, fallback=$FALLBACK" diff --git a/.github/workflows/claude-comments.yml b/.github/workflows/claude-comments.yml deleted file mode 100644 index 353622ec2..000000000 --- a/.github/workflows/claude-comments.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Claude Code - -# Only triggers that carry comment bodies worth @claude-scanning. -# pull_request_review is excluded: it fires on every review submit (even bare -# approvals), so the @claude filter would show a "Skipped" check in the PR jobs -# list every time. Inline review comments still fire through -# pull_request_review_comment, so we don't lose the typical reviewer flow. -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - -# For PR review events (where head.ref is available), share the branch queue -# with claude-improvement and claude-healing so we never push concurrently to -# the same head. For issue_comment / issues events, fall back to a per-issue -# key — can't resolve the branch at concurrency-evaluation time without an API -# call, but at least successive @claude comments on one PR/issue serialize. -concurrency: - group: claude-pr-${{ github.event.pull_request.head.ref || github.event.issue.number || github.run_id }} - cancel-in-progress: false - -jobs: - claude: - # Gate on author association: Claude runs with --dangerously-skip-permissions - # and contents: write, so user-controlled prompt content from untrusted - # commenters would be a prompt-injection vector. Only trusted associations - # (repo owner, org member, collaborator) can invoke @claude. - if: | - ( - github.event.comment.author_association == 'OWNER' || - github.event.comment.author_association == 'MEMBER' || - github.event.comment.author_association == 'COLLABORATOR' || - github.event.issue.author_association == 'OWNER' || - github.event.issue.author_association == 'MEMBER' || - github.event.issue.author_association == 'COLLABORATOR' - ) && ( - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - ) - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - issues: write - id-token: write - actions: read - steps: - - name: Resolve PR head ref - id: ref - env: - GH_TOKEN: ${{ github.token }} - REPO: ${{ github.repository }} - PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} - ISSUE_PR_URL: ${{ github.event.issue.pull_request.url }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - run: | - # pull_request_review* events expose head.ref directly. - # issue_comment events on a PR expose issue.pull_request.url; resolve - # the branch via gh. Plain issues have neither → stay on default branch. - if [[ -n "$PR_HEAD_REF" ]]; then - ref="$PR_HEAD_REF" - elif [[ -n "$ISSUE_PR_URL" ]]; then - ref=$(gh pr view "$ISSUE_NUMBER" --repo "$REPO" --json headRefName --jq .headRefName) - else - ref="" - fi - echo "ref=$ref" >> "$GITHUB_OUTPUT" - echo "Resolved checkout ref: '${ref:-}'" - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ steps.ref.outputs.ref }} - fetch-depth: 20 - - - name: Classify task complexity with Haiku - id: classify - uses: ./.github/actions/classify-complexity - with: - oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - prompt: | - Classify this coding task's complexity. Reply with EXACTLY one lowercase word and nothing else: 'simple' or 'complex'. - - simple = typo fix, docs tweak, one-file obvious bug, rename, trivial refactor within a single function - complex = multi-file change, new feature, architecture or API change, deep debugging, performance work, anything with unclear scope or touching more than ~3 files - - TITLE: ${{ github.event.issue.title }} - - REQUEST: - ${{ github.event.comment.body || github.event.issue.body }} - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - plugin_marketplaces: | - https://github.com/anthropics/claude-code.git - https://github.com/abnegate/claudes.git - plugins: 'skills@claudes' - additional_permissions: | - actions: read - use_sticky_comment: true - use_commit_signing: true - exclude_comments_by_actor: 'dependabot[bot],renovate[bot]' - claude_args: | - --model ${{ steps.classify.outputs.model }} - --fallback-model ${{ steps.classify.outputs.fallback }} - --dangerously-skip-permissions diff --git a/.github/workflows/claude-healing.yml b/.github/workflows/claude-healing.yml deleted file mode 100644 index 408bdb5a5..000000000 --- a/.github/workflows/claude-healing.yml +++ /dev/null @@ -1,143 +0,0 @@ -name: Claude CI Healing - -on: - workflow_run: - workflows: [Tests] - types: [completed] - -# Shared with claude-improvement and claude-comments: all three mutate the PR -# branch (commit + push), so they must not run simultaneously. Keying on head -# branch serializes runs targeting the same PR into a single queue. -concurrency: - group: claude-pr-${{ github.event.workflow_run.head_branch }} - cancel-in-progress: false - -jobs: - fix-failures: - # Only fire on PR failures (not push-to-main) and skip fork PRs. Blocking - # fork PRs also mitigates prompt injection via CI logs — only collaborators - # with push access can introduce branch content that would feed the prompt. - if: > - github.event.workflow_run.conclusion == 'failure' && - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.head_repository.full_name == github.repository - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - issues: read - actions: read - checks: read - id-token: write - - steps: - - name: Find the PR - id: pr - env: - GH_TOKEN: ${{ github.token }} - HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} - run: | - pr_number=$(gh pr list --repo "$GITHUB_REPOSITORY" \ - --head "$HEAD_BRANCH" --state open --json number --jq '.[0].number') - if [ -z "$pr_number" ]; then - echo "No open PR found — skipping." - echo "skip=true" >> "$GITHUB_OUTPUT" - else - echo "number=$pr_number" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Checkout PR branch - if: steps.pr.outputs.skip != 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.workflow_run.head_branch }} - fetch-depth: 0 - - - name: Get failure logs - if: steps.pr.outputs.skip != 'true' - env: - GH_TOKEN: ${{ github.token }} - RUN_ID: ${{ github.event.workflow_run.id }} - run: | - gh run view "$RUN_ID" --log-failed 2>&1 | tail -200 > /tmp/ci-failure-logs.txt - echo "Captured $(wc -l < /tmp/ci-failure-logs.txt) lines of failure logs" - - - name: Extract failure log excerpt - if: steps.pr.outputs.skip != 'true' - id: excerpt - run: | - { - echo 'log<<__EOF__' - tail -120 /tmp/ci-failure-logs.txt - echo '__EOF__' - } >> "$GITHUB_OUTPUT" - - - name: Classify failure complexity with Haiku - if: steps.pr.outputs.skip != 'true' - id: classify - uses: ./.github/actions/classify-complexity - with: - oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - prompt: | - Classify this CI failure's fix complexity. Reply with EXACTLY one lowercase word and nothing else: 'simple' or 'complex'. - - simple = lint violation, formatting, obvious typo, missing import, trivial test assertion fix, single-file obvious mistake - complex = compile error with unclear cause, real test failure exposing a bug, flaky infrastructure, multi-file breakage, anything with unclear root cause or scope - - FAILURE LOGS (tail): - ${{ steps.excerpt.outputs.log }} - - - name: Claude fixes CI failures - if: steps.pr.outputs.skip != 'true' - uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104 - env: - PR_NUMBER: ${{ steps.pr.outputs.number }} - FAILED_RUN_ID: ${{ github.event.workflow_run.id }} - HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - plugin_marketplaces: | - https://github.com/anthropics/claude-code.git - https://github.com/abnegate/claudes.git - plugins: 'skills@claudes' - use_sticky_comment: 'false' - use_commit_signing: 'true' - show_full_output: 'true' - claude_args: | - --model ${{ steps.classify.outputs.model }} - --fallback-model ${{ steps.classify.outputs.fallback }} - --dangerously-skip-permissions - prompt: | - CI failed on PR #$PR_NUMBER (run $FAILED_RUN_ID). - - The failure logs are at /tmp/ci-failure-logs.txt. Read them first. - - ## Coordination with the code reviewer - The `claude-improvement` and `claude-comments` workflows also push to - this PR branch. We share a concurrency group (branch-keyed), so only one - of us runs at a time — but the other may have pushed fix commits between - the failing CI run and this job starting. Before STEP 6, - `git fetch origin` and `git pull --rebase origin $HEAD_BRANCH`. If rebase - conflicts, resolve them (prefer the other workflow's changes unless they - directly contradict your CI fix), then continue. After rebasing, re-check - whether the CI failure is still reproducible — the other workflow may have - already fixed it, in which case post a comment saying so and STOP without - pushing. - - Your job: - 1. Read the failure logs to identify the root cause (compile error, test failure, lint violation, etc.) - 2. Read the project's CLAUDE.md for build/test/lint commands and conventions - 3. Fix the issue directly in the codebase - 4. Verify your fix by re-running the failed step (use whatever build system the project uses) - 5. Post a brief PR comment explaining what failed and what you fixed: - `gh pr comment $PR_NUMBER --repo $GITHUB_REPOSITORY --body "..."` - 6. `git fetch origin && git pull --rebase origin $HEAD_BRANCH`, then commit with - message: `(fix): CI — ` - 7. Push to the PR branch - - Rules: - - Only fix the CI failure. Don't refactor, clean up, or improve other code. - - If the failure is a flaky test (passes on retry), just re-run it and comment that it was flaky. - - If you can't determine the root cause, post a comment asking for help instead of guessing. - - Never push to main. Only push to the PR head branch. diff --git a/.github/workflows/claude-improvement.yml b/.github/workflows/claude-improvement.yml deleted file mode 100644 index b5b9aa931..000000000 --- a/.github/workflows/claude-improvement.yml +++ /dev/null @@ -1,181 +0,0 @@ -name: Claude Code Improvements - -on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - -# Shared with claude-healing and claude-comments: all three mutate the PR branch -# (commit + push), so they must not run simultaneously. Keying on head branch -# serializes runs targeting the same PR into a single queue. -concurrency: - group: claude-pr-${{ github.event.pull_request.head.ref }} - cancel-in-progress: false - -jobs: - claude-review: - runs-on: ubuntu-latest - # Defense in depth against prompt injection via PR title/body: require the - # PR author to be a trusted association in addition to the head being - # internal to this repo (which already blocks forks). - if: > - github.event.pull_request.head.repo.full_name == github.repository && - ( - github.event.pull_request.author_association == 'OWNER' || - github.event.pull_request.author_association == 'MEMBER' || - github.event.pull_request.author_association == 'COLLABORATOR' - ) - permissions: - contents: write - pull-requests: write - issues: read - id-token: write - - steps: - - name: Checkout PR branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 0 - - - name: Compute PR diff stats - id: diff - env: - BASE_REF: ${{ github.event.pull_request.base.ref }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - git fetch --no-tags origin "+refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF" - merge_base=$(git merge-base "origin/$BASE_REF" "$HEAD_SHA") - changed_files=$(git diff --name-only "$merge_base..$HEAD_SHA" | wc -l | tr -d ' ') - changed_lines=$(git diff --shortstat "$merge_base..$HEAD_SHA" | awk '{ ins=0; del=0; for (i=1;i<=NF;i++) { if ($i ~ /insertion/) ins=$(i-1); if ($i ~ /deletion/) del=$(i-1) } print ins + del }') - changed_lines=${changed_lines:-0} - - { - echo "files=$changed_files" - echo "lines=$changed_lines" - echo 'file_list<<__EOF__' - git diff --name-only "$merge_base..$HEAD_SHA" | head -50 - echo '__EOF__' - } >> "$GITHUB_OUTPUT" - - - name: Classify PR complexity with Haiku - id: classify - uses: ./.github/actions/classify-complexity - with: - oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - prompt: | - Classify this pull request's review complexity. Reply with EXACTLY one lowercase word and nothing else: 'simple' or 'complex'. - - simple = typo fix, docs tweak, one-file obvious bug, rename, trivial refactor within a single function, small test-only change - complex = multi-file change, new feature, architecture or API change, deep debugging, performance work, security-sensitive code, anything with unclear scope or touching more than ~3 files - - TITLE: ${{ github.event.pull_request.title }} - - STATS: ${{ steps.diff.outputs.files }} files, ${{ steps.diff.outputs.lines }} lines changed - - FILES: - ${{ steps.diff.outputs.file_list }} - - DESCRIPTION: - ${{ github.event.pull_request.body }} - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104 - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} - HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - plugin_marketplaces: | - https://github.com/anthropics/claude-code.git - https://github.com/abnegate/claudes.git - plugins: | - code-review@claude-code-plugins - skills@claudes - use_sticky_comment: 'false' - use_commit_signing: 'true' - show_full_output: 'true' - claude_args: | - --model ${{ steps.classify.outputs.model }} - --fallback-model ${{ steps.classify.outputs.fallback }} - --dangerously-skip-permissions - prompt: | - You are reviewing and fixing PR #$PR_NUMBER in $REPO. - - ## Coordination with the CI watcher - The `claude-healing` and `claude-comments` workflows also push to this PR - branch. We share a concurrency group (branch-keyed), so only one of us runs - at a time — but the OTHER may have pushed between the last event and this - job starting. Before committing in STEP 5, `git fetch origin` and - `git pull --rebase origin `. If rebase conflicts, resolve them - (prefer the other side's changes unless they contradict a finding you're - fixing), then continue. - - ## STEP 1 — Analyze - Run `/code-review:code-review` for all CRITICAL/HIGH/MEDIUM findings. Skip low/nits. - - ## STEP 2 — Post inline review - Build /tmp/review.json with this structure and post it: - ```json - { - "event": "COMMENT", - "body": "## Code Review\n\n**N finding(s)**\n\nSee inline comments. Fixes incoming.", - "comments": [ - {"path": "file.php", "line": 42, "body": "**[SEVERITY]** ...\n\nExplanation + fix."} - ] - } - ``` - Post: `gh api repos/$REPO/pulls/$PR_NUMBER/reviews --input /tmp/review.json` - - If zero findings: post "No critical/high/medium findings." and STOP. - - ## STEP 3 — Fix in parallel via isolated worktree subagents - For MAXIMUM speed, launch one Agent per finding using worktree isolation. - Findings in different files run in TRUE parallel — launch them ALL in one message. - Findings in the SAME file go to the SAME agent to avoid conflicts. - - Each agent prompt must be self-contained: - - Include the finding: severity, file path, line numbers, what's wrong, how to fix - - Tell it to verify the fix compiles (read CLAUDE.md for the build command) - - Tell it NOT to touch other files or make unrelated changes - - Example — 3 findings in 3 files, all launched at once: - Agent({description: "Fix 1", isolation: "worktree", prompt: "Fix [HIGH] ... in file.php line 42 ..."}) - Agent({description: "Fix 2", isolation: "worktree", prompt: "Fix [MEDIUM] ... in other.php line 99 ..."}) - Agent({description: "Fix 3", isolation: "worktree", prompt: "Fix [MEDIUM] ... in third.php line 7 ..."}) - - ## STEP 4 — Consolidate - After all agents finish, apply their changes to the main checkout: - - Each worktree agent returns the files it changed - - Cherry-pick or manually apply each agent's diff to the working tree - - If two agents touched the same file, merge carefully - - Verify final result compiles - - ## STEP 5 — Commit and push - - `git fetch origin && git pull --rebase origin $HEAD_BRANCH` to absorb any - commits the watcher (or the PR author) pushed while this job was queued - - Capture the pre-commit tip: `BEFORE_SHA=$(git rev-parse HEAD)` - - Stage only fix files - - Commit: `(fix): address review findings — X HIGH, Y MEDIUM` - - Body: one bullet per finding - - Push to PR branch - - Capture the post-push tip: `AFTER_SHA=$(git rev-parse HEAD)` - - ## STEP 6 — Post summary comment with compare link - After a successful push, add a follow-up comment linking to a compare view - of everything this run added, so reviewers can see exactly what changed: - - COMPARE_URL="https://github.com/$REPO/compare/$BEFORE_SHA...$AFTER_SHA" - gh pr comment $PR_NUMBER --repo $REPO --body "Fixes pushed: $COMPARE_URL - - " - - If BEFORE_SHA equals AFTER_SHA (nothing was actually pushed — e.g. all - fixes were no-ops after rebase), skip this step. - - Rules: - - Do NOT skip findings. - - Maximize parallelism — launch as many worktree agents as there are independent file groups. - - Each agent prompt must be fully self-contained (it has no context from this conversation). - - Never push to main. diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 000000000..46933b313 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,27 @@ +name: Claude + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + pull_request_review: + types: [submitted] + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + workflow_run: + workflows: [Tests] + types: [completed] + +jobs: + claude: + uses: abnegate/claude-pr-owner/.github/workflows/orchestrator.yml@main + secrets: + oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + with: + improvement: true + healing: true + bots: true + comments: true From a663ab6cee2abb983ab5c774bac755cee799b2fc Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 23 Apr 2026 18:07:51 +1200 Subject: [PATCH 7/9] (fix): grant permissions on Claude caller job --- .github/workflows/claude.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 46933b313..0ab69f62b 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -17,6 +17,14 @@ on: jobs: claude: + # Caller must grant the union of every permission the callee's jobs ask + # for; reusable workflows can't exceed the caller's ceiling. + permissions: + contents: write + pull-requests: write + issues: write + actions: read + id-token: write uses: abnegate/claude-pr-owner/.github/workflows/orchestrator.yml@main secrets: oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} From 157d127231d50bc809772847344dcba3fa455ca8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 23 Apr 2026 18:14:31 +1200 Subject: [PATCH 8/9] (chore): retrigger Claude workflow From 6db64ff986a7b53bec7a5bf1d648756cda121f50 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 23 Apr 2026 22:28:40 +1200 Subject: [PATCH 9/9] (chore): pin claude-pr-owner to v0.1.0 --- .github/workflows/claude.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 0ab69f62b..ca56742df 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -25,7 +25,7 @@ jobs: issues: write actions: read id-token: write - uses: abnegate/claude-pr-owner/.github/workflows/orchestrator.yml@main + uses: abnegate/claude-pr-owner/.github/workflows/orchestrator.yml@e6baaab0ae24628a4d6e8b695b49a77f37e797f7 # v0.1.0 secrets: oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} with: