1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PRが作られたタイミングでGitHub PagesにStorybookをデプロイするCI【GitHub Actions】

Last updated at Posted at 2025-10-14

PRが作られたタイミングでGitHub PagesにStorybookをデプロイするCI【GitHub Actions】

はじめに

お疲れ様です。ふぁるです。

デザインシステムやコンポーネントライブラリを運用していると、「PRでStorybookのプレビューを確認したい」という要望はほぼ必ず出てくるかと思います。

今回は、PRが作られたタイミングで自動的にStorybookをビルドしてGitHub PagesにデプロイするGitHub Actionsワークフローを実装したので、その内容をまとめます。

(VercelやChromaticとの詳しい比較は後述します)

この記事でやること

  • PRごとに独立したStorybookプレビューをGitHub Pagesにデプロイ
  • PRにコメントでプレビューリンクを自動投稿
  • PRがクローズされたら自動でプレビューを削除
  • 古いプレビューの自動クリーンアップ(最新10個のみ保持)

最終的な構成

gh-pages ブランチ
├── main/          # mainブランチの最新Storybook
├── pr-1/          # PR #1のプレビュー
├── pr-2/          # PR #2のプレビュー
└── pr-10/         # PR #10のプレビュー

こんな感じで、PR番号ごとにディレクトリを切ってデプロイします。

実装

1. PRプレビューをデプロイするワークフロー

.github/workflows/pr-storybook-preview.yml を作成します。

name: PR Storybook Preview

on:
  pull_request:
    types: [opened, synchronize, reopened]
    branches: [main]

