Firebaseで必要な環境設定をまとめていたら思ったより長くなってしまいました。
ClientもServerもtypescriptで書いたり共通のlint設定などで管理する場合モノレポ形式でやると良いです。
ビルド環境からテスト環境、CIの環境まで解説します。
完成後のレポジトリはこちらです:https://github.com/kyasbal-1994/firebase-monorepo
イケてるモノレポ環境構築
Node.jsでサーバーサイドを書いたり、複数のクライアントプロジェクトがあると以下のような問題に直面すると思います。
- 共用できるコードなのに別々に書いてる
- 一度持ってきた共用コードなのに古くなってる
- 同じようなプロジェクトなのにデプロイの設定とかいろいろがオリジナリティ溢れる
- チーム内で分担によって知らないコードが増えてしまう
このような問題を解決すべく、Lernaを用いたプロジェクトを作成します。
先ほどのような実例としての問題だけではなく、これによって作れる複数レポジトリを一つにまとめて全体を管理するやり方では以下のような欠点、利点があるとされます。
Pros
パッケージ間が密に連携していることによる問題を解決できる
どちらのリポジトリにIssue登録するか問題
1つ問題を修正するのに、複数リポジトリを修正してそれぞれ別にプルリク送らないといけない問題
packageをまたいだテストを行いやすくなる
各パッケージで共通のnpm moduleはトップレベルのpackage.jsonおよびnode_modulesで管理でき、ストレージ節約になる
バージョン番号を統一できる
git submoduleで管理するのと比べるとgit submodule updateを意識する必要がなくなるので環境構築が楽になる(個人差ありそう)
Cons
リポジトリがでかくなる
エディタの動作が重くなるかもしれない
lernaの学習コストがかかる
lernaの扱いが面倒な部分がある
lernaを使ってnpmプロジェクトをモノレポ化するより引用
今回作ってみるプロジェクト構成
- クライアントサイド
- webpack
- Typescript
- babel
- React.js
- サーバーサイド
- Node.js
- Firebase function
さらにそれぞれstaging
環境とproduction
環境を分けた実践的なプロジェクト構成を作ってみるものとし、それぞれCircleCIを用いてデプロイされるような環境までを作成します。
プロジェクト構造作成編
モノレポの作成 / サブプロジェクトの作成
$ mkdir firebase-monorepo
$ cd firebase-monorepo
$ npm install lerna --save-dev
$ npx lerna init
最後のlerna init
をすると以下のようなログが出力され、monorepoを構成する複数のプロジェクトが入るpackages
フォルダが生成されます。
lerna notice cli v3.4.3
lerna info Initializing Git repository
lerna info Creating package.json
lerna info Creating lerna.json
lerna info Creating packages directory
lerna success Initialized Lerna files
さらに、いい感じにgitignoreも作っておきます。lernaでは各サブプロジェクトにnode_modules
フォルダができるのでこれらもignoreしておきます。
$ gibo dump Node MacOS Windows Linux > .gitignore
$ echo "**/node_modules" >> .gitignore
$ echo "**/lib" >> .gitignore
$ echo "**/dist" >> .gitignore
以下のようにlerna create
を用いてサブプロジェクトを作成します。createの後ろにつくプロジェクト名は必ずしも@XXX
によって先行する必要はありませんが、統一のために共通のパッケージの名前空間(@XXX
の部分のこと)を持つと良いでしょう。
$ lerna create @firebase-monorepo/server
すると以下のようにいろいろ聞かれ(npm initと基本は同じはずです)、サブプロジェクトがpackages/server
として生成されます。
package name: (@firebase-monorepo/server)
version: (0.0.0)
description:
keywords:
homepage:
license: (ISC)
entry point: (lib/server.js) lib/index.js
git repository:
今後の進行のためentrypoint
はlib/index.js
を指定します。
また、package.json
にtypings
という属性を追加しlib/index.d.ts
を指定します。
他のサブプロジェクトも作っておきましょう。作るサブプロジェクトは今作った@firebase-monorepo/server
を含めて以下の3つです。
-
@firebase-monorepo/server
・・・ Firebase functionにpublishされるサーバ本体 -
@firebase-monorepo/client
・・・ Firebase hostingにpublishされるクライアント -
@firebase-monorepo/api-schema
・・・クライアントとサーバが共有するAPIのスキーマ
$ lerna create @firebase-monorepo/client
$ lerna create @firebase-monorepo/api-schema
プロジェクトの参照関係を追加する
プロジェクト名を見るとわかる通り、api-schema
はserver
とclient
の両方から参照されています。
以下のようにしてserver,clientからapi-schemaへの参照関係を作ります。
$ lerna add @firebase-monorepo/api-schema --scope @firebase-monorepo/server
$ lerna add @firebase-monorepo/api-schema --scope @firebase-monorepo/client
各サブプロジェクトの中のnode_modules
がエイリアスを貼ってくれたのがわかると思います。
ビルド環境作成編
Typescriptの環境をセットアップする
今回のプロジェクトではtypescriptはどのプロジェクトでも参照します。ルート階層でlerna add
を用いて外部のプロジェクトも追加することができます。
$ lerna add typescript
ルート階層に以下のような内容のtsconfig.jsonを作成します。
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"lib": [
"es6"
]
},
"exclude": [
"node_modules"
]
}
これらのtsconfig.jsonを参照するようにpackages/server/tsconfig.json
,packages/client/tsconfig.json
,packages/api-schema/tsconfig.json
の3つを同じように以下のように作ります。
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir":"./lib",
"declarationDir": "./lib",
"rootDir": "./src",
"baseUrl": "./"
},
"include": ["./src"]
}
サブプロジェクトそれぞれのpackage.json
にbuildコマンドを追加します。
"scripts":{
"build":"tsc"
}
packages/api-schema/src/index.ts
に以下のように記述しましょう。
export class CounterResult{
public count:number;
}
lerna run build
コマンドでプロジェクトそれぞれでビルドを走らせます。
$ lerna run build
他にファイルがないのでエラーが一部出ると思いますが、これでそれぞれのプロジェクトにnpm run build
しているのと同じ状況になります。
試しにserver.ts
でこのファイルを参照してみましょう。
import {CounterResult} from "@firebase-monorepo/api-schema";
CounterResult.name
しっかりとTypescriptの型が効いていると思います。
Prettierを設定する
まずはビルド環境よりlint環境です。コードを書き始めてからでは遅いので先に設定します。
全プロジェクトで同じコーディング規約を定めることとしましょう。
lerna add prettier --dev
をしてPrettierを追加します。
$ lerna add prettier --dev
ルートディレクトリに.prettierrc.json
を設置して、各プロジェクトに.prettierignore
を設置します。
.prettierignore.json
**/package-lock.json
*.scss.d.ts
**/lib/
**/node_modules/
.prettierrc.json
{
"semi": false,
"singleQuote": true
}
各プロジェクトのpackage.jsonのscriptsにlintを追加します。
"scripts":{
"lint":"lint": "prettier --list-different './src/**/*.{js,ts,tsx,scss,json}'",
"lint:format": "prettier --write './src/**/*.{js,ts,tsx,scss,json}'"
}
同様にしてlerna run lint:format
などで動作をチェックすることが可能です。
Huskeyでgitコマンドのフックをしてlint-stagedでformatを強制する
$ npm install huskey --dev
$ lerna add lint-staged
huskeyだけはルートにだけ存在すればいいのでnpm installをする。
huskeyでprecommitをフックしてコミット前にlintしてstageすることを強制する。
ルートのpackage.jsonに以下のような設定を追加する。
"husky": {
"hooks": {
"pre-commit": "lerna exec npx lint-staged"
}
}
これにより、pre-commit
時に全てのサブプロジェクトでlint-staged
が実行される。これによってgitのステージングに上がっているファイルにprettierをかけることができる。
各プロジェクトのpackage.jsonには以下のようにlint-staged
の設定を置く。
"lint-staged": {
"./src/**/*.{js,ts,tsx,scss,json}": [
"npm run lint:format",
"git add"
]
}
クライアント側のビルドの設定をする
必要そうなmoduleをどんどん入れます。lernaだと複数個を一気に追加できないので今回だけclientのフォルダに入ります。まずはバンドリング周りの設定から。今回はwebpackでreactの環境をビルドすることを想定します。
$ cd ./packages/client
$ npm i webpack webpack-cli webpack-dev-server webpack-merge html-webpack-plugin \
mini-css-extract-plugin uglifyjs-webpack-plugin \
optimize-css-assets-webpack-plugin babel-loader typings-for-css-modules-loader \
sass-loader postcss-loader style-loader \
@babel/core @babel/plugin-proposal-class-properties \
@babel/preset-typescript @babel/plugin-proposal-decorators @babel/polyfill \
@babel/preset-env @babel/preset-react cpx --save-dev
各moduleの説明
-
webpack
,webpack-cli
: webpackを使うために最小限必要なもの -
webpack-dev-server
: 開発用のサーバを開いてくれるやつ -
webpack-merge
: webpackのコンフィグをマージするやつ - webpackプラグインたち
-
html-webpack-plugin
: ビルド時に同時にhtmlを生成するプラグイン -
mini-css-extract-plugin
: 一つのcssにまとめ直してくれるプラグイン -
uglifyjs-webpack-plugin
: jsのminifyのプラグイン -
optimize-css-assets-webpack-plugin
: CSSの最適化を行なってくれるプラグイン
-
- webpackのローダーたち
-
babel-loader
: Babelを通すためのローダ。今回はtsのコンパイルにも用いる。 -
typings-for-css-modules-loader
: CSS modulesの型定義を生成するローダ -
scss-loader
: SCSSのコンパイルをするローダ -
postcss-loader
: CSSのAutoprefixをするため -
style-loader
: デバッグ時にスタイルを埋め込むためのローダ
-
- Babel系
-
@babel/core
: Babel本体 -
babel-preset-env
: Babelの基本的なプリセットを選んでくれるやつ -
@babel/preset-typescript
: Typescriptのビルドを行なってくれるプリセット -
@babel/preset-react
: Reactのjsx記法のビルドを行なってくれるプリセット -
@babel/polyfill
: 古いブラウザサポート用のpolyfill -
@babel/plugin-proposal-decorators
: デコレータ記法を使えるようにするためのplugin -
@babel/plugin-proposal-class-properties
: React周りでよく見るやつ。staticメンバを追加できるようになる
-
- その他
-
cpx
: コピーに使う。あとで出てきます。
-
これらをインストールした上で以下の3つのwebpack.config.jsによって成り立たせます。
- webpack.config.js
- webpack.dev.config.js
- webpack.prod.config.js
packages/client/webpack.config.js
const path = require("path");
const webpack = require("webpack");
const HtmlPlugin = require("html-webpack-plugin");
module.exports = {
entry: {
index: ["@babel/polyfill", "./src/index.tsx"]
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].[hash].js"
},
plugins: [
new HtmlPlugin({
filename: "index.html",
template: "template/index.html",
minify: {
removeComments: true,
collapseWhitespace: true
}
})
],
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: "babel-loader"
}
]
}
]
},
resolve: {
extensions: [".js", ".json", ".ts", ".tsx"]
}
};
webpack.dev.config.js
const webpack = require("webpack");
const common = require("./webpack.config");
const merge = require("webpack-merge");
const devSpecific = {
mode: "development",
devtool: "eval-source-map",
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("development")
})
],
module: {
rules: [
{
test: /\.s?css$/,
use: [
{
loader: "style-loader"
},
{
loader: "typings-for-css-modules-loader",
options: {
modules: true,
sass: true,
namedExport: true,
camelCase: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
},
{
loader: "sass-loader"
}
]
}
]
}
};
module.exports = merge(common, devSpecific)
webpack.prod.config.js
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const common = require("./webpack.config");
const merge = require("webpack-merge");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const prodSpecific = {
mode: "production",
devtool: "source-map",
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: false,
extractComments: true
}),
new OptimizeCssAssetsPlugin()
]
},
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
}),
new MiniCssExtractPlugin(),
new webpack.optimize.OccurrenceOrderPlugin()
],
module: {
rules: [
{
test: /\.s?css$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: "typings-for-css-modules-loader",
options: {
modules: true,
sass: true,
namedExport: true,
camelCase: true,
localIdentName: '[hash:base64:4]'
}
},
{
loader:"postcss-loader"
},
{
loader: "sass-loader"
}
]
}
]
}
};
module.exports = merge(common, prodSpecific);
注釈
- entry.outputを
filename: "[name].[hash].js"
のように書いておくとjsにhashが追加された形で生成されるためにキャッシュされなくなります
このままだとstaticなファイルの置き場所に困るのでpackages/client/static
を作成します。その上でビルド後にstaticのファイルをdistにコピーされるように設定するため、cpx
を入れます。
htmlのテンプレートとしてpackages/client/templates/index.html
を作成します。内容は以下のようなもので、ビルドされたjsへの参照は自動的に追加されるようになっています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ものれぽ</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
lerna add cpx --scope @firebase-monorepo/client
client側のpackage.jsonにビルド、デバッグ用のscriptsを追加します。
"build": "webpack --config ./webpack.prod.config.js && cpx 'static/**/*' dist --verbose",
"build:dev": "webpack --config ./webpack.dev.config.js && cpx 'static/**/*' dist --verbose",
"start": "webpack-dev-server --watch --config ./webpack.dev.config.js --content-base static/"
さて、ここでlerna run build
でクライアントも一緒にビルドされる様子をみたいところなのですが、先ほどnpm installで一気にインストールしてしまったのでこのままだとサブプロジェクトへの参照がうまく機能しません。
lerna clean
lerna bootstrap
を行うとlerna run build
でクライアントがビルドできるようになります。
サーバー側のビルド環境を設定する
Firebase functionでサーバーを立てる場合、Lernaと一緒に使うには少々トリックが必要のようです。
その理由は
- デプロイされると
npm install
を自動で行うので、Lernaがサブプロジェクトをdependencies
に含めた結果シンボリックリンクが動かず、結局動作しない - シンプルに全体をwebpackでバンドリングしてもトップにexportsが必要なのでうまく動作しない(?)
なんにせよサブプロジェクトの部分を含むべくサーバー側もwebpackに通してあげれば動作するはずです。
webpack.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
target: 'node',
entry: {
api: './src/api/api.ts'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name]/[name].js',
libraryTarget: 'commonjs2'
},
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: 'babel-loader'
}
]
}
]
},
resolve: {
extensions: ['.js', '.json', '.ts']
}
}
webpackの設定はこんな感じです。target
がnode
になっていること、output.libraryTarget
がcommonjs2
になっていることを確認してください。
.babelrcは以下のようにします。Typescriptの設定しかしていません。
{
"presets": [
"@babel/preset-typescript"
]
}
src/api/api.ts
を作成して以下のような内容を含めます。
import * as express from 'express'
import { CounterResult } from '@firebase-monorepo/api-schema'
const app = express()
const result = new CounterResult()
app.get("/",(req,res)=>{
res.send(""+result.count);
});
export default app;
src/index.js
も作成し以下のような内容を含めます。
const functions = require('firebase-functions')
const APIRouter = require("./api/api").default;
module.exports = {
api:functions.https.onRequest(APIRouter)
}
さらにデプロイ時に含めるためのsrc/package.json
も作成してこのようにdependencyがほとんどないpackage.jsonを作ります。
{
"name": "release",
"scripts": {
},
"dependencies": {
"firebase-admin": "^6.2.0",
"firebase-functions": "^2.1.0"
},
"devDependencies": {
}
}
こうした上で以下のようなビルドコマンドを書いてdistのサブフォルダにバンドリングされた結果が入っていくようにします。
"build": "webpack && npm run build:copy_entry",
"build:copy_entry": "cp ./src/*.* ./dist",
サーバーサイドをバンドリングするというのはなかなか奇妙な構成ですが、サブプロジェクトの動作もしっかりしますし、型も十分に動作します。サーバー側のエラーもソースマップを用いることができる構成です。
デプロイ環境作成編
Firebaseにデプロイできるようにします。今回はstagin環境
、production環境
の2つを用意することにしましょう。
ここではFirebaseのコンソールから、ステージング用のプロジェクトと本番環境用のプロジェクトの二つを以下のように用意しました。
- qiita-firebase-monorepo(本番環境)
- qiita-firebase-monorepo-stg(ステージング環境)
同じ名前は取れないと思うので、実際に試す方は自分で好きな名前を入れて環境を作成しましょう。
ルートフォルダにてnpm install firebase-tools --save-dev
を実行してfirebase toolsを入れましょう。
ルートのpackage.jsonに以下のように追加します。
"deploy": "lerna run build && npx firebase deploy"
.firebasercを設定する
ルートフォルダ直下に以下のような.firebaserc
を入れます。こうすると、firebase use staging
とするとデプロイ先がstagingになり、firebase use production
とするとデプロイ先が本番環境になります。
{
"projects": {
"production": "qiita-firebase-monorepo",
"staging": "qiita-firebase-monorepo-stg"
}
}
firebase.jsonを作成する
ルートフォルダ直下に以下のようなfirebase.json
を含めます。`
{
"hosting": {
"public": "./packages/client/dist",
"ignore":[
"**/.*"
]
},
"functions": {
"source": "./packages/server/dist"
}
}
それぞれdist
の中身がpublishされることに注意が必要です。さらにfunctionsの方はdist
の中のpackage.jsonのdependencyはデプロイ時にリモート側でインストールされます。必要があればここに外部参照を記述して、webpackのビルドの時に一部のrequireだけ残すのも良いでしょう。
(本当はサブプロジェクトだけバンドリングできたら一番いいのだけれど)
テスト環境作成編
jestを導入する
jestを用いてテスト環境を構築してみましょう。
ルートフォルダで以下のようなコマンドを実行します。
$ lerna add jest --dev
$ lerna add ts-jest-babel-7 --dev
$ lerna add @types/jest --dev
次にサブプロジェクトのそれぞれのpackage.jsonに以下のような項目を追加します。
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest-babel-7"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
さらにpackage.jsonのscriptsのtestコマンドをjest
にします。
"test": "jest"
試しにserverのサブプロジェクトにテストを追加します。src/__test__
を作成して、api.test.ts
を追加します。
import server from "./../api"
import { CounterResult } from "@firebase-monorepo/api-schema";
describe('hello', () => {
it('hello("jest") to be "Hello Jest!!"', () => {
expect('Hello Jest!!').toBe('Hello Jest!!')
})
it('Requireing sub project should work', () => {
expect(CounterResult).not.toBe(null)
})
})
lerna run test
をルートフォルダで実行すればテストが全サブプロジェクトで動作することが確認できます。
コードカバレッジを測定する
各プロジェクトにtest:coverage
を以下のように追加します。jestはこれだけでカバレッジ出してくれます。神
"test:coverage": "jest --coverage",
CI/CD環境作成編
CircleCIを用いていい感じにCI/CD環境を作成します。手動デプロイはどのバージョンがデプロイされているかわからなくなったりしますし、ビルドするの忘れてデプロイしてしまったりします。
.circleci/config.yml
を作成します。
依存関係のインストール+ビルド+テスト
circleciのキャッシュの設定が少々複雑です。というのも、それぞれのサブパッケージ単位でのインストールされたモジュールのキャッシュ化、ビルド済みファイルの同一リビジョンのjob間でのキャッシュの利用のためです。
とりあえずは以下のようなconfig.ymlにします。
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2.1
executors:
node:
docker:
- image: circleci/node:8
commands:
restore_built:
description: "Restore the build result from cache"
steps:
- restore_cache:
key: built-api-schema-{{ .Revision }}
- restore_cache:
key: built-client-{{ .Revision }}
- restore_cache:
key: built-server-{{ .Revision }}
restore_dependency:
description: "Restore the dependencies result from cache"
steps:
- restore_cache:
keys:
- dependencies-root-{{ checksum "package.json" }}
- dependencies-root-
- restore_cache:
keys:
- dependencies-api-schema-{{ checksum "packages/api-schema/package.json" }}
- dependencies-api-schema-
- restore_cache:
keys:
- dependencies-client-{{ checksum "packages/client/package.json" }}
- dependencies-client-
- restore_cache:
keys:
- dependencies-server-{{ checksum "packages/server/package.json" }}
- dependencies-server-
jobs:
install_dependency:
executor: node
steps:
- checkout
- restore_dependency
- run:
name: ルートの依存関係のインストール
command: npm install
- run:
name: サブプロジェクトの依存関係のインストール
command: npx lerna bootstrap
- run:
name: package.jsonの避難
command: cp ./package.json ./package.copy.json
- run:
name: package.jsonの避難
command: cp ./packages/api-schema/package.json ./packages/api-schema/package.copy.json
- run:
name: package.jsonの避難
command: cp ./packages/client/package.json ./packages/client/package.copy.json
- run:
name: package.jsonの避難
command: cp ./packages/server/package.json ./packages/server/package.copy.json
- save_cache:
paths:
- ./node_modules/
key: dependencies-root-{{ checksum "package.copy.json" }}
- save_cache:
paths:
- ./packages/api-schema/node_modules/
key: dependencies-api-schema-{{ checksum "packages/api-schema/package.copy.json }}
- save_cache:
paths:
- ./packages/client/node_modules/
key: dependencies-client-{{ checksum "packages/client/package.copy.json" }}
- save_cache:
paths:
- ./packages/server/node_modules/
key: dependencies-server-{{ checksum "packages/server/package.copy.json" }}
build:
executor: node
steps:
- checkout
- restore_dependency
- run:
name: ビルド
command: npx lerna run build
- save_cache:
paths:
- ./packages/api-schema/lib/
key: built-api-schema-{{ .Revision }}
- save_cache:
paths:
- ./packages/client/dist/
key: built-client-{{ .Revision }}
- save_cache:
paths:
- ./packages/server/dist/
key: built-server-{{ .Revision }}
test:
executor: node
steps:
- checkout
- restore_dependency
- restore_built
- run:
name: テスト
command: npx lerna run test:coverage
workflows:
main:
jobs:
- install_dependency
- build:
requires:
- install_dependency
- test:
requires:
- build
キャッシュの復元部分は今後書いていく他のjobでも共通なので、commandsに書いておくと便利です。
npm install
がpackage.json
を変えてしまう可能性があるので、依存関係をインストールする前にインストール後にハッシュが計算ができるように避難をします。
デプロイの設定をする
ビルドができているのでステージング環境に自動でデプロイされるようにします。
jobs
に以下のコマンドを追加します。
deploy_staging:
executor: node
steps:
- checkout
- restore_dependency
- restore_built
- run:
name: デプロイ先の切り替え(Staging)
command: npx firebase use staging
- run:
name: デプロイ
command: npx firebase deploy
pre_deploy_production:
executor: node
steps:
- run:
name: send notification to slack
command: >
curl -X POST -H 'Content-type: application/json'
--data "{\"text\": \"本番環境へのデプロイにはManual Approvalが必要です。https://XXXXK を確認の上、本番環境へデプロイしていい場合は次のアドレスから承認してください。<https://circleci.com/workflow-run/${CIRCLE_WORKFLOW_ID}|here>.\"}"
$WEBHOOK_URL
deploy_production:
executor: node
steps:
- checkout
- restore_dependency
- restore_built
- run:
name: デプロイ先の切り替え(Production)
command: npx firebase use production
- run:
name: デプロイ
command: npx firebase deploy
workflow全体は以下のようになります。
workflows:
main:
jobs:
- install_dependency
- build:
requires:
- install_dependency
- test:
requires:
- build
- deploy_staging:
requires:
- test
filters:
branches:
only:
- staging
- master
- pre_deploy_production:
requires:
- deploy_staging
filters:
branches:
only:
- master
- approve_deploy_production:
type: approval
requires:
- pre_deploy_production
filters:
branches:
only:
- master
- deploy_production:
requires:
- approve_deploy_production
filters:
branches:
only: master
この設定ファイルで行われるのは以下の通りです。
- stagingブランチもしくはmasterブランチの場合、stagingの方のfirebaseプロジェクトとしてpublishされます。
- masterだった場合、さらにslackなどに通知するjobを通った後で、productionにデプロイする手前でmanual approveを要求します。
- CircleCI上でデプロイにOKをしたら勝手にデプロイされます。
デプロイ時にタグをつける
デプロイされているのがどのバージョンかわかるようにするためにタグをつけることにしましょう。
- run:
name: 'プロダクションリリースへのタグ付け'
command: 'git tag production/rev-${CIRCLE_BUILD_NUM}'
- run:
name: 'タグのpush'
command: 'git push origin --tags'
のようなコードをデプロイに含めるだけでCircleCIからgit上でタグづけされます。非常に便利。
有用な外部サービスと連携する
Codecovとの連携
カバレッジが集計されているのでこれを可視化しましょう。ルートでnpm i codecov --save-dev
をします。
testジョブを以下のように書き換えるとcodecovにカバレッジが表示されるようになります。
test:
executor: node
steps:
- checkout
- restore_dependency
- restore_built
- run:
name: テスト
command: npx lerna run test:coverage
- run:
name: Codecovに送る
command: npx codecov
CircleCIの管理画面でCODECOV_TOKEN環境変数を登録する必要があります。
Cloudflareのキャッシュをパージする
Firebasehostingのドメインを独自ドメインにしている場合などにCloudflareを用いていると、デプロイした場合もCloudflareのキャッシュのせいでクライアントでリロードしても更新されない場合があります。このような場合にはデプロイの最後にキャッシュのパージをするAPIを叩く処理を入れれば大丈夫です。
- run:
name: 'CloudflareのキャッシュをPurge'
command: 'curl -X POST "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONEID/purge_cache"\
-H "X-Auth-Email: $CLOUDFLARE_AUTHMAIL" -H "X-Auth-Key: $CLOUDFLARE_AUTHKEY" -H "Content-Type: \
application/json" --data "{\"purge_everything\":true}"'
CLOUDFLARE_ZONEID、CLOUDFLARE_AUTHMAIL CLOUDFLARE_AUTHKEYの環境変数を設定する必要があります。
CIの設定ファイルの振り返り
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2.1
executors:
node:
docker:
- image: circleci/node:8
commands:
restore_built:
description: "Restore the build result from cache"
steps:
- restore_cache:
key: built-api-schema-{{ .Revision }}
- restore_cache:
key: built-client-{{ .Revision }}
- restore_cache:
key: built-server-{{ .Revision }}
restore_dependency:
description: "Restore the dependencies result from cache"
steps:
- restore_cache:
keys:
- dependencies-root-{{ checksum "package.json" }}
- dependencies-root-
- restore_cache:
keys:
- dependencies-api-schema-{{ checksum "packages/api-schema/package.json" }}
- dependencies-api-schema-
- restore_cache:
keys:
- dependencies-client-{{ checksum "packages/client/package.json" }}
- dependencies-client-
- restore_cache:
keys:
- dependencies-server-{{ checksum "packages/server/package.json" }}
- dependencies-server-
jobs:
install_dependency:
executor: node
steps:
- checkout
- restore_dependency
- run:
name: ルートの依存関係のインストール
command: npm install
- run:
name: サブプロジェクトの依存関係のインストール
command: npx lerna bootstrap
- run:
name: package.jsonの避難
command: cp ./package.json ./package.copy.json
- run:
name: package.jsonの避難
command: cp ./packages/api-schema/package.json ./packages/api-schema/package.copy.json
- run:
name: package.jsonの避難
command: cp ./packages/client/package.json ./packages/client/package.copy.json
- run:
name: package.jsonの避難
command: cp ./packages/server/package.json ./packages/server/package.copy.json
- save_cache:
paths:
- ./node_modules/
key: dependencies-root-{{ checksum "package.copy.json" }}
- save_cache:
paths:
- ./packages/api-schema/node_modules/
key: dependencies-api-schema-{{ checksum "packages/api-schema/package.copy.json" }}
- save_cache:
paths:
- ./packages/client/node_modules/
key: dependencies-client-{{ checksum "packages/client/package.copy.json" }}
- save_cache:
paths:
- ./packages/server/node_modules/
key: dependencies-server-{{ checksum "packages/server/package.copy.json" }}
build:
executor: node
steps:
- checkout
- restore_dependency
- run:
name: ビルド
command: npx lerna run build
- save_cache:
paths:
- ./packages/api-schema/lib/
key: built-api-schema-{{ .Revision }}
- save_cache:
paths:
- ./packages/client/dist/
key: built-client-{{ .Revision }}
- save_cache:
paths:
- ./packages/server/dist/
key: built-server-{{ .Revision }}
test:
executor: node
steps:
- checkout
- restore_dependency
- restore_built
- run:
name: テスト
command: npx lerna run test:coverage
- run:
name: Codecovに送る
command: npx codecov
deploy_staging:
executor: node
steps:
- checkout
- restore_dependency
- restore_built
- run:
name: デプロイ先の切り替え(Staging)
command: npx firebase use staging
- run:
name: デプロイ
command: npx firebase deploy --token "$FIREBASE_TOKEN"
- run:
name: 'プロダクションリリースへのタグ付け'
command: 'git tag staging/rev-${CIRCLE_BUILD_NUM}'
- run:
name: 'タグのpush'
command: 'git push origin --tags'
pre_deploy_production:
executor: node
steps:
- run:
name: send notification to slack
command: >
curl -X POST -H 'Content-type: application/json'
--data "{\"text\": \"本番環境へのデプロイにはManual Approvalが必要です。http://XXXX を確認の上、本番環境へデプロイしていい場合は次のアドレスから承認してください。<https://circleci.com/workflow-run/${CIRCLE_WORKFLOW_ID}|here>.\"}"
$WEBHOOK_URL
deploy_production:
executor: node
steps:
- checkout
- restore_dependency
- restore_built
- run:
name: デプロイ先の切り替え(Production)
command: npx firebase use production
- run:
name: デプロイ
command: npx firebase deploy --token "$FIREBASE_TOKEN"
- run:
name: 'プロダクションリリースへのタグ付け'
command: 'git tag production/rev-${CIRCLE_BUILD_NUM}'
- run:
name: 'タグのpush'
command: 'git push origin --tags'
workflows:
main:
jobs:
- install_dependency
- build:
requires:
- install_dependency
- test:
requires:
- build
- deploy_staging:
requires:
- test
filters:
branches:
only:
- staging
- master
- pre_deploy_production:
requires:
- deploy_staging
filters:
branches:
only:
- master
- approve_deploy_production:
type: approval
requires:
- pre_deploy_production
filters:
branches:
only:
- master
- deploy_production:
requires:
- approve_deploy_production
filters:
branches:
only: master
全体としてこのような設定ファイルになっていれば大丈夫です。これは最低限度のCIの設定で、理想的にはE2Eのテスト環境を導入したかったり、Sentryなどのエラー管理ツールとの連携もする必要があるでしょう。