はじめに
「毎日手動で更新し続けるのは無理だ」
海外テックメディアを日本語で届けるサービスを作ろうと決めたとき、一番最初にぶつかった壁がこれでした。
毎朝RSSを手動で確認して、AIに貼り付けて、翻訳して、サイトに投稿する。これを毎日やるのは現実的じゃない。
そこで構築したのが、GitHub Actionsで全自動化されたパイプラインです。
朝起きたら新しい記事が承認待ちになっている。それだけを確認して承認ボタンを押せばいい。そういう状態を作ることが目標でした。
この記事では、その仕組みと、構築中にハマったポイントを紹介します。
作ったもの
Mirai Signal — 海外AI・ロボティクス・半導体などの最先端技術情報を毎日自動収集・日本語化して届けるメディアです。
技術スタックはこちら。
- フロントエンド:Next.js(App Router)+ TypeScript
- データベース:Supabase
- AI処理:Gemini API(gemini-2.5-flash-lite)
- メール配信:Resend
- デプロイ:Vercel
- 自動化:GitHub Actions(←この記事のメインテーマ)
パイプラインの全体像
毎朝JST 7:00 に以下が自動で動きます。
GitHub Actions(毎朝7時に起動)
↓
save-articles.ts(RSS収集)
↓
process-articles.ts(Gemini APIでスコアリング・翻訳)
↓
send-newsletter.ts(ニュースレター配信)
↓
管理画面で手動承認
↓
サイトに公開
GitHub Actionsの設定
実際のワークフローファイルがこちらです。
name: Daily Pipeline
on:
schedule:
- cron: '0 22 * * *' # UTC 22:00 = JST 7:00
workflow_dispatch: # 手動実行も可能
jobs:
pipeline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Run pipeline
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
run: |
npx tsx scripts/save-articles.ts
npx tsx scripts/process-articles.ts
npx tsx scripts/send-newsletter.ts
シンプルに見えますが、ここにたどり着くまでにかなりハマりました。
ポイント:cronの書き方
GitHub ActionsのスケジュールはUTC基準です。JST 7:00に実行したい場合は'0 22 * * *'と書きます(JSTはUTC+9なので、7-9=-2、つまり前日の22:00)。
ポイント:環境変数の渡し方
APIキーなどの秘密情報はGitHub SecretsにセットしておいてYAMLで参照します。${{ secrets.変数名 }}という形式で使えます。
セットの手順:
- GitHubのリポジトリページを開く
- Settings → Secrets and variables → Actions
- 「New repository secret」から追加
ポイント:TypeScriptスクリプトの実行
npx tsxを使うとTypeScriptのスクリプトをコンパイルなしで直接実行できます。Node.jsプロジェクトなのでnpm ciで依存パッケージをインストールしてから実行します。
ハマったポイント4選
1. YAMLの構文エラーが見えないところにあった
一番長く詰まったのがこれです。
PowerShellでYAMLファイルを直接編集したところ、見えない文字や改行コード(CRLF)の問題が混入してしまいました。特にrun: |のブロック内で起きやすく、何日もワークフローが失敗し続けました。
解決策:YAMLファイルを直接編集するのをやめて、Node.jsスクリプトでファイルを書き出す方式に切り替えました。PowerShellは日本語や特殊文字の扱いが独特で、ファイルを直接触ると文字化けや改行コードの問題が起きやすいです。
// write-workflow.js
const fs = require('fs');
const yaml = `name: Daily Pipeline
on:
schedule:
- cron: '0 22 * * *'
...`;
fs.writeFileSync('.github/workflows/daily-pipeline.yml', yaml, 'utf8');
2. 環境変数を設定したのに渡っていなかった
GitHub SecretsにAPIキーを登録したのに、スクリプトが「Missing API key」エラーで落ちる。原因が全然分からず数日悩みました。
原因はシンプルで、YAMLのenv:セクションに記載するのを忘れていただけでした。
# NG:envセクションにRESEND_API_KEYがない
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
# RESEND_API_KEY を書き忘れ!
# OK:全部ちゃんと書く
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
GitHub SecretsはあくまでGitHubが管理している変数で、YAMLでenv:に書いて初めてスクリプトから参照できます。登録しただけでは使えません。
3. pushトリガーと勘違いしていた
コードをpushするたびにActionsのページに新しい実行ログが表示されるので、「あれ、スケジュール実行されているのかな?」と思っていました。
でも実態は違いました。表示されていたのはスケジュール実行の失敗ログで、pushによって実行されていたわけではありませんでした。
GitHubのActionsタブでは、失敗したワークフローは赤いアイコンで残り続けます。新しく何かが動いたように見えても、それが何のトリガーで動いたのかをちゃんと確認することが重要です。
4. 「Re-run failed jobs」では直らなかった
ワークフローファイルを修正してpushした後、失敗していた古いワークフローのページにある「Re-run failed jobs」ボタンを押していました。でも何度やっても同じエラーになる。
原因は、「Re-run failed jobs」は修正前の古いワークフローファイルで再実行するからです。ワークフローファイルを変更した場合、新しいpushで実行されたワークフローを確認する必要があります。
実際に動かしてみた結果
構築から約1ヶ月、毎日安定して動き続けています。
- 毎朝7時に自動で起動
- 12ソースのRSSから記事を収集
- Gemini APIでスコアリング・翻訳
- 承認待ち記事が管理画面に並ぶ
朝起きて管理画面を開くと、前日夜から集まった記事が並んでいます。それを確認して承認するだけ。運営にかかる時間は1日数分になりました。
GitHub Actionsのログ画面で実行状況もリアルタイムで確認できるので、何か問題が起きたときもすぐ気づけます。
まとめ
GitHub Actionsで自動化したことで、手動で毎日更新するという無理ゲーが現実的な運営に変わりました。
ハマったポイントをまとめると:
- PowerShellで直接YAMLを編集すると改行コード問題が起きやすい → Node.jsスクリプト経由で書き出す
- GitHub Secretsに登録するだけでは不十分 → YAMLの
env:セクションにも書く - Actionsのログはどのトリガーかをよく確認する
- ワークフロー修正後は「Re-run failed jobs」ではなく新しいpushを確認する
個人開発でも自動化を入れると継続のハードルが一気に下がります。同じようなことをやろうとしている方の参考になれば嬉しいです。
前回の記事:
- コーディング未経験の大学1年生がNext.js+Supabase+Gemini APIで海外テックメディアを1ヶ月で作った話
- Gemini APIで海外ニュースを自動スコアリング・翻訳する仕組みを作った話
X(Twitter)でも毎日情報発信しています。→ @MqS_quest