diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 7d4a9196..8aab2b19 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -23,7 +23,7 @@ jobs: api: ${{ steps.filter.outputs.api }} indexer_api_ci: ${{ steps.filter.outputs.indexer_api_ci }} indexer_api_review: ${{ steps.filter.outputs.indexer_api_review }} - general_review: ${{ steps.filter.outputs.general_review }} + general_review: ${{ steps.general.outputs.general_review }} can_run_ai_reviews: ${{ steps.ai-gate.outputs.can_run_ai_reviews }} ai_reviews_block_reason: ${{ steps.ai-gate.outputs.ai_reviews_block_reason }} can_run_claude_reviews: ${{ steps.ai-gate.outputs.can_run_claude_reviews }} @@ -65,12 +65,21 @@ jobs: - ".github/workflows/pr-ci.yml" - ".github/prompts/**" - ".github/actions/**" - general_review: - - "**" - - "!contracts/**" - - "!client/**" - - "!indexer/**" - - "!api/**" + + # True iff the PR touches a file outside contracts/, client/, indexer/, api/. + - name: Compute general_review + id: general + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + NON_SCOPED=$(gh pr diff "$PR_NUMBER" --name-only \ + | grep -Ev '^(contracts|client|indexer|api)/' || true) + if [ -n "$NON_SCOPED" ]; then + echo "general_review=true" >> "$GITHUB_OUTPUT" + else + echo "general_review=false" >> "$GITHUB_OUTPUT" + fi - name: Determine AI review availability id: ai-gate @@ -478,6 +487,7 @@ jobs: echo "${DELIMITER}" >> "$GITHUB_OUTPUT" - name: Run Claude review + id: claude # v1.0.111 — pin by SHA for security (third-party action with write perms + secrets). uses: anthropics/claude-code-action@fefa07e9c665b7320f08c3b525980457f22f58aa with: @@ -487,45 +497,54 @@ jobs: --model claude-opus-4-7 --allowedTools "Bash(git diff *),Bash(git log *),Bash(git show *),Read,Glob,Grep" - - name: Check for blocking findings + - name: Extract review output if: always() env: - GH_TOKEN: ${{ github.token }} - HEADER: "## Claude Review - Cairo/Starknet Contract Review" - REVIEW_MARKER: run=${{ github.run_id }} attempt=${{ github.run_attempt }} sha=${{ github.event.pull_request.head.sha }} scope=contracts + EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} run: | - MAX_ATTEMPTS=6 - SLEEP_SECONDS=10 - BODY="" - - for i in $(seq 1 "$MAX_ATTEMPTS"); do - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg header "$HEADER" --arg marker "$REVIEW_MARKER" \ - '[.[] | select((.body // "") | contains($header) and contains($marker))] | last | .body // ""') - - if [ -z "$BODY" ]; then - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg header "$HEADER" --arg run_token "run=${{ github.run_id }}" --arg sha_token "sha=${{ github.event.pull_request.head.sha }}" \ - '[.[] | select((.body // "") | contains($header) and contains($run_token) and contains($sha_token))] | last | .body // ""') - fi + : > /tmp/review.txt - if [ -n "$BODY" ]; then - echo "Found Claude review comment on attempt $i" - break - fi + if [ -z "$EXECUTION_FILE" ] || [ ! -f "$EXECUTION_FILE" ]; then + 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) + if [ -z "$REVIEW" ]; then + 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" + exit 1 + fi + printf '%s\n' "$REVIEW" > /tmp/review.txt - if [ "$i" -lt "$MAX_ATTEMPTS" ]; then - echo "Claude review comment not found yet; sleeping ${SLEEP_SECONDS}s... (attempt $i/$MAX_ATTEMPTS)" - sleep "$SLEEP_SECONDS" + - name: Post review comment + if: always() + 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 - done + } > /tmp/review-formatted.txt - if [ -z "$BODY" ]; then - echo "::error::Could not find Claude review output for contracts" + gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review-formatted.txt + + - name: Check for blocking findings + if: always() + run: | + if [ ! -s /tmp/review.txt ]; then + echo "::error::No review output was produced despite reviewable changes" exit 1 fi - if echo "$BODY" | grep -qE '\[(CRITICAL|HIGH)\]'; then + if grep -qE '\[(CRITICAL|HIGH)\]' /tmp/review.txt; then echo "::error::Review found CRITICAL or HIGH severity issues" exit 1 fi @@ -712,6 +731,7 @@ jobs: echo "${DELIMITER}" >> "$GITHUB_OUTPUT" - name: Run Claude review + id: claude # v1.0.111 — pin by SHA for security (third-party action with write perms + secrets). uses: anthropics/claude-code-action@fefa07e9c665b7320f08c3b525980457f22f58aa with: @@ -721,45 +741,54 @@ jobs: --model claude-opus-4-7 --allowedTools "Bash(git diff *),Bash(git log *),Bash(git show *),Read,Glob,Grep" - - name: Check for blocking findings + - name: Extract review output if: always() env: - GH_TOKEN: ${{ github.token }} - HEADER: "## Claude Review - React/Frontend Review" - REVIEW_MARKER: run=${{ github.run_id }} attempt=${{ github.run_attempt }} sha=${{ github.event.pull_request.head.sha }} scope=client + EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} run: | - MAX_ATTEMPTS=6 - SLEEP_SECONDS=10 - BODY="" - - for i in $(seq 1 "$MAX_ATTEMPTS"); do - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg header "$HEADER" --arg marker "$REVIEW_MARKER" \ - '[.[] | select((.body // "") | contains($header) and contains($marker))] | last | .body // ""') - - if [ -z "$BODY" ]; then - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg header "$HEADER" --arg run_token "run=${{ github.run_id }}" --arg sha_token "sha=${{ github.event.pull_request.head.sha }}" \ - '[.[] | select((.body // "") | contains($header) and contains($run_token) and contains($sha_token))] | last | .body // ""') - fi + : > /tmp/review.txt - if [ -n "$BODY" ]; then - echo "Found Claude review comment on attempt $i" - break - fi + if [ -z "$EXECUTION_FILE" ] || [ ! -f "$EXECUTION_FILE" ]; then + 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) + if [ -z "$REVIEW" ]; then + 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" + exit 1 + fi + printf '%s\n' "$REVIEW" > /tmp/review.txt - if [ "$i" -lt "$MAX_ATTEMPTS" ]; then - echo "Claude review comment not found yet; sleeping ${SLEEP_SECONDS}s... (attempt $i/$MAX_ATTEMPTS)" - sleep "$SLEEP_SECONDS" + - name: Post review comment + if: always() + 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 - done + } > /tmp/review-formatted.txt + + gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review-formatted.txt - if [ -z "$BODY" ]; then - echo "::error::Could not find Claude review output for client" + - name: Check for blocking findings + if: always() + run: | + if [ ! -s /tmp/review.txt ]; then + echo "::error::No review output was produced despite reviewable changes" exit 1 fi - if echo "$BODY" | grep -qE '\[(CRITICAL|HIGH)\]'; then + if grep -qE '\[(CRITICAL|HIGH)\]' /tmp/review.txt; then echo "::error::Review found CRITICAL or HIGH severity issues" exit 1 fi @@ -946,6 +975,7 @@ jobs: echo "${DELIMITER}" >> "$GITHUB_OUTPUT" - name: Run Claude review + id: claude # v1.0.111 — pin by SHA for security (third-party action with write perms + secrets). uses: anthropics/claude-code-action@fefa07e9c665b7320f08c3b525980457f22f58aa with: @@ -955,57 +985,54 @@ jobs: --model claude-opus-4-7 --allowedTools "Bash(git diff *),Bash(git log *),Bash(git show *),Read,Glob,Grep" - - name: Check for blocking findings + - name: Extract review output if: always() env: - GH_TOKEN: ${{ github.token }} - HEADER: "## Claude Review - Indexer/API Review" - REVIEW_MARKER: run=${{ github.run_id }} attempt=${{ github.run_attempt }} sha=${{ github.event.pull_request.head.sha }} scope=indexer-api + EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} run: | - MAX_ATTEMPTS=6 - SLEEP_SECONDS=10 - BODY="" - - for i in $(seq 1 "$MAX_ATTEMPTS"); do - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg header "$HEADER" --arg marker "$REVIEW_MARKER" \ - '[.[] | select((.body // "") | contains($header) and contains($marker))] | last | .body // ""') - - if [ -z "$BODY" ]; then - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg header "$HEADER" --arg run_token "run=${{ github.run_id }}" --arg sha_token "sha=${{ github.event.pull_request.head.sha }}" \ - '[.[] | select((.body // "") | contains($header) and contains($run_token) and contains($sha_token))] | last | .body // ""') - fi + : > /tmp/review.txt - if [ -z "$BODY" ]; then - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg marker "$REVIEW_MARKER" \ - '[.[] | select((.user.login // "") == "claude" and ((.body // "") | contains($marker)))] | last | .body // ""') - fi + if [ -z "$EXECUTION_FILE" ] || [ ! -f "$EXECUTION_FILE" ]; then + echo "::error::Claude action did not produce an execution file" + exit 1 + fi - if [ -z "$BODY" ]; then - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg run_token "run=${{ github.run_id }}" --arg sha_token "sha=${{ github.event.pull_request.head.sha }}" \ - '[.[] | select((.user.login // "") == "claude" and ((.body // "") | contains($run_token) and contains($sha_token) and contains("scope=indexer-api")))] | last | .body // ""') - fi + 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) + fi + if [ -z "$REVIEW" ]; then + echo "::error::Claude review produced no extractable output" + exit 1 + fi + printf '%s\n' "$REVIEW" > /tmp/review.txt - if [ -n "$BODY" ]; then - echo "Found Claude review comment on attempt $i" - break + - name: Post review comment + if: always() + 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 - if [ "$i" -lt "$MAX_ATTEMPTS" ]; then - echo "Claude review comment not found yet; sleeping ${SLEEP_SECONDS}s... (attempt $i/$MAX_ATTEMPTS)" - sleep "$SLEEP_SECONDS" - fi - done + gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review-formatted.txt - if [ -z "$BODY" ]; then - echo "::error::Could not find Claude review output for indexer/api" + - name: Check for blocking findings + if: always() + run: | + if [ ! -s /tmp/review.txt ]; then + echo "::error::No review output was produced despite reviewable changes" exit 1 fi - if echo "$BODY" | grep -qE '\[(CRITICAL|HIGH)\]'; then + if grep -qE '\[(CRITICAL|HIGH)\]' /tmp/review.txt; then echo "::error::Review found CRITICAL or HIGH severity issues" exit 1 fi @@ -1193,6 +1220,7 @@ jobs: echo "${DELIMITER}" >> "$GITHUB_OUTPUT" - name: Run Claude review + id: claude # v1.0.111 — pin by SHA for security (third-party action with write perms + secrets). uses: anthropics/claude-code-action@fefa07e9c665b7320f08c3b525980457f22f58aa with: @@ -1202,45 +1230,54 @@ jobs: --model claude-opus-4-7 --allowedTools "Bash(git diff *),Bash(git log *),Bash(git show *),Read,Glob,Grep" - - name: Check for blocking findings + - name: Extract review output if: always() env: - GH_TOKEN: ${{ github.token }} - HEADER: "## Claude Review - General Engineering Review" - REVIEW_MARKER: run=${{ github.run_id }} attempt=${{ github.run_attempt }} sha=${{ github.event.pull_request.head.sha }} scope=general + EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} run: | - MAX_ATTEMPTS=6 - SLEEP_SECONDS=10 - BODY="" - - for i in $(seq 1 "$MAX_ATTEMPTS"); do - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg header "$HEADER" --arg marker "$REVIEW_MARKER" \ - '[.[] | select((.body // "") | contains($header) and contains($marker))] | last | .body // ""') - - if [ -z "$BODY" ]; then - BODY=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" --paginate \ - | jq -r --arg header "$HEADER" --arg run_token "run=${{ github.run_id }}" --arg sha_token "sha=${{ github.event.pull_request.head.sha }}" \ - '[.[] | select((.body // "") | contains($header) and contains($run_token) and contains($sha_token))] | last | .body // ""') - fi + : > /tmp/review.txt - if [ -n "$BODY" ]; then - echo "Found Claude review comment on attempt $i" - break - fi + if [ -z "$EXECUTION_FILE" ] || [ ! -f "$EXECUTION_FILE" ]; then + echo "::error::Claude action did not produce an execution file" + exit 1 + fi - if [ "$i" -lt "$MAX_ATTEMPTS" ]; then - echo "Claude review comment not found yet; sleeping ${SLEEP_SECONDS}s... (attempt $i/$MAX_ATTEMPTS)" - sleep "$SLEEP_SECONDS" + 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) + fi + if [ -z "$REVIEW" ]; then + echo "::error::Claude review produced no extractable output" + exit 1 + fi + printf '%s\n' "$REVIEW" > /tmp/review.txt + + - name: Post review comment + if: always() + 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 - done + } > /tmp/review-formatted.txt - if [ -z "$BODY" ]; then - echo "::error::Could not find Claude review output for general changes" + gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/review-formatted.txt + + - name: Check for blocking findings + if: always() + run: | + if [ ! -s /tmp/review.txt ]; then + echo "::error::No review output was produced despite reviewable changes" exit 1 fi - if echo "$BODY" | grep -qE '\[(CRITICAL|HIGH)\]'; then + if grep -qE '\[(CRITICAL|HIGH)\]' /tmp/review.txt; then echo "::error::Review found CRITICAL or HIGH severity issues" exit 1 fi