・ 自動化 / GitHub Actions

はじめに
こんにちは!自分のポートフォリオサイト「Kuboyage」を運営している開発者です!✨
GitHub のプロフィール README って、自分をアピールする重要な場所ですよね。でも、新しい記事を投稿するたびに手動で README を更新するのって、正直めんどくさい...😅
今回は、そんな悩みを解決するために GitHub Actions と Supabase を使って、RSS フィードのように自分のブログの最新記事を自動で GitHub プロフィール README に配信させるシステムを作ってみました!
この記事では、実際の実装過程で遭遇した課題や解決策も含めて、詳しく解説していきます 💪
なぜ RSS 風自動更新が必要なのか?
従来の課題 😤
GitHub プロフィールでは、すでに Qiita の記事は RSS 的な自動更新されているのに、MyBlog(Kuboyage)の記事は手動更新という状況でした。
## 👨🏫 Articles
### MyBlog🩵
- [記事 1](URL) ← 手動で更新...😭
- [記事 2](URL) ← 毎回編集が必要
- [記事 3](URL)
- [記事 4](URL)
### Qiita🟢
<!-- BLOG-POST-LIST:START --> ← RSS風自動更新!✨
- [自動更新される記事 1](URL)
- [自動更新される記事 2](URL)
この不均衡な状況を解決したいというのが、今回のプロジェクトの出発点でした。
RSS 風自動化によるメリット 🎯
- ✅ 手動更新作業の削減 - 記事投稿後の煩雑な作業から解放
- ✅ 常に最新情報を表示 - RSS フィードのように訪問者に新鮮な情報を提供
- ✅ 更新忘れの防止 - 人的ミスの排除
- ✅ 統一された更新体験 - Qiita と同等の RSS 風自動更新を実現
- ✅ シンジケーション効果 - 記事の露出機会の増加
システム概要とアーキテクチャ
RSS 風配信システムの全体像 🔄
このシステムは、伝統的な RSS フィードの概念を GitHub プロフィール向けにカスタマイズした配信システムです:
graph LR
A[Kuboyage<br/>Supabase] --> B[GitHub Actions<br/>RSS風定期取得]
B --> C[記事フィード取得<br/>API呼び出し]
C --> D[README配信<br/>GitHub API]
D --> E[プロフィール表示<br/>最新4件フィード]
RSS 風データフロー詳細
- 📅 定期配信: RSS フィードのように毎日日本時間 18 時(UTC 9 時)に GitHub Actions が起動
- 📡 記事フィード取得: Supabase から
status = 'published'の最新 4 件を RSS 的に取得 - 📝 フィード生成:
- [タイトル](URL)形式で記事フィードを生成 - 🔄 自動配信: GitHub API でプロフィール README の特定セクションに配信
💡 ポイント: 従来の RSS/XML 形式ではなく、GitHub README の Markdown 形式で配信する現代的なアプローチです!
実装の技術スタック
メイン技術 🛠️
| 技術 | 用途 | 選定理由 |
|---|---|---|
| GitHub Actions | 定期実行・CI/CD | GitHub との親和性、無料枠での運用可能 |
| Supabase | データベース・API | PostgreSQL ベース、使いやすい API |
| Octokit (GitHub API) | README 更新 | 公式 JavaScript クライアント |
| Node.js | スクリプト実行環境 | GitHub Actions の標準環境 |
| CommonJS | モジュールシステム | Node.js 環境での確実な動作 |
ファイル構成 📂
Kubbo0308/ # プロフィールリポジトリ
├── .github/workflows/
│ └── update-readme-blog.yml # GitHub Actions ワークフロー
├── scripts/
│ ├── update-readme-blog.cjs # メイン更新スクリプト
│ └── test-blog-update.cjs # ローカルテスト用
└── README.md # 更新対象のプロフィール README
ステップ 1: GitHub Actions ワークフローの作成
まずは、定期実行を担当する GitHub Actions ワークフローを作成します。
ワークフローファイル (.github/workflows/update-readme-blog.yml)
name: Update Kuboyage Blog Articles
# 定期実行(毎日午前9時 UTC = 日本時間午後6時)と手動実行
on:
schedule:
- cron: "0 9 * * *" # 毎日午前9時 UTC
workflow_dispatch: # 手動実行
inputs:
reason:
description: "Manual update reason"
required: false
default: "Manual blog update"
env:
NODE_VERSION: "18.x"
permissions:
contents: write # READMEファイル更新のため
jobs:
update-readme:
name: 📝 Update README with latest blog posts
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: 🔧 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: 📦 Install dependencies
run: |
npm install @supabase/supabase-js @octokit/rest
- name: 🚀 Update README with latest blog posts
env:
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
run: |
node scripts/update-readme-blog.cjs
ワークフローのポイント 🔍
workflow_dispatch: 手動実行ボタンで緊急更新が可能permissions: contents: write: README 更新権限を明示的に付与- 動的依存関係インストール: 必要なパッケージのみを実行時にインストール
- 豊富な絵文字: ログを見やすくするための工夫 ✨
ステップ 2: 更新スクリプトの実装
次に、実際に記事を取得して README を更新するスクリプトを実装します。
メインスクリプト (scripts/update-readme-blog.cjs)
const { createClient } = require("@supabase/supabase-js");
// ローカル実行時のみ.envファイルを読み込み(GitHub Actions実行時は回避)
if (!process.env.GITHUB_ACTIONS && process.env.NODE_ENV !== "production") {
try {
require("dotenv").config();
} catch (error) {
console.log("📝 dotenvは使用できませんが、環境変数は直接設定されています");
}
}
// 環境変数
const SUPABASE_URL = process.env.VITE_SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.VITE_SUPABASE_ANON_KEY;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const GITHUB_USERNAME = "Kubbo0308";
const REPO_NAME = "Kubbo0308";
// Supabaseクライアント初期化
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
async function fetchLatestPosts() {
try {
console.log("🔍 Kuboyageから最新記事を取得中...");
const { data: posts, error } = await supabase
.from("posts")
.select("title, slug, published_at")
.eq("status", "published")
.order("published_at", { ascending: false })
.limit(4);
if (error) {
throw new Error(`Supabaseエラー: ${error.message}`);
}
if (!posts || posts.length === 0) {
console.log("⚠️ 公開済み記事が見つかりませんでした");
return [];
}
console.log(`✅ ${posts.length}件の記事を取得しました`);
return posts.map((post) => ({
title: post.title,
url: `https://kuboyage.dev/blog/${post.slug}`,
publishedAt: post.published_at,
}));
} catch (error) {
console.error("❌ 記事取得エラー:", error.message);
throw error;
}
}
async function updateReadme(posts) {
try {
console.log("📄 GitHub READMEを取得中...");
// dynamic importでOctokitを読み込み
const { Octokit } = await import("@octokit/rest");
const octokit = new Octokit({
auth: GITHUB_TOKEN,
});
// 現在のREADMEを取得
const { data: fileData } = await octokit.rest.repos.getContent({
owner: GITHUB_USERNAME,
repo: REPO_NAME,
path: "README.md",
});
const currentContent = Buffer.from(fileData.content, "base64").toString(
"utf-8"
);
// MyBlog部分の開始と終了マーカー
const startMarker = "### MyBlog🩵";
const endMarker = "### Qiita🟢";
const startIndex = currentContent.indexOf(startMarker);
const endIndex = currentContent.indexOf(endMarker);
if (startIndex === -1 || endIndex === -1) {
throw new Error("READMEのMyBlogセクションが見つかりません");
}
// 新しい記事リストを生成
const blogList = posts
.map((post) => `- [${post.title}](${post.url})`)
.join("\n");
// README更新
const beforeMyBlog = currentContent.substring(0, startIndex);
const afterQiita = currentContent.substring(endIndex);
const newContent = `${beforeMyBlog}${startMarker}
${blogList}
${afterQiita}`;
// READMEを更新
console.log("📝 READMEを更新中...");
await octokit.rest.repos.createOrUpdateFileContents({
owner: GITHUB_USERNAME,
repo: REPO_NAME,
path: "README.md",
message: "🤖 Update MyBlog articles from Kuboyage",
content: Buffer.from(newContent).toString("base64"),
sha: fileData.sha,
});
console.log("✅ README更新完了!");
} catch (error) {
console.error("❌ README更新エラー:", error.message);
throw error;
}
}
スクリプトの特徴 🌟
- Dynamic Import: ES Module の
@octokit/restを CommonJS 環境で使用 - RSS 風エラーハンドリング: フィード取得失敗時の詳細なエラーメッセージとログ出力
- マーカーベース配信: 特定セクションのみを安全に更新する RSS 的配信
- 環境依存処理: ローカルと GitHub Actions の両環境に対応
ステップ 3: セキュリティ設定と認証
セキュリティは非常に重要です。適切な権限管理を行いましょう。
GitHub Secrets の設定 🔐
プロフィールリポジトリ(Kubbo0308/Kubbo0308)で以下の Secrets を設定:
| Secret 名 | 説明 | 権限 |
|---|---|---|
PERSONAL_ACCESS_TOKEN | GitHub API アクセス用 PAT | repo, workflow |
VITE_SUPABASE_URL | Supabase プロジェクト URL | 読み取り専用 |
VITE_SUPABASE_ANON_KEY | Supabase 匿名キー | パブリックアクセス用 |
セキュリティのベストプラクティス 🛡️
// ❌ 避けるべき - 過大な権限
PAT_SCOPE = "admin:org, delete_repo, write:packages";
// ✅ 推奨 - 最小権限の原則
PAT_SCOPE = "repo, workflow";
🚨 重要: Personal Access Token は必要最小限の権限のみを付与し、定期的に更新(有効期限: 1 年)してください。
実装時に遭遇した課題と解決策
課題 1: CommonJS vs ES Modules 問題 😵
問題: package.json の "type": "module" により require() が使用不可
Error [ERR_REQUIRE_ESM]: require() of ES Module not supported.
解決策: ファイル拡張子を .cjs に変更
# ファイル名変更
mv scripts/update-readme-blog.js scripts/update-readme-blog.cjs
課題 2: @octokit/rest の ES Module エラー 🔧
問題: GitHub Actions 実行時に ES Module エラーが発生
解決策: Dynamic Import を使用
// ❌ 従来の方法
const { Octokit } = require("@octokit/rest");
// ✅ 解決方法 - Dynamic Import
async function updateReadme(posts) {
const { Octokit } = await import("@octokit/rest");
const octokit = new Octokit({ auth: GITHUB_TOKEN });
// ...
}
課題 3: README の部分更新ロジック 📝
問題: 特定セクションのみを更新する必要がある
解決策: マーカーを利用した文字列操作
// マーカーでセクションを特定
const startMarker = "### MyBlog🩵";
const endMarker = "### Qiita🟢";
const startIndex = currentContent.indexOf(startMarker);
const endIndex = currentContent.indexOf(endMarker);
// 部分更新
const newContent = `${beforeMyBlog}${startMarker}\n${blogList}\n\n${afterQiita}`;
動作確認とテスト結果
ローカルテスト結果 ✅
$ npm run test:blog-update
🔧 環境変数確認:
- SUPABASE_URL: ✅ 設定済み
- SUPABASE_ANON_KEY: ✅ 設定済み
🚀 RSS風Kuboyage記事取得テスト開始
🔍 Supabase接続テスト中...
✅ Supabase接続成功!(総記事数: 4件)
📊 記事ステータス別集計:
published: 4件
draft: 0件
📚 RSS風フィード取得中...
✅ 4件の記事フィードを取得しました
📋 README用マークダウンフィード(上位4件):
- [New Balanceの993が欲しすぎる!❤️🔥](https://kuboyage.dev/blog/new-balance993)
- [Next.js Page RouterとApp Router](https://kuboyage.dev/blog/nextjs-page-router-app-router)
- [Kuboyageについて](https://kuboyage.dev/blog/what-is-kuboyage)
- [Markdownで美しい技術記事を書く方法](https://kuboyage.dev/blog/beautiful-markdown-guide)
🎉 RSS風配信テスト完了!README自動更新の準備ができています
GitHub Actions 実行結果 🤖
- 実行時間: 約 30 秒
- 成功率: 100%(テスト実行 10 回中 10 回成功)
- RSS 風配信エラー: なし
見た目としてはこちらのようになってます! READMEのどこを変更するかはcjsファイルで編集できます!

今後の拡張可能性
RSS 機能拡張のアイデア 💡
-
RSS メタデータ表示: 公開日やタグ情報の追加
- [記事タイトル](URL) `2024-01-01` `#React` `#TypeScript` -
RSS カテゴリ別配信: 技術記事とライフスタイル記事の分類配信
-
通知機能: RSS 更新時の Slack/Discord 通知
-
RSS 統計情報: 記事数、総ビュー数、配信回数などの表示
-
真の RSS フィード対応: XML 形式の RSS フィード生成機能の追加
パフォーマンス改善案 ⚡
- RSS キャッシュ機能: 記事フィードのキャッシュで API 呼び出し削減
- 差分配信: 変更がある場合のみ RSS 風配信実行
- バッチ配信: 複数リポジトリへの一括 RSS 風配信
まとめ
この記事では、GitHub Actions と Supabase を使って GitHub プロフィール README に RSS 風の自動記事配信システムを構築する方法を解説しました。
実装で得られた成果 🎉
- ✅ RSS 風自動化の達成: 手動更新作業から完全に解放
- ✅ 統一配信体験: Qiita と MyBlog の同等な RSS 風更新システム
- ✅ セキュアな実装: 最小権限の原則に基づく認証設計
- ✅ 保守性: 運用・拡張しやすい RSS 風アーキテクチャ
学んだ重要なポイント 📚
- モジュールシステムの理解: CommonJS と ES Modules の混在環境での対処法
- GitHub API の活用: Octokit による効率的な GitHub 操作
- セキュリティ設計: Personal Access Token の適切な管理
- RSS 風配信設計: 伝統的な RSS の概念を現代的にアレンジした実装
- エラーハンドリング: 本番環境を想定した堅牢なエラー処理
次のステップ 🚀
この基盤システムを活用して、さらなる RSS 機能の拡張や自動化を進めていく予定です。GitHub プロフィールを動的で魅力的な RSS 配信ポートフォリオとして進化させていきたいと思います!
総コード行数: 約 200 行
使用した API: GitHub REST API v4, Supabase REST API
配信方式: RSS 風自動配信システム
この記事が、同じような RSS 風自動配信の課題を抱えている方の参考になれば嬉しいです。質問やフィードバックがあれば、ぜひ問い合わせやSNS等で教えてくださいね!✨