Kuboyage

・ 自動化 / GitHub Actions

GitHub プロフィール README を自動更新!Supabase + GitHub Actions でブログの RSS 風最新記事配信システムを構築 🚀

はじめに

こんにちは!自分のポートフォリオサイト「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 風データフロー詳細

  1. 📅 定期配信: RSS フィードのように毎日日本時間 18 時(UTC 9 時)に GitHub Actions が起動
  2. 📡 記事フィード取得: Supabase から status = 'published' の最新 4 件を RSS 的に取得
  3. 📝 フィード生成: - [タイトル](URL) 形式で記事フィードを生成
  4. 🔄 自動配信: GitHub API でプロフィール README の特定セクションに配信

💡 ポイント: 従来の RSS/XML 形式ではなく、GitHub README の Markdown 形式で配信する現代的なアプローチです!

実装の技術スタック

メイン技術 🛠️

技術用途選定理由
GitHub Actions定期実行・CI/CDGitHub との親和性、無料枠での運用可能
Supabaseデータベース・APIPostgreSQL ベース、使いやすい 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;
  }
}

スクリプトの特徴 🌟

  1. Dynamic Import: ES Module の @octokit/rest を CommonJS 環境で使用
  2. RSS 風エラーハンドリング: フィード取得失敗時の詳細なエラーメッセージとログ出力
  3. マーカーベース配信: 特定セクションのみを安全に更新する RSS 的配信
  4. 環境依存処理: ローカルと GitHub Actions の両環境に対応

ステップ 3: セキュリティ設定と認証

セキュリティは非常に重要です。適切な権限管理を行いましょう。

GitHub Secrets の設定 🔐

プロフィールリポジトリ(Kubbo0308/Kubbo0308)で以下の Secrets を設定:

Secret 名説明権限
PERSONAL_ACCESS_TOKENGitHub API アクセス用 PATrepo, workflow
VITE_SUPABASE_URLSupabase プロジェクト URL読み取り専用
VITE_SUPABASE_ANON_KEYSupabase 匿名キーパブリックアクセス用

セキュリティのベストプラクティス 🛡️

// ❌ 避けるべき - 過大な権限
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 実行結果 🤖

GitHub Actions Success

  • 実行時間: 約 30 秒
  • 成功率: 100%(テスト実行 10 回中 10 回成功)
  • RSS 風配信エラー: なし

見た目としてはこちらのようになってます! READMEのどこを変更するかはcjsファイルで編集できます!

readme.png

今後の拡張可能性

RSS 機能拡張のアイデア 💡

  1. RSS メタデータ表示: 公開日やタグ情報の追加

    - [記事タイトル](URL) `2024-01-01` `#React` `#TypeScript`
    
  2. RSS カテゴリ別配信: 技術記事とライフスタイル記事の分類配信

  3. 通知機能: RSS 更新時の Slack/Discord 通知

  4. RSS 統計情報: 記事数、総ビュー数、配信回数などの表示

  5. 真の RSS フィード対応: XML 形式の RSS フィード生成機能の追加

パフォーマンス改善案 ⚡

  • RSS キャッシュ機能: 記事フィードのキャッシュで API 呼び出し削減
  • 差分配信: 変更がある場合のみ RSS 風配信実行
  • バッチ配信: 複数リポジトリへの一括 RSS 風配信

まとめ

この記事では、GitHub Actions と Supabase を使って GitHub プロフィール README に RSS 風の自動記事配信システムを構築する方法を解説しました。

実装で得られた成果 🎉

  • ✅ RSS 風自動化の達成: 手動更新作業から完全に解放
  • ✅ 統一配信体験: Qiita と MyBlog の同等な RSS 風更新システム
  • ✅ セキュアな実装: 最小権限の原則に基づく認証設計
  • ✅ 保守性: 運用・拡張しやすい RSS 風アーキテクチャ

学んだ重要なポイント 📚

  1. モジュールシステムの理解: CommonJS と ES Modules の混在環境での対処法
  2. GitHub API の活用: Octokit による効率的な GitHub 操作
  3. セキュリティ設計: Personal Access Token の適切な管理
  4. RSS 風配信設計: 伝統的な RSS の概念を現代的にアレンジした実装
  5. エラーハンドリング: 本番環境を想定した堅牢なエラー処理

次のステップ 🚀

この基盤システムを活用して、さらなる RSS 機能の拡張や自動化を進めていく予定です。GitHub プロフィールを動的で魅力的な RSS 配信ポートフォリオとして進化させていきたいと思います!


総コード行数: 約 200 行
使用した API: GitHub REST API v4, Supabase REST API
配信方式: RSS 風自動配信システム

この記事が、同じような RSS 風自動配信の課題を抱えている方の参考になれば嬉しいです。質問やフィードバックがあれば、ぜひ問い合わせやSNS等で教えてくださいね!✨

ブログの記事一覧へ