Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
92 lines
4.0 KiB
Bash
92 lines
4.0 KiB
Bash
#!/usr/bin/env bash
|
|
# 测试环境发布:从干净的提交构建并替换 app 容器。
|
|
# 用法:bash scripts/release.sh [--no-build] [--allow-inflight]
|
|
# --no-build 只用已有镜像重建容器
|
|
# --allow-inflight 引擎里有未结束的任务也继续(缺省停下:新版本可能没有旧版本任务的处理器)
|
|
# 前提:工作区干净、CHANGELOG.md 已写好本版本条目、所有成员包版本号一致。
|
|
# 做的事:检查引擎里未结束的任务 → 打 tag v<版本>(已存在则须指向 HEAD)→ 写 .env.deploy 的 APP_VERSION / SOURCE_REVISION
|
|
# → 构建镜像 → 只替换 app(启动时自动迁移)→ 等 healthy 并核对健康接口版本。
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
|
|
ENV_FILE=.env.deploy
|
|
BUILD=1
|
|
ALLOW_INFLIGHT=0
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--no-build) BUILD=0 ;;
|
|
--allow-inflight) ALLOW_INFLIGHT=1 ;;
|
|
*) echo "未知参数 $a" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
die() { echo "✗ $*" >&2; exit 1; }
|
|
|
|
[ -f "$ENV_FILE" ] || die "缺少 $ENV_FILE"
|
|
[ -z "$(git status --porcelain)" ] || die "工作区有未提交的改动,先提交再发布(git status)"
|
|
|
|
version=$(deno eval --quiet 'console.log(JSON.parse(Deno.readTextFileSync("deno.json")).version)')
|
|
tag="v$version"
|
|
for f in apps/*/deno.json apps/web/package.json packages/*/deno.json; do
|
|
v=$(deno eval --quiet "console.log(JSON.parse(Deno.readTextFileSync('$f')).version ?? '')")
|
|
[ -z "$v" ] || [ "$v" = "$version" ] || die "$f 版本 $v 与根 deno.json $version 不一致"
|
|
done
|
|
grep -q "^## \[$version\]" CHANGELOG.md || die "CHANGELOG.md 还没有 ## [$version] 条目"
|
|
|
|
compose() { docker compose --env-file "$ENV_FILE" "$@"; }
|
|
|
|
# 升级前未结束的任务:新版本只认领自己注册的任务名 + 版本,旧版本的在途任务可能没人处理
|
|
if [ -n "$(compose ps -q postgres 2>/dev/null)" ]; then
|
|
inflight=$(compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "
|
|
select workflow_name || '"'"'@'"'"' || version || '"'"' '"'"' || status || '"'"' x'"'"' || count(*)
|
|
from openworkflow.workflow_runs where status not in ('"'"'completed'"'"', '"'"'failed'"'"', '"'"'canceled'"'"')
|
|
group by workflow_name, version, status order by 1"' 2>/dev/null || true)
|
|
if [ -n "$inflight" ]; then
|
|
echo "引擎里有未结束的任务:"; echo "$inflight" | sed 's/^/ /'
|
|
[ $ALLOW_INFLIGHT -eq 1 ] || die "等它们结束,或确认新版本仍有对应处理器后加 --allow-inflight"
|
|
fi
|
|
fi
|
|
|
|
head=$(git rev-parse --short HEAD)
|
|
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
|
|
[ "$(git rev-parse --short "$tag^{commit}")" = "$head" ] || die "$tag 已存在但不指向 HEAD($head)"
|
|
else
|
|
git tag -a "$tag" -m "$tag"
|
|
echo "已打 tag $tag → $head"
|
|
fi
|
|
|
|
set_env() {
|
|
if grep -q "^$1=" "$ENV_FILE"; then sed -i "s|^$1=.*|$1=$2|" "$ENV_FILE"; else echo "$1=$2" >>"$ENV_FILE"; fi
|
|
}
|
|
set_env APP_VERSION "$tag"
|
|
set_env SOURCE_REVISION "$head"
|
|
|
|
if [ $BUILD -eq 1 ]; then
|
|
echo "▶ 构建 uhd-media-tool:$tag"
|
|
# 构建阶段要下载 Deno 运行时等外网资源;.env.deploy 配了 BUILD_PROXY 时经它构建(直连可能卡住,见 lessons #30)
|
|
build_proxy=$(sed -n 's/^BUILD_PROXY=//p' "$ENV_FILE" | tail -1)
|
|
if [ -n "$build_proxy" ]; then
|
|
compose build --build-arg HTTPS_PROXY="$build_proxy" --build-arg HTTP_PROXY="$build_proxy" \
|
|
--build-arg NO_PROXY=localhost,127.0.0.1 app
|
|
else
|
|
compose build app
|
|
fi
|
|
fi
|
|
echo "▶ 替换 app"
|
|
compose up -d --no-deps app
|
|
|
|
echo "▶ 等待 healthy"
|
|
cid=$(compose ps -q app)
|
|
for _ in $(seq 1 90); do
|
|
st=$(docker inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || echo starting)
|
|
[ "$st" = healthy ] && break
|
|
[ "$st" = unhealthy ] && die "app unhealthy;看日志:docker compose --env-file $ENV_FILE logs --tail 100 app"
|
|
sleep 2
|
|
done
|
|
[ "$st" = healthy ] || die "等待 healthy 超时"
|
|
|
|
port=$(grep -E '^UMT_PORT=' "$ENV_FILE" | cut -d= -f2)
|
|
health=$(curl -fsS "http://127.0.0.1:${port:-4380}/api/v1/health")
|
|
echo "$health" | grep -q "\"version\":\"$version\"" || die "健康接口版本不对:$health"
|
|
echo "✓ 已发布 $tag($head):$health"
|