2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

さくらのAIエンジンとGithub Actionsを使って、Issue起票だけで動くアプリ自動開発環境を作ったら一発で1,200リクエスト消費した

2
Posted at

初めに

最近GoogleのJulesというAIエージェントを使い始めました。
これは、GithubでIssueを起票すると、Googleのクラウド環境でGeminiが勝手にコードの修正を行いプルリクまでしてくれるという優れものです。

image.png

Issue起票だけならスマホから出来るので、外出先でも片手間に開発を行えて非常に便利なのですが、結構処理が遅かったりカスタマイズ性に乏しかったりで微妙に使いづらさも感じています。
そこで、Github ActionsとさくらのAIエンジンを使ったら無料で似たようなものが作れるんじゃないかと思い、ワークフローの試作品を作成してみました。

なお、今回はこのワークフローを使ってブロック崩しゲームを作成しています。
https://pesipesi.github.io/sakura-breakout/

概要

スマホからでも開発できる環境を目指し、以下の要件で設計しています。

  • Github上でIssueを起票すると、その内容にしたがってAIが全自動で実装からプルリクエストまでを実施する
  • 最近はやりのループエンジニアリングを意識して、AIが開発したコードをそのままAIにレビューさせ、修正コードを作成したらまたレビューさせ……という継続的なループを作成する
  • 開発やレビューではGithubランナー上でClaude CodeのCLIを動かし、ClaudeのAPIリクエスト先にさくらのAIエンジンを指定する

最終的に出来上がったワークフローの仕様

  1. トリガーとブランチ作成
    GitHubを開き、Issueを作成して ai-task ラベルを付与すると、GitHub Actionsが起動します。自動的に feature/issue-[番号] という作業ブランチが切られます。
  2. システムプロンプトの注入
    AIに対して「あなたは自律型AIエンジニアである」「テストやビルドを行い、動作検証した上でコミット&プッシュする」という指示 (CLAUDE.md) を与え、作業ツリーに未保存の変更を残さずに完遂させます。
  3. AIによる実装とPush
    Issueのタイトルと本文がプロンプトとして渡され、AIがコードを読み書きします。ターミナル上でコマンドを叩きながら修正を行い、自らコミットを作成してGitHubへプッシュします。
  4. Pull Requestの自動作成
    修正がプッシュされると、AIの作業結果やトークン消費量(Usage Stats)をまとめたレポートとともに、自動でPull Requestが作成されます。
  5. AI同士の自動レビュー・修正ループ
    PRに ai-generated ラベルが付与されていると、AIが自身の作成したPRを自動レビューします。バグや改善点を見つけると自律的に修正し、再度プッシュします。このフィードバックループにより、品質が自動的に向上していきます。
    ※ 無限ループを防ぐため、レビューが10件を超えたら停止するようにしています。
  6. 手動での介入・対話
    PRのコメント欄で /claude とメンションすることで、手動で追加の指示を出すことも可能です。

というわけで、Issue起票をするだけでアプリの開発と継続的なレビューを全自動で回してくれるワークフローとなっています。

環境構築

このワークフローを動かすための設定方法を解説します。

以下設定をそのままパブリックリポジトリに適用すると、不特定多数のIssue起票でワークフローが実行される恐れがあります。
もし試す場合には必ずプライベートリポジトリで行ってください。

ディレクトリ構成

プロジェクトのルートディレクトリに以下のようにフォルダを作成し、ワークフローファイルを配置します。
ファイルとして用意するものはこれだけです。
テンプレートリポジトリとして登録すれば簡単に複製して使いまわせます。

プロジェクトルート/
└── .github/
    └── workflows/
        └── claude-coder.yml

ワークフローファイル

実際に利用する場合は、以下のコードをコピーして .github/workflows/claude-coder.yml として保存してください。
※なおファイル名は任意です。

claude-coder.yml (クリックして展開)
name: Claude Coder

