# 使用 CircleCI 部署 Node.jS App 到 GCP App Engine (一) CI ( Continuous Integration ) 中文為「持續性整合」,目的是讓專案能夠在每一次的變動中都能通過一些檢驗來確保專案品質。CD ( Continuous Deployment ) 中文則為「自動化佈署」,讓專案能夠自動在每次變動後能以最新版本呈現。 首先要改成 react 或者 vue 的話 先要實作一下簡單的 cicd https://medium.com/@zoratai/ci-cd-%E5%BE%9E%E9%9B%B6%E9%96%8B%E5%A7%8B-%E4%BD%BF%E7%94%A8-circleci-%E9%83%A8%E7%BD%B2-node-js-app-%E5%88%B0-gcp-app-engine-856244ba86d7 文章是 去年的 再來順一下 ![](https://i.imgur.com/OCabbty.png) ![](https://i.imgur.com/DBiJXLP.png) # 最終目錄結構 > tree -L 2 -I "node_modules" > ![](https://i.imgur.com/3F0pcfp.png) # 建立資料夾 >mkdir demo-server cd demo-server > # 初始化專案 > yarn init > # 加入版控 >git init > # 忽略 node_modules .gitignore >**/node_modules/ > # 安裝 express 及 babel express 是一個 Node.js 的後端框架,這次 demo 會用來處理 server。 由於 Node.js 處理檔案引入和匯出方法為 require 及 module.export , 而 es6 出現了 import 及 export,如要使用就要使用 babel 來 transpile。 >yarn add express yarn add @babel/core --dev yarn add @babel/cli --dev yarn add @babel/preset-env --dev yarn add @babel/node --dev > # add src/server.js ```javascript= const express = require('express'); const app = express(); const { PORT = 3000 } = process.env; const IS_TEST = !!module.parent; // If there's another file imports server.js, then module.parent will become true app.get('/', (req, res) => { res.status(200).send('Hello!CI/CD!2'); }); if (!IS_TEST) { app.listen(PORT, () => { console.log('Server is running on PORT:', PORT); }); } module.exports = app; ``` # 本機執行 > yarn babel-node src/server.js > ![](https://i.imgur.com/dP9PtPk.png) ![](https://i.imgur.com/j2IeQOH.png) # 使用 nodemon 快速開發 修改原始碼自動刷新 >yarn add nodemon --dev > # 新增nodemon.json ```json= { "//_comment": "monitor src folder", "watch": ["src"], "//_comment": "watch .js and .json extensions", "ext": "js json", "exec": "babel-node src/server.js" } ``` # package.json 插入 ```json= .... "scripts": { "dev": "nodemon" } ``` # 自動刷新測試 > yarn run dev > ![](https://i.imgur.com/h3FHHWt.png) # 測試框架 為了確保專案的品質,test 是必不可少的,這次要使用的測試框架是 jest,因為要測試 http request,使用的是 supertest 這個框架。 # 安裝 jest 及 supertest > yarn add jest supertest --dev > # package.json 插入 ```json= .... "scripts": { "test": "jest" } ``` # add test/server.test.js 新增測試例子 ```javascript= const supertest = require('supertest'); const app = require('../src/server'); const PORT = 3001; let listener; let request; beforeAll(() => { listener = app.listen(PORT); request = supertest(listener); }); afterAll(async () => { await listener.close(); }); test('Server Health Check', async () => { const res = await request.get('/'); expect(res.status).toEqual(200); expect(res.text).toBe('Hello!CI/CD!2'); }); test('Server Health Check2', async () => { const res = await request.get('/'); expect(res.status).toEqual(200); expect(res.text).toBe('Hello!CI/CD!2'); }); ``` # 測試框架啟用 > yarn run test > ![](https://i.imgur.com/v5UtNAh.png) # 安裝 eslint 及 prettier eslint 是 javascript linter 之一,可以用來預防語法錯誤,其實最大的好處是可以維持團隊的 coding style(ex. airbnb),但因為這次是個人專案這個優點就沒有被顯現出來了 XD prettier 是要維持程式碼的整齊性,可以設定在存檔時程式碼格式化,統整團隊的規範。例如:要加雙引號還是單引號。 值得注意的是在同時引用 prettier 及 eslint 時,兩者會一些功能相衝突,而可以使用 eslint-plugin-prettier 解決這個問題。 設定 eslint + prettier + babel 開始著手寫0設定檔 .eslint.js,這次 babel parser 會用到 babel-eslint,而在 extends 會用到 eslint-config-airbnb-base/ eslint-plugin-jest / eslint-plugin-prettier, eslint-plugin-import 是用來 lint es6 的 import 及 export。 我是執行下列 shell 才裝完 >yarn add eslint-plugin-prettier eslint prettier babel-eslint eslint-config-airbnb-base eslint-plugin-jest eslint-plugin-prettier eslint-plugin-import --dev yarn add format npm install --save-dev eslint-config-prettier > # add .eslint.js ```javascript= module.exports = { parser: 'babel-eslint', parserOptions: { ecmaVersion: 2020, sourceType: 'module', }, env: { node: true, 'jest/globals': true, }, extends: [ 'airbnb-base', 'plugin:jest/recommended', 'plugin:prettier/recommended', ], plugins: ['jest', 'prettier'], rules: { 'import/prefer-default-export': 'off', 'class-methods-use-this': 'warn', 'consistent-return': 'warn', 'no-unused-vars': 'warn', 'no-console': 'off', 'no-continue': 'off', 'no-bitwise': 'off', 'no-underscore-dangle': 'off', 'no-param-reassign': ['error', { props: false }], 'no-restricted-syntax': [ 'error', 'ForInStatement', 'LabeledStatement', 'WithStatement', ], }, }; ``` # add .prettierrc.json ```json= { "semi": true, "trailingComma": "all", "singleQuote": true, "arrowParens": "always" } ``` # package.json 插入 ```json= .... "scripts": { "lint": "eslint . --fix" } ``` # 避免糟糕的 commit 忘記先跑 eslint 及 test 就 commit 情形難免會有,這時候在 commit 之前先做檢查就變得非常重要。這專案使用的是 git hooks husky 及 lint-staged。 安裝 husky 及 lint-staged > yarn add husky lint-staged --dev # add .huskyrc.js 使用 husky 跑 eslint 及 test。 ```javascript= module.exports = { hooks: { 'pre-commit': 'lint-staged && yarn run test', }, }; ``` # add lint-staged.config.js 在 commit 之前會做到 eslint 及 git add。 ```javascript= module.exports = { '*.js': ['eslint . --fix', 'git add'], }; ``` # app build ```javascript= .... "scripts": { "build": "rm -rf build && babel src -d build --copy-files" } ``` # run build > yarn run build ![](https://i.imgur.com/uvdsl47.png) # add .eslintignore 忽略 eslint >/build/** # CircleCI 因為是要透過 GitHub 進行 CI/CD,所以是以 GitHub 帳號登入 CircleCI。首先會先進到 CircleCI 介面,選擇你要 CI/CD 的專案。 https://circleci.com/product/ ![](https://i.imgur.com/Ft8SAKR.png) ![](https://i.imgur.com/C45BeXJ.png) 登入 # 選取我們的專案 demo-server ![](https://i.imgur.com/iZPgsOm.png) # add .circleci > mkdir .circleci # add config.yml ```yml version: 2.1 # use CircleCI 2.1 jobs: # a collection of steps build: # runs not using Workflows must have a `build` job as entry point docker: # run the steps with Docker - image: circleci/node:latest # with this image as the primary container; this is where all `steps` will run steps: # a collection of executable commands - checkout # special step to check out source code to working directory - run: name: Check Node.js version command: node -v - run: name: Install yarn command: 'curl -o- -L https://yarnpkg.com/install.sh | bash' - restore_cache: # special step to restore the dependency cache name: Restore dependencies from cache key: dependency-cache-{{ checksum "yarn.lock" }} - run: name: Install dependencies if needed command: | if [ ! -d node_modules ]; then yarn install --frozen-lockfile fi - save_cache: # special step to save the dependency cache in case there's something new in yarn.lock name: Cache dependencies key: dependency-cache-{{ checksum "yarn.lock" }} paths: - ./node_modules - run: # run lint name: Lint command: yarn eslint . --quiet - run: # run tests name: Test command: yarn jest --ci --maxWorkers=2 - run: #run build name: Build command: npm run build - persist_to_workspace: # Special step used to persist a temporary file to be used by another job in the workflow.# We will run deploy later,it will be put in another job. root: . paths: - build - package.json - yarn.lock - app.yaml ``` 事先在目錄建置好 config.yml 然後 commit ![](https://i.imgur.com/RVMRNuX.png) # 因為 我們有設置 github hook 所以 commit 會觸發 ![](https://i.imgur.com/poTrgbI.png) 這邊可以看到原因 格式有問題 ![](https://i.imgur.com/gKkJezt.png) 我們來下 > yarn run lint > ![](https://i.imgur.com/fnkdUDe.png) ![](https://i.imgur.com/GsSvLOJ.png) 修復後上傳 ![](https://i.imgur.com/TVlwRWh.png) 可以看到成功了 到目前為止我們的 CI 已經完成囉 ,椅子爆了 QQ