multi-semantic-release
用于单仓库的黑客式语义发布
概述
这是一个概念验证项目,它包装了semantic-release以适用于单仓库。
这个包应该能够正常工作,但可能不够稳定,不适合用于重要的生产环境,因为它高度依赖于semantic-release的工作方式(所以在semantic-release的未来版本中可能会出现问题或过时)。
semantic-release最好的特点之一是可以忘记版本号。但在单仓库中,对于本地依赖(在同一个单仓库中被引用为dependencies
、devDependencies
或peerDependencies
的包)仍然需要大量的版本号管理。然而,在multi-semantic-release中,本地依赖的版本号会在发布时被写入package.json
。这意味着不再需要硬编码版本号(我们建议在您的仓库代码中直接使用*
星号)。
主要特性
- 命令行界面和JavaScript API
- 自动化且可配置的跨包版本升级
- 提供alpha和beta分支发布流程
- 支持npm(v7+)、yarn、pnpm(有限制)、基于bolt的单仓库
- 可选择忽略某些包
- 支持Linux/MacOS/Windows
目录
安装
yarn add multi-semantic-release --dev
npm i multi-semantic-release -D
要求
- Node.js >= 10
- 启用git-notes
使用方法
multi-semantic-release [选项]
npx multi-semantic-release [选项]
配置
发布的配置与semantic-release配置相同,即在package.json
的release
键下或任何类型的.releaserc
文件中设置,如.yaml
、.json
。
但在multi-semantic-release中,这种配置可以在全局(在您的顶级目录中)或每个包(在该单独包的目录中)中完成。如果您同时设置了两者,则每个包的设置将覆盖全局设置。
multi-semantic-release不支持任何命令行参数(这是不可能的,除非复制semantic-release的文件,而我一直在尽量避免这样做)。
multi-semantic-release自动检测以下包管理器的工作空间中的包:
yarn / npm (v7+)
确保在您的package.json
项目文件中有一个workspaces
属性。在那里,您可以设置一个包列表,这些包可能会在msr过程中被处理,也可以忽略其他包。例如,假设您的项目有4个包(即a、b、c和d),您只想处理a和d(忽略b和c)。您可以在package.json
文件中设置以下结构:
{
"name": "msr-test-yarn",
"author": "Dave Houlbrooke <dave@shax.com",
"version": "0.0.0-semantically-released",
"private": true,
"license": "0BSD",
"engines": {
"node": ">=8.3"
},
"workspaces": [
"packages/*",
"!packages/b/**",
"!packages/c/**"
],
"release": {
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator"
],
"noCi": true
}
}
pnpm
确保在项目根目录的pnpm-workspace.yaml
中有一个packages
属性。在那里,您可以设置一个包列表,这些包可能会在msr过程中被处理,也可以忽略其他包。例如,假设您的项目有4个包(即a、b、c和d),您只想处理a和d(忽略b和c)。您可以在pnpm-workspace.yaml
文件中设置以下结构:
packages:
- 'packages/**'
- '!packages/b/**'
- '!packages/c/**'
注意,包版本中的workspace:
前缀目前还不支持。issues/85
bolt
确保在您的package.json
项目文件中有一个bolt.workspaces
属性。在那里,您可以设置一个包列表,这些包可能会在msr过程中被处理,也可以忽略其他包。例如,假设您的项目有4个包(即a、b、c和d),您只想处理a和d(忽略b和c)。您可以在package.json
文件中设置以下结构:
{
"name": "msr-test-bolt",
"author": "Dave Houlbrooke <dave@shax.com",
"version": "0.0.0-semantically-released",
"private": true,
"license": "0BSD",
"engines": {
"node": ">=8.3"
},
"bolt": {
"workspaces": [
"packages/*",
"!packages/b/**",
"!packages/c/**"
]
},
"release": {
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator"
],
"noCi": true
}
}
命令行界面
有几个调整可以使msr适应一些特殊情况:
标志 | 类型 | 描述 | 默认值 |
---|---|---|---|
--sequential-init | 布尔值 | 避免假设的并发初始化冲突 | false |
--debug | 布尔值 | 输出调试信息 | false |
--first-parent | 布尔值 | 仅对当前分支应用提交过滤 | false |
--deps.bump | 字符串 | 定义依赖版本更新规则。
| override |
--deps.release | 字符串 | 如果任何依赖项发生更改,定义依赖包的发布类型。
| patch |
--deps.prefix | 字符串 | 如果--deps.bump 设置为override ,可选择附加到下一个版本的前缀。支持的值:^ | ~ | '' (空字符串) | '' (空字符串) |
--dry-run | 布尔值 | 演练模式 | false |
--ignore-packages | 字符串 | 在升级过程中要忽略的包列表(附加到package.json工作区中已存在的包) | null |
--ignore-private-packages | 布尔值 | 忽略私有包 | false |
示例:
$ multi-semantic-release --debug
$ multi-semantic-release --deps.bump=satisfy --deps.release=patch
$ multi-semantic-release --ignore-packages=packages/a/**,packages/b/**
你还可以将CLI的--ignore-packages
选项与package.json
的workspaces
属性中每个包内的!
操作符结合使用。尽管你可以使用CLI忽略选项,但不能使用它来设置要发布的包 - 也就是说,你仍需要在package.json
中设置workspaces
属性。
⚠️ 请注意,allowUnknownFlags
已启用,因此其余标志将作为options
参数传递给所有包的内部semrel
调用。
API
multi-semantic-release默认导出一个multirelease()
方法,该方法接受以下参数:
packages
包含package.json
文件字符串路径的数组options
包含默认semantic-release配置选项的对象
multirelease()
返回一个描述多重发布结果的对象数组(对应传入的packages
数组)。
const multirelease = require("multi-semantic-release");
multirelease([
`${__dirname}/packages/my-pkg-1/package.json`,
`${__dirname}/packages/my-pkg-2/package.json`,
]);
CI/CD
Multi-semantic release似乎与许多CI/CD系统兼容。至少我们确定了三个,以下是配置示例:
- GitHub Actions → https://github.com/qiwi/semantic-release-toolkit
- Travis CI → https://github.com/qiwi/pijma
- AppVeyor → https://github.com/qiwi/masker
故障排除
npm v8.5+: npm ERR! notarget 未找到匹配版本...
发布monorepo时,你可能会遇到npm ERR! code ETARGET
错误。这是因为npm version
在MSR尚未更新的未来依赖版本上创建了重新验证更新。
最简单的解决方法是在.npmrc中设置workspaces-update为false,或手动运行npm config set workspaces-update false
npm: 无效的npm令牌
发布monorepo时,你可能会遇到EINVALIDNPMTOKEN
错误。包越多,出错的机会就越大,很遗憾。
INVALIDNPMTOKEN 无效的npm令牌。
在NPM_TOKEN环境变量中配置的npm令牌(https://github.com/semantic-release/npm/blob/master/README.md#npm-registry-authentication)必须是有效的令牌(https://docs.npmjs.com/getting-started/working_with_tokens),允许发布到注册表https://registry.npmjs.org/。
不要急于更改你的令牌。_也许_这与你的注册表上的npm whoami
请求限流有关(仅是假设:https://github.com/semantic-release/npm/pull/416)。此时你可以:
- 根据需要多次重新运行你的构建。你可能会在新的尝试中成功。
- 使用semrel-extra/npm插件进行npm发布(推荐)。
git: 连接被对等方重置
这个错误似乎与并发的git调用有关(issues/24)。或者可能不是。
无论如何,我们添加了一个特殊的--sequental-init
标志来对这些调用进行排队。
实现说明(和其他想法)
对monorepo的支持
只要按照支持的包管理器之一的工作区功能配置工作区,就会自动查找包。
我知道Lerna现在是最知名的工具,但未来似乎很明显它将被Yarn和NPM的功能直接替代。如果你现在(2019年1月)使用Yarn工作区,那么发布是Lerna_真正_需要的唯一剩余功能(尽管如果Yarn添加并行脚本执行会很好)。因此,使用multi-semantic-release意味着你可能可以完全从项目中移除Lerna。
迭代vs协调
其他支持monorepo的semantic-release包通过迭代进入每个包并运行semantic-release
命令来工作。这在概念上很简单,但不幸的是不可行,因为:
- 如果发布的包依赖于兄弟包中的次要更改,可能会导致非常微妙的错误(最糟糕的那种!)- 如果项目严格遵循semver,这应该永远不会发生,但最好消除错误的_可能性_
- 依赖版本号需要反映发布时的_下一个_版本,因此包需要在正确发布之前知道_所有其他包_的状态 - 这种中央状态需要由某些东西来协调
本地依赖和版本号
一个关键要求是优雅地处理本地依赖版本号。multi-semantic-release执行以下操作:
- 首先确定所有包的下一个版本号
- 如果一个版本没有更改但有本地依赖已更改...对该包也进行
patch
升级 - 在发布包之前(在semantic-release的准备步骤中),将_所有_本地依赖的正确当前/下一个版本号写入
package.json
文件(覆盖任何现有值) - 这确保了在发布时,包与monorepo中的所有其他包保持原子正确性。
上述意味着,可能如果有人在多重发布_期间_(在所有依赖都以其下一个版本发布之前)升级依赖并从NPM拉取包,那么他们的npm install
将失败(如果他们几分钟后再次尝试,就会成功)。权衡之下,我认为保持原子正确性更重要(假设项目提交了它们的锁文件,这种情况应该相当罕见)。
与semantic-release的集成
这是multi-semantic-release最棘手的部分,也是最可能破坏依赖的部分。我预计这将在未来引起维护问题。在理想情况下,semantic-release将内置对monorepo的支持(使得这个包变得不必要)。
我最终集成的方式是为semantic-release创建一个自定义的"内联插件",并将其作为唯一的插件传递给semanticRelease()
。然后,这个插件调用任何其他配置的插件来检索并可能修改响应。
该插件同时启动所有发布,然后在不同点暂停它们(使用 Promises),以允许多发布中的其他包赶上进度。这主要是为了在发布任何包之前确定所有包的版本号。这使我们能够对本地依赖项已升级的发布进行"补丁"升级,并在每个 package.json
中准确写入本地依赖项的版本。
内联插件执行以下操作:
- verifyConditions: 未使用
- analyzeCommits:
- 将
context.commits
替换为仅限于该文件夹的提交列表 - 调用
plugins.analyzeCommits()
获取下一个发布类型(例如来自 @semantic-release/commit-analyzer) - 等待所有包赶上这一进度
- 对于未升级的包,检查它是否有已升级的本地依赖项(或依赖项的依赖项),如果是则返回
patch
- 将
- verifyRelease: 未使用
- generateNotes:
- 调用
plugins.generateNotes()
获取发布说明(例如来自 @semantic-release/release-notes-generator) - 附加一个列出任何本地依赖项升级的部分(例如 "my-pkg-2: 升级到 1.2.1")
- 调用
- prepare:
- 在
package.json
的dependencies
、devDependencies
、peerDependencies
中为本地依赖项写入正确的版本 - 将发布序列化,使它们一次只发生一个(因为 semantic-release 异步调用
git push
,多个同时发布会失败,因为 Git 引用未锁定 — semantic-release 应该使用execa.sync()
使 Git 操作具有原子性)
- 在
- publish: 未使用
- success: 未使用
- fail: 未使用
不完善之处
与 semantic release 的集成相当不完善 — 以下是这个包难以维护的原因简要总结:
- 必须在
@semantic-release/commit-analyzer
使用前过滤context.commits
对象(使其只列出相应目录的提交)。
- 实际的 Git 过滤非常简单:参见 getCommitsFiltered.js
- 但覆盖
context.commits
非常困难!我最终通过创建一个内联插件并通过options.plugins
将其传递给semanticRelease()
来实现 - 内联插件在 semantic release 和其他配置的插件之间进行代理。它执行所需操作,然后调用例如
plugins.analyzeCommits()
并覆盖context.commits
— 参见 createInlinePluginCreator.js - 我认为这很混乱 — 内联插件甚至没有文档 :(
- 需要在所有插件进入发布步骤之前运行提交分析步骤
- 内联插件为每个包返回一个 Promise,然后等待所有包分析它们的提交后再逐一解析它们
- 如果包有本地依赖项(例如 package.json 中的
dependencies
指向内部包),这一步还会在它们中任何一个升级时进行patch
升级。 - 这必须递归工作!参见 hasChangedDeep.js
- 配置可以分层(即全局
.releaserc
和每个目录的单个包覆盖)。
- 不得不复制 semantic release 的内部 cosmiconfig 设置才能使其工作 :(
- 我发现 Git 会因为例如
git tag
异步执行而陷入奇怪的状态
- 为了解决这个问题,我不得不错开包的发布,使它们一次只发布一个(这会降低速度)
- 我认为 semantic release 中对
execa()
的调用应该替换为execa.sync()
以确保 Git 的内部状态是原子的。 - 幸运的是,已经实现了另一种解决方法。
Synchronizer
是其中的精髓。它对于使标签和提交发布阶段严格按顺序进行至关重要。事件发射器允许:- 同步所有包的发布阶段。
- 确保检查的完整性和无冲突过程的条件充分性。
Git 标签
发布始终使用 my-pkg-1@1.0.1
格式的 tagFormat
作为 Git 标签,并始终覆盖 semantic-release 配置中设置的任何 gitTag
。
我个人可以看到这个选项在协调 semantic-release 方面的潜力(例如,使两个具有相同标签的包始终同时升级和发布)。不幸的是,由于 semantic-release 中可用的集成点,在发布时阻止第二个包创建重复标签(导致错误)实际上是不可能的。
要使 tagFormat
选项按预期工作,需要进行以下操作:
- semantic-release 需要检查给定的标签是否已存在于给定的提交中,如果是则不创建/推送它
- 多个包发布的发布说明需要合并,但 Github 发布只执行一次(通过在 semantic-release 级别合并说明但只发布一次,或让 Github 插件合并它们)
- 在文档中明确说明默认标签
v1.0.0
将具有与 Lerna 的固定模式相同的效果(所有更改的 monorepo 包同时发布)
贡献
欢迎提出任何类型的问题:错误、功能请求或问题。 你随时可以提出 PR。只需 fork 这个仓库,编写一些代码,添加一些测试,然后推送你的更改。 欢迎任何反馈。