前端自动化部署

225 阅读6分钟

前沿

面对频繁的需求变更和产品的不断迭代,需要对项目代码进行版本管理。

版本号定义规范

    * 版本号命名规则:`主版本号`.`次版本号`.`修订号`,如`2.1.13` 
    * 版本号仅标记于master分支,用于标识某个可发布/回滚的版本代码
    * 对master标记tag意味着该tag能发布到生产环境
    * 对master分支代码的每一次更新(合并)必须标记版本号
    * 仅项目管理员有权限对master进行合并和标记版本号

主分支是所有开发活动的核心分支。所有的开发活动产生的输出物最终都会反映到主分支的代码中。主分支分为Master分支和Develop分支

master分支

  • master分支存放的是随时可供生产环境中部署的稳定版本代码
  • master分支保存发布版本历史,release tag标识不同的发布版本
  • 仅在发布新的可供部署的代码时才更新master分支上的代码
  • 每次更新master,都需对master添加指定格式的tag,用于发布或回滚
  • master分支是保护分支,不可直接push到远程仓库master分支
  • master分支代码只能被release分支或hotfix分支合并

develop分支

  • develop分支是保存当前最新开发成果的分支
  • develop分支衍生出各个feature分支
  • develop分支是保护分支,不可直接push到远程仓库develop分支

辅助分支

