Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,39 @@ name: release
# 推 v* 标签就发版:先跑双平台测试,再校验 tag 与 package.json 版本一致,
# 然后 npm publish,最后用 .github/scripts/release-body.mjs 拼好的正文建 Release。
# 正文格式固定成「描述 / 本次变更 / 安装 / 链接」四段,别在 workflow 里手改文案。
#
# 想先看效果再真发?手动触发时勾上 dry_run:标签可以还没建(就是拿来试格式的),
# 照样跑测试与全部校验,把 Release 标题和正文打到日志里,但不碰 npm、不建 Release。
on:
push:
tags:
- 'v*'
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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand All @@ -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")"
Expand All @@ -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
Expand All @@ -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 自动生成的说明。
Expand All @@ -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: |
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 模式)
Expand Down
18 changes: 18 additions & 0 deletions tests/test-release-body.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading