fix(release): 发布前确认工作树干净且提交已推送,并显式指定 tag 指向的提交

实测的缺陷:v1.0.2 的二进制是对的(校验和与 b060f86 干净树上的本地构建逐字节一致),但
**tag 落在 1.0.1 那个提交上**——因为脚本不自己建 tag,而是让 Gitea 建,而 Gitea 把 release 的
tag 建在"创建那一刻远端默认分支的 HEAD"上。当时 b060f86 还没 push,origin/main 还是 f499d02,
于是 v1.0.1 和 v1.0.2 指向同一个树,去读该 release 源码的人拿到的是修复前的版本。

两处修正:

1. 创建 release 时显式给 `target_commitish`,不再让服务器决定。
2. 发布前要求工作树干净、且 HEAD 已在某个远端分支上。前者不只是洁癖:Go 默认把 VCS 信息打进
   二进制,工作树脏时构建产物带 "modified" 标记——实测同一个提交,干净树是 9c9c09dd…,脏树是
   1d9bf3a1…,所以"tag 必须指向二进制所来自的那个提交"这句话在有未提交改动时根本不成立。
This commit is contained in:
chick
2026-09-18 14:05:40 +08:00
parent b060f8664c
commit b31b7fe25a
+32 -2
View File
@@ -43,6 +43,33 @@ done
step() { printf '\n\033[1m== %s\033[0m\n' "$1"; } step() { printf '\n\033[1m== %s\033[0m\n' "$1"; }
# The commit the tag must point at, and the check that it is published.
#
# This is not tidiness. Gitea creates the tag for a release at whatever the remote's default-branch HEAD
# is *at the moment the release is created* — and this script does not create tags itself. So publishing
# before pushing puts the tag on the previous commit. Measured: v1.0.2 was published with the 1.0.2
# binaries (the checksums match a local build of the right commit) but its tag landed on the 1.0.1
# commit, because that was still origin/main; `v1.0.1` and `v1.0.2` ended up naming the same tree, and
# anyone reading the release's source would get the version before the fix.
#
# So the target is passed explicitly below, and a release whose commit is not pushed is refused: a tag
# that exists only locally would be created against whatever the remote has, which is the same defect
# with a different cause.
HEAD_SHA="$(git rev-parse HEAD)"
if [[ "$DRY_RUN" -eq 0 ]]; then
if [[ -n "$(git status --porcelain)" ]]; then
echo "release: the working tree has uncommitted changes; commit them first. The tag must name the" >&2
echo " commit the binaries were built from, and an uncommitted build names nothing." >&2
exit 1
fi
if ! git branch -r --contains "$HEAD_SHA" 2>/dev/null | grep -q .; then
echo "release: ${HEAD_SHA} is not on any remote branch. Push first: Gitea creates the tag at the" >&2
echo " remote's default-branch HEAD, so publishing now would tag the previous commit." >&2
exit 1
fi
fi
echo "tag ${TAG} will point at ${HEAD_SHA} ($(git log --oneline -1 "$HEAD_SHA" | cut -d' ' -f2-))"
step "building ${TAG}" step "building ${TAG}"
# VERSION is passed through so the binary reports the tag it was published under, which is what an # VERSION is passed through so the binary reports the tag it was published under, which is what an
# operator sees in `msa-agent version` and in the agent's startup log line. # operator sees in `msa-agent version` and in the agent's startup log line.
@@ -90,11 +117,14 @@ if [[ -n "$EXISTING" ]]; then
RELEASE_ID="$(printf '%s' "$EXISTING" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" RELEASE_ID="$(printf '%s' "$EXISTING" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
echo "reusing release ${TAG} (id ${RELEASE_ID})" echo "reusing release ${TAG} (id ${RELEASE_ID})"
else else
BODY="$(python3 - "$TAG" "$VERSION" <<'PY' BODY="$(python3 - "$TAG" "$VERSION" "$HEAD_SHA" <<'PY'
import json, sys import json, sys
tag, version = sys.argv[1], sys.argv[2] tag, version, head = sys.argv[1], sys.argv[2], sys.argv[3]
print(json.dumps({ print(json.dumps({
"tag_name": tag, "tag_name": tag,
# Pinned, rather than left to the server. Without it Gitea tags the default-branch HEAD as it is
# when the release is created, which is a different commit whenever the push has not happened yet.
"target_commitish": head,
"name": f"simadmin-relay agent {version}", "name": f"simadmin-relay agent {version}",
"body": ( "body": (
"Outbound-only node agent for SimAdmin nodes, cross-compiled for the ARM Debian boxes SimAdmin " "Outbound-only node agent for SimAdmin nodes, cross-compiled for the ARM Debian boxes SimAdmin "