在 CI/CD 环境中用 release-it 自动化版本发布:GitHub Actions、GitLab CI、Travis 与 Circle CI 配置实战
在 CI/CD 环境中用 release-it 自动化版本发布:GitHub Actions、GitLab CI、Travis 与 Circle CI 配置实战
导读
release-it 是一款自动化版本号递增与包发布的工具(仓库根目录 README.md 中描述为 "Automate versioning and package publishing")。当它被接入 Travis、Circle CI、GitHub Actions、GitLab CI 等持续集成/持续交付环境时,需要额外解决三类问题:Git 远端认证(推送 release commit 与 tag)、npm 注册表认证(发布包)、GitHub/GitLab Release 的 token 注入。本文以 docs/ci.md 为主体骨架,结合仓库源码(lib/config.js、lib/args.js、lib/plugin/npm/npm.js、lib/plugin/git/Git.js)与默认配置(config/release-it.json),给出可直接复制运行的 CI 配置片段,并深入解释 --ci 模式、npm 前置检查、tag already exists 等 CI 高频问题的底层原理。
CI 环境中 release-it 的行为基础:--ci 模式
在 CI 环境中运行 release-it 时,最核心的行为开关是 --ci:关闭所有交互式提示,全程无人工干预。lib/cli.js 的帮助文本对此的描述是:
--ci— No prompts, no user interaction; activated automatically in CI environments
也就是说,--ci 并不一定需要手动传入——lib/config.js 的 loadOptions 使用 ci-info 检测运行环境,并将 ci: isCI 合并进最终配置(lib/config.js):
const merged = defu(
{},
constructorConfig,
{
ci: isCI // 由 ci-info 自动检测(Travis、Circle、GitHub Actions、GitLab CI 等均会被识别)
},
localConfig,
defaultConfig
);
从配置对象的 isCI getter(lib/config.js)可以看到,isCI 为真还会被 --release-version、--changelog 这类非交互模式触发:
get isCI() {
return Boolean(this.options.ci) || this.isReleaseVersion || this.isChangelog;
}
--ci 在 lib/args.js 中被声明为布尔选项,也支持 --no-ci 覆盖。CI 模式下,prompt 会被替换为 spinner/直接执行(见 lib/plugin/Plugin.js 的 isCI && !isException ? spinner : showPrompt 分支),npm 发布的 OTP 回调也会被禁用(见下文)。
Git:让 CI 环境具备推回远端的能力
release-it 默认执行"提交 release commit + 打 tag + 推送"三步(默认配置 config/release-it.json 中 git.commit、git.tag、git.push 均为 true)。要把这些变更推回远端仓库,CI 环境必须持有对远端主机(如 GitHub)的认证凭证。
SSH(推荐)
如果仓库使用 SSH 形式的 URL(如 git@github.com:user/repo.git),只需将部署公钥添加到 CI/CD 环境即可,无需处理 token 拼接问题。GitLab CI 场景同样推荐 SSH(详见下文 GitLab CI 小节)。
HTTPS:将 GITHUB_TOKEN 注入 origin URL
使用 HTTPS 形式的 URL(如 https://github.com/user/project.git)时略复杂:需要先在 CI 环境配置 GITHUB_TOKEN,然后在运行 release-it 之前,把该 token 作为密码写入 origin URL。文档给出的 .travis.yml 片段:
script:
- git remote rm origin
- git remote add origin https://[user]:${GITHUB_TOKEN}@github.com/[user]/[project].git
- git symbolic-ref HEAD refs/heads/main
将 <a href="https://link.gitcode.com/i/4f9e7d7a1eda0f1307a499f7f123b7dd" target="_blank">user] 和 [project] 替换为实际值即可。关于 Git 插件在 CI 中如何校验仓库状态,可参考 [lib/plugin/git/Git.js:默认配置 git.requireCleanWorkingDir: true、git.requireUpstream: true 会分别在 CI 中强制要求工作区干净、当前分支已配置 upstream,否则直接抛出错误中止发布。
GitHub Actions:完整示例与 fetch-depth: 0
文档给出的 GitHub Actions job 片段,完成"配置 Git 用户 → 暴露 npm/GitHub token → 执行发布":
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: git config
run: |
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
- run: npm install
- run: npm run release
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
要点说明:
git config user.name / user.email:release-it 提交 release commit 时使用该身份。使用GITHUB_ACTOR与 GitHub 的 noreply 邮箱可避免身份为空导致提交失败。NPM_TOKEN:用于发布到 npm registry(见下文 npm 小节)。GITHUB_TOKEN:用于创建 GitHub Release。config/release-it.json 中默认github.tokenRef: "GITHUB_TOKEN"、gitlab.tokenRef: "GITLAB_TOKEN",即 release-it 默认从这两个环境变量名读取 token。fetch-depth: 0:仅当需要完整 Git 历史时才必要,例如使用@release-it/conventional-changelog等需要依据历史提交生成 changelog 的插件。
如果发布流程只需要"打版本号"而不需要交互,建议在 npm script 中显式加 --ci(如 release-it --ci),确保任何环境下行为一致。
npm:在 CI 中发布包
要在 CI/CD 环境(Travis、Circle 等)中把包发布到 npm registry,核心是在 release-it 执行发布前,把 NPM_TOKEN 写入 .npmrc 文件:
//registry.npmjs.org/:_authToken=$NPM_TOKEN
两种常见写入方式:
npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
或直接以 shell 重定向生成(见下文 Travis 示例)。注意:.npmrc 应加入 .gitignore,否则 release-it 的 git 插件会在提交 release commit 时把它一并提交进仓库,导致 token 泄露。
为什么 CI 中常需要 --npm.skipChecks
release-it 在正式发布前会执行 npm 前置检查(详见 docs/npm.md#prerequisite-checks)。从源码 lib/plugin/npm/npm.js 可以看到 init() 阶段的三个并发校验:
isRegistryUp():执行npm ping(npm.js);isAuthenticated():执行npm whoami(npm.js);getLatestRegistryVersion():执行npm show <name>@<tag> version。
问题在于:npm whoami 并不一定尊重 .npmrc 中的 token(文档明确指出该检查"does not seem to respect the .npmrc file"),因此在 CI 中可能误报未认证。此时使用 --npm.skipChecks(即 npm.skipChecks: true)跳过这些检查即可。从源码看,skipChecks 置真后 init() 直接 return,跳过全部校验与超时等待(npm.js)。
补充说明(来自 docs/npm.md 与源码):
- 部分私有注册表(如 Nexus)不支持
npm ping/npm whoami/npm access,若报错为E400/E404,release-it 会给出 warning 并继续(isRegistryUp、isAuthenticated中对/code E40[04]/的兜底逻辑); - 每个校验默认超时 10 秒(
DEFAULT_TIMEOUT = 10,npm.js),可通过npm.timeout调整; - 交互式环境(非 CI、非 dry-run、有 TTY)在认证失败时会自动运行
npm login重试,但该行为在 CI 中被显式禁用——canLogin条件包含!this.config.isCI && !this.config.isDryRun && process.stdin.isTTY(npm.js); - 包为
"private": true时,release-it 会自动跳过 npm 相关步骤(npm.js)。
Travis
文档给出的 .travis.yml 片段:
deploy:
script:
- echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc
- npm run release
Circle CI
文档给出的 .circleci/config.yml 关键片段:
jobs:
deploy:
steps:
- attach_workspace:
at: ~/repo
- run:
name: Authenticate with registry
command: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
注意:发布过程中 release-it 会更新项目的 package.json 以递增版本号。若希望 CircleCI 能把该变更推回仓库,需要为其配置来自 GitHub 账号的非只读 SSH 密钥对。
依赖注入:npm 相关的 CI 组合配置
npm.skipChecks 仅是 npm 命名空间下的一个布尔选项(schema/npm.json 中默认 false),与 --ci 独立。CI 中常用的 npm 组合还包括:
--npm.skipChecks:跳过 registry 连通性与权限校验;--npm.tag=beta:以指定 dist-tag 发布(预发布版本默认 tag 为next,详见 docs/npm.md#tags);--npm.otp=123456:提供一次性密码(CI 中不建议,2FA 意义会失效);npm.publishPath:仅发布dist等指定目录;npm.stage: true:使用 npm 的 staged publishing 提交到暂存队列,把 2FA 审批延后到npm stage approve,从而支持 CI 非交互式发布(docs/npm.md#staged-publishing)。
GitHub & GitLab Releases:token 环境变量
要发布(或草稿)GitHub/GitLab Release,只需保证 CI/CD 环境设置了对应的 token 环境变量,这与本地机器上完全一致:
- GitHub:
GITHUB_TOKEN(对应默认github.tokenRef); - GitLab:
GITLAB_TOKEN(对应默认gitlab.tokenRef)。
token 的注入与保护细节可参考 docs/environment-variables.md:不要将 token 提交进 Git 仓库,不要写入 release-it 配置,也不要提交 .env 文件;如需更换环境变量名,可使用 github.tokenRef="RELEASE_IT_GITHUB_TOKEN" 或 gitlab.tokenRef 覆盖。
GitLab CI
SSH(推荐)的前置条件
在 GitLab CI 中使用 release-it 时,文档列出的 SSH 要求:
- job 中安装
git与ssh包; - 预先执行
npm install; - 环境变量包含
GITLAB_TOKEN、SSH_PRIVATE_KEY、CI_EMAIL和CI_USER; - 仓库中添加拥有受保护分支写权限的用户或部署密钥(通过环境变量注入)。
Alpine 容器中的完整 pipeline
以下示例在 Alpine 中先安装 Git 与 OpenSSH,把 SSH 私钥加入 ssh-agent,配置 SSH,最后执行 release-it:
before_script:
- apk add --no-cache git openssh
- eval `ssh-agent -s`
- echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add - > /dev/null # add ssh key
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- git checkout $CI_COMMIT_REF_NAME
- git remote set-url origin "git@gitlab.com:$CI_PROJECT_PATH.git"
- git config --global user.name "${CI_USERNAME}"
- git config --global user.email "${CI_EMAIL}"
- npm install
script:
- npx release-it --ci
细节说明:
echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add -:tr -d '\r'用于剔除换行符的 CR 残留,这是 CI 环境变量常见陷阱;git remote set-url也可以不在 pipeline 里做,而是改用 release-it 配置中的git.pushRepo选项。该选项在 lib/plugin/git/Git.js 的getUpstreamArgs中被消费:当pushRepo配置且看起来像 URL 时,git push直接以该 URL 为目标;否则视为远端名并配合--set-upstream建立跟踪。npx release-it --ci:显式传入--ci确保全程无交互。
常见错误:fatal: tag vX.X.X already exists
GitLab CI 用户报告过该错误(社区 issue #573):CI 任务基于旧的 commit/tag 状态运行时,release-it 试图创建已存在的 Git tag。文档给出的缓解手段有两级:
- 先拉取远端分支再执行发布:
- git pull origin $CI_COMMIT_REF_NAME
- npm run release
- 在 release-it 的
before:init钩子中,以--prune-tags方式 fetch,提前删除远端已消失的本地 tag:
{
"hooks": {
"before:init": "git fetch --prune --prune-tags origin"
}
}
before:init 属于生命周期钩子,在插件 init 阶段之前执行(见 lib/index.js 的 runLifeCycleHook 实现中 runHook('before', name) 的调度)。若 tag 已存在且与最新 tag 相同,Git 插件会给出 warning 并继续(lib/plugin/git/Git.js 对 tag '.+' already exists 的容错),否则才抛错终止。
CI 模式下的源码级延伸:snapshot 与推送行为
从源码可以进一步推断 CI 场景下两个值得注意的联动行为:
- snapshot 模式的 CI 预置:在 lib/config.js 的
expandPreReleaseShorthand中,当使用--snapshot且为字符串形式时,release-it 会自动硬编码一组适合 CI 快照发布的选项:git.tagMatch = 0.0.0-<snapshot>.[0-9]*、git.getLatestTagFromAllRefs = true、git.requireBranch = '!main'(要求不在 main 分支)、git.requireUpstream = false、npm.ignoreVersion = true。这意味着 CI 中发布快照版本无需额外配置,release-it 会基于 Git tag 而非package.json计算版本。 - 推送失败的回滚:Git 插件在
push失败时,如果此前已打 tag 会执行git push origin --delete <tag>回滚远端 tag(lib/plugin/git/Git.js),并警告"package might have been released, but git push failed"——提醒 CI 用户在推送失败时注意 npm 侧可能已发布成功,需要人工核对。
常见问题与核查清单
| 场景 | 症状 | 解决方案 |
|---|---|---|
| Git push 401/权限拒绝 | HTTPS 方式未携带 token | 按上文重新 git remote add origin https://[user]:${GITHUB_TOKEN}@... |
npm whoami 失败但手动发布正常 |
registry 不支持该命令或 .npmrc 未生效 |
--npm.skipChecks;或确认 //registry.npmjs.org/:_authToken=$NPM_TOKEN 已写入 .npmrc |
.npmrc 被提交进 release commit |
token 泄露风险 | 将 .npmrc 加入 .gitignore |
tag already exists |
CI 基于过期状态运行 | before:init 中执行 git fetch --prune --prune-tags origin |
| CI 中 npm 发布触发交互 | 2FA/登录提示卡住 | 使用 --ci + NPM_TOKEN;或启用 npm.stage(staged publishing)把审批延后 |
| 包为 private 却尝试发布 | 认证错误 | 在 package.json 中设 "private": true,release-it 会自动跳过 npm 步骤 |
总结
在 CI/CD 环境中运行 release-it,本质上是把三个认证链路打通:Git 远端(SSH 或注入 token 的 HTTPS URL)、npm 注册表(.npmrc + NPM_TOKEN)、GitHub/GitLab Release(GITHUB_TOKEN/GITLAB_TOKEN),并配合 --ci(或依赖 ci-info 自动检测)关闭交互。遇到 npm 认证误报时用 --npm.skipChecks,遇到 tag 冲突时用 git fetch --prune --prune-tags 前置清理,遇到 2FA 卡顿时用 npm.stage 延后审批。本文所有配置片段与默认值均可对照 config/release-it.json、schema/npm.json 及 docs/git.md、docs/npm.md 继续深入查阅。