Monorepo 自动 release 的思考
简介
最近在 Monorepo 模式下开发开源软件 plus-pro-components 时遇到自动 release 的问题,调研了 Changesets,实际使用发现它发包灵活度太低而且过程繁琐,果断弃用。调研了 rushjs,发现 rush 很规范,但是对于团队来说,上手成本太高,也弃用。于是查看 vue 源码,发现它是自己写的,高度契合项目,对我来说这种解决方案非常完美,于是自己写了一个自动 release 的工具。
实现的效果
- 自动查询子包
- 按需更新子包(符合 semver 规范 )的版本
- 自动生成 changelog
- 自动 commitLint (符合 angular 提交规范 )
- 自动代码 lint
- 自动打 tag
- 自动提交代码
核心源码
import path from 'path'
import fs from 'fs'
import semver from 'semver'
import consola from 'consola'
import execa from 'execa'
import { checkbox, select, input } from '@inquirer/prompts'
import { findWorkspacePackages } from '@pnpm/find-workspace-packages'
import { projRoot, pcPackage, projPackage } from '../build/paths'
import { PKG_NAME } from '../build/utils'
type SemverRow = {
release: semver.ReleaseType
optionsOrLoose?: boolean | semver.Options | string
identifier?: string
}
// 打印
const echo = (msg: string) => consola.success(msg)
// 运行脚本
const run = (bin: string, args: string[], opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts })
// 版本列表
const versionIncrements: SemverRow[] = [
{
release: 'patch'
},
{
release: 'minor'
},
{
release: 'major'
},
{
release: 'prepatch',
optionsOrLoose: 'rc',
identifier: '1'
},
{
release: 'preminor',
optionsOrLoose: 'rc',
identifier: '1'
},
{
release: 'premajor',
optionsOrLoose: 'rc',
identifier: '1'
},
{
release: 'prerelease',
optionsOrLoose: 'alpha',
identifier: '1'
},
{
release: 'prerelease',
optionsOrLoose: 'beta',
identifier: '1'
}
]
// 获取工作空间包
const getWorkspaceList = async (dir = projRoot) => {
const pkgs = await findWorkspacePackages(projRoot)
return pkgs
.filter(pkg => pkg.dir.startsWith(dir))
.filter(pkg => pkg.manifest.private !== true && pkg.manifest.name)
}
/**
* 更新版本号
* @param {string} version
*/
const updatePackage = (version: string, pkgPath: string) => {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
pkg.version = version
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
// 获取版本
const getVersion = async (currentVersion: string) => {
// 发布版本
let version: string | null
const selectChoices = versionIncrements
.map(item => {
const value = semver.inc(
currentVersion,
item.release,
item.optionsOrLoose as any,
item.identifier
)
const name = `${item.release} (${value}})`
return {
name,
value
}
})
.concat({ name: 'custom', value: 'custom' })
version = await select({
message: 'Select release type',
choices: selectChoices
})
// 自定义版本
if (version === 'custom') {
version = await input({ message: 'Enter custom version' })
// 校验版本
if (!semver.valid(version)) {
throw new Error(`Illegal version: ${version}`)
}
}
return version
}
// 提交
async function commit(version?: string) {
try {
// 生成changelog
if (version) {
await run('npm', ['run', '--name', 'changelog'])
}
await run('git', ['add', '-A'])
// 打tag
if (version) {
await run('git', ['tag', '-a', version, '-m', `v${version}`])
}
// 规范化提交
await run('npm', ['run', '--name', 'gitcz'])
await run('git', ['pull'])
// push tag
if (version) {
await run('git', ['push', '--tags'])
}
await run('git', ['push'])
echo(`\ncommit success ${version}`)
} catch (error: any) {
throw new Error(error)
}
}
const main = async () => {
const workspaceList = await getWorkspaceList()
const workspaceNames = workspaceList.map(
item => item.manifest.name
) as string[]
const workspaceMaps = workspaceList.map(item => ({
dir: item.dir,
name: item.manifest.name,
version: item.manifest.version,
pkg: path.resolve(item.dir, 'package.json')
}))
// 选择需要更新的包
let selectPackages: string[] = []
const checkboxChoices = workspaceNames.map(item => ({
name: item,
value: item
}))
const packages = await checkbox({
message: 'Which packages would you like to include?',
choices: [{ name: 'all', value: 'all' }, ...checkboxChoices]
})
if (!packages.length) {
throw new Error('Please select one or more packages!')
}
if (packages.includes('all')) {
selectPackages = workspaceNames
} else {
selectPackages = [...packages]
}
// 更新版本号
for (let index = 0; index < selectPackages.length; index++) {
const name = selectPackages[index]
const packageInfo = workspaceMaps.find(i => i.name === name)
const version = await getVersion(packageInfo?.version as string)
updatePackage(version as string, packageInfo?.pkg as string)
consola.success(`Successfully updated version ${name}!`)
}
if (selectPackages.includes(PKG_NAME)) {
// 主包更新
const mainPkg = JSON.parse(fs.readFileSync(pcPackage, 'utf-8'))
updatePackage(mainPkg.version as string, projPackage)
commit(mainPkg.version)
} else {
commit()
}
}
main()
.then(() => {
console.log('success')
})
.catch(err => {
console.log(err)
})
运行效果
$ yarn release
yarn run v1.22.19
$ tsx scripts/release/index.ts
? Which packages would you like to include? (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
>( ) all
( ) @plus-pro-components/echarts
( ) @plus-pro-components/eslint-config
( ) plus-pro-components
( ) @plus-pro-components/utils
? Which packages would you like to include? plus-pro-components
? Select release type (Use arrow keys)
> patch (0.0.1})
minor (0.1.0})
major (1.0.0})
prepatch (0.0.2-rc.1})
preminor (0.1.0-rc.1})
premajor (1.0.0-rc.1})
prerelease (0.0.1-alpha.4})
(Move up and down to reveal more choices)
...