From fc83c6c568fa9152d5b3127fef33c6e9396d416f Mon Sep 17 00:00:00 2001 From: loothero Date: Sun, 3 May 2026 05:36:16 -0700 Subject: [PATCH 1/2] ci: iterate the JSON array root when extracting Claude review text The action writes \`execution_file\` as a single JSON array (\`JSON.stringify(messages, null, 2)\` in run-claude-sdk.ts), not as JSONL. The previous \`jq -r 'select(.type == "result") | .result\`\` filter evaluated \`.type\` against the array root, hit \`Cannot index array with string "type"\`, and exited 5. With \`bash -e\` that propagated and failed the Extract step before the explicit empty- output guard could fire, so all four \`claude-review-*\` jobs failed in ~35s with no captured comment (PR #144 run 25279155808). Add \`.[]\` to iterate over the array root in both the result-message and assistant-text-fallback filters. Verified locally with a sample matching run-claude-sdk.ts's structure: old filter -> exit 5 + parse error; new filter -> review text + exit 0. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pr-ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 8aab2b19..24acd6c7 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -509,9 +509,9 @@ jobs: exit 1 fi - REVIEW=$(jq -r 'select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) + REVIEW=$(jq -r '.[] | select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) if [ -z "$REVIEW" ]; then - REVIEW=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) + REVIEW=$(jq -r '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) fi if [ -z "$REVIEW" ]; then echo "::error::Claude review produced no extractable output" @@ -753,9 +753,9 @@ jobs: exit 1 fi - REVIEW=$(jq -r 'select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) + REVIEW=$(jq -r '.[] | select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) if [ -z "$REVIEW" ]; then - REVIEW=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) + REVIEW=$(jq -r '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) fi if [ -z "$REVIEW" ]; then echo "::error::Claude review produced no extractable output" @@ -997,9 +997,9 @@ jobs: exit 1 fi - REVIEW=$(jq -r 'select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) + REVIEW=$(jq -r '.[] | select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) if [ -z "$REVIEW" ]; then - REVIEW=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) + REVIEW=$(jq -r '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) fi if [ -z "$REVIEW" ]; then echo "::error::Claude review produced no extractable output" @@ -1242,9 +1242,9 @@ jobs: exit 1 fi - REVIEW=$(jq -r 'select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) + REVIEW=$(jq -r '.[] | select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) if [ -z "$REVIEW" ]; then - REVIEW=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) + REVIEW=$(jq -r '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) fi if [ -z "$REVIEW" ]; then echo "::error::Claude review produced no extractable output" From 4b7c45ae92bbd55c08ef3b12cfaf46bcdd15dfc7 Mon Sep 17 00:00:00 2001 From: loothero Date: Sun, 3 May 2026 05:46:22 -0700 Subject: [PATCH 2/2] ci: harden Claude review extraction against multi-line, error, and missing output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three additional bugs in the previous PR 146 commit (fc83c6c) that local end-to-end testing surfaced: 1. \`tail -1\` chops the multi-line .result string. Claude's review is a single string with embedded newlines; jq -r emits them as physical line breaks. \`tail -1\` would have kept only the final summary line ("Summary: 0 CRITICAL, 1 HIGH, ..."). The blocking-check greps for \`[(CRITICAL|HIGH)]\` (with brackets) which lives in the body, not the bracketed-summary form — so a real HIGH/CRITICAL finding would have been silently dropped from /tmp/review.txt and the job would have passed. Catastrophic. Fix: \`[.[] | select(.type == "result")] | last | .result // empty\` — collect, take last array element, no tail. 2. jq exits 5 on parse errors of malformed JSON. Under the GitHub Actions default shell (bash --noprofile --norc -eo pipefail {0}), pipefail propagates that through the substitution and set -e kills the step before our explicit empty-output guard fires. Add \`|| true\` to mask jq exit codes; the explicit \`[ -z "$REVIEW" ]\` guard then handles it. 3. SDKResultMessage has two variants: SDKResultSuccess (with .result: string) and SDKResultError (no .result, has .errors[]). When Claude errors mid-task, .result is missing — the assistant-text fallback now joins all text blocks across messages with double-newlines so the comment is informative rather than empty. Also: Extract step now writes a heading-prefixed fallback to /tmp/review.txt on failure paths so Post can post it directly without duplicating the heading (which both Claude's prompt instructions and the previous Post step were prepending). Single canonical heading. Verified end-to-end against the actual Claude Code stream-JSON shape (JSON.stringify(SDKMessage[]) per base-action/src/run-claude-sdk.ts): - multi-line .result: full body preserved ✓ - HIGH-finding multi-line .result: blocking-check still fires ✓ - SDKResultError (no .result): assistant-text fallback ✓ - empty array / malformed JSON / missing file: exit 1 with clear error, fallback comment posted, job fails ✓ Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pr-ci.yml | 100 +++++++++++++++--------------------- 1 file changed, 40 insertions(+), 60 deletions(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 24acd6c7..f1c118e9 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -501,19 +501,20 @@ jobs: if: always() env: EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} + HEADING: "## Claude Review - Cairo/Starknet Contract Review" run: | - : > /tmp/review.txt - if [ -z "$EXECUTION_FILE" ] || [ ! -f "$EXECUTION_FILE" ]; then + printf '%s\n\nNo review output was produced.\n' "$HEADING" > /tmp/review.txt echo "::error::Claude action did not produce an execution file" exit 1 fi - REVIEW=$(jq -r '.[] | select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) + REVIEW=$(jq -r '[.[] | select(.type == "result")] | last | .result // empty' "$EXECUTION_FILE" 2>/dev/null || true) if [ -z "$REVIEW" ]; then - REVIEW=$(jq -r '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) + REVIEW=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | join("\n\n")' "$EXECUTION_FILE" 2>/dev/null || true) fi if [ -z "$REVIEW" ]; then + printf '%s\n\nNo review output was produced.\n' "$HEADING" > /tmp/review.txt echo "::error::Claude review produced no extractable output" exit 1 fi @@ -524,17 +525,11 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - { - echo "## Claude Review - Cairo/Starknet Contract Review" - echo "" - if [ -s /tmp/review.txt ]; then - cat /tmp/review.txt - else - echo "No review output was produced." - fi - } > /tmp/review-formatted.txt - - gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review-formatted.txt + if [ ! -s /tmp/review.txt ]; then + echo "review file is empty; skipping comment" + exit 0 + fi + gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review.txt - name: Check for blocking findings if: always() @@ -745,19 +740,20 @@ jobs: if: always() env: EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} + HEADING: "## Claude Review - React/Frontend Review" run: | - : > /tmp/review.txt - if [ -z "$EXECUTION_FILE" ] || [ ! -f "$EXECUTION_FILE" ]; then + printf '%s\n\nNo review output was produced.\n' "$HEADING" > /tmp/review.txt echo "::error::Claude action did not produce an execution file" exit 1 fi - REVIEW=$(jq -r '.[] | select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) + REVIEW=$(jq -r '[.[] | select(.type == "result")] | last | .result // empty' "$EXECUTION_FILE" 2>/dev/null || true) if [ -z "$REVIEW" ]; then - REVIEW=$(jq -r '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) + REVIEW=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | join("\n\n")' "$EXECUTION_FILE" 2>/dev/null || true) fi if [ -z "$REVIEW" ]; then + printf '%s\n\nNo review output was produced.\n' "$HEADING" > /tmp/review.txt echo "::error::Claude review produced no extractable output" exit 1 fi @@ -768,17 +764,11 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - { - echo "## Claude Review - React/Frontend Review" - echo "" - if [ -s /tmp/review.txt ]; then - cat /tmp/review.txt - else - echo "No review output was produced." - fi - } > /tmp/review-formatted.txt - - gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review-formatted.txt + if [ ! -s /tmp/review.txt ]; then + echo "review file is empty; skipping comment" + exit 0 + fi + gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review.txt - name: Check for blocking findings if: always() @@ -989,19 +979,20 @@ jobs: if: always() env: EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} + HEADING: "## Claude Review - Indexer/API Review" run: | - : > /tmp/review.txt - if [ -z "$EXECUTION_FILE" ] || [ ! -f "$EXECUTION_FILE" ]; then + printf '%s\n\nNo review output was produced.\n' "$HEADING" > /tmp/review.txt echo "::error::Claude action did not produce an execution file" exit 1 fi - REVIEW=$(jq -r '.[] | select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) + REVIEW=$(jq -r '[.[] | select(.type == "result")] | last | .result // empty' "$EXECUTION_FILE" 2>/dev/null || true) if [ -z "$REVIEW" ]; then - REVIEW=$(jq -r '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) + REVIEW=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | join("\n\n")' "$EXECUTION_FILE" 2>/dev/null || true) fi if [ -z "$REVIEW" ]; then + printf '%s\n\nNo review output was produced.\n' "$HEADING" > /tmp/review.txt echo "::error::Claude review produced no extractable output" exit 1 fi @@ -1012,17 +1003,11 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - { - echo "## Claude Review - Indexer/API Review" - echo "" - if [ -s /tmp/review.txt ]; then - cat /tmp/review.txt - else - echo "No review output was produced." - fi - } > /tmp/review-formatted.txt - - gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review-formatted.txt + if [ ! -s /tmp/review.txt ]; then + echo "review file is empty; skipping comment" + exit 0 + fi + gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review.txt - name: Check for blocking findings if: always() @@ -1234,19 +1219,20 @@ jobs: if: always() env: EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} + HEADING: "## Claude Review - General Engineering Review" run: | - : > /tmp/review.txt - if [ -z "$EXECUTION_FILE" ] || [ ! -f "$EXECUTION_FILE" ]; then + printf '%s\n\nNo review output was produced.\n' "$HEADING" > /tmp/review.txt echo "::error::Claude action did not produce an execution file" exit 1 fi - REVIEW=$(jq -r '.[] | select(.type == "result") | .result // empty' "$EXECUTION_FILE" 2>/dev/null | tail -1) + REVIEW=$(jq -r '[.[] | select(.type == "result")] | last | .result // empty' "$EXECUTION_FILE" 2>/dev/null || true) if [ -z "$REVIEW" ]; then - REVIEW=$(jq -r '.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "$EXECUTION_FILE" 2>/dev/null) + REVIEW=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | join("\n\n")' "$EXECUTION_FILE" 2>/dev/null || true) fi if [ -z "$REVIEW" ]; then + printf '%s\n\nNo review output was produced.\n' "$HEADING" > /tmp/review.txt echo "::error::Claude review produced no extractable output" exit 1 fi @@ -1257,17 +1243,11 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - { - echo "## Claude Review - General Engineering Review" - echo "" - if [ -s /tmp/review.txt ]; then - cat /tmp/review.txt - else - echo "No review output was produced." - fi - } > /tmp/review-formatted.txt - - gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review-formatted.txt + if [ ! -s /tmp/review.txt ]; then + echo "review file is empty; skipping comment" + exit 0 + fi + gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review.txt - name: Check for blocking findings if: always()