GitHub Issue を閉じたら記事ができる — そんな仕組みを作ろう
技術ブログを運営していると、「Issue を解決したのに記事にする時間がない」という問題に直面します。せっかくの知見がリポジトリの中に埋もれたまま、誰の目にも触れない。この課題を解決するために、GitHub Issue のクローズをトリガーにして AI がブログ記事を自動生成し、PR として提出する仕組みを構築できます。
この記事では、設計の考え方から実装中に遭遇した3つの大きな壁、そしてその解決策まで具体的に解説します。
全体アーキテクチャ
処理の流れは次のとおりです。
Issue クローズ
→ GitHub Actions がラベル(blog:queued)を付与
→ スケジュールタスク(Cowork 等の定期実行基盤、または GitHub Actions の scheduled workflow)が定期的にキューを確認
→ AI が Issue メタデータを元に記事を生成
→ feature ブランチで PR を作成
→ 人間がレビュー → マージ → 自動デプロイ
ポイントは「完全自動だが、公開前に人間のレビューが入る」という設計です。AI が生成した記事をそのまま公開するのではなく、PR というレビューゲートを設けることで品質を担保しています。
記事タイプのラベル設計
Issue に付けるラベルで記事の種類を制御します。
blog:tech-article は 2000〜4000字の本格的な技術ブログ記事を生成します。SEO を意識したタイトルや description、構造化された見出しを含みます。一方、blog:dev-log は 500〜1500字の簡潔な開発ログで、「何をやったか」を事実ベースで記録します。
記事にしたくない Issue には blog:skip を付けておけばスキップされます。処理済みの Issue には自動的に blog:generated が付与され、二重生成を防ぎます。
GitHub Actions × AI 記事生成の実装で直面した3つの壁
FUSE マウントで git 操作が失敗する問題と回避策
サンドボックス環境(Cowork など)からユーザーのフォルダにアクセスする仕組みは、内部的に FUSE(Filesystem in Userspace)マウント(virtiofs)を使用している場合があります。このマウントではファイルの作成と書き込みはできますが、ファイルの削除(unlink)ができないという制限があります。
git はほぼすべてのコマンドで .lock ファイルを作成し、完了後に削除します。削除できないため、git add も git commit も git checkout もすべてエラーになりました。
fatal: Unable to create '/path/.git/index.lock': Function not implemented
解決策として、サンドボックス内部(削除が自由にできる領域)にリポジトリを丸ごとクローンし、そこで git 操作を完結させるようにしました。FUSE マウント側のファイルはあくまで「閲覧用」として扱い、git 操作は一切行いません。
ブランチ保護ルールと GitHub Actions GITHUB_TOKEN の権限制約
最初の設計では、Issue がクローズされたら GitHub Actions が blog-queue/ フォルダに JSON ファイルを書き込み、それを記事生成の待ちリストとして使う予定でした。
# 当初の設計(失敗)
- name: Write queue file
run: |
echo "$METADATA" > blog-queue/$ISSUE_NUMBER.json
git add blog-queue/
git commit -m "chore: queue blog request"
git push origin develop
ところが、保護されたブランチ(develop や main など)に対しては、GitHub Actions のデフォルトトークン(GITHUB_TOKEN)では直接 push が拒否されます。
remote: error: GH006: Protected branch update failed
解決策として、ファイルベースのキューを完全に廃止し、GitHub のラベルをキューとして使う設計に切り替えました。Issue がクローズされたら blog:queued ラベルを付けるだけ。ラベル操作は Issues API で完結するため、ブランチ保護の影響を受けません。
# 最終設計(成功)
- name: Queue blog request via label
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.addLabels({
owner, repo,
issue_number: issueNumber,
labels: ['blog:queued']
});
記事生成側は gh issue list --label "blog:queued" でキューを検索するだけです。シンプルで堅牢な設計になりました。
workflow_dispatch がデフォルトブランチでしか使えない制約
手動で記事生成をトリガーするために workflow_dispatch イベントを使っています。これにより、GitHub の UI やAPI から任意の Issue 番号を指定して記事生成を要求できます。
しかし、この機能にはひとつ落とし穴があります。ワークフローの YAML ファイルがデフォルトブランチ(main)に存在しない限り、GitHub の UI にも API にも表示されず、実行できません。
develop ブランチにマージしただけでは使えず、main にもマージして初めて workflow_dispatch が利用可能になります。これは GitHub の仕様で、ワークフロー一覧の取得がデフォルトブランチのみを参照するためです。
スケジュールタスクによるコスト削減
記事生成には AI を使いますが、定期実行基盤の選び方によってはコストを抑えられます。たとえば、Cowork のサブスクリプションに含まれるスケジュールタスク機能を使えば、30分間隔でキューを確認・処理しても追加の API 課金が発生しません。GitHub Actions の schedule トリガーを使う場合も、パブリックリポジトリでは無料枠内で十分対応できます。
スケジュールタスクが毎回新しいサンドボックスで実行される場合は、gh CLI のインストールや認証を毎回行う必要があります。この手順をタスク定義ファイル(AI エージェントに実行手順を伝えるドキュメント)に明記しておくと、タスクが自律的にセットアップできるようになります。
生成される記事の品質管理
AI が生成する記事には以下の品質基準を設けています。
Astro Content Collections のフロントマターには sourceIssue フィールドで元の Issue 番号を記録し、aiGenerated: true で AI 生成であることを明示します。tech-article は 2000〜4000字、dev-log は 500〜1500字の文字数範囲を設定し、冗長すぎず薄すぎない記事を目指しています。
SEO 面では、タイトルにメインキーワードを含め 30〜60字に収める、description を 120字以内にする、見出し(H2, H3)にサブキーワードを含めるといったルールを適用しています。
まとめ
GitHub Issue のクローズから記事生成までを自動化することで、「知見の蓄積」と「ブログ運営」を同時に回せる基盤が手に入ります。FUSE の制限、ブランチ保護との衝突、GitHub の仕様上の制約という3つの壁がありますが、ラベルベースのシンプルな設計に切り替えることで回避できます。
今後は記事のテンプレートを充実させ、カテゴリごとに最適化された記事生成を目指していきます。
関連記事
- generate-blog を PR作成止まりから公開完了まで自動化した実装ログ — PR 作成後のマージ・リリース・デプロイまで自動化した続編
- ブログ記事生成フローのコンセプト統合 — 記事生成プロンプトにサイトコンセプトを統合した改善
- Cowork スケジュールタスクで gh CLI 認証を永続化する方法 — 本記事の基盤となる Cowork 認証の永続化手順