permissions:
  contents: write
  pull-requests: write

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'yarn'

      - name: Enable Corepack
        run: corepack enable

      - name: Install dependencies
        run: yarn install

      - name: Build Storybook
        run: yarn build-storybook

      # gh-pagesブランチの準備(存在確認と初期化)
      - name: Setup gh-pages branch
        run: |
          # リモートにgh-pagesブランチが存在するか確認
          if git ls-remote --heads origin gh-pages | grep -q gh-pages; then
            echo "gh-pages branch exists. Checking out..."
            git clone --depth=1 --branch gh-pages https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git gh-pages
          else
            echo "gh-pages branch does not exist. Creating..."
            mkdir -p gh-pages
            cd gh-pages
            git init -b gh-pages
            git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
            echo "# Storybook Previews" > README.md
            git config user.name "github-actions[bot]"
            git config user.email "github-actions[bot]@users.noreply.github.com"
            git add README.md
            git commit -m "Initialize gh-pages branch"
            git push -u origin gh-pages
          fi

      # PRディレクトリを作成・更新
      - name: Deploy to PR directory
        run: |
          PR_DIR="gh-pages/pr-${{ github.event.pull_request.number }}"
          rm -rf "$PR_DIR"
          mkdir -p "$PR_DIR"
          cp -r storybook-static/* "$PR_DIR/"

          cd gh-pages
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add .
          git commit -m "Deploy Storybook for PR #${{ github.event.pull_request.number }}" || echo "No changes"
          git push origin gh-pages

      # 古いPRプレビューをクリーンアップ(最新10個のみ保持)
      - name: Cleanup old PR previews
        run: |
          cd gh-pages
          # pr-* ディレクトリを取得し、日付順にソートして10個より古いものを削除
          ls -dt pr-* 2>/dev/null | tail -n +11 | xargs rm -rf || echo "No old previews to clean"
          if [ -n "$(git status --porcelain)" ]; then
            git add .
            git commit -m "Cleanup old PR previews"
            git push origin gh-pages
          fi

      # PRにコメントを追加
      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            const prNumber = context.payload.pull_request.number;
            const url = `https://${context.repo.owner}.github.io/${context.repo.repo}/pr-${prNumber}/`;

            const comment = `## 📚 Storybook Preview

            プレビューが更新されました!

            🔗 **[Storybookを開く](${url})**

            > このプレビューはPRがマージまたはクローズされるまで利用可能です。
            > 最新10個のPRプレビューのみ保持されます。`;

            // 既存のコメントを探す
            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: prNumber,
            });

            const botComment = comments.find(c =>
              c.user.type === 'Bot' && c.body.includes('Storybook Preview')
            );

            if (botComment) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: botComment.id,
                body: comment,
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                body: comment,
              });
            }

2. PRクローズ時のクリーンアップワークフロー

PRがマージまたはクローズされたら、そのプレビューは不要になるので削除します。

.github/workflows/cleanup-pr-preview.yml を作成。

name: Cleanup PR Preview

on:
  pull_request:
    types: [closed]
    branches: [main]

permissions:
  contents: write
  pull-requests: write

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Setup gh-pages branch
        run: |
          # リモートにgh-pagesブランチが存在するか確認
          if git ls-remote --heads origin gh-pages | grep -q gh-pages; then
            git clone --depth=1 --branch gh-pages https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git gh-pages
          else
            echo "gh-pages branch does not exist"
            exit 0
          fi

      - name: Remove PR directory
        run: |
          cd gh-pages
          PR_DIR="pr-${{ github.event.pull_request.number }}"
          if [ -d "$PR_DIR" ]; then
            rm -rf "$PR_DIR"
            git config user.name "github-actions[bot]"
            git config user.email "github-actions[bot]@users.noreply.github.com"
            git add .
            git commit -m "Cleanup Storybook for PR #${{ github.event.pull_request.number }}"
            git push origin gh-pages
          else
            echo "No preview directory found for PR #${{ github.event.pull_request.number }}"
          fi

      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.pull_request.number,
              body: '🧹 Storybook プレビューが削除されました。',
            });

3. mainブランチのStorybookデプロイ(おまけ)

PRプレビューとは別に、mainブランチの最新版もデプロイしておくと便利です。

.github/workflows/deploy-storybook.yml

name: Deploy Storybook to Pages

on:
  push:
    branches: ["main"]
  workflow_dispatch:

permissions:
  contents: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'yarn'

      - name: Enable Corepack
        run: corepack enable

      - name: Install dependencies
        run: yarn install

      - name: Build Storybook
        run: yarn build-storybook

      - name: Setup gh-pages branch
        run: |
          if git ls-remote --heads origin gh-pages | grep -q gh-pages; then
            echo "gh-pages branch exists. Checking out..."
            git clone --depth=1 --branch gh-pages https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git gh-pages
          else
            echo "gh-pages branch does not exist. Creating..."
            mkdir -p gh-pages
            cd gh-pages
            git init -b gh-pages
            git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
            echo "# Storybook" > README.md
            git config user.name "github-actions[bot]"
            git config user.email "github-actions[bot]@users.noreply.github.com"
            git add README.md
            git commit -m "Initialize gh-pages branch"
            git push -u origin gh-pages
          fi

      - name: Deploy to main directory
        run: |
          cd gh-pages
          rm -rf main
          mkdir -p main
          cp -r ../storybook-static/* main/

          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add .
          git commit -m "Deploy Storybook from main branch" || echo "No changes"
          git push origin gh-pages

実装のポイント解説

gh-pagesブランチの存在確認

if git ls-remote --heads origin gh-pages | grep -q gh-pages; then

最初はこの確認を入れてなくて、初回実行時にエラーになりました。git clone しようとしてもブランチが存在しないから失敗する。

なので、git ls-remote でリモートブランチの存在を確認してから、存在しなければ初期化する処理を入れました。この辺りは試行錯誤の結果です。

PR番号ごとのディレクトリ分け

PR_DIR="gh-pages/pr-${{ github.event.pull_request.number }}"

github.event.pull_request.number でPR番号を取得できるので、これを使ってディレクトリ名を決めています。

古いプレビューの自動削除

ls -dt pr-* 2>/dev/null | tail -n +11 | xargs rm -rf

これがポイントで、ls -dt で日付順にソートして、tail -n +11 で11番目以降(つまり10個より古いもの)を取得して削除しています。

プロジェクトによって環境数は調整してください。

PRコメントの重複防止

const botComment = comments.find(c =>
  c.user.type === 'Bot' && c.body.includes('Storybook Preview')
);

if (botComment) {
  // 既存コメントを更新
  await github.rest.issues.updateComment({...});
} else {
  // 新規コメントを作成
  await github.rest.issues.createComment({...});
}

PRを更新するたびに新しいコメントが追加されると邪魔なので、既存のbotコメントを探して、あれば更新、なければ新規作成という処理にしています。

これで、1つのPRに対して常に1つのStorybookプレビューコメントが表示される状態を保てます。

プライベートリポジトリの場合の注意点

プライベートリポジトリでGitHub Pagesを使う場合、URLの形式が少し特殊になります。

https://<自動生成されたドメイン>.pages.github.io/pr-1/

例:https://barnacle-xxxxxx.pages.github.io/pr-1/

このドメインは、リポジトリのSettings → PagesでGitHub Pagesを有効化すると確認できます。

公開リポジトリの場合は、普通に https://<username>.github.io/<repo>/pr-1/ の形式になります。

実際に動かしてみた

実際にPRを作ってみると、こんな感じでコメントが自動で投稿されます:

## 📚 Storybook Preview

プレビューが更新されました!

🔗 **Storybookを開く**

> このプレビューはPRがマージまたはクローズされるまで利用可能です。
> 最新10個のPRプレビューのみ保持されます。

リンクをクリックすると、そのPR専用のStorybookが開きます。

PRを更新(新しいコミットをプッシュ)すると、コメントも自動で更新されて、最新のプレビューが反映されます。

そして、PRをマージまたはクローズすると、こんなコメントが投稿されます:

🧹 Storybook プレビューが削除されました。

同時に、gh-pagesブランチからもそのPRのディレクトリが削除されます。

ハマりポイント

1. permissions の設定

最初、contents: writepull-requests: write の両方を設定し忘れてて、pushもコメントもできませんでした。

GitHub Actionsのワークフローで外部操作をする場合は、必要な権限をちゃんと明示する必要があります。

2. GITHUB_TOKEN の使い方

git clone https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/...

最初は普通に git clone しようとしてたんですが、認証が必要なので x-access-token: プレフィックスをつけたトークンをURLに埋め込む形式にする必要がありました。

3. Storybookのビルド時間

当たり前ですが、PRを作るたびにStorybookをビルドするので、それなりに時間がかかります。

我々のプロジェクトでは3〜5分くらいかかってるので、大規模なStorybookの場合はキャッシュを工夫するなどの最適化が必要かもしれません。

他のサービスとの比較

さて、冒頭で触れたとおり、Storybookのプレビューデプロイにはいくつかのサービスがあります。主要なものと比較してみましょう。

Chromatic vs GitHub Pages

Chromaticは、Storybookの開発元が提供する公式のビジュアルテストツールです。

Chromaticの良いところ:

  • ビジュアルテスト機能: これが最大の強み。UIの変更を自動で検出してくれる
  • 公式ツール: Storybookとの統合が完璧
  • 高速: CDN配信で爆速
  • UIレビュー機能: PRごとにUIの変更を可視化してレビューできる
  • セットアップが楽: npmパッケージ入れて、トークン設定するだけ

Chromaticのつらいところ:

  • 無料枠が少ない: Free planだと月5,000スナップショットまで
    • 中規模のStorybookだと、すぐに使い切ってしまう
    • Storiesが100個あって、50PRあったら... もう無理
  • 有料プランが高い: 月$149〜等、個人プロジェクトや小規模チームには厳しい
  • 外部サービス依存: 会社のセキュリティポリシーで使えないケースも
  • Closedにする場合の認証関連: GitHubアカウントでの認証にした場合、非エンジニアのレイアウトRevを受けるために権限整備の必要が生じる

GitHub Pagesの良いところ:

  • 完全無料: 容量制限(1GB)はあるけど、課金は不要
  • GitHub完結: 外部サービスなし、セキュリティポリシーもクリアしやすい
  • 自由度が高い: ワークフローを自分好みにカスタマイズできる
  • 制限が緩い: スナップショット数の制限がない

GitHub Pagesのつらいところ:

  • ビジュアルテストなし: 単純にデプロイするだけ
  • セットアップが面倒: ワークフローを自分で書く必要がある
  • 遅い: ビルドに数分かかる(Chromaticはキャッシュが効くので速い)

結論:
ビジュアルテストが必要なら→ Chromatic一択です。UIの変更を自動で検出してくれる機能は、非常に強力です。

ただ、「単純にStorybookをプレビューしたいだけ」なら→ GitHub Pagesで十分だと思います。

Vercel vs GitHub Pages

Vercelは、Next.jsの開発元が提供するホスティングサービス。Storybookのデプロイにも使えます。

Vercelの良いところ:

  • 高速: ビルドもデプロイも速い
  • プレビューURL自動生成: PRごとに自動でURLが発行される
  • 使いやすいUI: ダッシュボードが見やすい
  • カスタムドメイン: 簡単に独自ドメインを設定できる

Vercelのつらいところ:

  • 無料枠の制限: Hobby planだとプライベートリポジトリは1つまで
    • 複数のプライベートプロジェクトがあると詰む
  • 商用利用不可: Hobby planは商用利用できない(個人プロジェクトのみ)
  • 有料プランが必要: Pro planは月$20〜、チームは$40/月

GitHub Pagesの良いところ(再掲):

  • 完全無料
  • GitHub完結
  • プライベートリポジトリ無制限

結論:
Next.jsアプリ全体をデプロイするなら→ Vercel最高です。

でも、Storybookだけなら→ わざわざVercel使わなくてもGitHub Pagesで十分じゃないかな、と。

比較表

Chromatic Vercel GitHub Pages
料金 無料枠あり(5,000スナップショット/月)
有料: $149/月〜
無料枠あり(Hobby plan)
有料: $20/月〜
完全無料(1GBまで)
ビジュアルテスト ✅ あり ❌ なし ❌ なし
セットアップ 簡単(5分) 簡単(5分) やや面倒(30分)
ビルド速度 速い(キャッシュ効く) 速い 普通(3〜5分)
外部サービス 必要 必要 不要
プライベートリポジトリ 制限あり 制限あり(Hobbyは1つ) 無制限
商用利用 可能(有料) 不可(Hobbyは個人のみ) 可能
カスタマイズ性 低い 高い

まとめ

GitHub ActionsとGitHub Pagesを使って、PRごとのStorybookプレビューを実現しました。

メリット:

  • 外部サービス不要、GitHub完結
  • 無料(GitHub Pagesの容量制限内であれば)
  • PRレビューがしやすくなる

デメリット:

  • ビルド時間がかかる
  • GitHub Pagesの容量制限に注意(1GBまで)
  • プライベートリポジトリだとURLが特殊
  • ビジュアルテスト機能はない(Chromaticにはある)

VercelやChromaticなどの専用サービスに比べるとちょっと手間はかかりますが、「単純にStorybookをプレビューしたいだけ」なら、GitHub Pagesでも十分実用的なプレビュー環境が作れることが分かりました。

ビジュアルテストが必要な場合はChromaticを、Next.jsアプリ全体をデプロイするならVercelを、Storybookのプレビューだけなら無料で使えるGitHub Pagesを、という感じで使い分けるのが良さそうです。

コンポーネントライブラリやデザインシステムを運用している方の参考になれば嬉しいです。

参考

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?