4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHubのTrendingを、AI編集部が無料枠だけで下書きしてくれる仕組みを作った

4
Last updated at Posted at 2026-08-20

1. はじめに

ソーイ株式会社の西浦です。

つい最近、GitHubで話題になっているリポジトリをまとめて見られる「Trending」というコンテンツの存在を知りました。眺めてみると普段の業務では出会わないような面白いプロジェクトが並んでいて、これは毎週チェックしたいと思いました。

それなら自分で見るだけでなく記事化してしまおうと考えたのですが、同時に「Qiitaへの自動投稿って実際どうやるんだろう」という素朴な興味もありました。この2つを組み合わせれば、面白いコンテンツを楽しみながら自動投稿の仕組み作りも学べると思い、作ってみることにしました。

この記事では、実際にどの画面で何を操作し、どんなコードを書いたかを順を追って再現できる形でまとめます。
受託開発の業務でも生成AIに触れる機会が増えており、要約や文章生成をどう設計すれば実務で使いやすいかを意識する場面が多くなっています。今回はその感覚を、業務とは離れた題材で試す機会にもなりました。

完成したもの

  • 毎週月曜朝9時に自動実行
  • GitHubで直近1週間に人気だったリポジトリを取得
  • AIが「なぜ人気か」「実務でのメリット」などを日本語で要約
  • Qiitaに自動投稿

2. 全体構成

処理の流れはシンプルです。

使った技術は以下の通りです。

カテゴリ 技術・サービス
データ取得 GitHub Search API
AI要約 Google Gemini API
記事投稿 Qiita API v2
定期実行 GitHub Actions

3. Step1: GitHub Search APIでデータを取得する

3-1. Personal Access Tokenを発行する

GitHubの Settings > Developer settings > Personal access tokens > Fine-grained tokens から新規発行します。

  • Repository access: Public Repositories (read-only)
  • 追加の権限設定は不要(検索APIの読み取りのみのため)
    スクリーンショット 2026-08-14 22.22.26.png

3-2. 環境構築とコード実装

mkdir qiita-github-trend && cd qiita-github-trend
python3 -m venv venv
source venv/bin/activate
pip install requests

「直近1週間で作成され、スター数が多く、フォークではない」リポジトリを取得する関数を実装します。

generate_article.py
def fetch_popular_repos(days=7, per_page=15, min_stars=500):
    since = (datetime.date.today() - datetime.timedelta(days=days)).isoformat()
    query = f"created:>{since} stars:>{min_stars} fork:false"
    url = "https://api.github.com/search/repositories"
    params = {"q": query, "sort": "stars", "order": "desc", "per_page": per_page}
    res = requests.get(url, params=params, headers=github_headers)
    res.raise_for_status()
    repos = res.json()["items"]
    return [r for r in repos if r["description"]]  # description未設定は要約材料が薄いため除外

3-3. 実行して確認する

export GH_SEARCH_TOKEN="発行したトークン"
python generate_article.py

リポジトリ名・スター数・言語が一覧で出力されれば成功です。

「作成日」ベースの設計上のトレードオフ
GitHub Trendingには公式APIが存在しないため、Search APIのcreated:>条件で代替しています。ただしこの方式だと「直近作成されたリポジトリ」限定になり、既存の古いリポジトリが今週急にバズったケースは拾えません。GitHub Trendingの「勢い」ロジックとは性質が異なる点は理解した上で採用しています。

4. Step2: AIで要約を生成する

ただリポジトリ名とスター数を並べるだけでは無機質なリストで終わってしまい、「面白そうだから触ってみよう」というところまで読者の気持ちを動かしにくいと感じました。そこで各リポジトリについて「なぜ人気なのか」「実務でのメリット」「どんな人におすすめか」をAIに生成させ、読んだ人が自分ごととして利用イメージを持てる記事にすることにしました。

