前言
最近新項目準備做自動發版,就去研究了一下 semantic-release 。
什么是 semantic-release
Fully automated version management and package publishing
semantic-release automates the whole package release workflow including: determining the next version number, generating the release notes and publishing the package.
This removes the immediate connection between human emotions and version numbers, strictly following the Semantic Versioning specification.
用人話說就是一個完全自動化的工具,可以幫助你做版本號管理、生成 changelog ,并且發布到包管理器 ( 比如 npm ) 。
如何使用 ( 官方文檔 )
Install semantic-release in your project
Configure your Continuous Integration service to
run semantic-release
Configure your Git repository and package manager repository authentication in your Continuous Integration service
Configure semantic-release options and plugins
cd your-module
npx semantic-release-cli setup
# For Node modules projects
npm install --save-dev semantic-release
# Then in the CI environment
npx semantic-release
以上是官方文檔的內容,操作真的是很簡單,簡單到不知道它是干嘛的,簡單到后面步入一個個坑不知道怎么解決。我照著官方文檔折騰了幾個小時愣是沒跑通,一直在 error 。要不是看在很多項目 ( 比如 octokit ) 都在用它,我都想直接放棄了。
官方文檔只是告訴你需要安裝這么一個包,有哪些參數,剩下的都沒細說,就算有也是藏在某個角落里生怕別人找到:
是否需要新建 GitHub 倉庫?
如何配置 GitHub 和 npm 的 token ?
何時觸發 發布 操作的?手動執行命令?push ?
如何測試?一定要在 CI 環境中?
正確打開方式
我特地建了一個新倉庫 semantic-release-test 來演示效果。感興趣的朋友可以照著我的提交記錄走一遍流程。
-
本地新建一個項目,比如
semantic-release-test
目錄結構
. ├─.github │ └─workflows │ └─release.yml ├─src │ └─index.js ├─.env ├─.releaserc.js ├─package.json
package.json
替換為自己的包名和倉庫地址
{ "name": "@anyesu/semantic-release-test", "version": "0.0.0-development", "description": "semantic-release-test", "main": "src/index.js", "scripts": { "build": "echo build success.", "semantic-release": "dotenv -c -- semantic-release", "semantic-release:local": "dotenv -c -- semantic-release --no-ci" }, "publishConfig": { "registry": "https://registry.npmjs.org/", "access": "public" }, "repository": { "type": "git", "url": "https://github.com/iewgggg/semantic-release-test.git" }, "author": "anyesu", "license": "MIT", "bugs": { "url": "https://github.com/iewgggg/semantic-release-test/issues" }, "homepage": "https://github.com/iewgggg/semantic-release-test#readme", "files": [ "src" ], "devDependencies": { "@semantic-release/git": "^9.0.0", "conventional-changelog-cmyr-config": "^1.2.3", "dotenv-cli": "^4.0.0", "semantic-release": "^17.2.1" } }
.releaserc.js
semantic-release 的配置文件,具體配置項見 文檔 。
module.exports = { plugins: [ "@semantic-release/commit-analyzer", [ "@semantic-release/release-notes-generator", { config: "conventional-changelog-cmyr-config", }, ], "@semantic-release/npm", [ "@semantic-release/git", { assets: ["package.json"], }, ], "@semantic-release/github", ], };
.env
環境變量配置文件,通過
dotenv-cli
在運行時讀取NPM_TOKEN 如果是私有倉庫或者不想發布到 npm 可以不填
GITHUB_TOKEN=****** NPM_TOKEN=******
.github/workflows/release.yml
CI - GitHub Actions 的配置文件,可選。
GitHub Actions 會自動創建名為 GITHUB_TOKEN 的 secret ,不需要手動添加
只需要 添加 額外的 NPM_TOKEN
name: Release on: repository_dispatch: types: [ semantic-release ] push: branches: - master - next - beta - "*.x" # maintenance releases such as 15.x jobs: release: name: release runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: 12 - run: npm i - run: npm run build - run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
-
初始化為 Git 項目,并提交初始化版本
git init git add package.json .releaserc.js .github/workflows/release.yml git commit -m "feat: :tada: Initial commit."
-
新建遠程倉庫 ( 比如 semantic-release-test )
很重要,一定要有遠程倉庫
-
推送到 GitHub
git remote add origin https://{GITHUB_TOKEN}@github.com/{owner}/{repo} git push -u origin master
如果按照上面步驟配置了 GitHub Actions ,那么打開 GitHub 中的項目可以看到已經成功生成了 release 。
同時還可以看到有一條新的提交記錄
chore(release): 1.0.0 [skip ci]
-
本地測試
安裝項目
git pull npm i
隨便修改一個文件提交一條新記錄
echo test > test.txt git add test.txt git commit -m "fix: :apple: Fixing something on macOS."
測試效果
npm run semantic-release
<details>
<summary>本地日志</summary>[semantic-release] ? i Running semantic-release version 17.2.1 [semantic-release] ? √ Loaded plugin "verifyConditions" from "@semantic-release/npm" [semantic-release] ? √ Loaded plugin "verifyConditions" from "@semantic-release/git" [semantic-release] ? √ Loaded plugin "verifyConditions" from "@semantic-release/github" [semantic-release] ? √ Loaded plugin "analyzeCommits" from "@semantic-release/commit-analyzer" [semantic-release] ? √ Loaded plugin "generateNotes" from "@semantic-release/release-notes-generator" [semantic-release] ? √ Loaded plugin "prepare" from "@semantic-release/npm" [semantic-release] ? √ Loaded plugin "prepare" from "@semantic-release/git" [semantic-release] ? √ Loaded plugin "publish" from "@semantic-release/npm" [semantic-release] ? √ Loaded plugin "publish" from "@semantic-release/github" [semantic-release] ? √ Loaded plugin "addChannel" from "@semantic-release/npm" [semantic-release] ? √ Loaded plugin "addChannel" from "@semantic-release/github" [semantic-release] ? √ Loaded plugin "success" from "@semantic-release/github" [semantic-release] ? √ Loaded plugin "fail" from "@semantic-release/github" [semantic-release] ? ? This run was not triggered in a known CI environment, running in dry-run mode. [semantic-release] ? ? Run automated release from branch master on repository https://[secure]@github.com/iewgggg/semantic-release-test.git in dry-run mode [semantic-release] ? √ Allowed to push to the Git repository [semantic-release] ? i Start step "verifyConditions" of plugin "@semantic-release/npm" [semantic-release] ? √ Completed step "verifyConditions" of plugin "@semantic-release/npm" [semantic-release] ? i Start step "verifyConditions" of plugin "@semantic-release/git" [semantic-release] ? √ Completed step "verifyConditions" of plugin "@semantic-release/git" [semantic-release] ? i Start step "verifyConditions" of plugin "@semantic-release/github" [semantic-release] [@semantic-release/github] ? i Verify GitHub authentication [semantic-release] ? √ Completed step "verifyConditions" of plugin "@semantic-release/github" [semantic-release] ? i Found git tag v1.0.0 associated with version 1.0.0 on branch master [semantic-release] ? i Found 1 commits since last release [semantic-release] ? i Start step "analyzeCommits" of plugin "@semantic-release/commit-analyzer" [semantic-release] [@semantic-release/commit-analyzer] ? i Analyzing commit: fix: :apple: Fixing something on macOS. [semantic-release] [@semantic-release/commit-analyzer] ? i The release type for the commit is patch [semantic-release] [@semantic-release/commit-analyzer] ? i Analysis of 1 commits complete: patch release [semantic-release] ? √ Completed step "analyzeCommits" of plugin "@semantic-release/commit-analyzer" [semantic-release] ? i The next release version is 1.0.1 [semantic-release] ? i Start step "generateNotes" of plugin "@semantic-release/release-notes-generator" [semantic-release] ? √ Completed step "generateNotes" of plugin "@semantic-release/release-notes-generator" [semantic-release] ? ? Skip step "prepare" of plugin "@semantic-release/npm" in dry-run mode [semantic-release] ? ? Skip step "prepare" of plugin "@semantic-release/git" in dry-run mode [semantic-release] ? ? Skip v1.0.1 tag creation in dry-run mode [semantic-release] ? ? Skip step "publish" of plugin "@semantic-release/npm" in dry-run mode [semantic-release] ? ? Skip step "publish" of plugin "@semantic-release/github" in dry-run mode [semantic-release] ? ? Skip step "success" of plugin "@semantic-release/github" in dry-run mode [semantic-release] ? √ Published release 1.0.1 on default channel [semantic-release] ? i Release note for version 1.0.1: ## 1.0.1 (https://github.com/iewgggg/semantic-release-test/compare/v1.0.0...v1.0.1) (2020-10-22) ### ? Bug Fixes * ? Fixing something on macOS. (c83864b (https://github.com/iewgggg/semantic-release-test/commit/c83864b))
</details>
已經可以看到根據 提交記錄 生成的 changelog 的內容,由于不是在 CI 環境中運行的,所以實際并沒有發布到 GitHub 和 npm 。
通過預覽知道已經 OK 了,下面可以在本地進行發布
npm run semantic-release:local
<details>
<summary>本地日志</summary>[semantic-release] ? i Running semantic-release version 17.2.1 [semantic-release] ? √ Loaded plugin "verifyConditions" from "@semantic-release/npm" [semantic-release] ? √ Loaded plugin "verifyConditions" from "@semantic-release/git" [semantic-release] ? √ Loaded plugin "verifyConditions" from "@semantic-release/github" [semantic-release] ? √ Loaded plugin "analyzeCommits" from "@semantic-release/commit-analyzer" [semantic-release] ? √ Loaded plugin "generateNotes" from "@semantic-release/release-notes-generator" [semantic-release] ? √ Loaded plugin "prepare" from "@semantic-release/npm" [semantic-release] ? √ Loaded plugin "prepare" from "@semantic-release/git" [semantic-release] ? √ Loaded plugin "publish" from "@semantic-release/npm" [semantic-release] ? √ Loaded plugin "publish" from "@semantic-release/github" [semantic-release] ? √ Loaded plugin "addChannel" from "@semantic-release/npm" [semantic-release] ? √ Loaded plugin "addChannel" from "@semantic-release/github" [semantic-release] ? √ Loaded plugin "success" from "@semantic-release/github" [semantic-release] ? √ Loaded plugin "fail" from "@semantic-release/github" [semantic-release] ? √ Run automated release from branch master on repository https://[secure]@github.com/iewgggg/semantic-release-test.git [semantic-release] ? √ Allowed to push to the Git repository [semantic-release] ? i Start step "verifyConditions" of plugin "@semantic-release/npm" [semantic-release] ? √ Completed step "verifyConditions" of plugin "@semantic-release/npm" [semantic-release] ? i Start step "verifyConditions" of plugin "@semantic-release/git" [semantic-release] ? √ Completed step "verifyConditions" of plugin "@semantic-release/git" [semantic-release] ? i Start step "verifyConditions" of plugin "@semantic-release/github" [semantic-release] [@semantic-release/github] ? i Verify GitHub authentication [semantic-release] ? √ Completed step "verifyConditions" of plugin "@semantic-release/github" [semantic-release] ? i Found git tag v1.0.0 associated with version 1.0.0 on branch master [semantic-release] ? i Found 1 commits since last release [semantic-release] ? i Start step "analyzeCommits" of plugin "@semantic-release/commit-analyzer" [semantic-release] [@semantic-release/commit-analyzer] ? i Analyzing commit: fix: :apple: Fixing something on macOS. [semantic-release] [@semantic-release/commit-analyzer] ? i The release type for the commit is patch [semantic-release] [@semantic-release/commit-analyzer] ? i Analysis of 1 commits complete: patch release [semantic-release] ? √ Completed step "analyzeCommits" of plugin "@semantic-release/commit-analyzer" [semantic-release] ? i The next release version is 1.0.1 [semantic-release] ? i Start step "generateNotes" of plugin "@semantic-release/release-notes-generator" [semantic-release] ? √ Completed step "generateNotes" of plugin "@semantic-release/release-notes-generator" [semantic-release] ? i Start step "prepare" of plugin "@semantic-release/npm" [semantic-release] [@semantic-release/npm] ? i Write version 1.0.1 to package.json in pathto\semantic-release-test v1.0.1 [semantic-release] ? √ Completed step "prepare" of plugin "@semantic-release/npm" [semantic-release] ? i Start step "prepare" of plugin "@semantic-release/git" [semantic-release] [@semantic-release/git] ? i Found 1 file(s) to commit [semantic-release] [@semantic-release/git] ? i Prepared Git release: v1.0.1 [semantic-release] ? √ Completed step "prepare" of plugin "@semantic-release/git" [semantic-release] ? i Start step "generateNotes" of plugin "@semantic-release/release-notes-generator" [semantic-release] ? √ Completed step "generateNotes" of plugin "@semantic-release/release-notes-generator" [semantic-release] ? √ Created tag v1.0.1 [semantic-release] ? i Start step "publish" of plugin "@semantic-release/npm" [semantic-release] [@semantic-release/npm] ? i Skip publishing to npm registry as package.json's private property is true [semantic-release] ? √ Completed step "publish" of plugin "@semantic-release/npm" [semantic-release] ? i Start step "publish" of plugin "@semantic-release/github" [semantic-release] [@semantic-release/github] ? i Published GitHub release: https://github.com/iewgggg/semantic-release-test/releases/tag/v1.0.1 [semantic-release] ? √ Completed step "publish" of plugin "@semantic-release/github" [semantic-release] ? i Start step "success" of plugin "@semantic-release/github" [semantic-release] ? √ Completed step "success" of plugin "@semantic-release/github" [semantic-release] ? √ Published release 1.0.1 on default channel
</details>
再打開 GitHub 中的項目可以看到 release v1.0.1 也生成了。
Tip: 可以注意到兩次 release 的作者是不一樣的,因為使用了不同的 GITHUB_TOKEN 。
此時雖然 GitHub Actions 中多了一個新的執行記錄:
chore(release): 1.0.1 [skip ci]
但不需要擔心會重復發布,在日志中可以看到下面的內容:
There are no relevant changes, so no new version is released.
原理分析
首先,我們結合 semantic-release
和 semantic-release:local
兩個命令的日志看下它究竟做了什么
加載插件。
判斷是否在 CI 環境中,不是則跳過執行插件的一些生命周期。
檢查遠程倉庫是否存在,以及合法的分支。
-
檢查是否有倉庫的寫入權限。
這一步比較坑爹,如果沒有設置 GITHUB_TOKEN 報錯竟然是版本落后???
Run automated release from branch master on repository https://github.com/iewgggg/semantic-release-test.git in dry-run mode The local branch master is behind the remote one, therefore a new version won't be published.
設置 GITHUB_TOKEN 后再看日志感受下區別
Run automated release from branch master on repository https://[secure]@github.com/iewgggg/semantic-release-test.git in dry-run mode √ Allowed to push to the Git repository
這個問題也是我 debug 源碼后才發現的。
檢查 NPM_TOKEN 是否存在,并寫入到 temp 目錄的 .npmrc 文件中。
檢查當前分支是否存在符合 語義化版本 的 tag,存在則為當前版本號。
對比 遠程倉庫 是否有新的提交記錄,無則終止。
插件
@semantic-release/commit-analyzer
開始對 新的提交記錄 逐條分析,篩選符合 提交規范 的記錄, 篩選結果 為空則終止。如果存在 當前版本號 ,則根據 篩選結果 確定升級類型 ( MAJOR / MINOR / PATCH ) 計算 新的版本號 ,否則 新版本號 為
1.0.0
。插件
@semantic-release/release-notes-generator
再對 新的提交記錄 逐條分析生成 changelog 。插件
@semantic-release/npm
將 新的版本號 重新寫入到 package.json 文件中。-
插件
@semantic-release/git
對修改后的 package.json 進行 提交 和 推送 。插件默認配置還會修改 CHANGELOG.md 文件,本文中配置為只修改 package.json
添加新版本號 tag 。
npm 打包 并 發布 ,如果 package.json 設置了
"private": true
則跳過。發布到 GitHub ,包括壓縮包和 changelog 。
整個流程比較清晰了,主要操作也是通過幾個插件來完成,所以通過對插件的組合、配置來實現一些個性化的需求。
缺點
semantic-release 強依賴于遠程倉庫的分支狀態,所以在正式使用前測試會非常麻煩,反復測試需要反復重置遠程倉庫的狀態,而且網絡不好就更惡心了。
-
release 中的 changelog 是發布一個版本生成一次的,這就意味著倉庫遷移的話,歷史記錄就沒有了,只剩下 tag 。
如果沒有使用插件
@semantic-release/git
的話可以不斷 reset HEAD 來恢復一個個版本,當然這也很麻煩。
FAQ
-
官方文檔太爛怎么辦?
直接閱讀源碼 + debug ,以力破法。
-
為何本地執行命令會卡住?
因為分別需要連接到 GitHub 和 npm ,網絡環境差就會卡住。
-
如何不發布到 npm?
移除插件
@semantic-release/npm
即可,但這樣 package.json 中的版本號得不到修改。在 package.json 中設置
"private": true
。插件
@semantic-release/npm
配置為npmPublish: false
。
-
registry 設置為 https://registry.npm.taobao.org 但需要發布到 npm?
在 package.json 中設置
"publishConfig": { "registry": "https://registry.npmjs.org/" }
-
npm 發布報錯?
npm ERR! 402 Payment Required - PUT https://registry.npmjs.org/ - You must sign up for private packages
第一次發布時會出現這個錯誤,因為 scoped packages 默認會被發布為 私有包 ,需要將其發布為 公有包 。見 官方文檔 。
下面有三種方案
-
修改 命令行 參數
npm publish --access public
-
在 .npmrc 中設置
access=public
-
在 package.json 中設置
"publishConfig": { "access": "public" }
-
-
npm 開啟雙因素認證 ( Two Factor Authentication ) 后發布報錯?
The npm token (https://github.com/semantic-release/npm/blob/master/README.md#npm-registry-authentication) configured in the NPM_TOKEN environment variable must be a valid token (https://docs.npmjs.com/getting-started/working_with_tokens) allowing to publish to
the registry https://registry.npmjs.org/.If you are using Two-Factor Authentication, make configure the auth-only level (https://docs.npmjs.com/getting-started/using-two-factor-authentication#levels-of-authentication) is supported. semantic-release cannot publish with the default auth-and-writes level.
雙因素認證 需要設置為 auth-only 級別 ( 文檔 ) 。
Access Tokens 類型選擇 Publish 而不是 Automation 。
-
如何同步更新
package.json
中的版本號?默認不更新版本號,可以添加插件
@semantic-release/git
來額外提交。 -
如何在
1.0.0
之前發布 prerelease 版本,如:1.0.0-beta.10 ?項目不包含 release.yml 時初始化后推送到 master 分支,這樣確保 遠程倉庫 有一個 release 分支 并且不會自動發布。
切到 beta 分支提交 release.yml 后推送,自動發布為
1.0.0-beta.1
。
這樣就生成 beta 版本了,后續迭代繼續在 beta 分支上進行,發布正式版只需將 beta 分支合并到 master 分支,然后所有 beta 版的 changelog 都會合并到
1.0.0
中。( 參考 ) -
如何手動管理版本?
可以使用 standard-version ,二者區別:
standard-version
takes a different approach by handling versioning, changelog generation, and git tagging for you without automatic pushing (to GitHub) or publishing (to an npm registry). Use ofstandard-version
only affects your local git repo - it doesn't affect remote resources at all. After you runstandard-version
, you can review your release state, correct mistakes and follow the release strategy that makes the most sense for your codebase."scripts": { "release:beta": "standard-version --release-as major --prerelease beta", "release:major": "standard-version --release-as major", "release:minor": "standard-version --release-as minor", "release:patch": "standard-version --release-as patch" }, "devDependencies": { "standard-version": "^9.0.0" }
-
如何手動發布?
本地執行
npm run semantic-release:local
。-
通過 GitHub Actions Hook 調用,詳見 文檔 。
也可以添加按鈕 - https://github-action-button.web.app
-
如何修改 changelog 的格式?
這部分功能底層依賴于 conventional-changelog ,已經內置幾種預設 (
angular
,atom
,codemirror
,ember
,eslint
,express
,jquery
,jshint
,conventionalcommits
) 可供選擇,我這里使用的是第三方配置 conventional-changelog-cmyr-config ,需要更加個性化詳見 conventional-changelog-writer 包配置項。本來我想配置成 antd 那樣的雙語 changelog ( 再復制一份用 翻譯 API 翻譯一下,再手動微調 ),但還要 fork 發包,那就先算了吧。
IDEA 插件推薦
-
通過表單的形式結構化輸入 commit message 。
符合 AngularJS 的 Git Commit Guidelines 規范。
-
根據輸入的內容來選擇 emoji 表情 的表達式。
-
Gitmoji 插件的 fork 版本,可以直接顯示 emoji 表情 而非表達式。
兩個插件都挺不錯的,缺點就是寫死了不可配置,再加上幾年沒維護了,用起來有點尷尬。
最后
按照文中的配置,基本上滿足我的需求了:
自動化版本管理,推送代碼或者合并 PR 即可發版。
自動生成 changelog ,不再需要單獨去寫。
自動發布到 npm ( ?? 直接省去了學習 npm 發包的步驟 ) 。
發布后對應的 PR 和 issue 下會添加 發布通知 的評論,就不需要自己再手動通知了。