Claude Code を GitHub Actions で動かす

🖊

Claude Code は claude -p でユーザーとの対話なしに一度だけクエリを実行することができる。

これを ANTHROPIC_API_KEY を設定した環境で実行することで CI として動作させることが可能になる(直近でこの部分にバグがあったが修正された)。

以下は GitHub Actions で Issue にラベルをつけると解決を試みてくれるサンプル:

name: Claude Issue Solver

on:
    issues:
        types: [opened, edited, labeled]

permissions:
    contents: write
    issues: write
    pull-requests: write

jobs:
    analyze-issue:
        runs-on: ubuntu-latest
        # Only run when issues have the 'claude-solve' label
        if: contains(github.event.issue.labels.*.name, 'claude-solve')
        steps:
            - name: Checkout repository
              uses: actions/checkout@v3
              with:
                  token: ${{ secrets.GITHUB_TOKEN }}
                  fetch-depth: 0

            - name: Setup Git
              run: |
                  git config --global user.name "GitHub Action"
                  git config --global user.email "[email protected]"

            - name: Setup Node.js
              uses: actions/setup-node@v4
              with:
                  node-version: 20.x

            - name: Setup PNPM
              uses: pnpm/action-setup@v3

            - name: Install dependencies
              run: pnpm install --frozen-lockfile

            - name: Install Claude Code
              run: npm install -g @anthropic-ai/claude-code

            - name: Create new branch
              run: |
                  BRANCH_NAME="claude-fix-issue-${{ github.event.issue.number }}"
                  git checkout -b "$BRANCH_NAME"

            - name: Save issue details
              env:
                  ISSUE_TITLE: ${{ github.event.issue.title }}
                  ISSUE_BODY: ${{ github.event.issue.body }}
                  ISSUE_NUMBER: ${{ github.event.issue.number }}
              run: |
                  # Save issue details to file for Claude to analyze
                  cat > issue.txt << EOF
                  Issue #${ISSUE_NUMBER}: ${ISSUE_TITLE}

                  ${ISSUE_BODY}
                  EOF
            - name: Evaluate issue solvability
              id: evaluate
              env:
                  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
              run: |
                  export ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}
                  echo $ANTHROPIC_API_KEY
                  # Use Claude Code to evaluate if the issue can be solved
                  EVALUATION_JSON=$(cat issue.txt | claude -p "あなたはソフトウェアエンジニアとして、このIssueを解決できるかどうか評価してください。このIssueを解決するために十分な情報が含まれていれば「[Yes]」と返してください。そうでない場合は「[No]」と返して、その次の行に理由を述べてください。" --json)
                  echo "EVALUATION_JSON: $EVALUATION_JSON"
                  # Extract cost information
                  COST_USD=$(echo "$EVALUATION_JSON" | jq -r '.cost_usd')
                  echo "評価コスト: $COST_USD USD"

                  # Extract the result
                  EVALUATION=$(echo "$EVALUATION_JSON" | jq -r '.result')

                  # Extract yes/no response and reason
                  if echo "$EVALUATION" | grep -q "\[Yes\]"; then
                    echo "solvable=true" >> $GITHUB_OUTPUT
                    echo "reason=Issueには解決するための十分な情報が含まれています。" >> $GITHUB_OUTPUT
                  else
                    echo "solvable=false" >> $GITHUB_OUTPUT
                    REASON=$(echo "$EVALUATION" | sed -n '/\[No\]/,/^$/p' | tail -n +2)
                    echo "reason<<EOF" >> $GITHUB_OUTPUT
                    echo "$REASON" >> $GITHUB_OUTPUT
                    echo "EOF" >> $GITHUB_OUTPUT
                  fi

                  # Save total cost for reporting
                  echo "cost_usd=$COST_USD" >> $GITHUB_OUTPUT
            - name: Comment on unsolvable issue
              if: steps.evaluate.outputs.solvable != 'true'
              env:
                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                  ISSUE_NUMBER: ${{ github.event.issue.number }}
                  COST_USD: ${{ steps.evaluate.outputs.cost_usd }}
              run: |
                  COMMENT="## Claude 評価結果

                  このIssueは自動的に解決できません。

                  **理由:** ${{ steps.evaluate.outputs.reason }}

                  より詳細な情報を提供するか、要件を明確にしてください。

                  **コスト:** ${COST_USD} USD"

                  gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"

            - name: Solve issue
              if: steps.evaluate.outputs.solvable == 'true'
              id: solve
              env:
                  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
              run: |
                  # Use Claude Code to solve the issue
                  SOLUTION_JSON=$(cat issue.txt | claude -p "このIssueを解決してください。リポジトリのコードを変更するために必要な作業を行ってください。" --json --allowedTools "Bash(git diff:*)" "Bash(git log:*)" Edit)

                  # Extract cost and result information
                  COST_USD=$(echo "$SOLUTION_JSON" | jq -r '.cost_usd')
                  RESULT=$(echo "$SOLUTION_JSON" | jq -r '.result')

                  echo "解決コスト: $COST_USD USD"
                  echo "解決結果: $RESULT"

                  # Save for reporting
                  echo "cost_usd=$COST_USD" >> $GITHUB_OUTPUT
                  echo "result<<EOF" >> $GITHUB_OUTPUT
                  echo "$RESULT" >> $GITHUB_OUTPUT
                  echo "EOF" >> $GITHUB_OUTPUT

            - name: Apply auto-fixes
              if: steps.evaluate.outputs.solvable == 'true'
              run: |
                  pnpm format || true
                  pnpm lint:fix || true
            - name: Create PR
              if: steps.evaluate.outputs.solvable == 'true'
              env:
                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                  ISSUE_NUMBER: ${{ github.event.issue.number }}
                  ISSUE_TITLE: ${{ github.event.issue.title }}
                  SOLVE_COST: ${{ steps.solve.outputs.cost_usd }}
                  EVAL_COST: ${{ steps.evaluate.outputs.cost_usd }}
                  SOLVE_RESULT: ${{ steps.solve.outputs.result }}
              run: |
                  BRANCH_NAME="claude-fix-issue-${ISSUE_NUMBER}"

                  # Calculate total cost
                  TOTAL_COST=$(echo "$EVAL_COST + $SOLVE_COST" | bc)
                  rm issue.txt
                  git add .
                  git commit -m "Fix: Issue #$ISSUE_NUMBER: $ISSUE_TITLE"
                  git push -u origin "$BRANCH_NAME"

                  PR_BODY="## Claude による自動修正

                  このPRはIssue #${ISSUE_NUMBER} を修正します。

                  **解決結果:**
                  ${SOLVE_RESULT}

                  **Claude AI コスト:** ${TOTAL_COST} USD

                  Claude AI 🤖 によって実装・テストされました"

                  gh pr create --title "Fix: ${ISSUE_TITLE}" --body "$PR_BODY" --base main

                  gh issue comment "$ISSUE_NUMBER" --body "修正用のPRを作成しました! 🎉 確認をお願いします。Claude AIのコスト: ${TOTAL_COST} USD"

Claude Code で編集したあと eslint と prettier を実行して PR を作らせている、情報が不足していたら Issue にコメントが来るはずだが、いまのところかなり適当な内容でも解決しようとする。 テストや eslint、prettier に失敗したらそれをもとに更に修正させることもしたかったが、yaml として書くのが面倒で今回はそこまではやっていない。

Roo Mode のような役割を与えるプロンプトを別途用意しておくと精度がよくなるかもしれない。

yaakai.to