Skip to content

feat(metagame): add extension-claim dispatch to Prize and EntryFee components #173

feat(metagame): add extension-claim dispatch to Prize and EntryFee components

feat(metagame): add extension-claim dispatch to Prize and EntryFee components #173

Workflow file for this run

name: PR CI
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: pr-ci-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
packages: ${{ steps.filter.outputs.packages }}
has_tests: ${{ steps.compute-matrix.outputs.has_tests }}
need_egs: ${{ steps.compute-matrix.outputs.need_egs }}
need_metagame: ${{ steps.compute-matrix.outputs.need_metagame }}
need_economy: ${{ steps.compute-matrix.outputs.need_economy }}
need_utilities: ${{ steps.compute-matrix.outputs.need_utilities }}
need_presets: ${{ steps.compute-matrix.outputs.need_presets }}
packages_review: ${{ steps.filter.outputs.packages_review }}
general_review: ${{ steps.filter.outputs.general_review }}
infra_ci: ${{ steps.filter.outputs.infra_ci }}
general_ci: ${{ steps.filter.outputs.general_ci }}
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 }}
claude_reviews_block_reason: ${{ steps.ai-gate.outputs.claude_reviews_block_reason }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
packages:
- "packages/**"
- "Scarb.toml"
- "Scarb.lock"
- ".tool-versions"
packages_review:
- "packages/**"
review_automation_changed:
- ".github/workflows/pr-ci.yml"
- ".github/prompts/**"
general_review:
- "**"
- "!packages/**"
general_ci:
- "codecov.yml"
- ".tool-versions"
infra_ci:
- ".github/workflows/**"
- ".github/prompts/**"
- ".github/pull_request_template.md"
embeddable_game_standard:
- "packages/embeddable_game_standard/**"
metagame_pkg:
- "packages/metagame/**"
economy:
- "packages/economy/**"
utilities:
- "packages/utilities/**"
interfaces:
- "packages/interfaces/**"
presets:
- "packages/presets/**"
testing:
- "packages/testing/**"
test_common:
- "packages/test_common/**"
tool_versions:
- ".tool-versions"
workspace_manifests:
- "Scarb.toml"
- "Scarb.lock"
- name: Compute test matrix from changed packages
id: compute-matrix
env:
FILTER_INTERFACES: ${{ steps.filter.outputs.interfaces }}
FILTER_TESTING: ${{ steps.filter.outputs.testing }}
FILTER_UTILITIES: ${{ steps.filter.outputs.utilities }}
FILTER_METAGAME_PKG: ${{ steps.filter.outputs.metagame_pkg }}
FILTER_ECONOMY: ${{ steps.filter.outputs.economy }}
FILTER_EGS: ${{ steps.filter.outputs.embeddable_game_standard }}
FILTER_TEST_COMMON: ${{ steps.filter.outputs.test_common }}
FILTER_PRESETS: ${{ steps.filter.outputs.presets }}
FILTER_TOOL_VERSIONS: ${{ steps.filter.outputs.tool_versions }}
FILTER_WORKSPACE_MANIFESTS: ${{ steps.filter.outputs.workspace_manifests }}
run: |
# Determine which group packages need testing based on transitive deps
# interfaces & testing & .tool-versions → ALL
# utilities → utilities + metagame + embeddable_game_standard + presets
# metagame_pkg → metagame + embeddable_game_standard + presets
# economy → economy + presets
# embeddable_game_standard → embeddable_game_standard + presets
# test_common → embeddable_game_standard + metagame + presets
# presets → presets
RUN_ALL="false"
NEED_EGS="false"
NEED_METAGAME="false"
NEED_ECONOMY="false"
NEED_UTILITIES="false"
NEED_PRESETS="false"
if [ "$FILTER_INTERFACES" = "true" ] || [ "$FILTER_TESTING" = "true" ] || [ "$FILTER_TOOL_VERSIONS" = "true" ] || [ "$FILTER_WORKSPACE_MANIFESTS" = "true" ]; then
RUN_ALL="true"
fi
if [ "$RUN_ALL" != "true" ]; then
if [ "$FILTER_UTILITIES" = "true" ]; then
NEED_UTILITIES="true"
NEED_METAGAME="true"
NEED_EGS="true"
NEED_PRESETS="true"
fi
if [ "$FILTER_METAGAME_PKG" = "true" ]; then
NEED_METAGAME="true"
NEED_EGS="true"
NEED_PRESETS="true"
fi
if [ "$FILTER_ECONOMY" = "true" ]; then
NEED_ECONOMY="true"
NEED_PRESETS="true"
fi
if [ "$FILTER_EGS" = "true" ]; then
NEED_EGS="true"
NEED_PRESETS="true"
fi
if [ "$FILTER_TEST_COMMON" = "true" ]; then
NEED_EGS="true"
NEED_METAGAME="true"
NEED_PRESETS="true"
fi
if [ "$FILTER_PRESETS" = "true" ]; then
NEED_PRESETS="true"
fi
fi
if [ "$RUN_ALL" = "true" ]; then
NEED_EGS="true"
NEED_METAGAME="true"
NEED_ECONOMY="true"
NEED_UTILITIES="true"
NEED_PRESETS="true"
fi
HAS_TESTS="false"
for flag in "$NEED_EGS" "$NEED_METAGAME" "$NEED_ECONOMY" "$NEED_UTILITIES" "$NEED_PRESETS"; do
if [ "$flag" = "true" ]; then
HAS_TESTS="true"
break
fi
done
echo "has_tests=$HAS_TESTS" >> "$GITHUB_OUTPUT"
echo "need_egs=$NEED_EGS" >> "$GITHUB_OUTPUT"
echo "need_metagame=$NEED_METAGAME" >> "$GITHUB_OUTPUT"
echo "need_economy=$NEED_ECONOMY" >> "$GITHUB_OUTPUT"
echo "need_utilities=$NEED_UTILITIES" >> "$GITHUB_OUTPUT"
echo "need_presets=$NEED_PRESETS" >> "$GITHUB_OUTPUT"
echo "Packages to test: egs=$NEED_EGS metagame=$NEED_METAGAME economy=$NEED_ECONOMY utilities=$NEED_UTILITIES presets=$NEED_PRESETS"
- name: Determine AI review availability
id: ai-gate
env:
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
REVIEW_AUTOMATION_CHANGED: ${{ steps.filter.outputs.review_automation_changed }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
CODEX_AUTH_DOT_JSON: ${{ secrets.CODEX_AUTH_DOT_JSON }}
run: |
CAN_RUN_AI_REVIEWS="true"
AI_REVIEWS_BLOCK_REASON="available"
CAN_RUN_CLAUDE_REVIEWS="true"
CLAUDE_REVIEWS_BLOCK_REASON="available"
IS_TRUSTED_BOT_PR="false"
if printf '%s' "$PR_AUTHOR" | tr '[:upper:]' '[:lower:]' | grep -Eq '^(dependabot\[bot\]|dependabot-preview\[bot\]|renovate\[bot\])$'; then
IS_TRUSTED_BOT_PR="true"
fi
if [ "$IS_FORK" = "true" ]; then
CAN_RUN_AI_REVIEWS="false"
AI_REVIEWS_BLOCK_REASON="fork"
echo "AI reviews disabled for fork pull requests."
elif [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ] || [ -z "$CODEX_AUTH_DOT_JSON" ]; then
CAN_RUN_AI_REVIEWS="false"
if [ "$IS_TRUSTED_BOT_PR" = "true" ]; then
AI_REVIEWS_BLOCK_REASON="missing_secrets_trusted_bot"
echo "AI reviews disabled because required secrets are unavailable for a trusted bot PR."
else
AI_REVIEWS_BLOCK_REASON="missing_secrets"
echo "AI reviews disabled because required secrets are unavailable."
fi
fi
if [ "$CAN_RUN_AI_REVIEWS" != "true" ]; then
CAN_RUN_CLAUDE_REVIEWS="false"
CLAUDE_REVIEWS_BLOCK_REASON="$AI_REVIEWS_BLOCK_REASON"
elif [ "$REVIEW_AUTOMATION_CHANGED" = "true" ]; then
CAN_RUN_AI_REVIEWS="false"
AI_REVIEWS_BLOCK_REASON="review_automation_modified"
CAN_RUN_CLAUDE_REVIEWS="false"
CLAUDE_REVIEWS_BLOCK_REASON="review_automation_modified"
echo "AI reviews disabled because .github/workflows/pr-ci.yml or .github/prompts/** are modified in this PR."
fi
echo "can_run_ai_reviews=$CAN_RUN_AI_REVIEWS" >> "$GITHUB_OUTPUT"
echo "ai_reviews_block_reason=$AI_REVIEWS_BLOCK_REASON" >> "$GITHUB_OUTPUT"
echo "can_run_claude_reviews=$CAN_RUN_CLAUDE_REVIEWS" >> "$GITHUB_OUTPUT"
echo "claude_reviews_block_reason=$CLAUDE_REVIEWS_BLOCK_REASON" >> "$GITHUB_OUTPUT"
infra-validate:
needs: changes
if: needs.changes.outputs.infra_ci == 'true' || needs.changes.outputs.general_ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- name: Validate workflow YAML
run: |
YAML_CLI="yaml@2.8.1"
for file in .github/workflows/*.yml .github/workflows/*.yaml; do
[ -f "$file" ] || continue
npx -y "$YAML_CLI" valid < "$file"
done
if [ -f codecov.yml ]; then
npx -y "$YAML_CLI" valid < codecov.yml
fi
- name: Validate package count matches codecov config
run: |
matrix_count=$(grep -cE '^\s+module:' .github/workflows/main-ci.yml)
codecov_count=$(grep 'after_n_builds:' codecov.yml | grep -oE '[0-9]+')
echo "Matrix modules: $matrix_count"
echo "Codecov after_n_builds: $codecov_count"
if [ "$matrix_count" != "$codecov_count" ]; then
echo "::error::Matrix has $matrix_count modules but codecov.yml expects $codecov_count builds"
echo "::error::Update codecov.yml after_n_builds to match module count"
exit 1
fi
echo "Configuration validated: $matrix_count modules"
lint:
needs: changes
if: needs.changes.outputs.packages == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- name: Setup Scarb
uses: software-mansion/setup-scarb@v1
with:
tool-versions: .tool-versions
cache: false
- name: Scarb fmt
run: scarb fmt --check --workspace
setup:
needs: [changes, lint]
if: needs.changes.outputs.has_tests == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
test_matrix: ${{ steps.build-matrix.outputs.test_matrix }}
steps:
- uses: actions/checkout@v6
- name: Build test matrix from package flags
id: build-matrix
env:
NEED_EGS: ${{ needs.changes.outputs.need_egs }}
NEED_METAGAME: ${{ needs.changes.outputs.need_metagame }}
NEED_ECONOMY: ${{ needs.changes.outputs.need_economy }}
NEED_UTILITIES: ${{ needs.changes.outputs.need_utilities }}
NEED_PRESETS: ${{ needs.changes.outputs.need_presets }}
run: |
# Build JSON matrix from per-package boolean flags
# Module catalog must match main-ci.yml
INCLUDES="[]"
add() { INCLUDES=$(echo "$INCLUDES" | jq -c --arg p "$1" --arg m "$2" --arg r "$3" --argjson f "$4" '. + [{package:$p,module:$m,runner:$r,fuzzer_runs:$f}]'); }
if [ "$NEED_EGS" = "true" ]; then
add game_components_embeddable_game_standard token ubuntu-latest-32 32
add game_components_embeddable_game_standard minigame ubuntu-latest-8 32
add game_components_embeddable_game_standard metagame ubuntu-latest-8 32
add game_components_embeddable_game_standard registry ubuntu-latest-8 32
fi
if [ "$NEED_METAGAME" = "true" ]; then
add game_components_metagame leaderboard ubuntu-latest-4 256
add game_components_metagame registration ubuntu-latest-4 256
add game_components_metagame entry_requirement ubuntu-latest-4 256
add game_components_metagame entry_fee ubuntu-latest-4 256
add game_components_metagame prize ubuntu-latest-4 256
add game_components_metagame ticket_booth ubuntu-latest-4 256
add game_components_metagame merkledrop ubuntu-latest-4 256
fi
if [ "$NEED_ECONOMY" = "true" ]; then
add game_components_economy tokenomics ubuntu-latest-4 256
fi
if [ "$NEED_UTILITIES" = "true" ]; then
add game_components_utilities math ubuntu-latest-4 256
add game_components_utilities distribution ubuntu-latest-4 256
add game_components_utilities utils ubuntu-latest-4 256
add game_components_utilities renderer ubuntu-latest-4 256
fi
if [ "$NEED_PRESETS" = "true" ]; then
add game_components_presets presets ubuntu-latest-4 256
fi
MATRIX=$(echo "$INCLUDES" | jq -c '{include:.}')
echo "test_matrix=$MATRIX" >> "$GITHUB_OUTPUT"
echo "Matrix: $(echo "$INCLUDES" | jq 'length') modules"
echo "$MATRIX" | jq .
- name: Extract tool versions
run: |
echo "SCARB_VERSION=$(grep '^scarb ' .tool-versions | awk '{print $2}')" >> "$GITHUB_ENV"
echo "FOUNDRY_VERSION=$(grep '^starknet-foundry ' .tool-versions | awk '{print $2}')" >> "$GITHUB_ENV"
- uses: software-mansion/setup-scarb@v1
with:
scarb-version: ${{ env.SCARB_VERSION }}
cache: false
- uses: foundry-rs/setup-snfoundry@v4
with:
starknet-foundry-version: ${{ env.FOUNDRY_VERSION }}
- name: Cache Scarb dependencies
uses: actions/cache@v4
with:
path: ~/.cache/scarb
key: ${{ runner.os }}-scarb-deps-${{ hashFiles('**/Scarb.toml', '**/Scarb.lock') }}
restore-keys: |
${{ runner.os }}-scarb-deps-
- name: Cache Cargo (snforge plugin + Scarb Rust deps)
uses: actions/cache@v4
with:
path: ~/.cargo
key: ${{ runner.os }}-cargo-snforge-${{ env.FOUNDRY_VERSION }}
restore-keys: |
${{ runner.os }}-cargo-snforge-
- name: Fetch dependencies
run: scarb fetch
- name: Warm snforge plugin cache
run: snforge --version
- name: Verify workspace compiles (no warnings)
run: |
scarb build --workspace 2>&1 | tee /tmp/build.log
if grep -qE '^ --> .*\.cairo:[0-9]+:[0-9]+' /tmp/build.log; then
echo "::error::Build produced compiler warnings. Fix all warnings before merging."
grep -B1 -E '^ --> .*\.cairo:[0-9]+:[0-9]+' /tmp/build.log
exit 1
fi
test:
name: test (${{ matrix.module }})
needs: [changes, setup]
if: needs.changes.outputs.has_tests == 'true'
runs-on: ${{ matrix.runner }}
timeout-minutes: 10
strategy:
fail-fast: true
matrix: ${{ fromJson(needs.setup.outputs.test_matrix) }}
steps:
- uses: actions/checkout@v6
- name: Extract tool versions
run: |
echo "SCARB_VERSION=$(grep '^scarb ' .tool-versions | awk '{print $2}')" >> "$GITHUB_ENV"
echo "FOUNDRY_VERSION=$(grep '^starknet-foundry ' .tool-versions | awk '{print $2}')" >> "$GITHUB_ENV"
- uses: software-mansion/setup-scarb@v1
with:
scarb-version: ${{ env.SCARB_VERSION }}
cache: false
- uses: foundry-rs/setup-snfoundry@v4
with:
starknet-foundry-version: ${{ env.FOUNDRY_VERSION }}
- name: Cache Scarb dependencies
uses: actions/cache@v4
with:
path: ~/.cache/scarb
key: ${{ runner.os }}-scarb-deps-${{ hashFiles('**/Scarb.toml', '**/Scarb.lock') }}
restore-keys: |
${{ runner.os }}-scarb-deps-
- name: Cache Cargo (snforge plugin)
uses: actions/cache@v4
with:
path: ~/.cargo
key: ${{ runner.os }}-cargo-snforge-${{ env.FOUNDRY_VERSION }}
restore-keys: |
${{ runner.os }}-cargo-snforge-
- name: Cache tools
uses: actions/cache@v4
id: cache-tools
with:
path: |
~/.local/bin/cairo-coverage
~/.local/bin/universal-sierra-compiler
key: ${{ runner.os }}-tools-cairo-coverage-universal-sierra-compiler-v1
- name: Install tools
if: steps.cache-tools.outputs.cache-hit != 'true'
run: |
(curl -L https://raw.githubusercontent.com/software-mansion/cairo-coverage/main/scripts/install.sh | sh) &
(curl -L https://raw.githubusercontent.com/software-mansion/universal-sierra-compiler/master/scripts/install.sh | sh) &
wait
- name: Cache compiled packages
uses: actions/cache@v4
with:
path: target/
key: ${{ runner.os }}-target-${{ matrix.package }}-${{ matrix.module }}-${{ env.SCARB_VERSION }}-${{ hashFiles('packages/**/*.cairo', '**/Scarb.toml', '**/Scarb.lock') }}
restore-keys: |
${{ runner.os }}-target-${{ matrix.package }}-${{ matrix.module }}-${{ env.SCARB_VERSION }}-
- name: Run tests with coverage
run: |
if [ "${{ matrix.package }}" = "game_components_${{ matrix.module }}" ]; then
snforge test -p ${{ matrix.package }} --fuzzer-runs ${{ matrix.fuzzer_runs }} --coverage 2>&1 | tee /tmp/test.log
else
snforge test -p ${{ matrix.package }} "::${{ matrix.module }}::" --fuzzer-runs ${{ matrix.fuzzer_runs }} --coverage 2>&1 | tee /tmp/test.log
fi
- name: Check for compiler warnings
if: always() && steps.cache-tools.outcome != 'failure'
run: |
if [ -f /tmp/test.log ] && grep -qE '^ --> .*\.cairo:[0-9]+:[0-9]+' /tmp/test.log; then
echo "::error::Test compilation produced warnings. Fix all warnings before merging."
grep -B1 -E '^ --> .*\.cairo:[0-9]+:[0-9]+' /tmp/test.log
exit 1
fi
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage.lcov
flags: ${{ matrix.module }}
token: ${{ secrets.CODECOV_TOKEN }}
claude-review-packages:
needs: [changes, lint]
if: needs.changes.outputs.packages_review == 'true' && needs.changes.outputs.can_run_claude_reviews == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Build review prompt
id: build-prompt
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
REVIEW_MARKER: run=${{ github.run_id }} attempt=${{ github.run_attempt }} sha=${{ github.event.pull_request.head.sha }} scope=packages
run: |
{
cat .github/prompts/cairo-review.md
echo ""
echo "---"
echo ""
echo "This is PR #${{ github.event.pull_request.number }} for ${{ github.repository }}."
echo ""
echo "Review ONLY the changes introduced by the PR. Run this command to see the diff:"
echo " git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- packages/"
echo ""
echo "Pull request title: ${PR_TITLE}"
echo "Pull request body:"
echo "${PR_BODY}"
echo ""
echo "---"
echo ""
echo "Start your response with this exact heading:"
echo "## Claude Review - Cairo/Starknet Contract Review"
echo "Include this exact marker on its own line near the top of your response:"
echo "$REVIEW_MARKER"
echo ""
cat <<'PROMPT_EOF'
Format each finding as:
[SEVERITY] file_path:line_number - description
Impact: what could go wrong
Fix: concrete code change
Where SEVERITY is one of: CRITICAL, HIGH, MEDIUM, LOW, INFO
End with a summary: counts of findings by severity.
IMPORTANT: Always produce output, even when no issues are found.
If there are no actionable findings, respond with:
"No issues found." followed by "Summary: 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW, 0 INFO"
PROMPT_EOF
} > /tmp/prompt.txt
DELIMITER=$(openssl rand -hex 16)
echo "prompt<<${DELIMITER}" >> "$GITHUB_OUTPUT"
cat /tmp/prompt.txt >> "$GITHUB_OUTPUT"
echo "${DELIMITER}" >> "$GITHUB_OUTPUT"
- name: Run Claude review
id: claude-review
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
direct_prompt: ${{ steps.build-prompt.outputs.prompt }}
allowed_tools: "Bash(git diff *),Bash(git log *),Bash(git show *),Read,Glob,Grep"
- name: Check for blocking findings
if: always() && steps.claude-review.outcome != 'cancelled' && steps.claude-review.outcome != 'skipped'
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=packages
REVIEW_OUTCOME: ${{ steps.claude-review.outcome }}
run: |
if [ "$REVIEW_OUTCOME" = "failure" ]; then
echo "::error::Claude review step failed — no comment expected"
exit 1
fi
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
if [ -n "$BODY" ]; then
echo "Found Claude review comment on attempt $i"
break
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"
fi
done
if [ -z "$BODY" ]; then
echo "::error::Could not find Claude review output for packages"
exit 1
fi
if echo "$BODY" | grep -qE '\[(CRITICAL|HIGH)\]'; then
echo "::error::Review found CRITICAL or HIGH severity issues"
exit 1
fi
echo "No blocking findings detected"
codex-review-packages:
needs: [changes, lint]
if: needs.changes.outputs.packages_review == 'true' && needs.changes.outputs.can_run_ai_reviews == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Inject OAuth session
run: |
mkdir -p ~/.codex
echo '${{ secrets.CODEX_AUTH_DOT_JSON }}' > ~/.codex/auth.json
chmod 600 ~/.codex/auth.json
- name: Build review prompt
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
{
cat .github/prompts/cairo-review.md
echo ""
echo "---"
echo ""
echo "This is PR #${{ github.event.pull_request.number }} for ${{ github.repository }}."
echo ""
echo "Review ONLY the changes introduced by the PR. Run this command to see the diff:"
echo " git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- packages/"
echo ""
echo "Pull request title: ${PR_TITLE}"
echo "Pull request body:"
echo "${PR_BODY}"
echo ""
echo "---"
echo ""
cat <<'PROMPT_EOF'
Format each finding as:
[SEVERITY] file_path:line_number - description
Impact: what could go wrong
Fix: concrete code change
Where SEVERITY is one of: CRITICAL, HIGH, MEDIUM, LOW, INFO
End with a summary: counts of findings by severity.
IMPORTANT: Always produce output, even when no issues are found.
If there are no actionable findings, respond with:
"No issues found." followed by "Summary: 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW, 0 INFO"
PROMPT_EOF
} > /tmp/prompt.txt
- name: Run Codex review
run: |
npx -y @openai/codex@0.98.0 exec --full-auto \
-c 'model="gpt-5.3-codex"' \
-c 'model_reasoning_effort="high"' \
-o /tmp/review.txt \
"$(cat /tmp/prompt.txt)" 2>&1 || {
echo "Review process failed to complete." > /tmp/review.txt
exit 1
}
if [ -f /tmp/review.txt ]; then
cat /tmp/review.txt
else
echo "Codex did not produce /tmp/review.txt"
fi
- name: Post review comment
if: always()
env:
GH_TOKEN: ${{ github.token }}
run: |
{
echo "## Codex 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
- 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 grep -qE '\[(CRITICAL|HIGH)\]' /tmp/review.txt; then
echo "::error::Review found CRITICAL or HIGH severity issues"
exit 1
fi
echo "No blocking findings detected"
claude-review-general:
needs: [changes]
if: needs.changes.outputs.general_review == 'true' && needs.changes.outputs.can_run_claude_reviews == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Build review prompt
id: build-prompt
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
REVIEW_MARKER: run=${{ github.run_id }} attempt=${{ github.run_attempt }} sha=${{ github.event.pull_request.head.sha }} scope=general
run: |
{
cat .github/prompts/general-purpose.md
echo ""
echo "---"
echo ""
echo "This is PR #${{ github.event.pull_request.number }} for ${{ github.repository }}."
echo ""
echo "Review ONLY the changes introduced by the PR, with focus on files outside packages/."
echo "Run this command to see the targeted diff:"
echo " git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- . ':(exclude)packages/**'"
echo ""
echo "Pull request title: ${PR_TITLE}"
echo "Pull request body:"
echo "${PR_BODY}"
echo ""
echo "---"
echo ""
echo "Start your response with this exact heading:"
echo "## Claude Review - General Engineering Review"
echo "Include this exact marker on its own line near the top of your response:"
echo "$REVIEW_MARKER"
echo ""
cat <<'PROMPT_EOF'
Format each finding as:
[SEVERITY] file_path:line_number - description
Impact: what could go wrong
Fix: concrete code change
Where SEVERITY is one of: CRITICAL, HIGH, MEDIUM, LOW, INFO
End with a summary: counts of findings by severity.
IMPORTANT: Always produce output, even when no issues are found.
If there are no actionable findings, respond with:
"No issues found." followed by "Summary: 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW, 0 INFO"
PROMPT_EOF
} > /tmp/prompt.txt
DELIMITER=$(openssl rand -hex 16)
echo "prompt<<${DELIMITER}" >> "$GITHUB_OUTPUT"
cat /tmp/prompt.txt >> "$GITHUB_OUTPUT"
echo "${DELIMITER}" >> "$GITHUB_OUTPUT"
- name: Run Claude review
id: claude-review
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
direct_prompt: ${{ steps.build-prompt.outputs.prompt }}
allowed_tools: "Bash(git diff *),Bash(git log *),Bash(git show *),Read,Glob,Grep"
- name: Check for blocking findings
if: always() && steps.claude-review.outcome != 'cancelled' && steps.claude-review.outcome != 'skipped'
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
REVIEW_OUTCOME: ${{ steps.claude-review.outcome }}
run: |
if [ "$REVIEW_OUTCOME" = "failure" ]; then
echo "::error::Claude review step failed — no comment expected"
exit 1
fi
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
if [ -n "$BODY" ]; then
echo "Found Claude review comment on attempt $i"
break
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"
fi
done
if [ -z "$BODY" ]; then
echo "::error::Could not find Claude review output for general changes"
exit 1
fi
if echo "$BODY" | grep -qE '\[(CRITICAL|HIGH)\]'; then
echo "::error::Review found CRITICAL or HIGH severity issues"
exit 1
fi
echo "No blocking findings detected"
codex-review-general:
needs: [changes]
if: needs.changes.outputs.general_review == 'true' && needs.changes.outputs.can_run_ai_reviews == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Inject OAuth session
run: |
mkdir -p ~/.codex
echo '${{ secrets.CODEX_AUTH_DOT_JSON }}' > ~/.codex/auth.json
chmod 600 ~/.codex/auth.json
- name: Build review prompt
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
{
cat .github/prompts/general-purpose.md
echo ""
echo "---"
echo ""
echo "This is PR #${{ github.event.pull_request.number }} for ${{ github.repository }}."
echo ""
echo "Review ONLY the changes introduced by the PR, with focus on files outside packages/."
echo "Run this command to see the targeted diff:"
echo " git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- . ':(exclude)packages/**'"
echo ""
echo "Pull request title: ${PR_TITLE}"
echo "Pull request body:"
echo "${PR_BODY}"
echo ""
echo "---"
echo ""
cat <<'PROMPT_EOF'
Format each finding as:
[SEVERITY] file_path:line_number - description
Impact: what could go wrong
Fix: concrete code change
Where SEVERITY is one of: CRITICAL, HIGH, MEDIUM, LOW, INFO
End with a summary: counts of findings by severity.
IMPORTANT: Always produce output, even when no issues are found.
If there are no actionable findings, respond with:
"No issues found." followed by "Summary: 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW, 0 INFO"
PROMPT_EOF
} > /tmp/prompt.txt
- name: Run Codex review
run: |
npx -y @openai/codex@0.98.0 exec --full-auto \
-c 'model="gpt-5.3-codex"' \
-c 'model_reasoning_effort="high"' \
-o /tmp/review.txt \
"$(cat /tmp/prompt.txt)" 2>&1 || {
echo "Review process failed to complete." > /tmp/review.txt
exit 1
}
if [ -f /tmp/review.txt ]; then
cat /tmp/review.txt
else
echo "Codex did not produce /tmp/review.txt"
fi
- name: Post review comment
if: always()
env:
GH_TOKEN: ${{ github.token }}
run: |
{
echo "## Codex 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
- 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 grep -qE '\[(CRITICAL|HIGH)\]' /tmp/review.txt; then
echo "::error::Review found CRITICAL or HIGH severity issues"
exit 1
fi
echo "No blocking findings detected"
pr-ci:
needs:
- changes
- infra-validate
- lint
- setup
- test
- claude-review-packages
- codex-review-packages
- claude-review-general
- codex-review-general
if: always()
runs-on: ubuntu-latest
steps:
- name: Evaluate results
env:
NEEDS_JSON: ${{ toJson(needs) }}
CHANGES_PACKAGES_REVIEW: ${{ needs.changes.outputs.packages_review }}
CHANGES_GENERAL_REVIEW: ${{ needs.changes.outputs.general_review }}
CAN_RUN_AI_REVIEWS: ${{ needs.changes.outputs.can_run_ai_reviews }}
AI_REVIEWS_BLOCK_REASON: ${{ needs.changes.outputs.ai_reviews_block_reason }}
CAN_RUN_CLAUDE_REVIEWS: ${{ needs.changes.outputs.can_run_claude_reviews }}
CLAUDE_REVIEWS_BLOCK_REASON: ${{ needs.changes.outputs.claude_reviews_block_reason }}
run: |
FAILED=$(echo "$NEEDS_JSON" | jq -r '
to_entries[]
| select(.value.result == "failure" or .value.result == "cancelled")
| .key
')
if [ -n "$FAILED" ]; then
echo "::error::Failing jobs:"
echo "$FAILED"
exit 1
fi
if [ "$CAN_RUN_AI_REVIEWS" != "true" ]; then
if [ "$AI_REVIEWS_BLOCK_REASON" = "fork" ] || [ "$AI_REVIEWS_BLOCK_REASON" = "missing_secrets_trusted_bot" ] || [ "$AI_REVIEWS_BLOCK_REASON" = "review_automation_modified" ]; then
echo "Skipping review-job skipped enforcement because AI reviews are unavailable ($AI_REVIEWS_BLOCK_REASON)."
elif [ "$AI_REVIEWS_BLOCK_REASON" = "missing_secrets" ]; then
if [ "$CHANGES_PACKAGES_REVIEW" = "true" ] || [ "$CHANGES_GENERAL_REVIEW" = "true" ]; then
echo "::error::AI review secrets are unavailable for a first-party PR with reviewable changes."
echo "::error::Restore CLAUDE_CODE_OAUTH_TOKEN and CODEX_AUTH_DOT_JSON or resolve secret access before merge."
exit 1
fi
echo "AI review secrets are unavailable, but no reviewable changes were detected."
else
echo "::error::Unexpected AI review availability state: $AI_REVIEWS_BLOCK_REASON"
exit 1
fi
else
MISSING_REVIEWS=""
check_skipped_reviews() {
local changed="$1"
shift
if [ "$changed" != "true" ]; then
return
fi
for job in "$@"; do
RESULT=$(echo "$NEEDS_JSON" | jq -r --arg job "$job" '.[$job].result // "unknown"')
if [ "$RESULT" = "skipped" ]; then
MISSING_REVIEWS="${MISSING_REVIEWS}${job}"$'\n'
fi
done
}
if [ "$CAN_RUN_CLAUDE_REVIEWS" = "true" ]; then
check_skipped_reviews "$CHANGES_PACKAGES_REVIEW" "claude-review-packages" "codex-review-packages"
check_skipped_reviews "$CHANGES_GENERAL_REVIEW" "claude-review-general" "codex-review-general"
else
echo "Skipping Claude review skipped enforcement because Claude reviews are unavailable ($CLAUDE_REVIEWS_BLOCK_REASON)."
check_skipped_reviews "$CHANGES_PACKAGES_REVIEW" "codex-review-packages"
check_skipped_reviews "$CHANGES_GENERAL_REVIEW" "codex-review-general"
fi
if [ -n "$MISSING_REVIEWS" ]; then
echo "::error::Required review jobs were skipped:"
echo "$MISSING_REVIEWS"
exit 1
fi
fi
echo "All required jobs passed or were skipped"