feat(metagame): add extension-claim dispatch to Prize and EntryFee components #191
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |