はじめに
GitHub でのプロジェクト管理において,Storybookを継続的にどこかにデプロイして,ローカルでわざわざビルドする必要を削減したいという欲求があるとします.
例えば,デザイナ側とコーダ側でデザインの擦り合わせをする際,以下の理由から URL から気楽にアクセス可能であると取り回しが良いと考えられます.
- デザイナ側に環境設定を含めて Storybook のビルドをさせるのに負担がある.
- ビルドそのものにそれなりに時間がかかる.
このような用途を目的として,Storybook のメンテナによって提供されているChromaticというサービスが存在しており, これは GitHub と良く統合されます.参考. とはいえ,ちょっと個人/小規模な組織に対して価格,機能の両面ではオーバーすぎるという感が否めません.
本記事では,GitHub Actionsを用いて, Storybook をNetlifyに手動でデプロイする方法について記載します.
成果物,およびリポジトリは以下.
用語について
- Live URL
- 例えば,
storybook-preview.netlify.app
. - 基本的に恒久的に変わらない.
- 例えば,
- Preview URL
- 例えば,
xxxxxxxxxxxxx-storybook-preview.netlify.app
. - 基本的に使い捨ての URL.
- 例えば,
- Storybook Production
- 今回は
main
ブランチに push する度にデプロイされる. - Live URL でアクセス可能.
- 今回ではここにあります
- 今回は
- Storybook Preview
- Pull Request を作成,更新するたびにデプロイされる.
- Preview URL でアクセス可能.
- Heroku の Review Appのようなものを想定.
- 今回ではここにあります
選定理由
デプロイ先としていくつかある中から Netlify が選定した理由を述べます.
今日日のデプロイ先として考えられるサービス,その利点と欠点については大体この記事にまとまっています. 記事内で挙げられているものとしては以下.
これらの他,Surge.shというホスティングサービスもあります.
この内,Cloudflare Pages,Ampllify と Cloud Run は私が触ったことがないので今回は断念しました. それ以外について,採用しなかった理由を列挙すると以下.
- Surge.sh
- CLI からの出力がリッチでパースしずらい.
- この件に関して Issue / PR を投げようか迷ったが,開発自体がかなり停滞していたことから断念.
- CLI からの出力がリッチでパースしずらい.
- Firebase Hosting
- 公式の Actionsにおいて,同一リポジトリで複数サイトを運用するとプレビュー URL のコメントが他のプレビューのコメントと競合する.
- Storybook のデプロイという目的の為だけに Firebase のプロジェクトを建てるのはちょっとオーバーすぎる.
- Vercel
- 1 リポジトリあたり 1 つのプロジェクトしか紐付けられない.
- これは例えば,プロジェクト自体が Vercel であった場合に困る.
- 1 リポジトリあたり 1 つのプロジェクトしか紐付けられない.
- GitHub Pages
- そもそもプレビュー機能が無い.
上記の問題に対して Netlify が以下の点で優れていたので,今回のケースで採用しました.
- プレビュー機能がある.
- 複数のサイトを一つのリポジトリに紐付けられる.
- CLI が優秀.
- JSON 形式で出力を返すことが可能.
Workflows の設定
まず workflow の設定を以下に全文示します. Composite Actionを使って Action を再利用することに注意.これは後で説明します.
# https://github.com/SnO2WMaN-HQ/deploy-storybook-to-netlify/blob/df1649d8cb3fefcc30ec747c412b43d4441deeb6/.github/workflows/ci.yml
name: CI
on:
push:
branches:
- "main"
pull_request:
types:
- opened
- synchronize
- reopened
jobs:
storybook-preview:
name: Storybook Preview
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
environment:
name: Storybook Preview
url: ${{ steps.deploy.outputs.preview_url }}
steps:
- uses: actions/checkout@v2
- id: deploy
uses: ./.github/workflows/composites/deploy_storybook_to_netlify
with:
netlify_site_id: ${{ secrets.STORYBOOK_NETLIFY_SITE_ID }}
netlify_auth_token: ${{ secrets.STORYBOOK_NETLIFY_AUTH_TOKEN }}
- name: Comment on PR
if: ${{ success() }}
env:
PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
LOGS_URL: ${{ steps.deploy.outputs.logs_url }}
uses: actions/github-script@v5
with:
script: |
const { PREVIEW_URL, LOGS_URL } = process.env;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Storybook Preview\n||URL|\n|-|:-|\n|Preview|${PREVIEW_URL}\n|Logs|${LOGS_URL}`,
});
storybook-production:
name: Storybook Production
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
environment:
name: Storybook Production
url: ${{ steps.deploy.outputs.live_url }}
steps:
- uses: actions/checkout@v2
- id: deploy
uses: ./.github/workflows/composites/deploy_storybook_to_netlify
with:
production: "true"
netlify_site_id: ${{ secrets.STORYBOOK_NETLIFY_SITE_ID }}
netlify_auth_token: ${{ secrets.STORYBOOK_NETLIFY_AUTH_TOKEN }}
# https://github.com/SnO2WMaN-HQ/deploy-storybook-to-netlify/blob/df1649d8cb3fefcc30ec747c412b43d4441deeb6/.github/workflows/composites/deploy_storybook_to_netlify/action.yml
name: "Deploy Storybook to Netlify"
description: "Deploy Storybook to Netlify"
inputs:
production:
description: "Is production"
required: false
default: "false"
netlify_site_id:
description: "Netlify site ID"
required: true
netlify_auth_token:
description: "Netlify auth token"
required: true
outputs:
preview_url:
description: "Netlify Preview URL"
value: ${{ steps.deploy_to_netlify.outputs.preview_url }}
live_url:
description: "Netlify Live URL"
value: ${{ steps.deploy_to_netlify.outputs.live_url }}
logs_url:
description: "Netlify Logs URL"
value: ${{ steps.deploy_to_netlify.outputs.logs_url }}
runs:
using: composite
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "16"
- uses: actions/cache@v2
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: pnpm/action-setup@v2.0.1
with:
version: 6.24.1
run_install: |
- args: [--frozen-lockfile]
- run: pnpm run storybook:build
shell: bash
- id: deploy_to_netlify
env:
PRODUCTION: ${{ inputs.production == 'true' }}
NETLIFY_SITE_ID: ${{ inputs.netlify_site_id }}
NETLIFY_AUTH_TOKEN: ${{ inputs.netlify_auth_token }}
run: |
DEPLOY_COMMAND="netlify deploy -d storybook-static --json"
if [ "$PRODUCTION" = "true" ]; then DEPLOY_COMMAND+=" --prod"; fi
RESULT=$(pnpx $DEPLOY_COMMAND)
echo "::set-output name=preview_url::$(echo $RESULT | jq -r .deploy_url)"
echo "::set-output name=logs_url::$(echo $RESULT | jq -r .logs)"
echo "::set-output name=live_url::$(echo $RESULT | jq -r .url)"
shell: bash
pnpm について
今回なんとなく npm および yarn の Alternative として pnpm を使ってみました. 従来のパッケージマネージャとそう変わりませんが,一応説明を入れておきます.
pnpm install --frozen-lockfile
npm ci
あるいはyarn --frozen-lockfile
と同等.- Actions 内では
pnpm/action-setup
内で実行されています.
pnpm run storybook:build
npm run
と同等.ここでは Storybook のビルドを行う.
pnpx
npx
のエイリアス.
Composite Action について
GitHub Actions では重複する設定について Composite Action を用いて再利用することが出来ます.
日本語ではこの記事が参考になると思います.
今回,Storybook Production, Preview で Environment やデプロイ後の処理を変更しますが,デプロイ処理に関してはある程度共通化できるので利用しました.
いくつかの点で注意する点があるので記載.
Composite Action の呼び出し方
示したコードの中では,具体的にはこのように呼び出しています.
steps:
- uses: actions/checkout@v2
- id: deploy
uses: ./.github/workflows/composites/deploy_storybook_to_netlify
with:
netlify_site_id: ${{ secrets.STORYBOOK_NETLIFY_SITE_ID }}
netlify_auth_token: ${{ secrets.STORYBOOK_NETLIFY_AUTH_TOKEN }}
まず,Composite Action 自体はどこに配置しても良いのですが, 必ずaction.yml
として保存されなければなりません. 今回は.github/workflows/compsites
以下に置きましたが,Actionlintがcomposites/
以下も見てしまい文法エラーを踏むので, .github/composites
ぐらいに置くのが良いでしょう.
配置後,チェックアウトしたパスの起点から./<action_dir>
で利用することが出来ます.このとき,先頭の./
は必ず記載しなければなりません.
それ以外については従来のuses
と同様に利用することが出来ます.
Composite Action の書き方
runs
以下で必ずusing: composite
を記載しなければなりません.
その他に関しては,大体目立った文法の違いなどは無いので割愛.
run
全てのrun
を利用する step で,毎回shell
を指定する必要があります.
参考にした記事では複数行(|
)が使えないと書いてありますが,使えるようになっています.
inputs
Composite Action 側でinputs
を定義することで, 再利用する側でwith
を利用して引数を渡すことが出来ます.
この際,全ての引数は文字列( String
)である必要があり,Boolean
など,つまりtrue
やfalse
を渡すことは出来ません.
渡された引数は,Composite Action 側では${{ inputs.xxx }}
で利用可能です.
outputs
Composite Action 側から何らかの出力を返すのはトリッキーな手法を取る必要があるので,これを解説します.
まず,Composite Action 内のsteps
で, id
がSTEP_ID
の step 内のrun
でecho "::set-output name=NAME::VALUE"
を実行すると, ${{ steps.STEP_ID.outputs.NAME }}
としてVALUE
を取得することが出来ます.
次に,outputs
の中で,value: ${{ steps.STEP_ID.outputs.NAME }}
を呼び出すことで,出力を返すことが出来ます.
出力は,Composite Action を呼び出した側では,${{ steps.CALLER_STEP_ID.outputs.NAME }}
で利用可能です.(id
がCALLER_STEP_ID
だとして)
CI 本体について
以下抜粋.main
ブランチへの push とpull_request
のopened
,synchronize
(プルリクエストの対象ブランチに追加でコミットが発生した際),reopened
イベントで走らせる job を切り分けています.(CI を走らせる条件はこの記事を参考にしています.)
on:
push:
branches:
- "main"
pull_request:
types:
- opened
- synchronize
- reopened
jobs:
storybook-preview:
name: Storybook Preview
if: ${{ github.event_name == 'pull_request' }}
environment:
name: Storybook Preview
url: ${{ steps.deploy.outputs.preview_url }}
storybook-production:
name: Storybook Production
if: ${{ github.event_name == 'push' }}
environment:
name: Storybook Production
url: ${{ steps.deploy.outputs.live_url }}
environment
environment
を各 job で指定するとこのようにデプロイの履歴などが見れます.このとき,GitHub 側 で予め作っておく必要はなく,GitHub Actions 側が勝手に作ってくれます.
actions/github-script
- name: Comment on PR
if: ${{ success() }}
env:
PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
LOGS_URL: ${{ steps.deploy.outputs.logs_url }}
uses: actions/github-script@v5
with:
script: |
const { PREVIEW_URL, LOGS_URL } = process.env;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Storybook Preview\n||URL|\n|-|:-|\n|Preview|${PREVIEW_URL}\n|Logs|${LOGS_URL}`,
});
actions/github-scriptを用いて Storybook Preview のデプロイが成功した場合,その URL をプルリクエストにコメントする step を組みました.気をつけるべき点はprocess.env
で環境変数を取り出す点ぐらいでしょうか.
おわりに
actによるローカルでの CI 設定のチェックが出来るかどうかは試してないので今後の課題とします.
リポジトリ内の複数サイトを Netlify でデプロイするとき,素朴に考えるならyarn build
とyarn storybook:build
を実行するプロジェクトを 2 個作っておけば良いのですが,それらの設定をファイルとしてnetlify.toml
として書いておけないので(どちらかのプロジェクトの設定しか書いておけない,少なくとも私が調べた限り),今回このような措置が行われています.正直なところとしてはそれで全然気にしないのであればそちらでも構わないと思います.
デプロイ先が必要であるのはそもそも GitHub Actions の成果物( Artifacts )が Web ページとして閲覧できない(zip ファイルとしてダウンロードされる)からなのですが, 例えば Circle CI では Artifacts をそのままページとして閲覧できます(本来はテストカバレッジの為に). 身も蓋もない話をすれば,この記事でやったことは Circle CI を使えばまあまあスマートに解決出来る筈です.
トホホ…
参考文献
- 実質無料で使える Hosting Service の比較や使い分けの紹介 2021 (Firebase Hosting, Cloudflare Pages, Vercel, Netlify, GitHub Pages, Amplify, CloudRun)
- Creating a composite action
- GitHub Actions の composite action で YAML を再利用する (2021 年 9 月版)
- GitHub Actions の push イベントと pull_request イベントでは GITHUB_SHA が異なる