Repository navigation
Expand file tree
/
Copy pathbeta-release.sh
More file actions
executable file
·387 lines (349 loc) · 14.2 KB
/
Copy pathbeta-release.sh
File metadata and controls
executable file
·387 lines (349 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
#!/usr/bin/env bash
set -Eeuo pipefail
# 用法:
# script/beta-release.sh --base 0.23.0 --notes-file /private/tmp/navop-v0.23.0-beta.1-notes.md
# script/beta-release.sh --next --notes-file /private/tmp/navop-v0.23.0-beta.2-notes.md
# script/beta-release.sh --next --notes-file ... --dry-run
#
# 作用:在 dev 分支上一条命令发测试版。脚本会把 crates/navop_app/Cargo.toml 与三个
# app crate(apps/navop、apps/navop-shell、apps/navop-data)的
# [package].version 统一改成 X.Y.Z-beta.N(--base 从 beta.1 起步,--next
# 递增当前 beta 序号),同步 Cargo.lock 与 CHANGELOG.md,提交后创建并推送
# v* tag。统一版本:一个 tag 同发三个产品,版本必须一起 bump。
#
# tag 推送会触发 release-trigger.yml -> release.yml。预发布 tag 自动走 beta
# 通道:GitHub Release 打上 --prerelease,R2 更新清单写到
# updates/beta/latest.json,稳定清单 updates/latest.json 完全不受影响。
#
# 前置条件(缺一不可):
# 1. 当前分支是 dev(beta 版本线只走 dev,main 只承载稳定发版);
# 2. 工作区干净(--dry-run 只提示不中止);
# 3. 本地与远端都不存在同名 tag;
# 4. --notes-file 指向包含双语段落的发布说明(release.yml 会硬校验
# CHANGELOG.md 里的条目,缺了连构建都不会启动)。
#
# 版本线约定:beta 的 base 是「下一个稳定版」,例如 0.23.0-beta.N -> 0.23.0,
# 不新开 0.22.1-beta 这类补丁线。
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel)"
cd "${REPO_ROOT}"
BASE=""
NEXT="false"
NOTES_FILE=""
DRY_RUN="false"
REMOTE="${REMOTE:-origin}"
MIRROR_REMOTE="${MIRROR_REMOTE:-cnb}"
MAIN_MANIFEST="${MAIN_MANIFEST:-${REPO_ROOT}/crates/navop_app/Cargo.toml}"
APP_MANIFESTS=(
"${REPO_ROOT}/apps/navop/Cargo.toml"
"${REPO_ROOT}/apps/navop-shell/Cargo.toml"
"${REPO_ROOT}/apps/navop-data/Cargo.toml"
)
LOCKFILE="${LOCKFILE:-${REPO_ROOT}/Cargo.lock}"
CHANGELOG="${CHANGELOG:-CHANGELOG.md}"
COMMITTED="false"
TAGGED="false"
PUSHED="false"
usage() {
cat <<'EOF'
用法:
script/beta-release.sh --base <X.Y.Z> --notes-file <PATH> [--dry-run]
script/beta-release.sh --next --notes-file <PATH> [--dry-run]
参数:
--base <X.Y.Z> 把版本设为 <X.Y.Z>-beta.1(<X.Y.Z> 必须是下个稳定版)
--next 把当前 <X.Y.Z>-beta.N 递增为 <X.Y.Z>-beta.(N+1)
--notes-file <PATH> 发布说明文件(必填,changelog.py upsert 的输入)
--dry-run 只打印将要执行的操作,不写任何文件、不提交、不打 tag
-h, --help 显示本帮助
环境变量:
REMOTE 推送远端,默认 origin(tag 必须推到 GitHub 才会触发 release-trigger.yml)
MIRROR_REMOTE 镜像远端,默认 cnb(成功后同步推 dev 与 tag,失败只警告)
MAIN_MANIFEST Cargo.toml 路径,默认 crates/navop_app/Cargo.toml
LOCKFILE Cargo.lock 路径,默认 Cargo.lock
CHANGELOG CHANGELOG.md 路径,默认 CHANGELOG.md
示例:
script/beta-release.sh --base 0.23.0 \
--notes-file /private/tmp/navop-v0.23.0-beta.1-notes.md
EOF
}
die() {
echo "错误:$*" >&2
exit 1
}
on_error() {
local code=$?
trap - ERR
echo "" >&2
echo "错误:beta 发布流程中止(exit ${code}),未完成的部分可按下面回滚:" >&2
if [[ "${PUSHED}" == "true" ]]; then
echo " git push ${REMOTE} --delete ${TAG} # 删除已推送的远端 tag" >&2
fi
if [[ "${TAGGED}" == "true" ]]; then
echo " git tag -d ${TAG}" >&2
fi
if [[ "${COMMITTED}" == "true" ]]; then
echo " git reset --hard HEAD~1 # 回退版本 + changelog 提交" >&2
echo " git push --force-with-lease ${REMOTE} dev # 若 dev 已推送" >&2
else
echo " git checkout -- ${MAIN_MANIFEST} ${LOCKFILE} ${CHANGELOG}" >&2
fi
exit "${code}"
}
trap on_error ERR
need_value() {
if [[ $# -lt 2 || -z "${2:-}" ]]; then
die "参数 $1 需要一个值。"
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
--base)
need_value "$@"
BASE="$2"
shift 2
;;
--next)
NEXT="true"
shift
;;
--notes-file)
need_value "$@"
NOTES_FILE="$2"
shift 2
;;
--dry-run)
DRY_RUN="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "错误:未知参数 $1" >&2
usage >&2
exit 1
;;
esac
done
# 1. 参数与发布说明校验(正式执行前全部做完,避免写了一半再失败)。
if [[ "${NEXT}" == "true" && -n "${BASE}" ]]; then
die "--base 与 --next 只能二选一。"
fi
if [[ "${NEXT}" == "false" && -z "${BASE}" ]]; then
die "必须指定 --base <X.Y.Z> 或 --next。例如:script/beta-release.sh --base 0.23.0 --notes-file <PATH>"
fi
if [[ -n "${BASE}" && ! "${BASE}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
die "--base 必须是稳定版本号 X.Y.Z(例如 0.23.0):beta 的 base 是下一个稳定版,不是 0.22.1 这样的补丁线。"
fi
if [[ -z "${NOTES_FILE}" ]]; then
echo "错误:缺少 --notes-file。beta 发布必须提供双语发布说明,release.yml 会硬校验 CHANGELOG.md 条目。" >&2
echo "示例:script/beta-release.sh --base 0.23.0 --notes-file /private/tmp/navop-v0.23.0-beta.1-notes.md" >&2
exit 1
fi
if [[ ! -f "${NOTES_FILE}" || ! -s "${NOTES_FILE}" ]]; then
die "发布说明文件不存在或为空:${NOTES_FILE}"
fi
read_manifest_version() {
local manifest="${1:-${MAIN_MANIFEST}}"
awk -F'"' '
/^\[package\]$/ { in_package = 1; next }
/^\[/ { in_package = 0 }
in_package && /^version = "/ { print $2; exit }
' "${manifest}"
}
read_lock_version() {
local package_name="${1:-navop_app}"
awk -v want="${package_name}" '
/^\[\[package\]\]$/ { in_package = 1; name = ""; next }
in_package && /^name = "/ { split($0, parts, "\""); name = parts[2] }
in_package && /^version = "/ {
split($0, parts, "\"")
if (name == want) { print parts[2]; exit }
}
' "${LOCKFILE}"
}
current_version="$(read_manifest_version)"
if [[ -z "${current_version}" ]]; then
die "无法从 ${MAIN_MANIFEST} 读取 [package].version。"
fi
for app_manifest in "${APP_MANIFESTS[@]}"; do
app_version="$(read_manifest_version "${app_manifest}")"
if [[ -z "${app_version}" ]]; then
die "无法从 ${app_manifest} 读取 [package].version。"
fi
if [[ "${app_version}" != "${current_version}" ]]; then
die "统一版本校验失败:${app_manifest} 为 ${app_version},navop_app 为 ${current_version}。请先手动对齐再发 beta。"
fi
done
echo "统一版本校验通过:navop_app 与三个 app crate 均为 ${current_version}"
# 2. 计算目标版本。
if [[ "${NEXT}" == "true" ]]; then
if [[ ! "${current_version}" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-beta\.([0-9]+)$ ]]; then
die "当前版本 ${current_version} 不是 X.Y.Z-beta.N,--next 只能递增 beta 序号;首次发测试版请用 --base X.Y.Z。"
fi
BASE="${BASH_REMATCH[1]}"
VERSION="${BASE}-beta.$((10#${BASH_REMATCH[2]} + 1))"
else
case "${current_version}" in
"${BASE}-beta."*)
die "当前已是 ${current_version},继续发测试版请用 --next。"
;;
esac
VERSION="${BASE}-beta.1"
fi
TAG="v${VERSION}"
echo "本次 beta 发布:${VERSION}(当前 ${current_version},tag ${TAG})"
# 3. 前置校验:分支、工作区、tag 唯一性。
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "${BRANCH}" != "dev" ]]; then
die "beta 版本线必须走 dev(当前:${BRANCH})。稳定版请走 dev -> main 发布 PR + script/release-tag.sh。"
fi
echo "分支校验通过:dev"
if [[ -n "$(git status --porcelain)" ]]; then
if [[ "${DRY_RUN}" == "true" ]]; then
echo "警告:工作区不干净,--dry-run 继续预览;正式执行前请先提交或暂存变更。" >&2
else
die "工作区不干净,请先提交或暂存变更(git status --porcelain 必须为空)。"
fi
fi
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
die "本地已存在标签 ${TAG}。如需重发测试版,请用 --next 递增序号。"
fi
if git ls-remote --exit-code --tags "${REMOTE}" "refs/tags/${TAG}" >/dev/null 2>&1; then
die "远端 ${REMOTE} 已存在标签 ${TAG}。如需重发测试版,请用 --next 递增序号。"
fi
if [[ -n "${MIRROR_REMOTE}" && "${MIRROR_REMOTE}" != "${REMOTE}" ]] \
&& git ls-remote --exit-code --tags "${MIRROR_REMOTE}" "refs/tags/${TAG}" >/dev/null 2>&1; then
die "镜像远端 ${MIRROR_REMOTE} 已存在标签 ${TAG}。如需重发测试版,请用 --next 递增序号。"
fi
echo "tag 唯一性校验通过:${TAG} 在本地、${REMOTE} 与 ${MIRROR_REMOTE} 均不存在"
# 4. --dry-run:只打印计划,绝不写文件/提交/打 tag。
if [[ "${DRY_RUN}" == "true" ]]; then
echo ""
echo "== --dry-run:以下操作不会执行 =="
echo " 1. 四个 manifest 的 [package].version: ${current_version} -> ${VERSION}"
echo " - ${MAIN_MANIFEST}"
for app_manifest in "${APP_MANIFESTS[@]}"; do
echo " - ${app_manifest}"
done
echo " 2. ${LOCKFILE}: 同步 navop_app 与三个 app 包版本到 ${VERSION}"
echo " 3. CHANGELOG.md: python3 script/changelog.py upsert --tag ${TAG} --date $(date +%F) --notes-file ${NOTES_FILE} --changelog ${CHANGELOG}"
echo " 4. git add 四个 manifest ${LOCKFILE} ${CHANGELOG}"
echo " 5. git commit -m \"chore(release): 发布 ${TAG} 测试版\""
echo " 6. git tag -a ${TAG} -m ${TAG}"
echo " 7. git push ${REMOTE} dev && git push ${REMOTE} ${TAG}(镜像 ${MIRROR_REMOTE})"
echo ""
echo "本次 tag:${TAG}"
echo "通道:beta"
echo "R2 更新清单将写到:updates/beta/latest.json"
echo "稳定清单 updates/latest.json 不会受影响。"
exit 0
fi
# 5. 改写 navop_app 与三个 app crate 的 [package].version(统一版本)。
for bump_manifest in "${MAIN_MANIFEST}" "${APP_MANIFESTS[@]}"; do
python3 - "${bump_manifest}" "${VERSION}" <<'PY'
import re
import sys
from pathlib import Path
manifest_path = Path(sys.argv[1])
version = sys.argv[2]
lines = manifest_path.read_text(encoding="utf-8").splitlines(keepends=True)
in_package = False
updated = False
for index, line in enumerate(lines):
stripped = line.strip()
if stripped == "[package]":
in_package = True
continue
if in_package and stripped.startswith("["):
in_package = False
if in_package and re.match(r'^version = "', line):
ending = "\r\n" if line.endswith("\r\n") else "\n" if line.endswith("\n") else ""
lines[index] = f'version = "{version}"{ending}'
updated = True
break
if not updated:
raise SystemExit(f"[package].version not found in {manifest_path}")
manifest_path.write_text("".join(lines), encoding="utf-8")
PY
done
# 6. 同步 Cargo.lock:工作区成员的版本由本脚本精确改写。
# 注意:`cargo metadata --no-deps` 不会重算 lock 里的成员版本,不能靠它;
# 成员条目没有 checksum,直接改 version 字段是安全的,改完由下面的回读校验兜底。
python3 - "${LOCKFILE}" "${VERSION}" <<'PY'
import re
import sys
from pathlib import Path
lock_path = Path(sys.argv[1])
version = sys.argv[2]
names = {"navop_app", "navop", "navop-shell", "navop-data"}
lines = lock_path.read_text(encoding="utf-8").splitlines(keepends=True)
in_package = False
package_name = ""
updated = set()
for index, line in enumerate(lines):
stripped = line.strip()
if stripped == "[[package]]":
in_package = True
package_name = ""
continue
if stripped.startswith("[") and stripped != "[[package]]":
in_package = False
if in_package:
name_match = re.match(r'^name = "([^"]*)"$', stripped)
if name_match:
package_name = name_match.group(1)
if package_name in names and re.match(r'^version = "', stripped):
ending = "\r\n" if line.endswith("\r\n") else "\n" if line.endswith("\n") else ""
lines[index] = f'version = "{version}"{ending}'
updated.add(package_name)
missing = names - updated
if missing:
raise SystemExit(f'packages not found in lockfile: {sorted(missing)}')
lock_path.write_text("".join(lines), encoding="utf-8")
PY
for lock_package in navop_app navop navop-shell navop-data; do
lock_version="$(read_lock_version "${lock_package}")"
if [[ "${lock_version}" != "${VERSION}" ]]; then
die "${LOCKFILE} 中 ${lock_package} 包版本为 ${lock_version:-<missing>},未能同步到 ${VERSION}。"
fi
done
echo "Cargo.lock 已同步:navop_app / navop / navop-shell / navop-data -> ${VERSION}"
# 7. 写入 CHANGELOG.md 条目并回读校验(release.yml 硬校验的就是这条)。
python3 script/changelog.py upsert \
--tag "${TAG}" \
--date "$(date +%F)" \
--notes-file "${NOTES_FILE}" \
--changelog "${CHANGELOG}"
python3 script/changelog.py validate \
--tag "${TAG}" \
--changelog "${CHANGELOG}"
echo "CHANGELOG.md 已写入并校验:${TAG}"
# 8. 提交、打 tag、推送。
git add -- "${MAIN_MANIFEST}" "${APP_MANIFESTS[@]}" "${LOCKFILE}" "${CHANGELOG}"
git commit -m "chore(release): 发布 ${TAG} 测试版"
COMMITTED="true"
git tag -a "${TAG}" -m "${TAG}"
TAGGED="true"
echo "推送 dev 与 tag 到 ${REMOTE} ..."
git push "${REMOTE}" dev
git push "${REMOTE}" "${TAG}"
PUSHED="true"
# tag 只有落到 GitHub 才会触发 release-trigger.yml;镜像远端同步一份,
# 推不动只警告,不影响已经在 GitHub 上开始的发布。
if [[ -n "${MIRROR_REMOTE}" && "${MIRROR_REMOTE}" != "${REMOTE}" ]]; then
if git push "${MIRROR_REMOTE}" dev && git push "${MIRROR_REMOTE}" "${TAG}"; then
echo "已同步 dev 与 ${TAG} 到镜像远端 ${MIRROR_REMOTE}。"
else
echo "警告:镜像远端 ${MIRROR_REMOTE} 同步失败,不影响 GitHub 侧的发布流程。" >&2
fi
fi
echo ""
echo "完成:已推送 ${TAG},release-trigger.yml -> release.yml 将按 beta 通道执行。"
echo "本次 tag:${TAG}"
echo "通道:beta(GitHub Release 会标记 --prerelease)"
echo "R2 更新清单将写到:updates/beta/latest.json"
echo "稳定清单 updates/latest.json 不会受影响;老客户端仍只读稳定清单。"
echo ""
echo "beta 稳定后:走正常 dev -> main 发布 PR,把版本改成 ${BASE} 后运行 script/release-tag.sh v${BASE}。"