アプリ公開に伴ってStorybookとドキュメントを自動で最新化＋デプロイする

# 概要
アプリ公開時に以下の要件を満たす環境が欲しかったため、試行錯誤した際の備忘録。

* GitHubへのプッシュに伴って自動的にアプリをビルドして公開したい。
* Storybookも自動的にビルドして公開したい。
* ドキュメントも自動的に最新化して公開したい。

# 環境
* ソースコード管理: GitHub
* ホスティングサービス: Netlify
* CIツール: Travis CI

# 使用ツール
**Storybook**
UIコンポーネントのカタログを作成するツール。アプリとしてビルドすることも出来る。

**TypeDoc**
TypeScriptソースコードからドキュメントを自動生成するツール。

# 公開方法
いずれも本番環境のアプリとは無関係なものであるため、アプリと独立した環境で公開するものとする。

## Netlify（ブランチデプロイ）
Storybookもまた動的なアプリであるため、ホスティングサーバー上でビルド＆デプロイするべくNetlifyのブランチデプロイを用いる。

## GitHub Pages
ドキュメントは静的なHTMLファイル群であるため、予め生成してGitHub Pagesで公開する。

# デプロイの流れ
デプロイの大まかな流れは以下の通りである。詳細な解説は、後述の[詳解](#詳解)を参照されたい。

## リモートへのプッシュからTravis CIまで
**[ローカル]**
1. ソースコードをGitHubへプッシュする。

**[Travis CI]**
2. masterブランチを基準にstorybookブランチを作成する。
3. Netlifyの設定ファイルをStorybook用のものに置き換える。
4. リモートリポジトリのstorybookブランチにプッシュする。
5. masterブランチを基準にgh-pagesブランチを作成する。
6. ドキュメントをTypeDocで自動生成する。
7. `/docs` 内のデータのみをリモートリポジトリのgh-pagesブランチにプッシュする。

## CI後からサイト公開まで
前項の処理が完了したのち、Storybookとドキュメントは以下の処理を経て公開される。

**Storybook**
Travis CIがstorybookブランチに対して実行したプッシュを検知してビルドが実行される。Netlifyの設定ファイルがmasterと異なるため、このビルドではStorybookのビルドが実行され、出力データの格納先である `storybook-static` がサイトとして公開される。

**ドキュメント**
gh-pagesブランチにプッシュされたデータは即座にGitHub Pagesとして処理を経て公開される。

# 手順
## プロジェクト作成
プロジェクトを作成してGitHubへプッシュする。

```terminal
$ create-react-app deploy-app-storybook-docs --typescript
$ cd deploy-app-storybook-docs
$ git remote add origin git@github.com:hogehoge/deploy-app-storybook-docs.git
$ git push -u origin master
```

## サービス連携
### Netlify
#### リポジトリ登録
Netlifyの[ダッシュボード](https://app.netlify.com/account/sites)へアクセスし、`New site from Git` をクリックしてリポジトリを登録する。途中のステップはそれぞれ以下のように進めること。

1. Connect to Git provider: GitHubを選択する。
2. Pick a repository: リポジトリを選択する。
3. Build options, and deploy!: デフォルトのまま ｀Deploy site` をクリックする。

#### ブランチデプロイの設定
master以外にstorybookブランチもデプロイするよう、設定を追加する。Setting → Build & deploy → Deploy contexts → Branch deploysへと進んで `Let me add individual branches` を選択し、`storybook` と入力して `Save` をクリックする。

### Travisと連携
travis-ci.comの[アカウント設定ページ](https://travis-ci.com/account/repositories)へアクセスし、`Manage repositories on GitHub` をクリックしてリポジトリを登録する。

## ローカル環境構築
### ツールのインストール
以下のコマンドにて、StorybookとTypeDocをインストールする。

```terminal
$ sb init
$ yarn add -D typedoc
```

### 各種設定
#### Netlify
以下のファイルをプロジェクトのルートディレクトリに配置する。それぞれmaster、storybookブランチ用にビルドとデプロイを個別に設定している。

```toml:netlify.toml
[build]
  publish = "build/"
  command = "yarn build"
```

```toml:netlify.storybook.toml
[build]
  publish = "storybook-static/"
  command = "yarn build-storybook"
```

#### Travis CI
##### 設定ファイル配置
以下のファイルをプロジェクトのルートディレクトリに配置する。

```yaml:.travis.yml
language: node_js
node_js:
- 11.1.0
before_install:
script:
- yarn test
after_success:
# Prepare for GitHub
- openssl aes-256-cbc -K $encrypted_************_key -iv $encrypted_************_iv -in ./id_rsa.enc -out ~/.ssh/id_rsa -d
- chmod 600 ~/.ssh/id_rsa
- echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
- git config --global user.email "hogehoge@fugafuga.com"
- git config --global user.name "hogehoge"
- git config --global url."git@github.com:".insteadOf "https://github.com/"
# Build Storybook
- git checkout -b storybook master
- mv netlify.storybook.toml netlify.toml
- git add .
- git commit -m "$TRAVIS_COMMIT_MESSAGE"
- git push -u origin storybook --force
# Generate docs by TypeDoc
- git checkout -b gh-pages master
- git subtree add --prefix docs origin gh-pages
- rm -rf docs
- node node_modules/.bin/typedoc --out docs src
- touch docs/.nojekyll
- git add docs --force
- git commit -m "$TRAVIS_COMMIT_MESSAGE"
- git subtree push --prefix docs origin gh-pages

branches:
  only:
  - master
```

##### SSHキー発行
Travis CIからGitHubへプッシュを行うため、SSHキーの設定を行う。以下のコマンドにて、GitHubにSSH接続する際に用いている `~/.ssh/id_rsa` を暗号化する。

```terminal
$ travis login --pro
$ travis encrypt-file --repo hogehoge/deploy-app-storybook-docs ~/.ssh/id_rsa ./id_rsa.enc --pro
encrypting /Users/hogehoge/.ssh/id_rsa for hogehoge/deploy-app-storybook-docs
storing result as ./id_rsa.enc
storing secure env variables for decryption

Please add the following to your build script (before_install stage in your .travis.yml, for instance):

    openssl aes-256-cbc -K $encrypted_************_key -iv $encrypted_************_iv -in ./id_rsa.enc -out ~\/.ssh/id_rsa -d

Pro Tip: You can add it automatically by running with --add.

Make sure to add ./id_rsa.enc to the git repository.
Make sure not to add /Users/hogehoge/.ssh/id_rsa to the git repository.
Commit all changes to your .travis.yml.
```

出力されたコマンド `openssl` を `.travis.yml` に記述されているものと置き換える。
🚨このコマンドには不要なバックスラッシュが含まれているため、削除して用いること。

**バックスラッシュの削除**
`-out ~\/.ssh/id_rsa -d`
　　　　　　↓
`-out ~/.ssh/id_rsa -d`

#### その他
#####.gitignore
node_modulesのような動的に生成されるディレクトリやファイルはリポジトリから除外することが望ましい。StorybookとTypeDocが生成するデータの出力先を`.gitignore` に追記し、リモートリポジトリにプッシュされないようにする。

```diff:.gitignore
npm-debug.log*
yarn-debug.log*
yarn-error.log*

+ /storybook-static
+ /docs
```

## 動作確認
### 設定後の初回プッシュ
設定完了後に全ての変更ファイルをステージングしてコミットし、リモートリポジトリへプッシュする。

```terminal
$ git add .
$ git commit -m "Integrated with Storybook and TypeDoc"
$ git push
```

#### 検証
プッシュ完了後、下記の項目が期待通り満たされているかを検証する。

* GitHub上でmaster、storybook、gh-pagesブランチが存在すること。また、いずれもコミットが反映されていること。
* masterブランチがビルドされて **https\://hogehoge.netlify.com/** にデプロイされていること。
* storybookブランチがビルドされて **https\://storybook--hogehoge.netlify.com/** にデプロイされていること。
* gh-pagesブランチが **https\://hogehoge.github.io/deploy-app-storybook-docs/** にデプロイされていること。

### 更新をプッシュ
各種ファイルを更新してプッシュする。

**アプリ用の更新**
`src/App.tsx` に以下の更新を行う。

```diff:src/App.tsx
class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
-           Edit <code>src/App.tsx</code> and save to reload.
+           Storybook, docs and I are every time up to date!
          </p>
```

**Storybook用の更新**
`src/stories/index.js` に以下の更新を行う。

```diff:src/stories/index.js
storiesOf('Button', module)
  .add('with text', () => <Button onClick={action('clicked')}>Hello Button</Button>)
+ .add('avec text', () => <Button onClick={action('clicked')}>Bonjour Bouton</Button>)
  .add('with some emoji', () => (
    <Button onClick={action('clicked')}>
      <span role="img" aria-label="so cool">
        😀 😎 👍 💯
      </span>
    </Button>
  ));
```

**ドキュメント用の更新**
以下のファイルをプロジェクトのルートディレクトリに配置する。

```json:typedoc.json
{
  "exclude": [
    "src/index.tsx",
    "src/serviceWorker.ts",
    "src/**/*.test.tsx"
  ]
}
```

#### 検証
プッシュ完了後、下記の項目が期待通り満たされているかを検証する。

* GitHub上のmaster、storybook、gh-pagesブランチの全てに更新が反映されていること。
* **https\://hogehoge.netlify.com/** にアプリの更新が反映されてデプロイされていること。
* **https\://storybook--hogehoge.netlify.com/** にstorybookの更新がデプロイされていること。
* **https\://hogehoge.github.io/deploy-app-storybook-docs/** にドキュメントの更新がデプロイされていること。

# 詳解
## .travis.yml
### GitHubとの通信設定

```
 9: - openssl aes-256-cbc -K $encrypted_************_key -iv $encrypted_************_iv -in ./id_rsa.enc -out ~/.ssh/id_rsa -d
10: - chmod 600 ~/.ssh/id_rsa
11: - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
12: - git config --global user.email "hogehoge@fugafuga.com"
13: - git config --global user.name "hogehoge"
14: - git config --global url."git@github.com:".insteadOf "https://github.com/"
```

**9行目**
暗号化された `./id_rsa.enc` を `~/.ssh/id_rsa` に復号している。

**11行目**
`~/.ssh/known_hosts` に記述のないホストへアクセスする際のプロンプトを抑制している。これを忘れると `Are you sure you want to continue connecting (yes/no)?` とプロンプトが表示されて処理が停止してタイムアウトしてしまう。

**12〜13行目**
プッシュを行うアカウントを登録している。

**14行目**
Travis CIはデフォルトでGitHubとの通信をhttpsで行っているため、公開鍵を用いるSSHとは認証方法が異なる。自分は普段SSHを使っていてhttpsの扱いに不慣れであったため、リモートリポジトリのURLをSSH用の`git@github.com:`に変更している。

### Storybook

```
16: - git checkout -b storybook master
17: - mv netlify.storybook.toml netlify.toml
18: - git add .
19: - git commit -m "$TRAVIS_COMMIT_MESSAGE"
20: - git push -u origin storybook --force
```

**16行目**
masterブランチを基準にstorybookブランチを新規作成している。

**17行目**
`netlify.toml`をStorybook用の設定ファイルで置き換えている。

**19行目**
ローカル環境からプッシュした時のコミットメッセージを流用してコミットしている。

**20行目**
リモートリポジトリのstorybookブランチにプッシュしている。Travis CI上のstorybookブランチはリモートリポジトリ上のそれとヒストリが繋がっていないため、`--force` オプションを付与して強制的にプッシュしている。

### ドキュメント

```
22: - git checkout -b gh-pages master
23: - git subtree add --prefix docs origin gh-pages
24: - rm -rf docs
25: - node node_modules/.bin/typedoc --out docs src
26: - touch docs/.nojekyll
27: - git add docs --force
28: - git commit -m "$TRAVIS_COMMIT_MESSAGE"
29: - git subtree push --prefix docs origin gh-pages
```

**22行目**
masterブランチを基準にgh-pagesブランチを新規作成している。

**23行目**
origin/gh-pagesブランチをサブツリーとして追加し、`/docs` と同期している。

**24行目**
TypeDocは出力先ディレクトリを都度クリアしてドキュメントを生成するが、TypeDocが生成し得ないファイルが存在するとエラーを発生させる。これを避けるため、出力先ディレクトリを削除している。

**25行目**
TypeDocにてドキュメントを自動生成している。

**26行目**
GitHub Pagesはjekyllの処理を経て公開されるが、TypeDocが出力する多くのファイルに見られるようなアンダースコアから始まるファイルは除外される。これは避けるため、gh-pagesブランチのルートディレクトリとなる場所に `.nojekyll` を配置し、jekyllの処理を通さず公開している。

**27行目**
ローカル環境からプッシュした時のコミットメッセージを流用してコミットしている。

**27行目**
`/docs` が `.gitignore` に記述されているため、`--force` オプションを付与して強制的にステージングしている。

**29行目**
`/docs` をサブツリーとしてorigin/gh-pagesブランチにプッシュしている。