ci(common): update claude action with devcontainer setup #2333
Workflow file for this run
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: claude-review | |
| # Triggered by @claude mention in PR comments. | |
| # The action extracts text after "@claude" as the prompt. | |
| # | |
| # Security model: | |
| # - Only write/admin users can trigger (enforced by the action) | |
| # - Network sandbox: Squid proxy (L7 domain allowlist) + iptables (L3 egress block) | |
| # - All dependencies pre-installed before lockdown; action skips its internal installs | |
| # | |
| # Why pre-install? The action internally runs setup-bun, bun install, and claude install. | |
| # These use fetch() which ignores HTTP_PROXY and gets blocked by iptables. | |
| # We do them beforehand and pass paths via inputs to skip those steps. | |
| # | |
| # Secrets: | |
| # - CLAUDE_CODE_OAUTH_TOKEN: Anthropic API auth (from `claude setup-token`) | |
| # - CLAUDE_ACCESS_TOKEN: PAT with 'repo' scope for cloning private repos (zama-marketplace, tech-spec) | |
| on: | |
| issue_comment: | |
| types: [created] | |
| pull_request_review_comment: | |
| types: [created] | |
| pull_request: | |
| types: [opened, synchronize] | |
| permissions: {} | |
| jobs: | |
| claude-review: | |
| name: claude-review/respond | |
| if: | | |
| contains(github.event.comment.body, '@claude') && | |
| (github.event.issue.pull_request || github.event_name == 'pull_request_review_comment') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # Checkout repository code and read files | |
| pull-requests: write # Post review comments and update PR status | |
| issues: write # Respond to @claude mentions in issue comments | |
| id-token: write # OIDC token for GitHub App token exchange | |
| actions: read # Read workflow run context for action inputs | |
| steps: | |
| # ── Phase 1: Setup (full network) ────────────────────────────────── | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b | |
| - name: Clone private repositories | |
| run: | | |
| gh repo clone zama-ai/zama-marketplace /tmp/zama-marketplace | |
| gh repo clone zama-ai/tech-spec /tmp/tech-spec | |
| env: | |
| GH_TOKEN: ${{ secrets.CLAUDE_ACCESS_TOKEN }} | |
| - name: Build custom system prompt | |
| id: custom_prompt | |
| run: | | |
| if [[ "$EVENT_NAME" == "pull_request" ]] || [[ -n "$ISSUE_PR_URL" ]]; then | |
| if [[ "$EVENT_NAME" == "pull_request" ]]; then | |
| PR_TITLE="$PR_TITLE_INPUT" | |
| PR_AUTHOR="$PR_AUTHOR_INPUT" | |
| PR_HEAD="$PR_HEAD_INPUT" | |
| PR_BASE="$PR_BASE_INPUT" | |
| PR_STATE="$PR_STATE_INPUT" | |
| PR_ADDITIONS="$PR_ADDITIONS_INPUT" | |
| PR_DELETIONS="$PR_DELETIONS_INPUT" | |
| PR_COMMITS="$PR_COMMITS_INPUT" | |
| PR_FILES="$PR_FILES_INPUT" | |
| else | |
| PR_NUMBER="$ISSUE_NUMBER_INPUT" | |
| PR_DATA=$(gh pr view "$PR_NUMBER" --json title,author,headRefName,baseRefName,state,additions,deletions,commits) | |
| PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') | |
| PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') | |
| PR_HEAD=$(echo "$PR_DATA" | jq -r '.headRefName') | |
| PR_BASE=$(echo "$PR_DATA" | jq -r '.baseRefName') | |
| PR_STATE=$(echo "$PR_DATA" | jq -r '.state') | |
| PR_ADDITIONS=$(echo "$PR_DATA" | jq -r '.additions') | |
| PR_DELETIONS=$(echo "$PR_DATA" | jq -r '.deletions') | |
| PR_COMMITS=$(echo "$PR_DATA" | jq -r '.commits | length') | |
| PR_FILES=$(gh pr view "$PR_NUMBER" --json files | jq '.files | length') | |
| fi | |
| FORMATTED_CONTEXT="PR Title: ${PR_TITLE} | |
| PR Author: ${PR_AUTHOR} | |
| PR Branch: ${PR_HEAD} -> ${PR_BASE} | |
| PR State: ${PR_STATE^^} | |
| PR Additions: ${PR_ADDITIONS} | |
| PR Deletions: ${PR_DELETIONS} | |
| Total Commits: ${PR_COMMITS} | |
| Changed Files: ${PR_FILES} files" | |
| else | |
| FORMATTED_CONTEXT="Issue Title: ${ISSUE_TITLE_INPUT} | |
| Issue Author: ${ISSUE_AUTHOR_INPUT} | |
| Issue State: ${ISSUE_STATE_INPUT^^}" | |
| fi | |
| SYSTEM_PROMPT="You are Claude, an AI assistant designed to help with GitHub issues and pull requests. Think carefully as you analyze the context and respond appropriately. Here's the context for your current task: | |
| <formatted_context> | |
| ${FORMATTED_CONTEXT} | |
| </formatted_context>" | |
| { | |
| echo "CUSTOM_SYSTEM_PROMPT<<EOF" | |
| echo "$SYSTEM_PROMPT" | |
| echo "EOF" | |
| } >> "$GITHUB_ENV" | |
| env: | |
| GH_TOKEN: ${{ secrets.CLAUDE_ACCESS_TOKEN }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| ISSUE_PR_URL: ${{ github.event.issue.pull_request.url || '' }} | |
| PR_TITLE_INPUT: ${{ github.event.pull_request.title }} | |
| PR_AUTHOR_INPUT: ${{ github.event.pull_request.user.login }} | |
| PR_HEAD_INPUT: ${{ github.event.pull_request.head.ref }} | |
| PR_BASE_INPUT: ${{ github.event.pull_request.base.ref }} | |
| PR_STATE_INPUT: ${{ github.event.pull_request.state }} | |
| PR_ADDITIONS_INPUT: ${{ github.event.pull_request.additions }} | |
| PR_DELETIONS_INPUT: ${{ github.event.pull_request.deletions }} | |
| PR_COMMITS_INPUT: ${{ github.event.pull_request.commits }} | |
| PR_FILES_INPUT: ${{ github.event.pull_request.changed_files }} | |
| ISSUE_NUMBER_INPUT: ${{ github.event.issue.number }} | |
| ISSUE_TITLE_INPUT: ${{ github.event.issue.title }} | |
| ISSUE_AUTHOR_INPUT: ${{ github.event.issue.user.login }} | |
| ISSUE_STATE_INPUT: ${{ github.event.issue.state }} | |
| # ── Phase 2: Pre-install dependencies (before lockdown) ──────────── | |
| # The action's internal setup-bun, bun install, and claude install all | |
| # use fetch() which ignores HTTP_PROXY → blocked by iptables. | |
| # Pre-installing and passing paths via inputs skips those steps entirely. | |
| # OIDC → Anthropic exchange → GitHub App token (normally done inside the action) | |
| - name: Exchange OIDC for GitHub App token | |
| id: oidc-exchange | |
| run: | | |
| OIDC_TOKEN=$(curl -sf \ | |
| -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ | |
| "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=claude-code-github-action" | jq -r '.value') | |
| if [ -z "$OIDC_TOKEN" ] || [ "$OIDC_TOKEN" = "null" ]; then | |
| echo "::error::OIDC token request failed"; exit 1 | |
| fi | |
| APP_TOKEN=$(curl -sf -X POST \ | |
| -H "Authorization: Bearer $OIDC_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"permissions":{"contents":"write","pull_requests":"write","issues":"write"}}' \ | |
| "https://api.anthropic.com/api/github/github-app-token-exchange" | jq -r '.token') | |
| if [ -z "$APP_TOKEN" ] || [ "$APP_TOKEN" = "null" ]; then | |
| echo "::error::Token exchange failed"; exit 1 | |
| fi | |
| echo "::add-mask::$APP_TOKEN" | |
| echo "app_token=$APP_TOKEN" >> "$GITHUB_OUTPUT" | |
| # Bun runtime — needed by the action's TypeScript orchestrator | |
| - name: Install Bun | |
| uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 | |
| with: | |
| bun-version: latest | |
| # Action's node_modules — `bun install` inside the action would need network | |
| - name: Pre-install action dependencies | |
| id: setup-deps | |
| run: | | |
| cd "/home/runner/work/_actions/anthropics/claude-code-action/b433f16b30d54063fd3bab6b12f46f3da00e41b6" | |
| bun install --production | |
| echo "bun_path=$(which bun)" >> "$GITHUB_OUTPUT" | |
| # Claude Code CLI via npm — native binary ignores HTTP_PROXY (anthropics/claude-code#14165) | |
| - name: Install Claude Code (npm) | |
| id: setup-claude | |
| run: | | |
| npm install -g @anthropic-ai/claude-code@2.1.42 | |
| echo "path=$(which claude)" >> "$GITHUB_OUTPUT" | |
| # ── Phase 3: Network sandbox ─────────────────────────────────────── | |
| - name: Start Squid proxy | |
| run: | | |
| docker run -d --name sandbox-proxy -p 3128:3128 \ | |
| -v "${{ github.workspace }}/.github/squid/sandbox-proxy-rules.conf:/etc/squid/conf.d/00-sandbox-proxy-rules.conf:ro" \ | |
| ubuntu/squid | |
| # Wait for readiness (api.github.com returns 200 without auth, unlike api.anthropic.com) | |
| for i in $(seq 1 30); do | |
| curl -sf -x http://localhost:3128 -o /dev/null https://api.github.com 2>/dev/null && break | |
| [ "$i" -eq 30 ] && { echo "::error::Squid proxy failed to start"; docker logs sandbox-proxy; exit 1; } | |
| sleep 2 | |
| done | |
| # Verify: allowed domain works, blocked domain is rejected | |
| HTTP_CODE=$(curl -s -x http://localhost:3128 -o /dev/null -w '%{http_code}' https://api.github.com) | |
| if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 400 ]; then | |
| echo "::error::Allowed domain returned $HTTP_CODE"; exit 1 | |
| fi | |
| if curl -sf -x http://localhost:3128 -o /dev/null https://google.com 2>/dev/null; then | |
| echo "::error::Blocked domain reachable!"; exit 1 | |
| fi | |
| - name: Lock down iptables | |
| run: | | |
| RUNNER_UID=$(id -u) | |
| # Allow established connections, loopback, and Docker bridge | |
| sudo iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT | |
| sudo iptables -A OUTPUT -o lo -j ACCEPT | |
| sudo iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT | |
| # Block new outbound TCP from runner UID — forces proxy use | |
| sudo iptables -A OUTPUT -m owner --uid-owner "$RUNNER_UID" -p tcp --syn -j REJECT --reject-with tcp-reset | |
| # Verify: direct blocked, proxy works | |
| if curl -sf --max-time 5 -o /dev/null https://google.com 2>/dev/null; then | |
| echo "::error::Direct connection not blocked!"; exit 1 | |
| fi | |
| if ! curl -sf --max-time 10 -x http://localhost:3128 -o /dev/null https://api.github.com 2>/dev/null; then | |
| echo "::error::Proxy broken!"; exit 1 | |
| fi | |
| # ── Phase 4: Run Claude Code (sandboxed) ─────────────────────────── | |
| - name: Run Claude Code | |
| id: run-claude | |
| uses: anthropics/claude-code-action@b433f16b30d54063fd3bab6b12f46f3da00e41b6 # 2026-02-10 | |
| env: | |
| HTTP_PROXY: http://localhost:3128 | |
| HTTPS_PROXY: http://localhost:3128 | |
| NO_PROXY: localhost,127.0.0.1 | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| github_token: ${{ steps.oidc-exchange.outputs.app_token }} | |
| path_to_bun_executable: ${{ steps.setup-deps.outputs.bun_path }} | |
| path_to_claude_code_executable: ${{ steps.setup-claude.outputs.path }} | |
| plugin_marketplaces: "/tmp/zama-marketplace" | |
| plugins: | | |
| project-manager@zama-marketplace | |
| zama-developer@zama-marketplace | |
| prompt: "" | |
| claude_args: | | |
| --model opus | |
| --dangerously-skip-permissions | |
| --system-prompt "${{ env.CUSTOM_SYSTEM_PROMPT }}" | |
| # ── Cleanup ──────────────────────────────────────────────────────── | |
| # The action skips token revocation when github_token is provided — do it ourselves | |
| - name: Revoke GitHub App token | |
| if: always() && steps.oidc-exchange.outputs.app_token != '' | |
| run: | | |
| curl -sf -X DELETE \ | |
| -H "Authorization: Bearer $APP_TOKEN" \ | |
| "https://api.github.com/installation/token" | |
| env: | |
| APP_TOKEN: ${{ steps.oidc-exchange.outputs.app_token }} |