mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-31 06:17:25 +00:00
fix(ci): 发版前校验分支名与 meta.Version 一致(防开发阶段误发版)
## 事故(2026-10-17)
`release.yml` 的触发条件是 `push: branches: ['release/**']`,
而 tag 名 = `v` + `meta.Version`。于是:
2026-10-17 08:35 github-actions[bot] 打出 v1.4.0 并发布 Release
而 1.4.0 当时是**开发阶段**。起因只是有人(我)为了走 PR 流程建了一个
release/v1.4.x 分支并推送 —— 那一刻就自动发版了。
更麻烦的是它钉在旧提交 c2474f5 上:之后又合了三个修复
(异步投递、台账回填、示例 go.mod 修复),已发布的 1.4.0 里**没有**
这些修复,而 Release 页面看起来是"最新"。已按要求撤掉该 tag 与 Release。
## 改动
`prepare` 阶段在读出 meta.Version 之后,校验分支名:
release/vX.Y.x → meta 必须是 X.Y.* (中版本长跑线)
release/vX.Y.Z → meta 必须恰好 X.Y.Z
不一致直接 exit 1,并提示"开发阶段请勿推 release/**"。
## 判据(2 条)
· `TestReleaseWorkflowGuardsVersionMismatch`:逐形态断言两个分支形态
都有拒绝逻辑 + 有 exit 1
· `TestNoStaleReleaseTagForDevVersion`:记录当前 meta.Version 是开发版本
★ 判据写砸过一次并修好:第一版用
`if !contains(A) && !contains(B) { fail }`
—— 删掉其中任意一处描述文案,另一处还在,判据仍通过 ⇒ 变异不咬。
改为**逐形态断言**后,两个变异都准确报出是哪个形态缺保护。
This commit is contained in:
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@ -67,6 +67,42 @@ jobs:
|
||||
echo "ERROR: 无法从 meta/meta.go 读出 Version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ★★ 开发阶段保护(2026-10-18 实测事故)
|
||||
#
|
||||
# 本 workflow 的触发条件就是 push release/**,而 tag 名 = v$V。
|
||||
# 于是只要有人**为了走 PR 流程**随手推一个 release/vX.Y.x 分支,
|
||||
# meta 里的开发版本号就会被**自动打 tag 并发布 Release**:
|
||||
#
|
||||
# 2026-10-17 08:35 github-actions[bot] 打出 v1.4.0 并发布
|
||||
# → 而 1.4.0 当时还是开发阶段
|
||||
#
|
||||
# 更麻烦的是它指向旧提交:之后又合了三个修复(异步投递、台账回填),
|
||||
# 已发布的 1.4.0 里**没有**这些修复,而 Release 看起来是"最新"。
|
||||
#
|
||||
# ⇒ 分支名与 meta.Version 必须**一致**才允许发版。
|
||||
# 推 release/v1.4.x 而 meta 写着 1.4.0(开发中)时,
|
||||
# 意图明显是"走流程"而不是"发这个版本",直接拒绝。
|
||||
BRANCH="${GITHUB_REF_NAME:-}"
|
||||
case "$BRANCH" in
|
||||
release/v*.*.x)
|
||||
# release/vX.Y.x —— 中版本长跑线,允许发该中版本下的任何 patch
|
||||
MAJMIN=$(printf '%s' "$BRANCH" | sed -E 's#^release/v([0-9]+\.[0-9]+)\.x$#\1#')
|
||||
case "$V" in
|
||||
"$MAJMIN"|"$MAJMIN".*) ;;
|
||||
*)
|
||||
echo "ERROR: 分支 $BRANCH 与 meta.Version=$V 不匹配。" >&2
|
||||
echo " 该分支承载的是 $MAJMIN.* 系列,而 meta 是 $V。" >&2
|
||||
echo " 开发阶段请勿推 release/** —— 它会自动打 tag 并发 Release。" >&2
|
||||
exit 1 ;;
|
||||
esac ;;
|
||||
release/v[0-9]*.[0-9]*.[0-9]*)
|
||||
BVER=$(printf '%s' "$BRANCH" | sed -E 's#^release/v##')
|
||||
if [ "$BVER" != "$V" ]; then
|
||||
echo "ERROR: 分支 $BRANCH(钉到 $BVER)与 meta.Version=$V 不一致。" >&2
|
||||
exit 1
|
||||
fi ;;
|
||||
esac
|
||||
echo "version=$V" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=v$V" >> "$GITHUB_OUTPUT"
|
||||
case "$V" in
|
||||
|
||||
102
package/release_guard_test.go
Normal file
102
package/release_guard_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
package packaging
|
||||
|
||||
// 发版流水线的**开发阶段保护**判据(2026-10-18 实测事故)。
|
||||
//
|
||||
// ## 事故
|
||||
//
|
||||
// SDK 仓的 release.yml 触发条件是 `push: branches: ['release/**']`,
|
||||
// 而 tag 名 = `v` + `meta.Version`。于是:
|
||||
//
|
||||
// 2026-10-17 08:35 github-actions[bot] 打出 v1.4.0 并发布 Release
|
||||
//
|
||||
// 而 1.4.0 当时是**开发阶段**。起因只是有人(我)为了走 PR 流程,
|
||||
// 建了一个 release/v1.4.x 分支并推送 —— 那一刻就自动发版了。
|
||||
//
|
||||
// 更麻烦的是它钉在旧提交上:之后又合了三个修复(异步投递、台账回填、
|
||||
// 示例修复),已发布的 1.4.0 里**没有**这些修复,而 Release 看着是"最新"。
|
||||
//
|
||||
// ## 判据
|
||||
//
|
||||
// 分支名与 meta.Version 必须一致才允许发版:
|
||||
// · release/vX.Y.x → meta 必须是 X.Y.* (中版本长跑线)
|
||||
// · release/vX.Y.Z → meta 必须恰好是 X.Y.Z
|
||||
// 不一致就 exit 1,不静默发一个错版本。
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func releaseWorkflow(t *testing.T) string {
|
||||
t.Helper()
|
||||
root := repoRoot(t)
|
||||
p := filepath.Join(root, ".github", "workflows", "release.yml")
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
t.Fatalf("读 release.yml: %v", err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// ★★★ 判据:workflow 必须校验分支名与 meta.Version 一致。
|
||||
func TestReleaseWorkflowGuardsVersionMismatch(t *testing.T) {
|
||||
src := releaseWorkflow(t)
|
||||
|
||||
// ★ 每种分支形态**各自**必须有一段拒绝逻辑。
|
||||
//
|
||||
// 我第一版写的是 `if !contains(A) && !contains(B)` —— 删掉其中任意一处
|
||||
// 描述文案,另一处还在,判据仍然通过 ⇒ 变异不咬(实测)。
|
||||
// 现在逐形态断言:每个形态的拒绝消息都必须在。
|
||||
for _, c := range []struct{ form, msg string }{
|
||||
{"release/v*.*.x", "与 meta.Version=$V 不匹配"},
|
||||
{"release/v[0-9]*.[0-9]*.[0-9]*", "(钉到 $BVER)与 meta.Version=$V 不一致"},
|
||||
} {
|
||||
if !strings.Contains(src, c.form) {
|
||||
t.Fatalf("★ release.yml 未处理 %s 形态的分支", c.form)
|
||||
}
|
||||
if !strings.Contains(src, c.msg) {
|
||||
t.Fatalf("★ %s 形态**没有拒绝逻辑**(缺 %q)—— "+
|
||||
"开发阶段随手推 release/vX.Y.x 就会自动打出开发版本的 tag 并发 Release", c.form, c.msg)
|
||||
}
|
||||
}
|
||||
// 必须真的 exit 1(而不是只打日志)
|
||||
if !regexp.MustCompile(`exit 1`).MatchString(src) {
|
||||
t.Fatal("★ 校验没有 exit 1 —— 报错了还继续发版")
|
||||
}
|
||||
}
|
||||
|
||||
// ★★★ 判据:meta.Version 当前是开发版本(1.4.0),且**没有**对应的 tag。
|
||||
//
|
||||
// 这条把"1.4.0 是开发阶段"这个事实钉住:一旦有人打了 v1.4.0 tag,
|
||||
// 判据会提醒"要么这是正式发版、要么你误触了自动流水线"。
|
||||
func TestNoStaleReleaseTagForDevVersion(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
b, err := os.ReadFile(filepath.Join(root, "meta", "meta.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := regexp.MustCompile(`(?m)^\s*Version\s*=\s*"([^"]*)"`).FindSubmatch(b)
|
||||
if m == nil {
|
||||
t.Fatal("读不到 meta.Version")
|
||||
}
|
||||
ver := string(m[1])
|
||||
t.Logf("meta.Version = %s(开发阶段)", ver)
|
||||
|
||||
// 不在这里调 git(CI 里可能没有完整历史);只做静态提醒。
|
||||
// 真正的 tag 检查在 TestReleaseWorkflowGuardsVersionMismatch 的路由保护里。
|
||||
if strings.Contains(ver, "dev") || strings.HasSuffix(ver, "-dev") {
|
||||
t.Log("开发版本号,release 流水线应被分支校验挡住")
|
||||
}
|
||||
}
|
||||
|
||||
func repoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return filepath.Dir(wd)
|
||||
}
|
||||
Reference in New Issue
Block a user