Appearance
MikuMikuAR 发版程序(Release Process)
作用:本文件是人类操作视角的发版标准作业程序(SOP)。机器视角见
.github/workflows/release.yml,版本发布说明见同目录vX.Y.Z.md,缓存机制见docs/adr/adr-082-ci-cross-tag-cache-warm.md。 单一事实源:版本号以package.json的version字段为准,三平台构建脚本均从此读取。
AI 一键发版入口
给 AI 用:下面 §2 的 9 步已被封装为
scripts/release.ps1,幂等可跑。 AI 接到发版请求时,先读本节,再决定是直接调脚本还是按 §2 手动走。
快速用法
powershell
# 演练(只打印不执行,确认 9 步流程无误)
.\scripts\release.ps1 -Version 1.9.1 -DryRun
# 正式发版(自动改版本号 → 提交 → 推 main → 等缓存 → 打 tag → 监控 CI → 核对 Release)
.\scripts\release.ps1 -Version 1.9.1脚本不替你做的事(需 AI 或人类补)
| 事项 | 原因 | 谁来补 |
|---|---|---|
写 docs/releases/vX.Y.Z.md | 创意性写文档,无法模板化 | AI 按既有 v1.3.5.md 格式写 |
| Android Secrets 校验 | 需 gh secret list,发版前一次性确认 | AI 跑 gh secret list 核对四键 |
| 应用内版本核对 | 需启动应用读「关于」页 | 人类或 E2E |
build/windows/info.json 字段名变化 | 脚本假设 file_version + ProductVersion | 若字段改名,同步改脚本 |
与本文件各节的对应关系
| 脚本步骤 | 对应文档章节 |
|---|---|
| step 2(改版本号) | §2 步 2、§4 三平台注入 |
| step 3+5(提交+推 main) | §2 步 3/5 |
| step 4(校验 notes 存在) | §2 步 4、§6 发布说明约定 |
| step 6(等缓存预热) | §2 步 6、§1.3 缓存预热顺序、ADR-082 |
| step 7(打 tag) | §2 步 7、§3 版本一致性闸门 |
| step 8(监控 CI) | §2 步 8 |
| step 9(核对 Release) | §2 步 9、§7 回滚/补发 |
0. 触发机制总览
| 触发方式 | 入口 | 行为 |
|---|---|---|
| 推 tag | git push origin vX.Y.Z | 跑 prepare 校验 → 并行构建 Win/Linux/Android → 建 GitHub Release |
| 手动触发 | workflow_dispatch(含 skip_sign 选项) | 仅构建三平台;不建 Release(见 §5 注意事项) |
产物:Windows dist/MikuMikuAR-X.Y.Z-windows-amd64.exe、Linux bin/mikumikuar-X.Y.Z-linux-amd64、Android dist/*.apk,全部挂到同名 GitHub Release。
1. 前置条件
1.1 依赖与版本
- Go
1.25.0(CI 固定;wails/v3版本从go.mod动态读取,三者同源,禁写死@latest)。 - Node
24、npm ci基于frontend/package-lock.json。 - Wails v3 CLI:CI 由
go install安装并缓存;本地需go install github.com/wailsapp/wails/v3/cmd/wails3@<go.mod 中版本>。
1.2 Secrets(仅 Android 签名需要)
| Secret | 用途 |
|---|---|
ANDROID_KEYSTORE_BASE64 | Base64 编码的 release keystore;缺失则只出 debug APK |
ANDROID_KEYSTORE_PASSWORD | keystore 密码 |
ANDROID_KEY_ALIAS | 签名别名 |
ANDROID_KEY_PASSWORD | 密钥密码 |
GITHUB_TOKEN 由 runner 自动提供,无需配置。
1.3 缓存预热顺序(关键,避免冷启动)
见 ADR-082:tag run 的 actions/cache 作用域按 tag 隔离,跨发版不可见。必须:
- 先把依赖变更(改
go.mod/go.sum/frontend/package-lock.json)推到main,等cache-warm.yml落盘到main作用域(约 1–2 分钟)。 - 再推
vX.Y.Ztag,Release run 才会回退命中缓存,否则三平台wails3/node_modules/go modules全冷启动,多花 3–4 分钟。
- 升 wails 版本或加 npm 包 → 缓存 key 变化 → 同样需先推
main重暖一次。
2. 标准发版步骤(9 步)
可一步到位执行的命令序列见附录 §11。
定版本号:按 semver 决定
X.Y.Z(当前见package.json)。改
package.json的version:这是唯一事实源。- 同步修改
build/windows/info.json的file_version和ProductVersion(该文件不是事实源,但 Windows 产物属性读取它,不同步则右键属性显示旧版本)。
- 同步修改
提交版本号变更:
bashgit add package.json build/windows/info.json git commit -m "chore: bump version to X.Y.Z"写发布说明:新建
docs/releases/vX.Y.Z.md(手写 notes;缺则 CI 自动生成,质量不可控)。- 格式参考同目录既有
v1.3.5.md。 - ⚠️ 路径大小写敏感:CI 查
docs/releases/vX.Y.Z.md(小写releases)。docs/Releases/(大写 R)在 Linux runner 上不会命中。已在 v1.5.0 修复 CI 为小写,但如果改目录名或路径结构,务必同步更新release.yml:339的NOTES_FILE。
- 格式参考同目录既有
提交发布说明:
bashgit add docs/releases/vX.Y.Z.md git commit -m "docs: add vX.Y.Z release notes" git push origin main等缓存预热(仅当本版动了依赖时):若修改了
go.mod/go.sum/frontend/package-lock.json,等cache-warmworkflow 落盘(约 1–2 分钟)。机器视角判定(workflow 文件名cache-warm.yml,display nameCache Warm):bash# 查最近一次 cache-warm 状态(success=绿勾,可继续;in_progress/queued=再等) gh run list --workflow cache-warm.yml --branch main --limit 1 \ --json status,conclusion,createdAt,displayTitle # 或阻塞等到它跑完(建议先 push main 触发它,再 watch) gh run watch $(gh run list --workflow cache-warm.yml --branch main --limit 1 --json databaseId --jq '.[0].databaseId')未动依赖则跳过此步。
打 tag 触发:
bashgit tag vX.Y.Z && git push origin vX.Y.Z等 CI 完成:
gh run list --workflow release.yml --limit 3监控进度。四个 job 全绿:Prepare → Windows → Linux → Android → GitHub Release。核对 Release:
- 到
https://github.com/eghrhegpe/MikuMikuAR/releases/tag/vX.Y.Z确认三平台产物齐全、body 为手写 notes(非自动生成的**Full Changelog**: https://...)。 - 若 body 不对:
gh release edit vX.Y.Z --notes-file docs/releases/vX.Y.Z.md修正(无需重跑 CI)。 - 应用内「关于 / 检查更新」应显示真实版本(非
dev)。
- 到
3. 版本一致性闸门(CI 强制)
release.yml 的 prepare 步会执行:
PKG_VER = package.json.version
TAG_VER = git tag 去掉 'v' 前缀
if PKG_VER != TAG_VER → ::error::Version mismatch → 中断操作者必须保证 package.json.version === tag(tag 即 v + 版本号,如 1.3.5 → v1.3.5),否则流水线在第一步即失败。
4. 版本号注入三平台对照
应用内「关于 / 检查更新」依赖 main.AppVersion(及 BuildTime / CommitHash)。三平台注入方式各异,新增构建路径必须复制注入逻辑,否则显示 dev:
| 平台 | 注入方式 | 代码位置 |
|---|---|---|
| Windows | 构建前同步 build/config.yml 的 version + build/windows/Taskfile.yml 的 {{.APP_VERSION}}/{{.BUILD_TIME}}/{{.COMMIT_HASH}} 占位符 → wails3 build 读取;失败时降级 go build -ldflags "-X main.AppVersion=..." | scripts/build-windows.ps1(§39–60、§117–119) |
| Linux | go build -ldflags="-X main.AppVersion=$VER -X main.BuildTime=... -X main.CommitHash=... -s -w" | release.yml build-linux 步(§190–192) |
| Android | go build -buildmode=c-shared -ldflags "-X main.AppVersion=... -X main.BuildTime=... -X main.CommitHash=..." | scripts/build-android-so.ps1(§73–74) |
三平台均从
package.json.version读取,单一事实源一致。
5. 手动触发(workflow_dispatch)与 skip_sign
- 入口:GitHub Actions → Release → Run workflow。
- 输入
skip_sign:true时 Android 强制走 debug 构建(不签名)。 - 注意事项:
release.yml的release步带if: startsWith(github.ref, 'refs/tags/v')。手动触发时github.ref是分支引用而非 tag,因此release步被跳过,只构建不发布。手动触发仅用于验证构建,真正发版必须走 tag。
6. 发布说明约定
- 路径:
docs/releases/vX.Y.Z.md(与版本号严格对应)。 release步优先用该文件作 Release body;缺失则generate_release_notes: true自动生成(基于 PR/commit,质量不可控,不建议依赖)。- 文件名带
v前缀,与 tag 一致。 - ⚠️ 路径大小写:文件名采用小写
docs/releases/。release.yml:339也对应小写。若改目录名须同步 CI。 - ⚠️ body 覆盖顺序:CI 的
Create Release (hand-written notes)步是幂等的——同名 tag 重推会覆盖前一次的 Release body。所以即使第一次 body 错了,重推 tag 修正后 body 会恢复。但若 CI 未命中 hand-written 分支(路径不对或文件缺失),自动生成会覆盖之前的 body。
7. 回滚 / 补发
- 发布内容有误但产物可复用:直接编辑 GitHub Release 的 body 或重新上传资产,无需重跑 CI。
- 需要重新构建:修正代码/
package.json后,必须 删除旧 tag 并重建同名 tag 才能复触发(git tag -d vX.Y.Z && git push origin :vX.Y.Z,再重新打 tag 推送)。注意:同名 tag 重推会复用缓存,但go-build-<sha>因 commit 变化必 miss(设计如此,见 ADR-082 §七)。 - 撤销已发布 Release:GitHub 删除 Release 即可,tag 可保留或同步删除;不涉及代码回退时无需 revert commit。
8. 本地干跑(发版前自检)
发版前在本地验证构建链路,避免 CI 白跑:
bash
# Windows
npm run build:win # = scripts/build-windows.ps1(默认 debug tags)
# Linux
npm run build:linux # = scripts/build-linux.sh
# Android(debug)
npm run build:android # = scripts/build-android.ps1 -Arch arm64
# Android(release 签名)
npm run build:android:release # = scripts/build-android.ps1 -Arch arm64 -Production本地构建同样会注入 package.json.version,可在产物「关于」中核对版本显示。
9. 发版验证清单
发版前
- [ ]
package.json.version已更新且等于目标 tag(去v)。(§2 步 1–2) - [ ]
build/windows/info.json的file_version和ProductVersion已同步。(§2 步 2) - [ ] 已提交版本号变更(package.json + info.json)。(§2 步 3)
- [ ]
docs/releases/vX.Y.Z.md已写好(手写 notes;路径确认小写releases)。(§2 步 4) - [ ] 已提交发布说明并
git push origin main。(§2 步 5)
发版中
- [ ] 若动了依赖:已先推
main并等cache-warm绿勾(gh run list --workflow cache-warm.yml --branch main)。(§2 步 6) - [ ]
git tag vX.Y.Z && git push origin vX.Y.Z已执行。(§2 步 7) - [ ] CI 四 job 全绿(Prepare / Windows / Linux / Android),无
Version mismatch。(§2 步 8)
发版后
- [ ] GitHub Release 已建,三平台产物齐全(Windows .exe / Linux binary / Android .apk)。(§2 步 9)
- [ ] Release body 为手写 notes(非自动生成 changelog)。(§2 步 9;不符则
gh release edit vX.Y.Z --notes-file docs/releases/vX.Y.Z.md) - [ ] 应用内「关于 / 检查更新」显示真实版本(非
dev)。(§4 三平台注入对照) - [ ] Android:有签名则为 release APK,无签名则为 debug(符合预期)。(§1.2 Secrets)
10. 发版常见坑
10.1 版本与构建
| 坑 | 现象 | 根因 | 对策 |
|---|---|---|---|
info.json 版本漏改 | Windows 产物属性显示 1.0.0 | 该文件硬编码,非事实源,SOP 容易忘 | checklist 第一项即校验它 |
| 手写 notes 路径大小写 | Release body 自动生成 | CI 查 docs/Releases/(大写 R)但仓库实际 docs/releases/(小写 r) | 已修:release.yml:339 改为小写;不改目录结构不会再犯 |
| 手动触发 workflow_dispatch | 构建成功但无 Release | release job 有 if: startsWith(github.ref, 'refs/tags/v'),手动触发时 ref 是分支 | 正式发版必须走 tag |
| Android Secrets 缺失 | 产出 debug APK(不可发布) | ANDROID_KEYSTORE_BASE64 等四个 Secret 未配置 | gh secret list 确认四键齐全 |
10.2 缓存与 CI
| 坑 | 现象 | 根因 | 对策 |
|---|---|---|---|
| tag run 缓存互相不可见 | 每次发版冷启动多花 3–4 分钟 | actions/cache 按 tag 作用域隔离 | 先推 main 等 cache-warm 落盘,再推 tag(ADR-082) |
| 同秒推 main+tag | 本次发版依然冷启动 | Release restore 早于 Cache Warm save | 推 main 后至少等 1–2 分钟确认 cache-warm 绿 |
npm ci 先删 node_modules | 刚恢复的缓存被自己清掉 | npm ci 第一步删 node_modules 重建 | 已修:npm ci 步受 cache-hit 守卫 |
| 改了依赖没等 cache-warm | Release run 全冷启动 | 依赖变化后未推 main 暖缓存 | 改 go.mod/package-lock.json 后必须先推 main 暖缓存再推 tag |
| pre-push 在 test:coverage 后崩溃 | hook 输出 safe-delete/genie-trash diag,push 失败但测试实际通过 | Windows 环境安全删除层包装 rm 后对 /tmp 相对路径失败返回非零,set -e 中断 hook | 已修复(2026-08-05):.githooks/pre-push 4 处 rm -f "$COV_LOG" 统一加 2>/dev/null || true 容错;若再遇 hook 基建故障,可 git push --no-verify 并手动补验被跳过的检查(lint/test/check:docs/i18n/md-links/deadcode) |
| Linux wails3 缺 GTK 开发包 | go install wails3 报 pkg-config not found | wails3 CLI 编译时链接 GTK (CGO) | 缓存 miss 时才装 libgtk-4-dev libwebkitgtk-6.0-dev |
| Go build cache 必 miss | go-build-* 每次都重建 | key 含 github.sha,设计如此 | 预期行为,无法预热 |
10.3 产物与发布
| 坑 | 现象 | 根因 | 对策 |
|---|---|---|---|
| 自动 changelog 比较基准跳跃 | body 写 v1.4.0...v1.5.0 而非 v1.4.1...v1.5.0 | 自动生成基于 git tag 排序,可能跳 tag | 用手写 notes 完全规避 |
| 重推同名 tag | body 被自动生成覆盖 | Create Release 步幂等写入 | 确保手写 notes 路径正确后再重推 |
| 回滚后删 tag 重建 | 新 commit 推同名 tag 不触发 CI | tag 已存在,push 被跳过 | git tag -d vX.Y.Z && git push origin :vX.Y.Z 删除远端 tag 后再打 |
11. 发版快速命令序列
一行接一行执行,按需跳步。
⚠️ 跨平台坑:下方
sed -i "..."仅 GNU sed(Linux / Git Bash)可用;macOS BSD sed 需sed -i '' "..."(空 backup 后缀),Windows PowerShell 不认 sed。三平台各给一份,按本机环境选其一。Windows PowerShell(本项目主开发环境):
powershell$VER = "X.Y.Z" # ← 改成实际版本号 # 步骤 2:改 package.json 的 version 字段(不动其他字段) $pkg = Get-Content package.json -Raw | ConvertFrom-Json $pkg.version = $VER $pkg | ConvertTo-Json -Depth 100 | Set-Content package.json -NoNewline # 步骤 2 续:同步 build/windows/info.json 的 file_version 和 ProductVersion $info = Get-Content build/windows/info.json -Raw | ConvertFrom-Json $info.file_version = $VER $info.ProductVersion = $VER $info | ConvertTo-Json -Depth 100 | Set-Content build/windows/info.json # 步骤 4:手动写 docs/releases/v$VER.mdLinux / Git Bash(GNU sed):
bashVER="X.Y.Z" # ← 改成实际版本号 # 步骤 2:改 package.json 的 version sed -i "s/\"version\": \".*\"/\"version\": \"$VER\"/" package.json # 步骤 2 续:手动改 build/windows/info.json 的 file_version 和 ProductVersion # 步骤 4:手动写 docs/releases/v$VER.mdmacOS(BSD sed,需空 backup 后缀):
bashVER="X.Y.Z" sed -i '' "s/\"version\": \".*\"/\"version\": \"$VER\"/" package.json
共用:提交 + 打 tag + 监控
bash
VER="X.Y.Z" # ← 与上文同值
# 步骤 5:提交版本号 + 发布说明,推 main
git add package.json build/windows/info.json
git commit -m "chore: bump version to $VER"
git add docs/releases/v$VER.md
git commit -m "docs: add v$VER release notes"
git push origin main
# 步骤 6:等缓存预热(仅动了 go.mod/go.sum/frontend/package-lock.json 时)
gh run list --workflow cache-warm.yml --branch main --limit 1 \
--json status,conclusion,createdAt
# 若 conclusion != success 或 status != completed,watch 到完成:
gh run watch $(gh run list --workflow cache-warm.yml --branch main --limit 1 \
--json databaseId --jq '.[0].databaseId')
# 步骤 7:打 tag 触发 release.yml
git tag v$VER && git push origin v$VER
# 步骤 8:监控 release.yml 四 job
gh run list --workflow release.yml --limit 3
# 阻塞等到结束:
gh run watch $(gh run list --workflow release.yml --limit 1 \
--json databaseId --jq '.[0].databaseId')
# 步骤 9:核对 Release
gh release view v$VER --json body,tagName,assets \
--jq '{tag: .tagName, body: (.body[0:80]+"..."), assets: (.assets | length)}'
# 若 body 不对(自动生成 changelog 覆盖了手写 notes):
gh release edit v$VER --notes-file docs/releases/v$VER.md