on:
  issues:
    types: [opened, reopened, labeled]
  pull_request:
    types: [opened, synchronize]
  issue_comment:
    types: [created]

jobs:
  claude-code:
    if: >-
      (github.event_name == 'issues' && (
        (github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'ai-task')) ||
        (github.event.action == 'reopened' && contains(github.event.issue.labels.*.name, 'ai-task')) ||
        (github.event.action == 'labeled' && github.event.label.name == 'ai-task')
      )) ||
      (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ai-generated')) ||
      (github.event_name == 'issue_comment' && github.event.issue.pull_request != null && contains(github.event.issue.labels.*.name, 'ai-generated') && github.actor != 'github-actions[bot]' && !contains(github.event.comment.body, '<!-- CLAUDE_BOT -->'))
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      issues: write
    steps:
      - name: 📥 リポジトリをチェックアウト
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.PAT_TOKEN }}
          fetch-depth: 0

      - name: ⚙️ Node.js環境のセットアップ (テスト・ビルド用)
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: 🌿 ブランチの準備とチェックアウト
        env:
          GH_TOKEN: ${{ secrets.PAT_TOKEN }}
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "github-actions[bot]@users.noreply.github.com"
          
          if [ "${{ github.event_name }}" = "issues" ]; then
            BRANCH_NAME="feature/issue-${{ github.event.issue.number }}"
            git fetch origin "$BRANCH_NAME" || true
            if git ls-remote --exit-code --heads origin "$BRANCH_NAME"; then
              git checkout "$BRANCH_NAME"
            else
              git checkout -b "$BRANCH_NAME"
              git push -u origin "$BRANCH_NAME"
            fi
          elif [ "${{ github.event_name }}" = "pull_request" ]; then
            BRANCH_NAME="${{ github.event.pull_request.head.ref }}"
            git fetch origin "$BRANCH_NAME"
            git checkout "$BRANCH_NAME"
          elif [ "${{ github.event_name }}" = "issue_comment" ]; then
            PR_NUMBER="${{ github.event.issue.number }}"
            BRANCH_NAME=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" --jq '.head.ref')
            git fetch origin "$BRANCH_NAME"
            git checkout "$BRANCH_NAME"
          fi

      - name: 📝 CLAUDE.md (システムプロンプト) の生成
        run: |
          cat << 'EOF' > CLAUDE.md
          # System Instructions for Claude Code
          - あなたは凄腕の自律型AIエンジニアです。
          - コードの変更を行う際は、必ず自律的にテストやビルドを実行して動作検証を行ってください。
          - コードを修正した場合は、必ず**自律的にコミットを作成し、リモートリポジトリにプッシュ**してください。作業ツリーに変更を残したまま終了してはいけません。
          - `CLAUDE.md` はシステムファイルです。絶対にコミットに含めないでください。
          EOF

      - name: 🔢 ループ回数の確認 (PR自動レビュー時のみ)
        id: check_loop
        env:
          GH_TOKEN: ${{ secrets.PAT_TOKEN }}
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            PR_NUMBER="${{ github.event.pull_request.number }}"
            # Botによるコメント数を取得
            COMMENT_COUNT=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
              --jq '[.[] | select(.body | contains("<!-- CLAUDE_BOT -->"))] | length')
            
            echo "Current bot comment count: ${COMMENT_COUNT}"
            if [ "$COMMENT_COUNT" -ge 10 ]; then
              echo "skip=true" >> "$GITHUB_OUTPUT"
              gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
                -f body="⚠️ 自動レビューと修正のループ上限(10回)に達しました。これ以上の自動修正は行いません。 <!-- CLAUDE_BOT -->" || true
            else
              echo "skip=false" >> "$GITHUB_OUTPUT"
            fi
          else
            echo "skip=false" >> "$GITHUB_OUTPUT"
          fi

      - name: 📝 プロンプトとモードの判定
        id: setup_prompt
        if: steps.check_loop.outputs.skip != 'true'
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
        run: |
          if [ "${{ github.event_name }}" = "issues" ]; then
            {
              echo "prompt<<EOF"
              echo "以下のIssueの要件を満たすコードを実装してください。完了後、行った実装内容の要約を出力してください。"
              echo ""
              echo "# $ISSUE_TITLE"
              echo "$ISSUE_BODY"
              echo "EOF"
            } >> "$GITHUB_OUTPUT"
            echo "run_mode=automation" >> "$GITHUB_OUTPUT"
          elif [ "${{ github.event_name }}" = "pull_request" ]; then
            {
              echo "prompt<<EOF"
              echo "このPRのコードをレビューしてください。バグや設計の改善点があれば、そのまま自律的に修正し、コミットしてプッシュしてください。完了後、行った修正内容の要約を出力してください。何も修正すべき点が無ければ「修正点はありません」と回答してください。"
              echo "EOF"
            } >> "$GITHUB_OUTPUT"
            echo "run_mode=automation" >> "$GITHUB_OUTPUT"
          else
            # issue_comment
            echo "prompt=" >> "$GITHUB_OUTPUT"
            echo "run_mode=interactive" >> "$GITHUB_OUTPUT"
          fi

      - name: 🤖 Claude Code の実行 (自動モード)
        if: steps.check_loop.outputs.skip != 'true' && steps.setup_prompt.outputs.run_mode == 'automation'
        uses: anthropics/claude-code-action@v1
        with:
          github_token: ${{ secrets.PAT_TOKEN }}
          prompt: ${{ steps.setup_prompt.outputs.prompt }}
          claude_args: "--dangerously-skip-permissions"
          show_full_output: true
        env:
          ANTHROPIC_API_KEY: ${{ secrets.AI_API_KEY }}
          ANTHROPIC_AUTH_TOKEN: ${{ secrets.AI_API_KEY }}
          ANTHROPIC_BASE_URL: ${{ secrets.AI_BASE_URL }}
          ANTHROPIC_DEFAULT_SONNET_MODEL: ${{ secrets.AI_MODEL }}
          ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ secrets.AI_MODEL }}

      - name: 🤖 Claude Code の実行 (手動対話モード)
        if: steps.check_loop.outputs.skip != 'true' && steps.setup_prompt.outputs.run_mode == 'interactive'
        uses: anthropics/claude-code-action@v1
        with:
          github_token: ${{ secrets.PAT_TOKEN }}
          trigger_phrase: "/claude"
          claude_args: "--dangerously-skip-permissions"
          show_full_output: true
        env:
          ANTHROPIC_API_KEY: ${{ secrets.AI_API_KEY }}
          ANTHROPIC_AUTH_TOKEN: ${{ secrets.AI_API_KEY }}
          ANTHROPIC_BASE_URL: ${{ secrets.AI_BASE_URL }}
          ANTHROPIC_DEFAULT_SONNET_MODEL: ${{ secrets.AI_MODEL }}
          ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ secrets.AI_MODEL }}

      - name: 📤 変更のプッシュ (フェイルセーフ)
        run: |
          rm -f CLAUDE.md
          if [ -n "$(git status --porcelain)" ]; then
            git add .
            git commit -m "chore: apply AI suggested changes" || true
          fi
          git push origin HEAD || true

      - name: 🚀 PRの作成 (Issue時)
        if: github.event_name == 'issues'
        env:
          GH_TOKEN: ${{ secrets.PAT_TOKEN }}
        run: |
          BRANCH_NAME="feature/issue-${{ github.event.issue.number }}"
          
          # 変更(コミット)が存在するか確認
          REMOTE_BRANCH_SHA=$(git ls-remote origin "refs/heads/$BRANCH_NAME" | awk '{print $1}')
          REMOTE_MAIN_SHA=$(git ls-remote origin refs/heads/main | awk '{print $1}')
          
          if [ -z "$REMOTE_BRANCH_SHA" ] || [ "$REMOTE_BRANCH_SHA" = "$REMOTE_MAIN_SHA" ]; then
            echo "No changes made by Claude on remote. Skipping PR creation."
            exit 0
          fi
          
          # レポートの抽出
          REPORT_BODY="Closes #${{ github.event.issue.number }}

            "
          if [ -f "${{ runner.temp }}/claude-execution-output.json" ]; then
            jq -s 'map(if type == "array" then .[] else . end) | map(select(.type == "result")) | last' "${{ runner.temp }}/claude-execution-output.json" > /tmp/claude-result.json
            CLAUDE_RESULT=$(jq -r '.result // ""' /tmp/claude-result.json)
            INPUT_TOKENS=$(jq -r '.modelUsage | to_entries | .[0].value.inputTokens // 0' /tmp/claude-result.json)
            OUTPUT_TOKENS=$(jq -r '.modelUsage | to_entries | .[0].value.outputTokens // 0' /tmp/claude-result.json)
            DURATION_MS=$(jq -r '.duration_ms // 0' /tmp/claude-result.json)
            TURNS=$(jq -r '.num_turns // 0' /tmp/claude-result.json)
            
            REPORT_BODY="${REPORT_BODY}### 🤖 Claude Report
            ${CLAUDE_RESULT}

            ### 📊 Usage Stats
            - **Input Tokens:** ${INPUT_TOKENS}
            - **Output Tokens:** ${OUTPUT_TOKENS}
            - **Duration:** $((DURATION_MS / 1000))s
            - **Turns:** ${TURNS}
            "
          fi
          
          # 既にPRが存在するか確認
          PR_EXISTS=$(gh pr list --head "$BRANCH_NAME" --json number --jq length)
          if [ "$PR_EXISTS" -eq 0 ]; then
            # 一時ファイルにボディを書き出して作成
            echo -e "$REPORT_BODY" > pr_body.txt
            gh pr create --title "Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }}" \
                         --body-file pr_body.txt \
                         --label "ai-generated" \
                         --base main \
                         --head "$BRANCH_NAME"
            rm -f pr_body.txt
          fi

      - name: 📝 結果のPRコメント投稿 (PR時・手動コメント時)
        if: github.event_name == 'pull_request' || github.event_name == 'issue_comment'
        env:
          GH_TOKEN: ${{ secrets.PAT_TOKEN }}
        run: |
          if [ -f "${{ runner.temp }}/claude-execution-output.json" ]; then
            jq -s 'map(if type == "array" then .[] else . end) | map(select(.type == "result")) | last' "${{ runner.temp }}/claude-execution-output.json" > /tmp/claude-result.json
            CLAUDE_RESULT=$(jq -r '.result // ""' /tmp/claude-result.json)
            INPUT_TOKENS=$(jq -r '.modelUsage | to_entries | .[0].value.inputTokens // 0' /tmp/claude-result.json)
            OUTPUT_TOKENS=$(jq -r '.modelUsage | to_entries | .[0].value.outputTokens // 0' /tmp/claude-result.json)
            DURATION_MS=$(jq -r '.duration_ms // 0' /tmp/claude-result.json)
            TURNS=$(jq -r '.num_turns // 0' /tmp/claude-result.json)
            
            COMMENT_BODY="${CLAUDE_RESULT}
            
            ### 📊 Usage Stats
            - **Input Tokens:** ${INPUT_TOKENS}
            - **Output Tokens:** ${OUTPUT_TOKENS}
            - **Duration:** $((DURATION_MS / 1000))s
            - **Turns:** ${TURNS}
            
            <!-- CLAUDE_BOT -->"
            
            echo -e "$COMMENT_BODY" > comment_body.txt
            PR_NUMBER="${{ github.event.pull_request.number || github.event.issue.number }}"
            gh pr comment "$PR_NUMBER" --body-file comment_body.txt
            rm -f comment_body.txt
          fi

