Monorepo 自动 release 的思考

Monorepo 自动 release 的思考

简介

最近在 Monorepo 模式下开发开源软件 plus-pro-components 时遇到自动 release 的问题,调研了 Changesets,实际使用发现它发包灵活度太低而且过程繁琐,果断弃用。调研了 rushjs,发现 rush 很规范,但是对于团队来说,上手成本太高,也弃用。于是查看 vue 源码,发现它是自己写的,高度契合项目,对我来说这种解决方案非常完美,于是自己写了一个自动 release 的工具。

实现的效果

  1. 自动查询子包
  2. 按需更新子包(符合 semver 规范 )的版本
  3. 自动生成 changelog
  4. 自动 commitLint (符合 angular 提交规范 )
  5. 自动代码 lint
  6. 自动打 tag
  7. 自动提交代码

核心源码

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)
...

源码地址

源码地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaofei0627

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值