@@ -14,13 +14,79 @@ permissions:
1414 contents : write
1515
1616jobs :
17+ # ── 发版前置守卫:tag 必须落在默认分支 main 上 ───────────────────────────
18+ # 收紧发版语义为「主干分支带 tag 才发布」:只有 tag 指向的提交是 main 的祖先时,
19+ # 才允许进入后续 test / build-and-release。守卫不通过 → 本 job 失败 → 下游 job 因
20+ # needs 不启动 → 整条工作流失败,绝不建 Release。
21+ #
22+ # 两条触发路径都要覆盖:
23+ # push tag → 触发引用即 tag,GITHUB_SHA 就是被标记的提交;
24+ # workflow_dispatch → 输入只是字符串 tag,本地未必存在该 ref,需先显式 fetch
25+ # 再用 rev-parse 解析成提交,统一走同一套判定。
26+ verify-tag :
27+ name : Verify tag is on main
28+ runs-on : ubuntu-latest
29+ steps :
30+ - name : Checkout
31+ uses : actions/checkout@v4
32+ with :
33+ # 需要完整历史,git merge-base 才能判断祖先关系
34+ fetch-depth : 0
35+
36+ - name : Verify tag points to a commit on main
37+ shell : bash
38+ env :
39+ EVENT_NAME : ${{ github.event_name }}
40+ INPUT_TAG : ${{ github.event.inputs.tag }}
41+ TRIGGER_REF_NAME : ${{ github.ref_name }}
42+ run : |
43+ set -euo pipefail
44+
45+ # 1) 解析本次要发布的 tag:dispatch 取输入,push 取触发引用名
46+ if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
47+ TAG="${INPUT_TAG}"
48+ else
49+ TAG="${TRIGGER_REF_NAME}"
50+ fi
51+
52+ # 2) tag 名格式校验:必须形如 v<主>.<次>.<修订>,与触发条件 v*.*.* 一致,
53+ # 避免 workflow_dispatch 传入分支名 / 非版本串后静默发出错误版本。
54+ if ! printf '%s' "${TAG}" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
55+ echo "::error::非法版本 tag '${TAG}':正式发版要求 tag 形如 v<主>.<次>.<修订>(例如 v1.3.0)。"
56+ exit 1
57+ fi
58+
59+ # 3) 显式取齐引用:origin/main 与目标 tag。
60+ # fetch-depth:0 通常已包含两者,但 workflow_dispatch 时目标 tag 可能是
61+ # 克隆之后才由他人推送的,故显式再取一次以保证其存在(取不到即报错,不静默)。
62+ git fetch --no-tags origin "refs/heads/main:refs/remotes/origin/main"
63+ git fetch --no-tags --force origin "refs/tags/${TAG}:refs/tags/${TAG}" \
64+ || { echo "::error::无法获取 tag '${TAG}',请确认该 tag 已推送到远端。"; exit 1; }
65+
66+ # 4) 解析 tag 指向的提交(^{commit} 会剥离 annotated tag,拿到真实提交)
67+ TAG_COMMIT=$(git rev-parse "refs/tags/${TAG}^{commit}")
68+ echo "目标 tag:${TAG}"
69+ echo "tag 指向提交:${TAG_COMMIT}"
70+ echo "main 当前提交:$(git rev-parse refs/remotes/origin/main)"
71+
72+ # 5) 判定:tag 指向的提交必须是 main 的祖先(含 main 自身)。
73+ # --is-ancestor 返回 0 = 是祖先(通过),返回 1 = 不是祖先(拦截)。
74+ if git merge-base --is-ancestor "${TAG_COMMIT}" refs/remotes/origin/main; then
75+ echo "::notice::tag ${TAG} 位于 main 上,允许发版。"
76+ else
77+ echo "::error::tag ${TAG} 未落在 main 上。正式发版要求 tag 必须打在 main 分支的提交上;请先将改动合并到 main、再在 main 的提交上打 tag。"
78+ exit 1
79+ fi
80+
1781 # ── 测试门禁(独立 job)────────────────────────────────────────────────
1882 # 「发版产物必须经过验证」的意图由本 job 承载:test 全绿才允许 release 启动。
1983 # 与 release 解耦:任一 flaky 测试失败不会让 release job 自己跑测试而反复失败,
2084 # 而是直接让发布流程不启动(fail-fast at gate)。
2185 test :
2286 name : Test gate (must pass before release)
2387 runs-on : ubuntu-latest
88+ # 先过 tag 守卫:tag 不在 main 上则连测试都不跑,直接置整条工作流为失败。
89+ needs : verify-tag
2490 steps :
2591 - name : Checkout
2692 uses : actions/checkout@v4
0 commit comments