🌸

GitHub PagesでPRごとにサイトのプレビューをできるようにActionsを設定する

2024/08/09に公開

やりたいこと

GitHub Pagesの公開元リポジトリで、PRを作成したら自動でプレビューサイトをdeployしてほしい!

GitHub PagesでのWebサイト構築でもステージング環境が欲しくなったので、自分なりに調べたり試してみた結果をまとめてみたいと思います。なお、参考サイト様に大いに助けられております。

無理やり感は否めませんが、結果的に目的は達成できたので参考になれば幸いです。ツッコミもお待ちしています!

前提

  • 筆者はGitHub Pages/Actions初心者です
  • GitHub Pagesを使用してWebサイトを構築しています
    参考)GitHub Pages について - GitHub Docs
  • WebサイトはNext.JSで作成し、パッケージマネージャーはbunを使用していますが、他の構成でも同じようなフローで作成できると思われます(未検証)

概要

今回作成したフローとブランチ構成は図のようになります:

ブランチ構成

このワークフローを適用するためには、特定の役割をもつブランチを2つ用意する必要があります。その他ブランチと合わせて3種類のブランチに分けられます。

  1. main: 仮想的な本番環境ブランチ
    ふつうにexample.github.ioにアクセスしたときに見えるページのソース。
    デフォルトのブランチにすると見通しが良いと思います。今回はデフォルトブランチとして話を進めます。
  2. gh-pages: デプロイのためだけのブランチ
    .github/workflows/以外はビルド後のソースが入る。mainではなくこちらをデプロイする。
  3. feature1,feature2,...: その他の普通のブランチ
    ↑これらのブランチからPRを作成したとき、ステージング環境としてデプロイしたい

main,gh-pagesは、基本的に名前を固定する前提です(この2つでなくてもOK)。具体的には、この名前をワークフローファイルに直接書きます。

ワークフロー

ワークフローも3種類作成しました。やっていることは以下のとおりです。

  1. preview.yml: PRが作成されたら、自動でプレビューをデプロイ
  2. deploy.yml: PRがマージされたら、更新されたソースをデプロイしてWebサイトを更新
  3. gh-pages.yml: gh-pagesをデプロイするためだけのワークフロー(練習のため書きましたが、Settingsを変更すればいらなくなるはずです。詳しくは補足を参照)

Next.JS側の設定

まず、next.config.mjsに以下のような設定を追加します。

next.config.mjs
const isPreview = process.env.IS_PREVIEW === "true";

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "export",
  // Configure basePath and assetPrefix to support preview deployments
  basePath: isPreview ? `/pr-preview-${process.env.PR_NUMBER}` : "",
  assetPrefix: isPreview ? `/pr-preview-${process.env.PR_NUMBER}/` : "",
};

// Export the configuration
export default nextConfig;

basePath, assetPathについての解説は公式ドキュメントに譲ります。

補足

お忙ぎの方は読み飛ばしてOKです!

今回はActionsのワークフローを書く練習として、単なるブランチgh-pagesからデプロイするワークフローgh-pages.ymlを書きました。ただ、普通はそんなことしなくとも以下のようにすればgh-pages.ymlは不要になります(検証済)

今回のSettings>Pages
リポジトリのSettings>Pages 今回のはSourceを"Actions"にしていますが……

本来はこうすれば事足りるはず
本来はSourceを"gh-pages"(デプロイ元ブランチ)にすれば事足りるはず

ワークフローの解説

1. preview.yml

ソース全体はこちら:

https://github.com/cherr0406/cherr0406.github.io/blob/main/.github/workflows/preview.yml

要所を解説します。

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

ここでは、ワークフローの対象となるトリガーイベントを設定します。

env:
  PAGE_DIR: pr-preview-${{ github.event.pull_request.number }}

gh-pagesブランチ内で、プレビューサイトを格納するディレクトリ名を環境変数として設定します。

jobs:
  build-and-commit:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

        ......
        
      - name: Build with Next.js
        run: |
          export IS_PREVIEW=true
          export PR_NUMBER=${{ github.event.pull_request.number }}
          bunx next build

