この記事はBASE Advent Calendar 2019の8日目の記事です。
エンジニアの右京です!
みなさん!Storybook は使っていますか?BASE ではUIコンポーネントの社内展開はもちろん、日々の業務の中でもサンプルの実装を共有したりするために Storybook が使われています。BASEではこれを「特定のリポジトリにコードをコミットすると、自動的に社内向けサーバーへデプロイされる仕組み (ようするに社内 GitHub Pages ですね)」を利用して社内共有しているのですが、毎度のセットアップが大変なので Gtihub Actions を使ってお手軽に設定できるようにしてみたよ、という内容です。
TL;DR
- 社内用向けドキュメントサーバーへのデプロイを他のリポジトリから使いやすいように Action 化して配信するようにしました
- プライベートリポジトリにある Action は直接参照することができないので Personal access token と npx を使います
- CircleCI から GitHub Actions に変更することで様々なイベントにフックすることができ、柔軟性のあるデプロイが可能になりました
デプロイ(コミット)の Action 化
これまでこのサーバーを利用したい時は、利用側の CircleCI で専用のリポジトリを clone して commit を作って push して...といった処理を用意する必要がありました。利用するプロジェクトが増えていく中、この利用法のスケールのしにくさに問題を感じていたため、GitHub Actions でスパっといけないか?と思ったのが今回のスタート地点です。 このエントリでは今後はこのリポジトリのことを「static-pages-repo」と呼ぶことにします。
まず、前提として static-pages-repo は master に push されると public
ディレクトリ以下が自動で社内向けのサーバーへデプロイされるようになっています。
└── public ├── webservice1/master/... ├── webservice1/hogefuga/... ├── webservice2/master/... ├── ... └── index.html // デプロイされているURLのリストが入っています
今回作る Action では、上記の構成に沿って以下のフローを実行します
- この配置に合わせて利用側から成果物をコピー
- コピーした成果物をコミット
- アクセスしやすいように
public/index.html
を更新 - リモートへ push
これを行うためには3つの引数が必要そうです。
- public の配置を決定するためのプロジェクト名(prefix)
- public の配置を決定するためのブランチ名
- 成果物(コピー元)のパスの指定
つまり Action としてはこんな感じに利用できるのがよさそうです。
- uses: static-pages-repo with: project: webservice1 branch: feature/hogefuga source: dist
早速実装に...と行きたいところですが、気をつけるべき点が2つあります。
- プライベートリポジトリのアクションは直接実行することはできない
- Workflow 内で他のリポジトリを扱う場合は
secrets.GITHUB_TOKEN
では権限不足
それぞれ見ていきます。
プライベートリポジトリのアクションは直接実行することはできない
パブリックなリポジトリの場合はリポジトリ名やパスを指定することで直接他のリポジトリから Action を参照して、利用することできます。ですが、プライベートリポジトリの場合はこの方法は使えないため、回避方法を考える必要があります。 static-pages-repo を clone...? と思いましたが、それだとあまり現状と変わらない...ので多少見栄えがよくなりそうだと npx で Action をインストールできるようにしてみることにしました。 一度 npx 経由で Action をコピーしてしまうことで、自身のリポジトリ内の Action として実行します。
- run: npx https://github.com/baseinc/static-pages-repo install - uses: ./.github/actions/static-pages-repo
Workflow 内で他のリポジトリを扱う場合は secrets.GITHUB_TOKEN では権限不足
GitHub Actions を使ったことがある人は知っているかもしれないのですが、元々 Workflow で使える変数として secrets.GITHUB_TOKEN
が用意されています。通常であれば、このトークンを使えばプライベートリポジトリでも GitHub API を呼び出したり、リポジトリに Git での書き込みができるのですが、残念ながらこのトークンではリポジトリを跨いだ操作を行うことはできません。
これは代わりに Personal access token (以下PAT) を使用することで解決します。actions/checkout にもこれに関する記述 があるので参考にしてみてください。
実際に Action を作る
では、以上の2つの注意点に気をつけながら実際に Action を作っていきます。
Action の開発方法にはDocker コンテナ版と JavaScript 版 があり、今回は Git での操作がメインになりそうだなという理由でシンプルに実装できそうなDockerコンテナ版を選択しました。
公式ドキュメントにそってまず Dockerfile を作ります。alpine でもよいですが、public/index.html
を Node.js で作成しているため、 node:12
を選択しています。
ファイルの置き方ですが、これらを直接 Actions が実行するわけではないので(コピー元)、安直に action
というディレクトリを作って配置することにしました。
FROM node:12 COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]
次に action.yml を作ります。action.yml は引数や出力、実行方法を設定するためのファイルです。
name: 'static-pages-repo' description: 'deploy static pages' inputs: # ここに追加したキーが引数となります pat: description: 'Actionで必要になるPAT' required: true project: description: 'ディレクトリ名のprefix' required: true branch: description: 'ディレクトリ名' required: false default: 'master' source: description: '配信したいコンテンツへのパス' required: true outputs: url: description: 'デプロイ先となるURL' runs: using: 'docker' image: 'Dockerfile' args: - ${{ inputs.pat }} - ${{ inputs.project }} - ${{ inputs.branch }} - ${{ inputs.source }}
最後に entrypoint.sh を作って実際に動かすスクリプトを書いていきます。PAT を受け取って、static-pages-repoを操作していきます。
#!/bin/sh -l # 作業用のリポジトリをクローンします、PAT($1)を付加していることに気をつけてください git clone https://$1@github.com/baseinc/static-pages-repo.git .github/tmp/repo # project/branchなディレクトリを作って成果物をsourceからコピーしてきます mkdir -p .github/tmp/repo/public/$2/$3 cp -rT $4 .github/tmp/repo/public/$2/$3 cd .github/tmp/repo # 配置されたファイルへのリンクを含む public/index.html を生成します npm ci npm run build-index # 最終的に差分が生まれていれば git commit して push します git add . git config user.name actions git config user.email actions@binc.jp if git commit --dry-run > /dev/null; then git commit -m "Update $2/$3" git push origin HEAD fi git push https://$1@github.com/baseinc/static-pages-repo.git master # このようにして値を出力しておくと、次の step でこれを元に検証したりすることができます echo "::set-output name=url::https://static-pages-repo.com/$2/$3/"
Action を配信する npx スクリプトを作る
npx にあまり馴染みのない方もいるかもしれませんが、簡単にいえば「インストールせずに一度だけ使えるパッケージ」という感じでしょうか。 package.json を作って以下のようなスクリプトで Action をローカルにコピーしています。
#!/usr/bin/env node const path = require('path') const fs = require('fs-extra') const src = path.resolve(__dirname, '../action') const out = path.resolve(process.cwd(), '.github/actions') ;(async() => { await fs.ensureDir(out) await fs.copy(src, `${out}/static-pages-repot`) })()
これを上のものと合わせると、最終的には他のリポジトリからこのような step で static-pages-repo デプロイすることができるようになります。
- run: npx https://${{secrets.PAT}}@github.com/baseinc/static-pages-repo install - uses: ./.github/actions/static-pages-repo with: pat: ${{ secrets.PAT }} project: webservice source: dist
便利そうに見えませんか?実際に PAT
を設定するのは、利用側であることに気をつけてください。
これで static-pages-repo 側は完成です! ここまでのディレクトリ構造はこのようになっています。
├── action │ ├── Dockerfile │ ├── action.yml │ └── entrypoint.sh ├── bin │ └── install.js // npx で実行されるスクリプト ├── webroot │ ├── webservice1/master/... │ ├── webservice1/hogefuga/... │ ├── webservice2/master/... │ ├── ... │ └── index.html ├── README.md ├── package-lock.json └── package.json
実際に Action を使ってデプロイする
これで準備が整ったので、あとは各リポジトリに導入するだけです!
今回は、実際に BASE で運用しているパターンから3つをご紹介します。
- master に push されたらデプロイ
- Pull Request が作られたらデプロイ
- Pull Request に特定のコメントがついたらデプロイ
master に push されたらデプロイ
一番簡単でわかりやすい、馴染みのある感じだと思います。 ほとんどのリポジトリは、master にマージされたときに更新を実行するようにしています。
name: deploy master on: push: branches: - master jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: node-version: '12.x' - uses: actions/cache@v1 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-node- - run: yarn install --frozen-lockfile - run: yarn storybook -o dist - run: npx https://${{secrets.PAT}}@github.com/baseinc/static-pages-repo install - uses: ./.github/actions/static-pages-repo with: pat: ${{ secrets.PAT }} project: webservice source: dist
Pull Request が作られたらデプロイ
UIコンポーネントのような基本的に Storyboard がセットになるものはこれを設定しています。 Pull Request が Open されると同時にデプロイされるため、レビューの際にはスムーズに Storybook を確認することができます。
name: open pull-request on: pull_request: types: [opened, reopened] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: node-version: '12.x' - run: yarn install --frozen-lockfile - run: yarn storybook -o dist - run: npx https://${{secrets.PAT}}@github.com/baseinc/static-pages-repo install - id: deploy uses: ./.github/actions/static-pages-repo with: pat: ${{ secrets.PAT }} project: webservice branch: ${{ github.event.pull_request.head.ref }} # 実行時の event を受け取って使うことができるため、ここから branch 名を取得して決定しています source: dist - if: "steps.deploy.outputs.url" # 前の step が成功したときに発行される url があったら、PRのコメントにURLを書き込みます env: URL: ${{ steps.deploy.outputs.url }} API_ENDPOINT: ${{ github.event.pull_request.comments_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ここは同じリポジトリ内なので GITHUB_TOKEN でアクセスできます run: | curl -X POST -H "Authorization: token ${GITHUB_TOKEN}" -i ${API_ENDPOINT} -d "`printf '{\"body\":\"deploy to %s\"}' ${URL}`"
Pull Request に特定のコメントがついたらデプロイ
必ずしも Storybook が必要ではないアプリケーションのリポジトリでは、Pull Request にコマンドコメントを書くことで任意のタイミングでデプロイが行えるようにしています。 これには issue_comment というイベントを使うのですが、これは特定ブランチには紐づかないイベントで、デフォルトブランチを起点に実行されるため少し複雑です。
name: on comment pull request on: issue_comment: types: [created] jobs: build: runs-on: ubuntu-latest steps: - id: check # コメントに deploy-storybook の文字列が含まれていて、pull_requrest なら実行します if: "contains(github.event.comment.body, 'deploy-storybook') && github.event.issue.pull_request" env: API_ENDPOINT: ${{ github.event.issue.pull_request.url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # pull_request のデータを API 経由で取得して、ブランチ名を特定します branch=`curl -X GET -H "Authorization: token ${GITHUB_TOKEN}" ${API_ENDPOINT} | jq -r '.head.ref'` echo ::set-output name=branch::$branch # このようにすることで id(=check) に紐づく outputs を step からも書き出すことができます # これ以降は branch が特定できていたら続行します - if: "steps.check.outputs.branch" uses: actions/checkout@v1 with: ref: ${{ steps.check.outputs.branch }} - if: "steps.check.outputs.branch" uses: actions/setup-node@v1 with: node-version: '12.x' - if: "steps.check.outputs.branch" run: | yarn install --frozen-lockfile yarn storybook -o dist npx https://${{secrets.PAT}}@github.com/baseinc/static-pages-repo install - id: deploy if: "steps.check.outputs.branch" uses: ./.github/actions/static-pages-repo with: pat: ${{ secrets.PAT }} project: webservice branch: ${{ steps.check.outputs.branch }} # 前の step で特定した branch 名を使用します source: dist - if: "steps.deploy.outputs.url" env: URL: ${{ steps.deploy.outputs.url }} API_ENDPOINT: ${{ github.event.issue.comments_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | curl -X POST -H "Authorization: token ${GITHUB_TOKEN}" -i ${API_ENDPOINT} -d "`printf '{\"body\":\"deploy to %s\"}' ${URL}`"
一つ気をつけるべき点として、 job 自体にも if を設定できもっと簡潔にかけそうなのですが、if が通らなかった場合に job が 0個となり Workflow 自体が失敗したことになってしまいます。
また、実際にはこれらと「 Pull Request が close された際にディレクトリを削除する Action 」をあわせて運用しています。
めでたしめでたし
Storybook を Move Fast にデプロイできるようになったことで、Speak Openlyな開発にまた一歩近くことができたと思います。 お気づきの方もいると思いますが、単なる静的ファイルホスティングなので Storybook 以外にももちろん使うことができます!
明日は id:kmotoki と id:tatsuta2 です!