辅助分支是用于组织解决特定问题的开发活动的分支。辅助完成版本发布工作以及对生产代码的缺陷进行紧急修复工作。这些分支与主分支不同,通常只会在有限的时间范围内存在。

  • 命名规则:feature/*
  • develop分支的功能分支
  • 以功能为单位从develop拉一个feature分支
  • 每个feature分支颗粒要尽量小,以利于快速迭代和避免冲突
  • 当其中一个feature分支完成后,它会合并回develop分支
  • 当一个功能因为各种原因不开发了或者放弃了,这个分支直接废弃,不影响develop分支
  • feature分支代码可以保存在开发者自己的代码库中而不强制提交到代码库里
  • feature分支只与develop分支交互,不能与master分支直接交互
release分支
  • 命名规则:release/*,' * '以本次发布的版本号为标识
  • release分支主要用来为发布新版的测试、修复做准备
  • 当需要为发布新版做准备时,从develop衍生出一个release分支
  • release分支可以从develop分支上指定Commit派生出
  • release分支测试通过后,合并到master分支并且给master标记一个版本号
  • release分支一旦建立就将独立,不可再从其他分支pull代码
  • 必须合并回develop分支和master分支
hotfix分支
  • 命名规则:hotfix/*
  • hotfix分支用来快速已发布产品修复或者微调功能
  • 只能从master分支指定tag版本衍生出来
  • 一旦完成修复bug,必须合并到master分支和develop分支
  • master被合并后,应该被标记一个新的版本号
  • hotfix分支一旦建立就将独立,不可再从其他分支pull代码

五、正常上线流程

1)管理员创建固定的分支master和develop,根据产品功能确定当前的版本号,然后从develop分支创建feature分支

2)每个研发人员拉取feature分支,并创建个人本地分支

3)研发人员进行编码,自测完成后合并本地分支到feature分支,在确定为会为下一个版本发布时,合并到develop分支

4)从develop分支衍生出一个release分支,并部署到测试环境

5)release分支经测试、发布、审核、进入灰度后,直到确定不再回滚正式发布后,合并到master和develop分支,并在master分支上打版本标签,并部署到生产环境

6)测试人员验证生成环境通过后,上线完成,如果生产环境验证不通过,马上回滚到master上一次版本代码

先说一下需求,我们要做一个什么样的东西,最终目的是要做一个前端自动化部署,生成版本号的需求,发布版本生成 tag, 最后生成 changelog.md 的文件,

生成CHANGELOG文档,需要有规范的 commit 的规范,我们通过一些工具来建立 commit message 进行检查

透过commitlint进行commit message 的检查(lint)

搭配husky在建立commit message 前就自动执行commitlint

透过commitizen方便开发者建立符合conventional commit 的commit message

使用conventional-changelog根据commit message 来产生CHANGELOG 档

使用standard version来同时更新版本号和产生CHANGELOG 档

commitlint:检查commit message

  • @commitlint/cli是用来执行commitlint 的工具

  • @commitlint/config-conventional是根据conventional commit 所建立的规范

    # 安装这两个依赖
    npm install --save-dev @commitlint/{config-conventional,cli}
    # mac 下新建 commitlint.config.js  window 就手动在根目录创建吧
    echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
    

安装成功以后使用

echo "add commitlint" | npx commitlint
⧗   input: add commitlint
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings

搭配husky

eslint 类似,如果不能在建立git commit message 时就自动检查规则的话,这个工具就会变得有点冗,这时候可以搭配husky 这套工具。

# 第一次安裝 husky 才需要執行
$ npx husky-init

# 建立 commitlint 用的 git hook
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'

执行 npx husky-init && npm install 的指令后,husky 会自动在 .husky 根目录建立 pre-commit 的文件,它会告诉husky 在建立git commit 前去执行 npm test 的指令。 如果不需要可以把 npm test 删除掉

commitizen:建立commit message

commitizen是非常多人使用的的工具,相较于 @commitlint/prompt-cli 用起来感觉更友善一些

# 安裝
npx commitizen init cz-conventional-changelog --save-dev

# 执行commit 的话
npx cz 

如果想在全局使用 commitizen

npm install -g commitizen
npm install -g cz-conventional-changelog

# create `.czrc` in home directory 仅限于 mac
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc

# 建立 git commit 时,只需要使用
$ git cz

执行我那 git cz 选择一种提交方式就可以了

cz-cli@4.2.5, cz-conventional-changelog@3.3.0

? Select the type of change that you're committing: (Use arrow keys)
❯ feat:     A new feature 
  fix:      A bug fix 
  docs:     Documentation only changes 
  style:    Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 
  refactor: A code change that neither fixes a bug nor adds a feature 
  perf:     A code change that improves performance 
  test:     Adding missing tests or correcting existing tests 
(Move up and down to reveal more choices)

conventional-changelog:建立CHANGELOG

在根据conventional commit 来写commit message 后,我们还可以自动产生对应的CHANGELOG 档。这里则会使用conventional-changelog-cli

$ npm install --save-dev conventional-changelog-cli

# 使用的 options 和說明
$ npx conventional-changelog --help

# 所有的日志
npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0

# 將新的更新 message 添加到 CHANGELOG
npx conventional-changelog -p angular -i CHANGELOG.md -s

产生CHANGELOG 的指令放到 package.json 的 scripts 中

"scripts": {
     "changelog": "conventional-changelog -i CHANGELOG.md -s",
 }

到这里还剩下两个步骤

更新版本, 和 新建 tag

因为前端用的微前端的方案,所以每个子项目都有一个 version.json 的来控制每个项目的版本号

更新版本,和 新建tag 都是通过 js 的方式来更新的

const fileSave = require('file-save')
const chalk = require('chalk')
const path = require('path')
// 获取那个项目的 version
let versionPath = path.join(__dirname, `../../packages/${process.env.MODULES}/version.json`)

let versionJson = require(versionPath)

module.exports = function (moduleData) {
		// 固定版本
    if (process.env.largeVersion) {
        moduleData.version = process.env.largeVersion
		// 自增版本
    } else if (process.env.autoVersion) {
        let version = versionJson.version
        let index = version.lastIndexOf('.') + 1
        let firstVersion = version.slice(0, index)
        let lastVersion = version.slice(index)
        moduleData.version = firstVersion + (++lastVersion)
    }
		// 在把版本更新回去, 下次更新版本适用最新的版本
    fileSave(path.join(__dirname, versionPath))
        .write(JSON.stringify({
            version: moduleData.version
        }, null, '  '), 'utf8')
        .end('\n').finish(() => {
        console.log(chalk.blue(`版本添加成功`))
    })
    return moduleData
}
const args = require('minimist')(process.argv.slice(2))
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const semver = require('semver')
const execa = require('execa')

const runIfNotDry = (bin, args, opts = {}) =>
    execa(bin, args, { stdio: 'inherit', ...opts })

const step = msg => console.log(chalk.cyan(msg))

async function main() {
		// 这里是获取到的,这里是为了测试
    let targetVersion = '1.0.8'

    // generate changelog
    step('\nGenerating changelog...')
    await runIfNotDry(`pnpm`, ['run', 'changelog'])

    const { stdout } = await runIfNotDry('git', ['diff'], { stdio: 'pipe' })
    if (stdout) {
        step('\nCommitting changes...')
        await runIfNotDry('git', ['add', '-A'])
        await runIfNotDry('git', ['commit', '-m', `release: v${targetVersion}`])
    } else {
        console.log('No changes to commit.')
    }

    // push to GitHub
    step('\nPushing to GitHub...')
    await runIfNotDry('git', ['tag', `v${targetVersion}`])
    await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
    await runIfNotDry('git', ['push'])

}

main()

在特定情况下,把这些串起来就可以了, 比如在制定参数下打包, 打包完成, 生成tag, 在生成tag 的同时 在生成CHANGELOG

standard version

在大部分场景下只要用这个就行, 能生成版本号的需求,发布版本生成 tag,能生成 CHANGELOG

根据conventional commit 的内容,依据semver 的原则来更新版号, 因为在我们项目中觉得不太适用这种情况就没有深究

产生对应的CHANGELOG


npx standard-version --help

# 第一次 release
npx standard-version --first-release

# 更新版号 CHANGELOG
npx standard-version

# 查看变化
npx standard-version --dry-run

# 更新到指定的版本
npx standard-version --release-as minor # 指定更新 minor 的版号
npx standard-version --release-as 1.1.0 # 制定更新到版号