From e82d21d6334908d59845f44245641353493a08d7 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 18 Oct 2026 08:05:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=E5=8F=91=E7=89=88=E5=89=8D=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E5=88=86=E6=94=AF=E5=90=8D=E4=B8=8E=20meta.Version=20?= =?UTF-8?q?=E4=B8=80=E8=87=B4=EF=BC=88=E9=98=B2=E5=BC=80=E5=8F=91=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E8=AF=AF=E5=8F=91=E7=89=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 事故(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 }` —— 删掉其中任意一处描述文案,另一处还在,判据仍通过 ⇒ 变异不咬。 改为**逐形态断言**后,两个变异都准确报出是哪个形态缺保护。 --- .github/workflows/release.yml | 36 ++++++++++++ package/release_guard_test.go | 102 ++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 package/release_guard_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a95191d..902de1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/package/release_guard_test.go b/package/release_guard_test.go new file mode 100644 index 0000000..e31bba9 --- /dev/null +++ b/package/release_guard_test.go @@ -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) +}