diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2eacd80..812727a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,9 @@ name: release # 推 v* 标签就发版:先跑双平台测试,再校验 tag 与 package.json 版本一致, # 然后 npm publish,最后用 .github/scripts/release-body.mjs 拼好的正文建 Release。 # 正文格式固定成「描述 / 本次变更 / 安装 / 链接」四段,别在 workflow 里手改文案。 +# +# 想先看效果再真发?手动触发时勾上 dry_run:标签可以还没建(就是拿来试格式的), +# 照样跑测试与全部校验,把 Release 标题和正文打到日志里,但不碰 npm、不建 Release。 on: push: tags: @@ -10,19 +13,29 @@ on: workflow_dispatch: inputs: tag: - description: '要发版的标签(必须已经存在,例如 v0.9.1)' + description: '要发版的标签(演练模式下可以填一个还不存在的)' required: true + dry_run: + description: '只演练:不发 npm、不建 Release' + type: boolean + default: false permissions: contents: write id-token: write +# 按「目标标签」而不是按 ref 分组:演练 v0.10.0 和真推 v0.10.0 落在同一组里, +# 免得演练还没跑完,真发版就跟它抢同一个版本号。 concurrency: - group: release-${{ github.ref }} + group: release-${{ github.event.inputs.tag || github.ref_name }} cancel-in-progress: false env: RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} + DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} + +# 注意:env 上下文在 step 的 with: 里不可用,所以检出用的 ref 只能就地写表达式。 +# 演练时标签可能还不存在,只能检出当前分支;真实发版才检出标签本身。 jobs: test: @@ -35,7 +48,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.tag || github.ref_name }} + ref: ${{ github.event.inputs.dry_run == 'true' && github.ref_name || github.event.inputs.tag || github.ref_name }} - uses: actions/setup-node@v4 with: @@ -69,7 +82,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.tag || github.ref_name }} + ref: ${{ github.event.inputs.dry_run == 'true' && github.ref_name || github.event.inputs.tag || github.ref_name }} fetch-depth: 0 - uses: actions/setup-node@v4 @@ -109,8 +122,19 @@ jobs: echo "published=true" >> "$GITHUB_OUTPUT" fi + - name: 演练模式:不发 npm + if: env.DRY_RUN == 'true' + run: | + name="$(node -p "require('./package.json').name")" + ver="$(node -p "require('./package.json').version")" + if [ "${{ steps.check.outputs.published }}" = "true" ]; then + echo "演练模式:本来会执行 npm publish --access public --provenance($name@$ver),这次跳过" + else + echo "演练模式:$name@$ver 在 registry 上已经有了,真实发版也会跳过 publish" + fi + - name: 发到 npm - if: steps.check.outputs.published == 'true' + if: steps.check.outputs.published == 'true' && env.DRY_RUN != 'true' env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | @@ -121,7 +145,7 @@ jobs: npm publish --access public --provenance - name: 发完回查一次 - if: steps.check.outputs.published == 'true' + if: steps.check.outputs.published == 'true' && env.DRY_RUN != 'true' run: | ver="$(node -p "require('./package.json').version")" name="$(node -p "require('./package.json').name")" @@ -148,7 +172,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.tag || github.ref_name }} + ref: ${{ github.event.inputs.dry_run == 'true' && github.ref_name || github.event.inputs.tag || github.ref_name }} fetch-depth: 0 - uses: actions/setup-node@v4 @@ -158,17 +182,30 @@ jobs: - name: 收集本次变更的提交 id: commits run: | + # 演练模式下这个标签往往还没建(就是拿来试格式的), + # 那就用当前检出点当终点,照样能看出正文长什么样。 + base="${RELEASE_TAG}" + if ! git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null; then + echo "标签 ${RELEASE_TAG} 还不存在,用当前检出点代替" + base="HEAD" + fi # 找上一个 tag 作为比较起点;没有就列全部历史。 - prev="$(git describe --tags --abbrev=0 "${RELEASE_TAG}^" 2>/dev/null || true)" + prev="$(git describe --tags --abbrev=0 "${base}^" 2>/dev/null || true)" if [ -n "$prev" ]; then echo "上一个 tag: $prev" - git log --pretty=format:'%s' "$prev..${RELEASE_TAG}" > commits.txt + git log --pretty=format:'%s' "$prev..${base}" > commits.txt else echo "没有上一个 tag,列出全部历史" - git log --pretty=format:'%s' "$RELEASE_TAG" > commits.txt + git log --pretty=format:'%s' "$base" > commits.txt fi echo "previous=$prev" >> "$GITHUB_OUTPUT" - echo "共 $(wc -l < commits.txt) 条提交" + # 别用 wc -l 数 commits.txt:git log --pretty=format: 末尾不补换行, + # 5 条提交只会数出 4 条。rev-list --count 才是准的。 + if [ -n "$prev" ]; then + echo "共 $(git rev-list --count "$prev..${base}") 条提交" + else + echo "共 $(git rev-list --count "$base") 条提交" + fi # 补发老标签时,那个 tag 的树里可能还没有这个脚本(脚本是后加的), # 就从默认分支取一份;取不到才退化成 GitHub 自动生成的说明。 @@ -191,7 +228,17 @@ jobs: --out release-body.md \ --title-out release-title.txt + - name: 演练模式:把标题与正文打出来 + if: env.DRY_RUN == 'true' + run: | + echo "标题:$(cat release-title.txt)" + echo "---------- 正文开始 ----------" + cat release-body.md + echo "---------- 正文结束 ----------" + echo "演练模式:不建 Release。确认没问题就去推这个标签,同一条流水线会真发。" + - name: 建 Release + if: env.DRY_RUN != 'true' env: GH_TOKEN: ${{ github.token }} run: | diff --git a/README.md b/README.md index fa42ee1..9f1f6a2 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,18 @@ npm install dsh-cura@0.9.1 需要仓库里配 `NPM_TOKEN`(Settings → Secrets → Actions),权限是 npm 的 Automation token;`--provenance` 需要 workflow 的 `id-token: write`,已经在文件里给了。 +### 发版演练 + +标签格式或正文想先看一眼再真发?手动触发时勾上 `dry_run`: + +```bash +gh workflow run release.yml -f tag=v0.10.0 -f dry_run=true +``` + +演练模式照样跑完整测试和全部校验(版本号对不对得上、registry 上有没有、正文怎么拼),然后把 Release 标题和正文打在日志里,**不发 npm、不建 Release**。这时候标签可以还不存在——反正不发布,就是拿来看格式的;`git describe` 找不到就退回用当前检出点当终点。 + +确认没问题,推同名标签,同一条流水线会真发。演练与真发按「目标标签」排队(不是按 ref),所以演练还没跑完时推标签不会两边抢同一个版本号。 + ## Roadmap - [x] 情境→表达规则库:LLM 从聊天记录抽取 `{"situation","style"}` 规则入库,按情境检索注入(MaiBot 1.x 模式) diff --git a/tests/test-release-body.mjs b/tests/test-release-body.mjs index 57a8ab0..3578338 100644 --- a/tests/test-release-body.mjs +++ b/tests/test-release-body.mjs @@ -160,5 +160,23 @@ ok('回查超时才算失败,不是首次没查到就失败', ok('回查只在上传过的那次跑(跳过 publish 时不该卡在回查)', /- name: 发完回查一次\s*\n\s*if: steps\.check\.outputs\.published == 'true'/.test(yml)); +// 演练模式:手动触发时可以只跑测试与校验、把正文打出来,不碰 npm 也不建 Release。 +// 少了这道闸,试标签格式就只能靠真发一版——版本号发出去就收不回来了。 +ok('workflow_dispatch 带 dry_run 开关', /workflow_dispatch:[\s\S]*?dry_run:[\s\S]*?type:\s*boolean/.test(yml)); +ok('dry_run 进到 env,后续步骤才读得到', /DRY_RUN:\s*\$\{\{\s*github\.event\.inputs\.dry_run/.test(yml)); +ok('演练时不发 npm', /- name: 发到 npm\s*\n\s*if: steps\.check\.outputs\.published == 'true' && env\.DRY_RUN != 'true'/.test(yml)); +ok('演练时也不回查(没发就没什么可查)', /- name: 发完回查一次\s*\n\s*if: steps\.check\.outputs\.published == 'true' && env\.DRY_RUN != 'true'/.test(yml)); +ok('演练时不建 Release', /- name: 建 Release\s*\n\s*if: env\.DRY_RUN != 'true'/.test(yml)); +ok('演练时把标题与正文打到日志里', /- name: 演练模式:把标题与正文打出来\s*\n\s*if: env\.DRY_RUN == 'true'\s*\n/.test(yml) && /cat release-body\.md/.test(yml)); +ok('演练时标签可以还不存在(拿当前检出点当终点)', + /refs\/tags\/\$\{RELEASE_TAG\}/.test(yml) && /base="HEAD"/.test(yml)); +ok('演练时检出的是分支而不是标签(标签可能还没建)', + /github\.event\.inputs\.dry_run == 'true' && github\.ref_name \|\| github\.event\.inputs\.tag/.test(yml)); + +// git log --pretty=format: 末尾不补换行,wc -l 会少数一条(6 条提交数出 5)。 +// 这个数会打在日志里,数错了看起来就像漏了提交。 +ok('提交条数用 rev-list --count 数,不用 wc -l 数文件', + /git rev-list --count/.test(yml) && !/wc -l < commits\.txt/.test(yml)); + console.log('\n' + (fail === 0 ? 'all release body tests passed ✓' : fail + ' 项失败')); if (fail) process.exitCode = 1;