pullreq毎にprivateなstorybook(や静的webサイト)プレビューを作る
お、pullreqきてる。コンポーネント生やしたのかなるほど。storybookにも追加されてるな。
...ちらっとでいいからstorybookの画面でも触りたいなぁ。
TL;DR
webアクセスをIP制限したS3バケットを用意し、pullreqごとにgithub actionsでstorybookをビルド・S3にアップ・pullreqにURLをコメント、という仕組みを作った
やりたいこと
- pullreqごとにstorybookの画面を見たい
- テスト環境にアップしてもらったり、checkoutしてビルドして、とかは面倒
- プレビューはインターネットに公開したくない(アクセス制限したい)
ちらっと見れればよいのです。pullreqにスッと置かれたリンクをおもむろに押して、軽く触ってみて、うんうん、と頷くくらいがよいのです。
なお成果物のリポジトリはこちら: https://github.com/cumet04/sbox_storybook-on-pullreq/tree/20201208_zenn
事前準備: storybook環境をつくる
というわけで、本記事ではそんな仕組みを作っていきます。
見れるようにする対象はstorybookなので、何はともあれstorybookが動くサンプル環境を用意します。ここは特に工夫は無いので、最小手数でいきます。
まずはサンプルとして適当なReactの環境を用意します[1]。npx create-react-app
でもいいのですが、今回は自分でサクッと用意しました。ひとまず、package.json
にreact
があれば大丈夫だと思います[2]。
次にstorybookを導入します。公式のガイドに従いnpx sb init
すると何やらサンプル環境一式が展開されます。
ここまでではnpm run build-storybook
でstorybookの静的ファイル群がビルドされれば良いです。
プライベートなS3のwebアクセス環境を作る
次に、ビルドされたstorybookを見る環境を用意します。静的webホスティングが簡単にでき、かつお手軽にアクセス制限を実施できるもの、ということでS3をチョイスしました。
S3のオブジェクトにはオブジェクトURLという読み取り用URLがあり、権限があればブラウザでアクセスすることができます。
通常はAWSコンソールにログインするなどしないと閲覧できないのですが、ここに「IPさえ合っていればユーザやサービスの条件を問わずにアクセス可能」という権限設定をすることで、事実上のIP制限付きwebホスティング環境とすることができます。
S3バケットを作る
ということでS3バケットを作ります。AWSコンソールから作成していきますが、バケット名以外はデフォルトで問題ありません。「パブリックアクセスをすべてブロック」にチェックが入っていると思いますが、そのままで良いです。
次にアクセス制限の設定を付与します。作成したバケット名をクリックし、詳細画面で「アクセス許可」のタブを選択し、バケットポリシーを編集します。
※実際にはタブの下に「ブロックパブリックアクセス (バケット設定)」がありますが、スクショの都合で邪魔だったため少しどいていただきました。
※スクショにある名前のバケットは記事公開時には消している予定です。
なお設定するバケットポリシーの内容は以下です:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-s3-bucket-name/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "xxx.xxx.xxx.xxx/32"
}
}
}
]
}
Resource
のバケット名の部分とaws:SourceIp
のIPの部分をいい感じに設定しましょう。IPを複数設定したい場合は"aws:SourceIp": ["xxx.xxx.xxx.xxx/aa", "yyy.yyy.yyy.yyy/bb"]
のように配列で指定できます。
ここまでできたら適当なファイルをバケットにアップロードし、オブジェクト詳細画面にあるオブジェクトURLに所定のIPからアクセスできることを確認しておきましょう。
アップロード用のIAMユーザを作る
このバケットにgithub actionsからファイルを読み書きするIAMユーザを作成します。
まずはユーザにアタッチするポリシーを作成します。内容は
- S3サービスへの
ListBucket
,GetObject
,DeleteObject
アクションの許可 - 対象リソースは
arn:aws:s3:::your-s3-bucket-name
,arn:aws:s3:::your-s3-bucket-name/*
です。JSONにすると
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:ListBucket",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::your-s3-bucket-name",
"arn:aws:s3:::your-s3-bucket-name/*"
],
"Effect": "Allow"
}
]
}
となります。作成したらユーザを作成し、このポリシーを紐付けておきましょう。
※ここまでの操作をAWS CDKで記述したものがこちらにあるので参考まで。
またユーザが作成できたらユーザのアクセスキーを作成し、アクセスキー及びシークレットキーをgithubのリポジトリに設定しておきます(github actionsから参照します)。
リポジトリページのSettings > Secretsより設定しますが、本記事ではAWS_ACCESS_KEY_ID
にアクセスキー、AWS_SECRET_ACCESS_KEY
にシークレットキー、BUCKET_NAME
にS3バケットの名前を入れます。
github actionsを作る
ここまででインターネットに公開しない(アクセス制限された)webホスティング環境が用意できたため、あとはいい感じにstorybookをビルドしてアップロードすればOKです。
具体的に実施したいことは以下です:
-
main
ブランチ[3]およびpullreqごとにbuild-storybook
しS3にアップロード - pullreqの場合はアップロードされたS3上のURLをpullreqにコメント
- pullreqがcloseされたら該当ファイルをS3上から消す
以降でも部分ごとにコードを提示しますが、先に全体を置いておきます(長いので折りたたみ)。
actions定義全体
name: deploy storybook
on:
push:
branches: [main]
paths:
- frontend
- .github/workflows/storybook.yml
pull_request:
types: [opened, reopened, synchronize, closed]
paths:
- frontend
- .github/workflows/storybook.yml
jobs:
deploy:
runs-on: ubuntu-latest
if: github.event.action != 'closed'
defaults:
run:
working-directory: frontend
steps:
# storybookのビルド
- uses: actions/checkout@v2
- name: Setup Node.js for use with actions
uses: actions/setup-node@v2.1.2
with:
node-version: 14.15.1
- name: Install Dependencies
run: npm ci
- name: Build storybook
run: npm run build-storybook
# S3へのアップロード
- name: set upload destination directory name
run: |
DEST_DIR=${{ github.event.pull_request.number }}
[ -z $DEST_DIR ] && DEST_DIR=main
echo "DEST_DIR=${DEST_DIR}" >> $GITHUB_ENV
- name: upload storybook-static
run: |
aws s3 cp --recursive \
./storybook-static \
s3://${{ secrets.BUCKET_NAME }}/storybook/${DEST_DIR}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ap-northeast-1
# pullreqへのURLコメント
- name: post preview url to pull-request
if: github.event.action == 'opened'
uses: actions/github-script@v3
with:
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'storybook preview created!!\n' +
'https://${{ secrets.BUCKET_NAME }}.s3-ap-northeast-1.amazonaws.com/storybook/${{ github.event.pull_request.number }}/index.html'
})
clean:
runs-on: ubuntu-latest
if: github.event.action == 'closed'
steps:
- name: remove storybook-static
run: |
DEST_DIR=${{ github.event.pull_request.number }}
aws s3 rm --recursive \
s3://${{ secrets.BUCKET_NAME }}/storybook/${DEST_DIR}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ap-northeast-1
※フロントエンド系のファイルはfrontend
がルートになっている想定です
それではブロックごとにポイントを見ていきます。
on
セクション
on:
push:
branches: [main]
paths:
- frontend
- .github/workflows/storybook.yml
pull_request:
types: [opened, reopened, synchronize, closed]
paths:
- frontend
- .github/workflows/storybook.yml
actionsの発火条件は、mainブランチの更新・pullreqの更新系・pullreqのcloseです。
ポイントとしてはpull_request.types
を指定している点です。デフォルトではopened
, reopened
, synchronize
のみですが、本件ではclosed
を加える必要があるために明示的に指定しています。
またpaths
フロントエンド関連のディレクトリとactionsの定義ファイルを指定しておきます[4]。
jobs.deploy
セクション
jobs:
deploy:
runs-on: ubuntu-latest
if: github.event.action != 'closed'
defaults:
run:
working-directory: frontend
steps:
jobsはビルド&デプロイのセクションとpullreq close時の掃除用セクションに分かれています。
そのためdeploy
セクション全体をif: github.event.action != 'closed'
することでclose時に発火しないようにしています。
また以降のstepはすべてfrontend
ディレクトリ下で実行するため、defaults.run.working-directory
を指定しています。
storybookのビルド
- uses: actions/checkout@v2
- name: Setup Node.js for use with actions
uses: actions/setup-node@v2.1.2
with:
node-version: 14.15.1
- name: Install Dependencies
run: npm ci
- name: Build storybook
run: npm run build-storybook
ここではnpm run build-storybook
できればよいので、github actionsでnodejsプロジェクトを扱う際のテンプレのようなstepが並んでいます。そして最後にビルドします。
S3へのアップロード
- name: set upload destination directory name
run: |
DEST_DIR=${{ github.event.pull_request.number }}
[ -z $DEST_DIR ] && DEST_DIR=main
echo "DEST_DIR=${DEST_DIR}" >> $GITHUB_ENV
- name: upload storybook-static
run: |
aws s3 cp --recursive \
./storybook-static \
s3://${{ secrets.BUCKET_NAME }}/storybook/${DEST_DIR}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ap-northeast-1
1つ目のstepではS3にアップロードする際のパスを決定しています。shellなので少々ややこしいですが、javascript風に書くと
dest_dir = github.event.pull_request.number || 'main'
GITHUB_ENV["DEST_DIR"] = dest_dir
のような雰囲気です。pullreqの番号もしくはブランチ名(main)を設定し、$GITHUB_ENV
に書き込むことで次以降のstepでこの値を使うことができます。
次のstepにてawsコマンドでビルド成果物をS3にアップロードします。転送先のパスの最後に上記で設定したDEST_DIR
を使っており、また作成したIAMユーザのアクセスキー・シークレットキーもここで環境変数として指定しています。
ちなみに、github actionsでubuntu-latest
などのマシンを動かすとデフォルトでaws
コマンドが使えるようです。便利ですね。
pullreqへのURLコメント
- name: post preview url to pull-request
if: github.event.action == 'opened'
uses: actions/github-script@v3
with:
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'storybook preview created!!\n' +
'https://${{ secrets.BUCKET_NAME }}.s3-ap-northeast-1.amazonaws.com/storybook/${{ github.event.pull_request.number }}/index.html'
})
S3のURLをpullreqにコメントしています。pullreqの最初にしかいらないのでif: github.event.action == 'opened'
で実行条件を絞っています。
使っているactions/github-scriptですが、雑に説明すると「javascript版のoctokitをactions内で使えるようにしたもの」のようです。issue comment作成のドキュメントの通りにパラメータを指定し、コメント本文にURLを含めた上で実行しています。
書き込み対象がactionsの親リポジトリだからか特にtokenなどの指定は不要なようです。一時的にリポジトリをprivateにしても動作しました。
jobs.clean
セクション
clean:
runs-on: ubuntu-latest
if: github.event.action == 'closed'
steps:
- name: remove storybook-static
run: |
DEST_DIR=${{ github.event.pull_request.number }}
aws s3 rm --recursive \
s3://${{ secrets.BUCKET_NAME }}/storybook/${DEST_DIR}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ap-northeast-1
if: github.event.action == 'closed'
にてpullreqのclose時(merge含む)に動作し、該当pullreqのstorybookファイルを削除します。
アップロード時と同じように該当パスをaws rm
で消しているだけのシンプルなjobです。
まとめ
以上の準備とコードがあれば、あとはいつものようにコードを書いてpullreqを作りmergeし...としているだけで、おもむろにstorybookのURLが飛んできます。後片付けだって完璧です。
actionsの定義は少々長いですが、一つ一つ見ていくと案外単純だったのではないでしょうか。
というわけで、みなさんもどんどんプレビューしましょう!
Discussion