CircleCIを使ってNuxt.jsをGitHub Pagesへデプロイする

# やったこと
職場でCircleCIを導入しているプロダクトが増えていることもあり、勉強がてらCircleCIを使ってNuxt.jsをGitHub Pagesにデプロイして公開してみました。 
本記事ではその手順について記載しています。

![flow-1.png](https://qiita-image-store.s3.amazonaws.com/0/191671/b0444b7c-2827-20ec-005f-facd79425fe2.png)
デプロイのプロセスとしてはこのような流れで行うようにしています。
❶.  Nuxt.jsのソースをmasterブランチにpushする
❷. CircleCIでlint, testを実施する
❸. CircleCIで公開ファイルを生成して、それをgh-pagesブランチへpushしてサイトを公開

開発環境は以下で行いました。

```txt
os: mac
npm: 6.4.1
node: v8.11.1
```

実際のソースはこちらで公開しています。
GitHub: https://github.com/yagisuke/nuxt-circle-ci
公開したサイト: https://yagisuke.github.io/nuxt-circle-ci

# 手順
開発手順です。これに沿って記載していきます。
なお、lintやtestの設定はしていますが、今回のミッションは「CircleCIを使ってNuxt.jsをGitHub Pagesにデプロイすること」なので、とりあえず設定したという程度で記載しています。

1. [CircleCIに登録する](#circleciに登録する)
2. [Nuxt.jsのプロジェクトを用意する](#nuxtjsのプロジェクトを用意する)
3. [lintを書いて設定する](#lintを書いて設定する)
4. [testを書いて設定する](#testを書いて設定する)
5. [CircleCIの実行ファイルを用意する](#circleciの実行ファイルを用意する)
6. [GitHub Pages用の公開ファイルを生成する](#github-pages用の公開ファイルを生成する)
7. [GitHub Pagesへのデプロイ処理を追加する](#github-pagesへのデプロイ処理を追加する)
8. [CircleCIのプロジェクトに追加する](#circleciのプロジェクトに追加する)
9. [CircleCIに環境変数を設定する](#circleciに環境変数を設定する)
10. [秘密鍵と公開鍵を登録する](#秘密鍵と公開鍵を登録する)
11. [CircleCIのステータスバッヂをつける](#circleciのステータスバッヂをつける)

## CircleCIに登録する
さっそくですが、CircleCIに登録していきます。

それではCicleCIの[登録画面](https://circleci.com/signup/)を開きます。
![circleci-register-1.png](https://qiita-image-store.s3.amazonaws.com/0/191671/628c4ff1-8307-dfeb-70b4-8370705cd11a.png)
GitHubアカウントと連携してアカウントを作成。
![circleci-register-2.png](https://qiita-image-store.s3.amazonaws.com/0/191671/3316b69b-e73e-9225-ce02-2bff92e31468.png)
CircleCIへの登録が完了しました。
![circleci-register-3.png](https://qiita-image-store.s3.amazonaws.com/0/191671/b5be60b1-9335-e170-5312-67003cf132cf.png)

## Nuxt.jsのプロジェクトを用意する
続いてGitHubにリポジトリを作り、そこにNuxt.jsのプロジェクトをpushします。
Nuxt.jsは以下のようにVue CLIで準備しました。

```sh
$ npm install -g @vue/cli @vue/cli-init
$ vue init nuxt-community/starter-template nuxt-circle-ci
$ cd nuxt-circle-ci
$ npm install
$ npm run dev
```

起動しておなじみの画面が表示されたら、用意したGitHubのリポジトリにpushしときます。
![completed.png](https://qiita-image-store.s3.amazonaws.com/0/191671/67e7c50a-2cd5-adff-b22b-ba4e1d6eb912.png)
commit log: https://github.com/yagisuke/nuxt-circle-ci/commit/b23eed087fcf10dd1af752e386bc24bcbcdcec97  

## lintを書いて設定する
今回は[Nuxt.jsのドキュメント](https://ja.nuxtjs.org/guide/development-tools/#eslint-と-prettier) と同じく、[ESLint](https://eslint.org/)と[Prettier](https://prettier.io/)を設定しました。

設定内容も[Nuxt.jsのドキュメント](https://ja.nuxtjs.org/guide/development-tools/#eslint-と-prettier)と同じようにしています。ただし、JSのフォーマットは、ダブルクオートよりシングルクオートの方が好みなので、ドキュメントのサンプルに加えて `.eslintrc.js` に `"singleQuote": true` のオプションを追加しています。詳しくは、下記commit logの.eslintrc.jsを参照ください。

また、`package.json` のscriptsに、lintコマンドを忘れずに登録しておいてください。
CircleCIのタスクで後ほど使用します。

```json
"scripts": {
  ...略
 "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
 ...略
}
```

commit log: https://github.com/yagisuke/nuxt-circle-ci/commit/d31a11e15e1fd8ba345fd6a898a96a598d72c83d  

## testを書いて設定する
lintの設定と同様に、testについても[Nuxt.jsのドキュメント](https://ja.nuxtjs.org/guide/development-tools/#エンドツーエンドテスト)に揃えて[AVA](https://github.com/avajs/ava)で設定しました。

ドキュメントのサンプルでは、 `page/index.vue` を `test/index.test.js` でテストをしていますが、トップページを変更したくなかったので、 `page/ava.vue` を `test/ava.test.js` でテストするようにしています。詳しくはcommit logを参照ください。

また `package.json` のscriptsに、testコマンドを忘れずに登録しておいてください。
CircleCIのタスクで後ほど使用します。

```json
"scripts": {
  ...略
 "test": "ava"
 ...略
}
```

commit log: https://github.com/yagisuke/nuxt-circle-ci/commit/23fc386761af0ff15e312ed785d53910e4ee1f8e  

## CircleCIの実行ファイルを用意する
CircleCIの実行ファイル `.circleci/config.yml` を用意します。
実行ファイルの中身は以下のように記載し、プロジェクトのrootディレクトリに配置します。

```yml
version: 2
jobs:
  build:
    docker:
      - image: circleci/node:8
    steps:
      - checkout
      - restore_cache:
          keys:
            - npm-{{ checksum "package.json" }}
      - run: npm install
      - save_cache:
          paths:
            - node_modules
          key: npm-{{ checksum "package.json" }}
      - run: npm run lint
      - run: npm run test
  deploy:
    docker:
      - image: circleci/node:8
    steps:
      - checkout
      - restore_cache:
          keys:
            - npm-{{ checksum "package.json" }}
      - run: npm install
      - run: git config user.name $GITHUB_NAME
      - run: git config user.email $GITHUB_EMAIL
      - run: npm run generate:gh-pages
      - run: npm run deploy:gh-pages
workflows:
  version: 2
  build-deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: master
```

commit log: https://github.com/yagisuke/nuxt-circle-ci/commit/0afeb3c163e2ed6f106e09cd4d4a21262367937b  

実行ファイルを以下の順にもう少し見ていきます。
1. [config.ymlの全体像](#configymlの全体像)
2. [buildジョブについて](#buildジョブについて)
3. [deployジョブについて](#deployジョブについて)
4. [workflowsについて](#workflowsについて)

### config.ymlの全体像
ざっくり説明すると、ブランチにソースをpushしたら、lintとtestを実施して、
gh-pagesブランチに公開ファイルをpushするようにしています。

![flow-1.png](https://qiita-image-store.s3.amazonaws.com/0/191671/b0444b7c-2827-20ec-005f-facd79425fe2.png)
冒頭でも記載しましたが、今回のGitHub Pagesへのデプロイはこのような流れとなっています。
❶.  Nuxt.jsのソースをmasterブランチにpushする
❷. CircleCIでlint, testを実施する
❸. CircleCIで公開ファイルを生成して、それをgh-pagesブランチへpushしてサイトを公開

この❷、❸がCircleCIのジョブとなり、このように記載しています。

```yml
version: 2
jobs:
  # ジョブを登録
  build: # ❷のジョブ
    # lintを実施する
    # testを実施する
  deploy: # ❸のジョブ
    # 公開ファイルを生成する
    # 公開ファイルをgh-pagesブランチへpushする
workflows:
  # ❷のジョブと❸のジョブを呼び出す
```

### buildジョブについて
buildジョブでは、package.jsonに定義したlintとtestを実施します。
(ジョブの名前はtestとかにしておけば良かったかも)

```yml
build:
  docker:
    - image: circleci/node:8
  steps:
    - checkout
    - restore_cache:
        keys:
          - npm-{{ checksum "package.json" }}
    - run: npm install
    - save_cache:
        paths:
          - node_modules
        key: npm-{{ checksum "package.json" }}
    - run: npm run lint
    - run: npm run test
```  

まず、lintとtestを実行するためには、nodeが必要なので、docker上にnode環境を作り、そこにソースをチェックアウトして、`npm install`で必要なパッケージをインストールします。インストールが終わったら、lintとtestを実施してbuildジョブは終了します。
(npmとかyarnはdockerイメージに用意してくれている)

`restore_cache`と`save_cache`については、npm installでできた`node_modules`を`save_cache`でキャッシュして、package.jsonに変更がなければ、`restore_cache`でキャッシュを再利用しています。

### deployジョブについて
deployジョブでは、公開ファイルを生成して、それをgh-pagesブランチへpushしています。

```yml
deploy:
  docker:
    - image: circleci/node:8
  steps:
    - checkout
    - restore_cache:
        keys:
          - npm-{{ checksum "package.json" }}
    - run: npm install
    - run: git config user.name $GITHUB_NAME  # GitHubのアカウント情報を設定
    - run: git config user.email $GITHUB_EMAIL
    - run: npm run generate:gh-pages  # 公開ファイルを生成
    - run: npm run deploy:gh-pages    # gh-pagesブランチへpush
```

「GitHubのアカウント情報を設定」のタスクで指定している`$GITHUB_NAME`と`$GITHUB_EMAIL`は、CircleCIのサイト上で変数を登録することができ、それを使用しています。
また、「公開ファイルを生成」と「gh-pagesブランチへpush」するタスクは、package.jsonに後ほど登録します。これらについては後ほど説明します。

### workflowsについて
config.ymlの一番下にあるworkflowsのブロックです。
workflowsは、jobsに指定したジョブを実行をすることができ、またその実行を制御することができます。

#### ジョブを実行させる
ジョブを実行させるにはこのように書きます。
この場合、「build」と「deploy」ジョブは並列で実行します。
今回は「build -> deploy」の順に実行させたいので、その制御を次で行ないます。

```yml
jobs:
  build:
    ...略
  deploy:
    ...略
workflows:
  version: 2
  build-deploy:
    jobs:
      - build  <-- buildジョブを実行
      - deploy <-- deployジョブを実行
```

#### 実行の順番を制御する
実行の順番を制御するには、`requires`を使います。
「build -> deploy」というジョブの順番にしたい場合は下記のように記載します。
ただし、「deploy」ジョブは、masterブランチにpushされた時しか実行して欲しくないので、その制御を次で行います。

```yml
jobs:
  ...略
workflows:
  ...略
      - build
      - deploy:
          requires:  <-- ここ
            - build
```

#### 特定のブランチでしか実行しないようにする
ジョブを実行するかどうかの条件は`filters`で指定し、特定のブランチでしか実行したくないジョブは、`branches`に記載します。今回の「deploy」ジョブは、masterブランチでしか実行したくないので、このようになりました。

```yml
jobs:
  ...略
workflows:
  ...略
      - deploy:
          ...略
          filters:  <-- ここ
            branches:
              only: master
```

workflowsについて詳しく調べたい方は[本家の解説](https://circleci.com/docs/2.0/workflows/)や、[こちらの記事](https://qiita.com/sawadashota/items/ba89382d563bc90bb5cd)をオススメします。

## GitHub Pages用の公開ファイルを生成する
Nuxt.jsは、アプリケーションをビルドして、ルートごとにHTMLファイルを生成してくれる`nuxt generate`という便利なコマンドを用意しています。
参照: https://ja.nuxtjs.org/guide/commands/

そのコマンドを実行すれば簡単に公開ファイルを生成できますが、GitHub PagesのURLは、`https://<username>.github.io/<repository-name>`となるため(カスタムドメインがない場合)、アプリケーションのベースURLに`/repository-name/`を設定する必要があります。

具体的には以下のようにソースを修正しています。

package.jsonのscripts

```json
"scripts": {
  ...略
  "generate:gh-pages": "DEPLOY_ENV=GH_PAGES nuxt generate"
}
```

nuxt.config.js

```js
// GitHub Pageへのデプロイ時には、ベースURLを変更している。
var baseRoute = env => (env === 'GH_PAGES' ? '/nuxt-circle-ci/' : '/')

module.exports = {
  ...略
    link: [{
      rel: 'icon',
      type: 'image/x-icon',
      href: baseRoute(process.env.DEPLOY_ENV) + 'favicon.ico'
    }]
  },
  router: {
    base: baseRoute(process.env.DEPLOY_ENV)
  },
  ...略
}
```

上記のソースは、[Nuxt.jsのドキュメント](https://ja.nuxtjs.org/faq/github-pages/#リポジトリに-github-pages-をデプロイする)に記載してあるものと一緒ですので、詳しくはそちらをご覧ください。

commit log: https://github.com/yagisuke/nuxt-circle-ci/commit/ca55cd1655e2a08b01c95d18bd0b2cf3c8c05d16  

## GitHub Pagesへのデプロイ処理を追加する
上記で生成された公開ファイルをgh-pagesブランチへpushする処理を追加していきます。
今回は[こちら](https://qiita.com/star__hoshi/items/490959aee12dbf528f7c)を参考に、[gh-pages](https://www.npmjs.com/package/gh-pages)というパッケージを使用してpushするようにしました。

まずは、`gh-pages`のインストールを行います。

```sh
$ npm install gh-pages --save-dev
```

次に、package.jsonのscriptsにdeployコマンドを設定します。

```json
"scripts": {
  ...略
  "deploy:gh-pages": "gh-pages -d dist -m 'chore: update [skip ci]' -t"
  ...略
}
```

commit log: https://github.com/yagisuke/nuxt-circle-ci/commit/7bd778e1eda378119aa1cca5a9fc39241f7be30a  

### gh-pagesパッケージのオプションについて
`gh-pages`のオプションを見ていきます。

#### -d dist
pushするディレクトリを指定します。
公開ファイルが配置される `dist` ディレクトリを指定します。

#### -m 'chore: update [skip ci]'
コミットメッセージを指定します。
リポジトリにコミットすると、CicleCIのジョブが起動してしまいますが、`[skip ci]`をコミットメッセージに含めると、起動せずにskipすることができます。

#### -t
pushするディレクトリ内のファイルを、`ドットファイルを含めて`pushするように指定しています。

GitHub Pagesは、defaultでjekyllという静的サイトジェネレータを採用しており、そのjekyllが、`_(アンダースコア)`から始まるディレクトリを読み込まないようにしてしまいます。
参照: https://blog.github.com/2009-12-29-bypassing-jekyll-on-github-pages/

<img width="287" alt="dist.png" src="https://qiita-image-store.s3.amazonaws.com/0/191671/7610a96a-bd38-8dfa-535b-5c7e7fe58bae.png">
`nuxt generate`で生成した公開ファイルの中には、`_nuxt`という`_(アンダースコア)`から始まるディレクトリがあるため、jekyllを使用して公開するとエラーとなります。それを避けるためにも、`nuxt generate`がdefaultで生成してくれる`.nojekyll`というファイルをpushして、jekyllを使用しないようにする必要があります。

## CircleCIのプロジェクトに追加する
CircleCIにGitHubのリポジトリを紐づけていきます。

まずは、CircleCIの「Add Projects」画面を開き、作業リポジトリの「Set Up Project」ボタンを押下します。
![s_setting-1.png](https://qiita-image-store.s3.amazonaws.com/0/191671/e4ba0cb7-0e5c-434b-648d-48a7d18034af.png)


「Set Up Project」画面が開くので、以下のように設定して、「Start building」ボタンを押下します。

```txt
Operating System: Linux
Language: Node
```

![s_setting-2.png](https://qiita-image-store.s3.amazonaws.com/0/191671/76cc0484-4db3-8b44-6405-232bb74041f7.png)
すると、CircleCIの実行ファイルのジョブが起動して、エラーになります。
これは、実行ファイルに定義した`$GITHUB_NAME`と`$GITHUB_EMAIL`が未設定なためです。
また、CircleCIからリポジトリにpushするためには、リポジトリのwrite権限が必要になるのでそちらも後ほど設定していきます。
![s_setting-4.png](https://qiita-image-store.s3.amazonaws.com/0/191671/a4148a64-9909-4930-fa11-ea19769259c6.png)

## CircleCIに環境変数を設定する
CircleCIの実行ファイル`.circle/config.yml`に設定した`$GITHUB_NAME`と`$GITHUB_EMAIL`の変数をCircleCIに登録していきます。

まずは「Jobs」画面を開いて、リポジトリの歯車アイコンを押下します。
<img width="454" alt="s_setting-5.png" src="https://qiita-image-store.s3.amazonaws.com/0/191671/62cb7edf-3fc7-c6de-a068-a89606231fbc.png">
プロジェクトの「Settings」画面が開くので、「Environment Variables」メニューを選び、「Add Variable」ボタンを押下して、変数を登録していきます。
![s_setting-7.png](https://qiita-image-store.s3.amazonaws.com/0/191671/113ad540-9324-8dac-ad52-7dffa9803bd7.png)
こちらは`$GITHUB_NAME`の設定です。
![s_setting-8.png](https://qiita-image-store.s3.amazonaws.com/0/191671/26831e62-e7d4-83d9-e3d5-cf3d3ff70dc8.png)
こちらは`$GITHUB_EMAIL`の設定です。
![s_setting-9.png](https://qiita-image-store.s3.amazonaws.com/0/191671/c57497a7-6b88-1e13-92c2-db7b36ecf20d.png)
ここで設定した変数は、実行ファイルから直接利用することができます。

## 秘密鍵と公開鍵を登録する
リポジトリに公開ファイルをpushするためには、CircleCIに秘密鍵を登録し、GitHubに公開鍵を登録する必要があるので、それを行なっていきます。

### 秘密鍵と公開鍵を作成する
以下のコマンドを叩いて、`秘密鍵(id_rsa)`と`公開鍵(id_rsa.pub)`を作成しました。

```sh
$ ssh-keygen -t rsa -b 4096 -C 任意の文字列 -f ./id_rsa # パスワードは空にすること
```

### GitHubに公開鍵を登録する
先ほど作成した`公開鍵(id_rsa.pub)`をGitHubに登録していきます。

まずはリポジトリの「settings」を開き、「Deploy keys」画面を開きます。
すでにCircleCIにはread権限が付与されていますが、今回はwrite権限が必要なので、
「Add Deploy key」ボタンから新たに追加します。
![s_gh-1.png](https://qiita-image-store.s3.amazonaws.com/0/191671/e06e9cd0-32e0-360c-c343-3e9cde2c7288.png)
`公開鍵(id_rsa.pub)`のファイルの中身を貼り付け、`Allow write access`にチェックをつけて追加してください。
![s_gh-2.png](https://qiita-image-store.s3.amazonaws.com/0/191671/6f6e4f96-01f9-1007-2182-76c2cec90f2e.png)
登録が完了したら、元からあったread onlyのKeyは不要になるので削除して、次はCircleCIに秘密鍵を設定します。
![s_gh-3.png](https://qiita-image-store.s3.amazonaws.com/0/191671/4e0a36f9-611b-e596-3824-a2592794e96c.png)

### CircleCIに秘密鍵を登録する
`秘密鍵(id_rsa)`をCircleCIに登録していきます。

CircleCIのプロジェクトの「Settings」画面を開いて、「SSH Permissions」メニューを選択します。
「Add SSH Key」ボタンを押下して秘密鍵を登録していきます。
![s_circle-2.png](https://qiita-image-store.s3.amazonaws.com/0/191671/025a2d68-dba2-e8ab-689a-0eecd7c7f7fb.png)
以下のように`秘密鍵(id_rsa)`を入力して、「Add SSH Key」を押して登録します。
![s_circle-3.png](https://qiita-image-store.s3.amazonaws.com/0/191671/01b31394-433a-ffed-8152-257592a9dabf.png)

また、GitHubで削除したread権限しかないkeyを削除しときます。
CircleCIのプロジェクトの「Settings」画面を開いて、「Checkout keys」メニューを選択します。
そこで登録されている認証keyの`×`ボタンを押して削除します。
![s_circle-1.png](https://qiita-image-store.s3.amazonaws.com/0/191671/08459d4a-5e5d-8efc-9418-80642fbc6bae.png)
参考: https://mikoto2000.blogspot.com/2018/05/circleci-sphinx-gh-pages.html#gh-pages-ブランチへ-push-するための設定

## CircleCIのステータスバッヂをつける
CircleCIのプロジェクトの「Settings」画面を開いて、「Status Badges」メニューを選択します。
![s_badge.png](https://qiita-image-store.s3.amazonaws.com/0/191671/5623ff8f-e33e-c385-5634-a398ecfcbc6e.png)
以下のようにして、出力された`Embed Code`をREADME.mdに追加してpushしました。これで、masterブランチのCircleCIのステータスがREADME.mdに表示されるようになります。

```txt
Branch: master
API Token: None
Embed Code: Markdown
```

commit log: https://github.com/yagisuke/nuxt-circle-ci/commit/a7f56b89cfc27bba8ea613383067571a4778cc87

## おわり
CircleCIのジョブが無事に終了すれば、GitHub Pagesが公開されます。

長文でまとまりのない文章になってしまいましたが、私個人としては色々はまって良い勉強の機会とすることができました。

お付き合いいただきありがとうございました。