CI的概念
首先,CI的全称是Continuous Integration,直译为可持续集成,而我们对其的理解是频繁地(一天多次)将代码集成到主干。针对这句话我们要理解两点:
- 主干指什么?
- 集成又是什么意思?
带着这点思考,我们先去了解下基于git的分支策略,毕竟CI这概念是基于分支管理实现的。
我们来看一下github flow的分支管理策略,如图所示:
github flow在开发新特性的运行模式如下所示:
- 基于
master创建新的分支feature进行开发。注意这需要保证master的代码和特性永远是最稳定的。 - 开发期间定期提交更改(
commit change)到远程仓库 - 通过创建
pull request去对master发起合并 pull request在经过审核确认可行后合并到master分支
如果用github flow这个分支管理模型去理解CI,可以对上面两个问题做出回答:
- 主干是指包含多个已上和即将上线的特性的分支,例如
github flow分支模型中的master分支,git flow分支模型中的release分支。 - 集成是指把含新特性的分支合并(
merge)到主干上
在理解CI的含义后,我们要思考的是,如何保证CI的实现。阮一峰老师的持续集成是什么?里说到过:
持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。
在上面的github flow分支模型里也提到:我们频繁地集成主干时,要保证主干的特性的代码和特性是稳定安全的,主干是可以随时发布部署的。那这样子就意味着,我们需要一系列措施来保证主干的安全可靠。网上教程中很多的CI保证措施是:新特性集成到主干后,触发一系列自动化测试去校验主干的安全稳定。但这里有个隐患是,当主干校验不通过时,此时到修复的时间过程里,主干都是不安全的,不能够用于部署和拉取分支开发,这样子不利于多人共同开发迭代。
因此我认为,CI的保证措施应该是:要先保证将要被集成的分支的安全可靠后,才能将其集成到主干上。如github flow的运行机制一样,在pull request发起后,通过一系列审核流程来保证开发分支的特性是安全可靠的、代码是符合规范的。
基于上面的思路,我们来动手学习实现下CI的保证措施。
学习实现CI
这个章节里,我会:
- 介绍市面上各种CI工具
- 快速精简地入门Github Actions
- 通过
vite及其自带的模板快速新建一个前端项目,然后基于这个项目通过接入Github Actions实现CI。
选择适合的CI工具
首先,由于这篇文章是带新手往自己的项目接入CI的,因此我们从新人的角度去筛选适合我们的CI工具:
- Gitlab CI: 非常强大的
CI工具,如今DevOps思潮下的产物,当前我司使用的CI工具,但需要在自己的服务器上安装Gitlab和Gitlab Runner,且因为要把代码上传到自己服务器里的Gitlab而不是Github上,因此不利于开源仓库的CI接入,因此去掉。 - Travis CI:现在只能免费使用一个月,去掉
- Circle CI:要钱,去掉
- Github Actions:相比于Gitlab CI比较轻量,当能满足我们新手的大部分需求,且是
Github官方推出的能与其无缝接合的工具。可行。
由此,我门决定用Github Actions实现CI。
精简学习Github Actions
这里要先了解下Github Actions的基础,当我们想往自己的项目里接入Github Actions时,要在项目目录里新建.github/workflows目录。然后通过编写yml格式文件定义工作流程(workflow)去实现CI。在阅读yml文件之前,我们要先搞懂两个比较重要的概念:
- Job(作业):一个工作流程中包含一个或多个Job,这些Job默认情况下并行运行,但我们也可以通过设置让其按顺序执行。每个Job都在指定的环境(虚拟机或容器)里开启一个Runner(可以理解为一个进程)运行,包含多个Step(步骤)。
- Step(步骤):Job的组成部分,用于定义每一部的工作内容。每个Step在运行器环境中以其单独的进程运行,且可以访问工作区和文件系统。
Job和Step两者的关系如下所示:
其实travis.ci和gitlab-ci的配置文件中也是有Job和Step这两种概念的,且作用和Github Actions的一样。
这里拿Github的官方教程中的例子来展示:
# 指定工作流程的名称
name: learn-github-actions
# 指定此工作流程的触发事件。 此示例使用 推送 事件
on: [push]
# 存放 learn-github-actions 工作流程中的所有Job
jobs:
# 指定一个Job的名称为check-bats-version
check-bats-version:
# 指定该Job在最新版本的 Ubuntu Linux 的 Runner(运行器)上运行
runs-on: ubuntu-latest
# 存放 check-bats-version 作业中的所有Step
steps:
# step-no.1: 运行actions/checkout@v3操作,操作一般用uses来调用,
# 一般用于处理一些复杂又频繁的操作例如拉取分支,安装插件
# 此处 actions/checkout 操作是从仓库拉取代码到Runner里的操作
- uses: actions/checkout@v3
# step-no.2: actions/setup-node@v3 操作来安装指定版本的 Node.js,此处指定安装的版本为v14
- uses: actions/setup-node@v3
with:
node-version: "14"
# step-no.3: 运行命令行下载bats依赖到全局环境中
- run: npm install -g bats
# step-no.4: 运行命令行查看bats依赖的版本
- run: bats -v
整个learn-github-actions工作流程弄成流程图可如下所示:
来一个项目试试手
首先,在npm i vite -g以保证可全局调用vite后,我们通过yarn create vite cicd-study --template react-ts创建一个技术栈为Vite、React、Typescript的前端项目。创建后的代码框架可看此处stackblitz。
创建且运行后页面效果如下所示:
接下来我们要对这个项目实现的代码扫描和自动化测试,代码扫描用于保证代码的语法和样式的统一,集成测试用于保证新特性的安全可靠稳定,这些都是CI实现的前提。
完整的代码项目地址我先放在cicd-study这里了,大家可以点击查阅。
1. 代码扫描
一般公司里都会通过类似Sonar这类代码质量管理插件来保证代码质量。不过我们也可以通过前端样式三剑侠:eslint+prettier+stylelint来简单保证。这里我直接使用本人比较喜好和经常使用的umi的代码规范:@umijs/fabric来规定三剑侠的规则了,使用方式如下所示:
.eslintrc.js
module.exports = {
extends: [require.resolve("@umijs/fabric/dist/eslint")],
};
.prettierrc.js
const fabric = require("@umijs/fabric");
module.exports = {
...fabric.prettier,
};
.stylelintrc.js
const fabric = require("@umijs/fabric");
module.exports = {
...fabric.stylelint,
};
然后在package.json的script上加上对应的执行命令即可:
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "npm run lint:js && npm run lint:style && npm run lint:prettier",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx ./src",
"lint:prettier": "prettier --check \"src/**/*\" --end-of-line auto",
"lint:style": "stylelint --fix 'src/**/*.{css,scss,less}' --cache"
}
这样子就完成了代码扫描部分了。在本地运行的效果如下所示:
2. 自动化测试
前端测试主要分单元测试(Unit Test)、集成测试(Integration Test)、UI 测试(UI Test)。由于项目里只有一个页面组件,所以只需对齐编写单元测试即可。
为了多写点测试用例给测试代码加点内容,我给页面对应组件App.tsx加了个props,代码如下所示:
import type { FC } from 'react';
import { useState } from 'react';
import logo from './logo.svg';
import './App.css';
interface Props {
value: string;
}
const App: FC<Props> = ({ value }) => {
const [count, setCount] = useState(0);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Hello Vite + React!!!!!!!!</p>
<p>
{/*
测试代码中需要获取的DOM元素用role属性标记,这里的role属性只会在测试代码中用到,
这样子就可以避免代码因需求改动时,因DOM属性改变导致测试不通过。有利于TDD(测试驱动开发)开发的进行
*/}
<button role="button" type="button" onClick={() => setCount((v) => v + 1)}>
count is: {count}
</button>
</p>
<p role="props">{value}</p>
</header>
</div>
);
};
export default App;
这里我采用ts-jest+@testing-library来编写测试代码(当然还有别的选择,例如ts-jest+enzyme),测试代码如下所示:
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import App from "./App";
test("props is avaliable", () => {
const value = "123";
// 为了多写点测试用例,我给App组件加了个prop
render(<App value={value} />);
expect(screen.getByRole("props")).toHaveTextContent(value);
});
test("click of button is avaliable", () => {
render(<App value="123" />);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByRole("button")).toHaveTextContent(`count is: 1`);
});
jest.config.js的配置可以从此处查看。配置好后运行yarn test后控制台输出如下所示:
3. 配置工作流程(workflow)
直接在.github/workflows上新建ci.yml定义工作流程:
name: CI
# 指定在main分支发生pull_request事件时才触发运行工作流程
on:
pull_request:
branches: main
jobs:
Test:
runs-on: ubuntu-latest
steps:
# 拉取项目代码
- name: Checkout repository
uses: actions/checkout@v2
# 下载node
- name: Use Node.js
uses: actions/setup-node@v3
with:
# vite 需要在 node>=12 的环境下执行
node-version: "12.x"
# 安装依赖
- name: Installing Dependencies
run: yarn install
# 运行代码扫描
- name: Running Lint
run: yarn lint
# 运行自动化测试
- name: Running Test
run: yarn test
4. 来一次工作流程的触发
当要把代码集成到主干时,我们在github里创建pull request,如下所示:
点击create pull request创建后,github会对.github/workflows里的工作流程进行判断后运行,此时我们可以看到下面的情况:
此时工作流程正在运行中,在其运行成功后的效果如下所示:
我们可以通过点击Details查看执行详细信息,如下所示:
确认代码安全可靠后就可以点击Merge pull request来把新代码集成到主干上。从而遵循CI的原则完成一次bug 修复或新特性迭代。
后记
之后会写一篇关于CD的文章来描述更多开发流程的内容。