Skip to content

Commit 1e6e69e

Browse files
committed
ci(release): 发版前校验 tag 必须落在 main 上
新增前置守卫 job verify-tag,test 通过 needs 依赖它:tag 不在 main 上 (或 tag 名不合 v<主>.<次>.<修订>)即整条工作流失败,绝不建 Release。 两条触发路径统一处理:push tag 取触发引用名;workflow_dispatch 取输入 tag 并显式 fetch 后 rev-parse 解析成提交,再以 git merge-base --is-ancestor 判定。
1 parent 0d7ba38 commit 1e6e69e

1 file changed

Lines changed: 66 additions & 0 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,79 @@ permissions:
1414
contents: write
1515

1616
jobs:
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

Comments
 (0)