各種設定方法 (GitHub Secrets)

さくらのAIエンジンとGitHubを連携させるため、リポジトリの Settings > Secrets and variables > Actions にアクセスし、以下のシークレット(Repository secrets)を登録します。

  • PAT_TOKEN: GitHubのPersonal Access Token。AIがコードをPushしたり、PRを作成するために必要です。(※ Contents, Pull requests, Issues の Read and Write 権限を付与した Fine-grained token を推奨)
  • AI_API_KEY: さくらのAIエンジンから発行されたAPIキー。
  • AI_BASE_URL: AIエンジンのエンドポイントURL。(https://api.ai.sakura.ad.jp
  • AI_MODEL: 利用するAIモデル名。(今回はpreview/Kimi-K2.7-Codeを使用)

これらを環境変数としてワークフローに渡すことで、本来Anthropic APIに向けられるClaude Codeの通信を、さくらのAIエンジンへ向けることができます。

実際の運用

実際にワークフローを動かした際の記録を残していきます。

初期設定

必要なワークフローファイルとGithubのシークレットを作成します。
image.png
image.png

Issue起票

Issueを手動で起票し、ai-taskというラベルを付けます。
今回は適当にブロック崩しゲームを作ることにしました。
仕様を考えるのも面倒だったのでAIにチャットで作らせた仕様を適当に張り付けてます。

プロンプト
以下の要件に従って、HTML、CSS、JavaScriptのみで動作する「ステージ制ブロック崩しゲーム」を作成してください。
コードは機能ごとに適切にファイル分割し、保守性と可読性を意識した設計にしてください。

### 1. ステージ・進行システム(最重要要件)
以下のいずれか(または両方)のステージシステムを実装してください:
- **複数ステージ構成(推奨: 3〜5ステージ)**:
  - ステージごとにブロックの配置パターン、行数・列数、耐久値(1回破壊/2回破壊)が変化する
  - 例: 
    - Stage 1: シンプルな四角形グリッド(入門)
    - Stage 2: ピラミッド型配置 + 一部2回叩くブロック
    - Stage 3: スペースインベーダー風のドット絵配置 + 硬いブロック多数
  - ステージ内の全ブロックを破壊すると「STAGE CLEAR」となり、ボール/パドル位置を初期化して次ステージへ遷移
  - 全ステージ制覇で「ALL STAGE CLEAR」画面と総合スコアを表示
- **ランダムステージ自動生成モード(または次ステージ自動生成)**:
  - ステージが進むごとにブロック密度や耐久値の高いブロックの割合が増加するアルゴリズム

### 2. ゲームの基本仕様
- **キャンバスサイズ**: 480px × 320px(または見やすいアスペクト比)
- **操作方法**:
  - キーボード: 「←」「→」または「A」「D」キーで左右移動
  - マウス / タッチ: カーソル移動またはタッチドラッグで追従移動
- **ボール挙動**:
  - 左右・上の壁、およびパドルに当たると跳ね返る
  - パドルの中心からの接触位置に応じて反射角が滑らかに変化する
  - ステージが進むごとにわずかにボール速度が上昇する
  - 画面下部に落下すると残機が1減少し、リセットされる
- **ブロック仕様**:
  - 耐久度(HP)に応じて色が変わる(例: HP2=濃い色、1回当たるとヒビが入るか薄い色に変化)
  - 破壊時にスコア加算
- **UI・ステータス表示**:
  - 現在の「STAGE」「SCORE」「LIVES(残機)」を画面上部に常時表示

### 3. デザイン・演出要件
- ネオン/サイバーパンク風のモダンで見やすいダークテーマデザイン
- ブロック破壊時の軽いパーティクル(火花)エフェクト演出
- リスタートボタン、次ステージ開始ボタン付きのモーダルUI

### 4. 技術的要件
- 外部ライブラリは使用せず Vanilla JS(HTML5 Canvas API)で実装
- `requestAnimationFrame` による描画ループ
- 2次元配列などを活用したステージマップ定義(保守・拡張しやすい設計)
- コード全体に日本語コメントを付与し、機能ごとにファイルを適切に分割して出力すること

image.png

Actions起動

Issueを起票すると、Actionsが動きGithubのランナー上でClaudeがコードを生成し始めます。
image.png
今回は9分ほど待って最初のプルリクエストが作成されました。
image.png

AIレビュー

プルリクエストの作成をトリガーにAIレビューとコード修正のジョブが発火します。
image.png
以降、AIレビューが10件を超えるまで「レビュー => 修正 => レビュー...」のループが走ります。

レビュー履歴(一部抜粋)

image.png

image.png

image.png

image.png

実装完了

最終的にレビューのループをくぐり抜けたコードをマージしたら実装完了です。
人間が手を動かすのは最初のIssue起票のみであり、その後はAIがワークフローをループして勝手に実装してくれました。
初回のIssueで投げるプロンプトを適当にしてしまったため、ボール複製や貫通アイテムなどのない非常に淡泊なゲーム性になってしまいましたが、普通に遊べるブロック崩しゲームとなっています。
https://pesipesi.github.io/sakura-breakout/

sakuar-breakout.gif

追加実装

もし実装に不満があれば、再度Issueを起票するか、マージ前のプルリクエストに/claudeとメンションしてコメントをすると引き続き改修を行ってくれます。
今回でいうと、ボールを落とした瞬間に休みなく次のボールが発射されてしまう、モニターのリフレッシュレートに応じてボールの速度が変わってしまう、などの問題が残っているため、気が付き次第AIに修正させましょう。
image.png

実装にかかったコスト

今回のワークフローでは実装やレビューごとに使ったトークン数などを出力させています。
image.png

  • Issue起票から最後のレビューが終わるまでが 約5時間半(16:37 - 22:03)
  • 総Input Token数 21,798,327
  • 総Output Token数 511,067
  • 合計ターン数(Claude CLIのアクション数) 405回

今回利用したモデル(Kimi-K2.7-Code)の料金は現状以下の通りなので、大体1,400円分くらいでしょうか。
¥0.52 / 10,000トークン (税込) ¥5.04 / 10,000トークン (税込)

合計APIリクエスト数は正確に数えていないのですが、さくらのクラウドコンソール上でIssue起票前が確か700台だったものが 1,991リクエストまで増加していたため、合計1,200リクエスト程を今回のワークフローで消費していると思われます。
Claude CLIでは1ターンあたり3APIリクエストを平均で消費していたようです。

image.png

さくらのAIエンジンが月3,000リクエスト、GithubのCI/CDランナーが月2,000分(約33時間)まで無料なので枠内には収まっていますが、あまり乱用はできないといった感じです。

所感

というわけで、さくらのAIエンジンを使った、無料のAI自動開発環境を構築することができました。
今回は試作品のため細かいところまでは詰められていないですが、思ったよりClaude CLIがバンバンAPIを消費するため、ループ回数を10回から減らすか、もう少しレビューの品質を落とすようなプロンプトに変更してもよいかもしれないです。

また、今回は最初のIssueで適当なプロンプトにしてしまったため、1,200リクエストも消費した割に淡泊なゲーム性となってしまいましたが、もっと細かく指示を出すことでより高度なアプリを開発できるかと思います。
ループエンジニアリング的なことをしているので当たり前ですが、最初のプロンプト設計が悪いと大量のトークンを消費しながら低品質なものしか出来上がらなくなってしまうため、プロンプト設計の重要さを身に染みて感じました。

なお、当初はClaudeではなく独自実装のスクリプトや、aider、 Qodo Merge(旧PR-Agent)からさくらのエンジンのAPIを叩いて実装やレビューを行わせようとしていましたがなかなか安定せず、結局Claude CLIを使う方法が一番うまく動きました。
API消費は激しいものの、さすがはClaudeという感じです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?