feat(release): 新增一键发布任务

This commit is contained in:
ysicing committed 2026-09-22 13:55:54 +08:00
1 parent b0f490a564
commit 8eb4cc8781
8 files changed
+449 -1

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# Copyright (C) 2026 ysicing
# SPDX-License-Identifier: AGPL-3.0-or-later
# 一键执行完整发版:测试 → 签名公证构建 → 提交推送版本元数据 → 发布外部制品。
# 调用这个脚本即授权本次 release commit、push、GitHub Release、对象存储、版本 API 与 Homebrew 写入。
set -Eeuo pipefail
cd "$(dirname "$0")/.."
if [ -z "${XSTATS_RELEASE_TOKEN:-}" ]; then
echo "error: XSTATS_RELEASE_TOKEN 不能为空,一键发布尚未开始。" >&2
exit 1
fi
BRANCH="$(git symbolic-ref --quiet --short HEAD)" \
|| { echo "error: 当前不是可发布的分支检出。" >&2; exit 1; }
[ "$BRANCH" = main ] \
|| { echo "error: 一键发布只能从 main 执行,当前是 ${BRANCH}。" >&2; exit 1; }
git fetch --quiet origin main
HEAD_SHA="$(git rev-parse HEAD)"
REMOTE_SHA="$(git rev-parse origin/main)"
[ "$HEAD_SHA" = "$REMOTE_SHA" ] \
|| { echo "error: HEAD 与 origin/main 不一致,请先处理未推送或远端更新。" >&2; exit 1; }
HEADING="$(grep -m1 '^## ' CHANGELOG.md || true)"
if [[ "$HEADING" =~ ^##[[:space:]](0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)[[:space:]]·[[:space:]][0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
else
echo "error: CHANGELOG.md 顶部不是正式 semver 版本标题:${HEADING:-(没有二级标题)}" >&2
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" >/dev/null 2>&1; then
echo "error: v${VERSION} 已存在;若是恢复中断的发布,请运行 task publish。" >&2
exit 1
fi
RECOVERY=""
on_error() {
local status=$?
case "$RECOVERY" in
push)
echo "error: release commit 已创建但尚未确认推送成功;请先运行 git push origin main。" >&2
;;
publish)
echo "error: release commit 已推送;请修复外部问题后运行 task publish 续跑,不要再次执行 release-all。" >&2
;;
esac
exit "$status"
}
trap on_error ERR
# 在投入签名和公证时间前拒绝任何未提交源码;版本元数据仍可按发布流程保持未提交。
python3 Scripts/release_provenance.py prepare >/dev/null
task test
task release
BUILT_VERSION="$(sed -nE 's/^ *MARKETING_VERSION: *"?([0-9.]+)"?.*/\1/p' project.yml | head -1)"
[ "$BUILT_VERSION" = "$VERSION" ] \
|| { echo "error: 构建版本 $BUILT_VERSION 与 CHANGELOG 版本 $VERSION 不一致。" >&2; exit 1; }
# 构建后再次检查,防止构建工具或并行编辑把源码变化混入 release commit。
python3 Scripts/release_provenance.py prepare >/dev/null
RELEASE_FILES=(
project.yml
CHANGELOG.md
README.md
README.en.md
README.ja.md
README.ko.md
Assets/readme/activity.svg
Assets/readme/activity.zh.svg
)
git add -- "${RELEASE_FILES[@]}"
if git diff --cached --quiet; then
echo "error: 没有可提交的发布元数据。" >&2
exit 1
fi
git commit -m "chore(release): 发布 ${VERSION}"
RECOVERY=push
git push origin main
RECOVERY=publish
./Scripts/publish_release.sh
RECOVERY=""
echo "✅ XStats ${VERSION} 完整发布完成"
+119
View File
@@ -0,0 +1,119 @@
#!/bin/bash
# Copyright (C) 2026 ysicing
# SPDX-License-Identifier: AGPL-3.0-or-later
set -euo pipefail
cd "$(dirname "$0")/.."
ROOT="$PWD"
[ -f Scripts/release_all.sh ] || { echo "缺少 Scripts/release_all.sh" >&2; exit 1; }
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
setup() {
rm -rf "${WORK:?}/repo" "${WORK:?}/bin"
mkdir -p "$WORK/repo/Scripts" "$WORK/repo/Assets/readme" "$WORK/bin"
cp "$ROOT/Scripts/release_all.sh" "$WORK/repo/Scripts/release_all.sh"
chmod +x "$WORK/repo/Scripts/release_all.sh"
cat > "$WORK/repo/project.yml" <<'YAML'
settings:
base:
MARKETING_VERSION: "0.7.0"
CURRENT_PROJECT_VERSION: "110"
YAML
cat > "$WORK/repo/CHANGELOG.md" <<'LOG'
# 更新日志
## 0.8.0 · 2026-09-22
- 示例。
LOG
for file in README.md README.en.md README.ja.md README.ko.md; do
printf '# XStats\n' > "$WORK/repo/$file"
done
: > "$WORK/repo/Assets/readme/activity.svg"
: > "$WORK/repo/Assets/readme/activity.zh.svg"
: > "$WORK/log"
cat > "$WORK/bin/git" <<'SH'
#!/bin/bash
set -euo pipefail
echo "git $*" >> "$RELEASE_ALL_TEST_LOG"
case "${1:-}" in
symbolic-ref) printf '%s\n' "${FAKE_BRANCH:-main}" ;;
rev-parse) printf '%s\n' "${FAKE_SHA:-abc123}" ;;
ls-remote) [ "${FAKE_TAG_EXISTS:-0}" = 1 ] ;;
diff) [ "${3:-}" != --quiet ] ;;
esac
SH
cat > "$WORK/bin/task" <<'SH'
#!/bin/bash
set -euo pipefail
echo "task $*" >> "$RELEASE_ALL_TEST_LOG"
if [ "${1:-}" = release ]; then
sed -i '' -E 's/(MARKETING_VERSION: *)"[^"]+"/\1"0.8.0"/' project.yml
sed -i '' -E 's/(CURRENT_PROJECT_VERSION: *)"[^"]+"/\1"111"/' project.yml
fi
SH
cat > "$WORK/bin/python3" <<'SH'
#!/bin/bash
set -euo pipefail
echo "python3 $*" >> "$RELEASE_ALL_TEST_LOG"
printf 'abc123\n'
SH
cat > "$WORK/repo/Scripts/publish_release.sh" <<'SH'
#!/bin/bash
set -euo pipefail
echo publish >> "$RELEASE_ALL_TEST_LOG"
SH
chmod +x "$WORK/bin/git" "$WORK/bin/task" "$WORK/bin/python3" \
"$WORK/repo/Scripts/publish_release.sh"
}
expect_failure() {
local message="$1"
shift
if "$@" >"$WORK/stdout" 2>"$WORK/stderr"; then
echo "错误地成功:$message" >&2
exit 1
fi
}
setup
expect_failure "缺少发布 token" env -u XSTATS_RELEASE_TOKEN \
PATH="$WORK/bin:$PATH" RELEASE_ALL_TEST_LOG="$WORK/log" \
"$WORK/repo/Scripts/release_all.sh"
grep -q 'XSTATS_RELEASE_TOKEN' "$WORK/stderr" \
|| { cat "$WORK/stderr" >&2; exit 1; }
[ ! -s "$WORK/log" ] || { echo "缺少 token 时不应执行任何命令" >&2; exit 1; }
setup
expect_failure "非 main 分支" env XSTATS_RELEASE_TOKEN=test FAKE_BRANCH=feature \
PATH="$WORK/bin:$PATH" RELEASE_ALL_TEST_LOG="$WORK/log" \
"$WORK/repo/Scripts/release_all.sh"
grep -q 'main' "$WORK/stderr" || { cat "$WORK/stderr" >&2; exit 1; }
grep -qx 'git symbolic-ref --quiet --short HEAD' "$WORK/log"
setup
env XSTATS_RELEASE_TOKEN=test PATH="$WORK/bin:$PATH" RELEASE_ALL_TEST_LOG="$WORK/log" \
"$WORK/repo/Scripts/release_all.sh"
cat > "$WORK/expected" <<'LOG'
git symbolic-ref --quiet --short HEAD
git fetch --quiet origin main
git rev-parse HEAD
git rev-parse origin/main
git ls-remote --exit-code --tags origin refs/tags/v0.8.0
python3 Scripts/release_provenance.py prepare
task test
task release
python3 Scripts/release_provenance.py prepare
git add -- project.yml CHANGELOG.md README.md README.en.md README.ja.md README.ko.md Assets/readme/activity.svg Assets/readme/activity.zh.svg
git diff --cached --quiet
git commit -m chore(release): 发布 0.8.0
git push origin main
publish
LOG
diff -u "$WORK/expected" "$WORK/log"
echo "一键发布脚本测试通过"