4-1. Gemini APIキーを発行する

  1. https://aistudio.google.com/apikey にアクセス
  2. 「新しいキーを作成する」→ 紐付けるGoogle Cloudプロジェクトを選択(まだ無ければ https://console.cloud.google.com/ で先に作成)
  3. 発行されたキーをコピー

詳細な手順や料金体系は公式ドキュメントも参照してください。

スクリーンショット 2026-08-19 10.51.36.png

4-2. SDKを導入する

pip install google-genai
export GEMINI_API_KEY="発行したキー"

4-3. 要約生成を実装する

全リポジトリ分をまとめて1回のリクエストで処理し、JSON形式で出力させます。

generate_article.py
def summarize_all_repos(repos, max_retries=3):
    repo_list_text = "\n".join(
        f"{i+1}. リポジトリ名: {r['full_name']} / 説明: {r['description']}"
        for i, r in enumerate(repos)
    )
    prompt = f"""以下は今週人気だったGitHubリポジトリの一覧です。それぞれについて、日本の読者向けに次の4項目を日本語でまとめてください。
 
{repo_list_text}
 
各リポジトリについて、以下のJSON配列形式で出力してください:
[
  {{"full_name": "...", "overview": "①どんなツールか", "why_popular": "②なぜ人気か",
    "merit": "③実務でのメリット", "recommended_for": "④どんな人におすすめか"}},
  ...
]
出典の文章をそのまま引用せず必ず自分の言葉で言い換えること
"""
    response = gemini_client.models.generate_content(
        model="gemini-3.6-flash",
        contents=prompt,
    )
    return json.loads(response.text)

4-4. 実行して確認する

python generate_article.py

各リポジトリについて4項目の要約が生成されれば成功です。

GitHub Modelsは開発中に廃止されていた
当初はGitHub Actionsとの相性の良さから「GitHub Models」(GITHUB_TOKENだけで呼べる無料枠)を使う予定でしたが、実装中に呼び出すと410 github_models_retirement_brownoutエラーに遭遇。調べると2026年7月30日付けで既存ユーザーも含め完全廃止されていました。急遽Gemini APIに乗り換えています。

無料枠は1日20リクエスト、モデル名も変わりやすい
開発中に何度もテスト実行していたらRESOURCE_EXHAUSTED(1日20リクエスト上限)にすぐ到達しました。原因は「1リポジトリ1リクエスト」で処理するのに件数分のリクエストを消費していたことです。上記のように全件をまとめて1リクエストにするバッチ方式に変更し、消費を大幅に抑えました。また指定したモデル名gemini-2.5-flashも「新規ユーザーには提供終了」で弾かれ、gemini-3.6-flash(2026年7月21日リリース)への切り替えが必要でした。無料枠のレート制限や利用可能なモデルの最新状況は、以下の公式ページで確認できます(記事執筆時点の情報のため、閲覧時に変わっている可能性があります)。

5. Step3: Qiitaに投稿する記事を組み立てる

5-1. Markdownを整形する

ランキング形式にし、URLを単独行で置くことでQiitaの自動リンクカード機能を活用しています。

generate_article.py
def build_article(repos_with_summary):
    lines = [f"直近1週間で新しく作成され、スター数が多かったGitHubリポジトリを{len(repos_with_summary)}件まとめました。", ""]
 
    if len(repos_with_summary) >= 3:
        lines.append("### 🏆 今週のトップ3")
        for i, (repo, _) in enumerate(repos_with_summary[:3], start=1):
            lines.append(f"{i}. [{repo['full_name']}]({repo['html_url']}) (⭐{repo['stargazers_count']:,})")
        lines.append("")
 
    for i, (repo, summary) in enumerate(repos_with_summary, start=1):
        lines.append(f"## {i}位: 📦 {repo['full_name']}")
        lines.append(f"| ⭐ Star数 | 言語 |\n|---|---|\n| {repo['stargazers_count']:,} | {repo['language'] or '-'} |")
        lines.append(f"### 概要\n{summary['overview']}")
        lines.append(f"### なぜ人気なのか\n{summary['why_popular']}")
        lines.append(f"### 実務でのメリット\n{summary['merit']}")
        lines.append(f"### どんな人におすすめか\n{summary['recommended_for']}")
        lines.append(repo['html_url'])  # 単独行のURL → Qiitaが自動でリンクカード化
 
    return "\n".join(lines)

5-2. タグを組み立てる

固定タグに加え、その週に登場した言語タグを動的に追加します。

generate_article.py
def build_tags(repos_with_summary):
    fixed_tags = ["GitHub", "GitHubTrending", "AI"]
    # dict.fromkeys()で重複を除きつつ、スター数順(repos_with_summaryの並び順)を維持する
    # setだと実行のたびに順序が変わり、同じ入力でもタグが変わってしまうため使わない
    languages = list(dict.fromkeys(
        repo["language"] for repo, _ in repos_with_summary if repo["language"]
    ))
    return fixed_tags + languages[:2]

5-3. Qiita APIトークンを発行する

Qiitaにログインした状態で https://qiita.com/settings/applications にアクセスし、「個人用アクセストークン」から新規発行します(write_qiitaスコープが必要)。
APIの詳細仕様は公式ドキュメントを参照してください。

スクリーンショット 2026-08-14 23.40.44.png

5-4. 投稿処理を実装して実行する

generate_article.py
def post_to_qiita(title, body, tags, private=True):
    url = "https://qiita.com/api/v2/items"
    headers = {"Authorization": f"Bearer {os.environ['QIITA_TOKEN']}", "Content-Type": "application/json"}
    payload = {"title": title, "body": body, "tags": [{"name": t, "versions": []} for t in tags], "private": private}
    res = requests.post(url, headers=headers, json=payload)
    res.raise_for_status()
    return res.json()
export QIITA_TOKEN="発行したトークン"
python generate_article.py

まずはprivate: true(下書き)で投稿し、実際のQiita画面での見た目を確認しました。
スクリーンショット 2026-08-14 23.49.03.png

6. Step4: GitHub Actionsで自動化する

6-1. リポジトリ化する

git init
echo -e "venv/\n__pycache__/\narticle.md" > .gitignore
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/ユーザー名/qiita-github-trend.git
git push -u origin main

6-2. Secretsを登録する

リポジトリの Settings > Secrets and variables > Actions から以下の3つを登録します。

Secret名 用途
GH_SEARCH_TOKEN GitHub Search API用の個人アクセストークン
GEMINI_API_KEY Gemini APIキー
QIITA_TOKEN Qiitaのアクセストークン

スクリーンショット 2026-08-15 0.18.49.png

GITHUB_TOKENという名前は使えない
GitHub Search API用のトークンをGITHUB_TOKENに設定しようとしたが失敗。これはActions内で自動的に予約されている変数名のため、GH_SEARCH_TOKENのような別名で登録する必要があります。

依存パッケージはrequirements.txtにまとめておきます。

pip freeze > requirements.txt

6-3. ワークフローファイルを作成する

mkdir -p .github/workflows
.github/workflows/weekly-post.yml
name: Weekly GitHub Trend Post
 
on:
  schedule:
    - cron: "0 0 * * 1"  # 毎週月曜 9:00 JST (UTC 0:00)
  workflow_dispatch:
 
jobs:
  post-article:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - env:
          GH_SEARCH_TOKEN: ${{ secrets.GH_SEARCH_TOKEN }}
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          QIITA_TOKEN: ${{ secrets.QIITA_TOKEN }}
        run: python generate_article.py
git add .
git commit -m "Add weekly GitHub Actions workflow"
git push

6-4. 手動実行で動作確認する

Actionsタブ → ワークフロー選択 → 「Run workflow」ボタンで、cronを待たずに即座に動作確認できます。

image.png

内容に問題がなければprivate=Falseに切り替え、公開投稿に移行します。

ハマった①: ヘッダーに不可視文字が混入

実行結果
requests.exceptions.InvalidHeader: Invalid leading whitespace, reserved character(s), or return character(s) in header value

トークンをコピー&ペーストする過程で、末尾に見えない改行コードが混入していたのが原因でした。環境変数を読み込む際に.strip()を挟むことで解決しています。

"Authorization": f"Bearer {os.environ['GH_SEARCH_TOKEN'].strip()}",

ハマった②: 本番投稿直後にQiita APIの502エラー
private: falseに切り替えた直後、Qiita側の一時的なサーバーエラー(502 Bad Gateway)に遭遇しました。502/503/504のときだけリトライする処理を追加しています。

for attempt in range(max_retries):
    res = requests.post(url, headers=headers, json=payload)
    if res.status_code in (502, 503, 504) and attempt < max_retries - 1:
        time.sleep(10 * (attempt + 1))
        continue
    res.raise_for_status()
    return res.json()

6-5. 公開は必ず人の目を通してから

当初はここまでの流れの通り、private=Falseで自動投稿し、生成から公開まで完全に無人で完結させる設計にしていました。ですがQiitaのコミュニティガイドラインを確認したところ、AIを利用した投稿であっても内容の正確性などについて執筆者が責任を持つ旨が明記されていることに気づき、この設計を見直しました。

Qiitaコミュニティガイドライン「☝ AIが生成した内容は正確性を確かめよう」より引用

記事等の執筆に便利なAIツールを活用することそのものは禁止していませんが、AIが生成した内容は、そのまま投稿するのではなく、正しいかどうかを検証した上で投稿するようにしましょう。(中略)内容に責任をもって投稿しましょう。

このガイドラインを踏まえ、GitHub Actionsからの自動投稿は常に限定共有(private: true)で行い、実際に内容を確認してから手動で公開に切り替える運用に変更しました。

generate_article.py
result = post_to_qiita(
    title=title,
    body=article,
    tags=build_tags(repos_with_summary),
    private=True,  # 自動投稿は常に限定共有。公開は確認後に手動で切り替える
)

限定共有のまま投稿された記事は、Qiitaの管理画面から内容を確認し、問題なければ編集画面の公開設定を「全体に公開」へ変更して手動で公開するよう変更しています。

自動化はあくまで「下書きの生成と一次確認までの効率化」に留め、最終的な公開判断は必ず人の目を通す形にしています。

7. 実際に動かしてみて

実際に自動投稿された記事がこちらです。

今後の改善余地

  • スター数の「増分」ではなく「累計」ベースのため、真の意味での「今週の勢い」とは若干異なる
  • AI要約の精度は概ね良好だが、専門性の高いリポジトリ(低レイヤ・研究系)ではやや説明が浅くなる傾向

8. まとめ

「ただAPIでデータを取得して並べるだけ」ではなく、AIに人気の理由や実務でのメリットを生成させることで、読んだ人が利用イメージを持ちやすい記事にできたのではないかと思います。GitHub Modelsの突然の廃止やGemini無料枠の制限など想定外の壁もありましたが、結果的に手順としては再現しやすい形にまとめられました。

まだ運用を始めたばかりなので、「こういう項目があると読みやすい」「こんなフィルタ条件があると導入しやすそう」といったご意見があれば、ぜひコメントで教えてください。

ソースコードは以下のリポジトリで公開しています。気になった方は覗いてみてください。

お知らせ

技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!

4
2
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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?