ここまではNext.JSのビルド+αだけです。
+αは環境変数の追加です。IS_PREVIEWはプレビューサイトか否か、PR_NUMBERはそのままPRのIDです。どちらもnext.config.mjsで参照する必要があるので、ビルド前にexportしておきます。

      - name: Move out directory
        run: mv out /tmp/out
      - name: Checkout gh-pages
        uses: actions/checkout@v4
        with:
          ref: gh-pages
      - name: Commit changes to gh-pages branch
        run: |
          git config --global user.name "GitHub Actions"
          git config --global user.email "action@github.com"
          cp -r /tmp/out/* ${{ env.PAGE_DIR }}
          git add .
          git commit -m "Deploy PR preview for #${{ github.event.pull_request.number }}"
          git push origin gh-pages

まず、ビルドしたソースが格納されているoutを、ホームディレクトリ以外の場所tmp/outなどに移動しておきます。
そうしたら、gh-pagesブランチにチェックアウトして、tmp/outの中身をPAGE_DIRで設定したディレクトリに入るようにします。ここはcpでもmvでも問題ないと思います。

変更をコミットし、プッシュします。
このpushも、gh-pages.ymlのトリガーにもなっているので、自動でデプロイされます。

また、git config --global ...の2行は単にコミットするユーザーをbotに設定しているだけです。Actionsで自動化した作業は、actions-userくんがやったことにして分かりやすくしています。

      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `Preview deployed to: https://${context.repo.owner}.github.io/${process.env.PAGE_DIR}`
            })

PRに、プレビューサイトのURLをコメントとして残しておきます。
PRのページを開くと一番下にこのコメントが来て、ワンクリックで飛べるようになっています。

  cleanup:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout gh-pages
        uses: actions/checkout@v4
        with:
          ref: gh-pages
      - name: Remove preview directory
        run: |
          rm -rf ${{ env.PAGE_DIR }} || exit 0
          git config user.name github-actions
          git config user.email github-actions@github.com
          git add .
          git commit -m "Remove PR preview for #${{ github.event.pull_request.number }}"
          git push origin gh-pages

PRがクローズされたら、プレビューを消したコミットをgh-pagesにプッシュします。

2. deploy.yml

ソース全体はこちら:

https://github.com/cherr0406/cherr0406.github.io/blob/main/.github/workflows/deploy.yml

こちらも要所を解説します。

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

こちらのトリガーは、mainブランチへのpush(commit)です。

jobs:
  build-and-commit:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout main
        uses: actions/checkout@v4

        ......
        
      - name: Build with Next.js
        run: bunx next build

preview.yml同様、ビルドします。プレビューではないので、環境変数は設定しません。

      - name: Move out directory
        run: mv out /tmp/out
      - name: Checkout gh-pages
        uses: actions/checkout@v4
        with:
          ref: gh-pages
          fetch-depth: 0

ここもpreview.yml同様、outディレクトリを退避させ、gh-pagesブランチをチェックアウトします。

      - name: Preserve specific directories and clear other files
        run: |
          mkdir -p /tmp/preserve
          find . -maxdepth 1 -type d -name 'pr-preview-*' -exec cp -r {} /tmp/preserve/ \;
          if [ -d .github/workflows ]; then
            mkdir -p /tmp/preserve/.github
            cp -r .github/workflows /tmp/preserve/.github/
          fi

続いて、pr-preview-*ディレクトリ、.githubディレクトリを退避しています。ここでは/tmp/preserve/に入れています。

          git rm -rf .
          git clean -fxd
          shopt -s dotglob nullglob
          mv /tmp/preserve/* . || true
          shopt -u dotglob nullglob

一旦現在のブランチにあるファイルを削除します。これらは前のmainブランチのビルド結果だからですね。その後、/tmp/preserve/以下を戻します。

      - name: Copy new build
        run: |
          cp -r /tmp/out/* .
          touch .nojekyll

その後、新しくビルドされたファイル群をブランチに持ってきます。

      - name: Commit changes to gh-pages branch
        run: |
          git config --global user.name "GitHub Actions"
          git config --global user.email "action@github.com"
          git add -N .
          set +e
          git diff --exit-code --quiet
          if [[ $? -eq 1 ]]; then
            git add .
            git commit -m "Deploy to gh-pages: ${{ github.sha }}"
            git push origin gh-pages
          fi

3. gh-pages.yml

ほぼほぼサンプル(Next.JS)のdeploy部分のコピーなので解説はほとんど省略しますが、1点だけ。

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

cancel-in-progresstrueにすることで、前のデプロイが終わらないまま次のデプロイジョブが渡された場合に前のデプロイをキャンセルできます。

私の運用の場合、mainにPRをマージした際に、deploy.ymlによるプッシュと、PRのクローズをトリガーとしたpreview.ymlによるph-pagesへのプッシュがバッティングすることがほとんどです。
バッティング自体をなくすのは一旦後回しにして、バッティングした場合に古いデプロイを待つ時間を短縮しました。

ソース全体はこちら:

https://github.com/cherr0406/cherr0406.github.io/blob/main/.github/workflows/gh-pages.yml

使ってみる

既存のリポジトリに適用してみます。

まずはじめに、gh-pagesブランチは手動でセットアップしておく必要があります。

  • gh-pagesブランチを作成し、リモート上に発行
  • gh-pages.ymlを使う場合、.github/workflows/gh-pages.ymlとして作成しておく

また、設定の変更もしておきます。

リポジトリのSettingsタブで、Environment>Github pagesを以下のように設定しておきます。

Environment/Github pages
gh-pagesブランチ(デプロイ元)を許可しておく

それでは、mainから適当なブランチを切り分けて、コミットを作成したらPRを作ってみます。
今回はZennのリンクを追加するためのブランチ・コミットを作成しました。

Actionsが動いて、成功すれば以下のようにプレビュー用のURLがコメントされます。

プレビューのURLへ飛んでみると…

変更が反映されていることがわかります。

マージすると…(Actionsのタブから確認できます)

こちらでも変更が確認できますね〜

今後の展望

  • Rulesetまでは作れていないので、デプロイが成功したらマージ可能にするなどの制約をつけたい
  • もう少しスマートな方法が思いついたらやってみたい

参考サイト

大いに参考にさせていただきました。この場を借りて感謝申し上げます。

Discussion