From 513262ab9ec336fe2687671ca9f3c49da599baca Mon Sep 17 00:00:00 2001 From: Adnaan Badr Date: Mon, 20 Jul 2026 00:56:50 +0000 Subject: [PATCH 1/3] ci(review): keep the review run log so failures are diagnosable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the review job fails, the job log reports only the aggregate result — is_error: true alongside a permission_denials_count — and never names the tool that was denied. That is the one fact needed to fix it, and it lives in the execution output file, which is written to the runner temp dir and discarded with the runner. Diagnosing the current failures ruled out three plausible causes and confirmed none: auth is fine (the run initializes and bills), quota is fine (it spent real budget across 4 turns), and the action version is unchanged (the v1 tag has not moved since before the last green run). The denial itself is all that is left, and it is the part not recorded. Upload it unconditionally. Re-triggering a run is cheap but reproducing one is not: which tool the model reaches for varies per PR, so a failure not captured when it happens may not recur on demand. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ui2cwpeGkrUfRt8rh2FgGG --- .github/workflows/claude-code-review.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index e806228..99206b5 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -45,3 +45,20 @@ jobs: claude_args: | --max-turns 20 --allowedTools "Read,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" + + # The run's turn-by-turn log, kept whatever the outcome. When the review + # fails, the job log reports only the aggregate — `is_error: true` with a + # `permission_denials_count` — and never names the tool that was denied, + # which is the one fact needed to fix it. That detail lives in this file, + # written to the runner's temp dir and otherwise discarded with the runner. + # Runs are cheap to re-trigger but not to reproduce: which tool the model + # reaches for varies per PR, so a failure not captured when it happens may + # not recur on demand. + - name: Upload review execution log + if: always() + uses: actions/upload-artifact@v4 + with: + name: claude-review-execution-log + path: ${{ steps.claude-review.outputs.execution_file }} + if-no-files-found: warn + retention-days: 14 From 78626406fd3262b503275298e97256f087d92678 Mon Sep 17 00:00:00 2001 From: Adnaan Badr Date: Mon, 20 Jul 2026 01:11:41 +0000 Subject: [PATCH 2/3] fix(ci): fall back to the temp path when the review step fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upload added in the parent commit produced no artifact on the very first failing run it was meant to capture. The job log showed the step running with an empty path: with: name: claude-review-execution-log if-no-files-found: warn A step that fails never sets its outputs, so steps.claude-review.outputs .execution_file is empty in exactly the case this upload exists for — it would only ever have captured the runs that did not need capturing. Fall back to the action documented temp path, which is where it writes the file regardless of outcome. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ui2cwpeGkrUfRt8rh2FgGG --- .github/workflows/claude-code-review.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 99206b5..5ed62f9 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -59,6 +59,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: claude-review-execution-log - path: ${{ steps.claude-review.outputs.execution_file }} + # Falls back to the action's documented temp path: a step that FAILED + # never sets its outputs, so execution_file is empty in exactly the + # case this upload exists for. + path: ${{ steps.claude-review.outputs.execution_file || format('{0}/claude-execution-output.json', runner.temp) }} if-no-files-found: warn retention-days: 14 From ffc689826650eae58d826e389904c92e2033737c Mon Sep 17 00:00:00 2001 From: Adnaan Badr Date: Mon, 20 Jul 2026 01:19:10 +0000 Subject: [PATCH 3/3] fix(ci): locate the execution log instead of assuming its path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second correction. The upload still captured nothing: ##[warning]No files were found with the provided path: /home/runner/work/_temp/claude-execution-output.json Two wrong assumptions, one per attempt. First, that a failed step sets its outputs — it does not, so execution_file was empty exactly when the capture mattered. Second, that the path the action prints during the run still holds the file when a later step looks — it does not, and the output is empty even on a passing run, so there was no correct value to fall back to either. Stop guessing and search, then upload whatever turns up. When the search finds nothing, print where it looked and what is actually in RUNNER_TEMP, so the next iteration starts from evidence rather than another guess. A silent no-op upload is worse than no upload at all: it reads as "captured" on the next failure, which is when someone will be relying on it. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ui2cwpeGkrUfRt8rh2FgGG --- .github/workflows/claude-code-review.yml | 37 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 5ed62f9..6db59ed 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -49,19 +49,38 @@ jobs: # The run's turn-by-turn log, kept whatever the outcome. When the review # fails, the job log reports only the aggregate — `is_error: true` with a # `permission_denials_count` — and never names the tool that was denied, - # which is the one fact needed to fix it. That detail lives in this file, - # written to the runner's temp dir and otherwise discarded with the runner. - # Runs are cheap to re-trigger but not to reproduce: which tool the model - # reaches for varies per PR, so a failure not captured when it happens may - # not recur on demand. + # which is the one fact needed to fix it. + # + # The file is located rather than assumed. The action's `execution_file` + # output is empty even on success, and the path it prints during the run + # ("Log saved to $RUNNER_TEMP/claude-execution-output.json") holds nothing + # by the time a later step looks. So search, and when the search comes up + # empty say where it looked — a step that silently uploads nothing is worse + # than no step, since it reads as "captured" on the next failure. + - name: Collect review execution log + if: always() + run: | + mkdir -p /tmp/claude-review-log + found=$(find "${RUNNER_TEMP}" "${GITHUB_WORKSPACE}/.." -maxdepth 4 \ + -name 'claude-execution-output*.json' -o -name 'claude-*output*.json' 2>/dev/null | head -20) + if [ -n "$found" ]; then + echo "$found" | while read -r f; do + cp "$f" "/tmp/claude-review-log/$(echo "$f" | tr '/' '_')" 2>/dev/null || true + done + echo "Collected:"; ls -la /tmp/claude-review-log + else + echo "No execution log found. Searched RUNNER_TEMP=${RUNNER_TEMP} and the workspace parent." + echo "--- RUNNER_TEMP contents ---" + ls -la "${RUNNER_TEMP}" 2>/dev/null | head -40 || echo "(unreadable)" + echo "--- *.json anywhere under RUNNER_TEMP ---" + find "${RUNNER_TEMP}" -name '*.json' -maxdepth 3 2>/dev/null | head -20 || true + fi + - name: Upload review execution log if: always() uses: actions/upload-artifact@v4 with: name: claude-review-execution-log - # Falls back to the action's documented temp path: a step that FAILED - # never sets its outputs, so execution_file is empty in exactly the - # case this upload exists for. - path: ${{ steps.claude-review.outputs.execution_file || format('{0}/claude-execution-output.json', runner.temp) }} + path: /tmp/claude-review-log/ if-no-files-found: warn retention-days: 14