From 2c7f8c14ebb4fcf5168e869e2584e1d733bf89e3 Mon Sep 17 00:00:00 2001 From: wangbo Date: Tue, 28 Jul 2026 04:36:41 +0800 Subject: [PATCH] =?UTF-8?q?chore(release):=20=E6=98=8E=E7=A1=AE=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E5=8F=91=E5=B8=83=E6=8E=88=E6=9D=83=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=9B=86=E7=BE=A4=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将发布与上线授权边界调整为一次明确的生产发布指令可连续执行 publish 和 deploy,同时保留仅构建镜像时的停止边界。 为现有发布脚本增加 compose/kubernetes helper 选择和显式 SSH 私钥支持,保持完整 SHA、digest 与生产基线校验不变。 验证:bash -n、ShellCheck、manual-release-test。 --- AGENTS.md | 6 ++--- scripts/deploy-production-release.sh | 37 +++++++++++++++++++++------- scripts/publish-release-images.sh | 25 +++++++++++++++---- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a04d1fe..5bc4846 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,14 +52,14 @@ docker compose -f docker-compose.yml config --quiet 2. `main` 不设置受保护分支或 required status;Agent 在获得用户明确授权后,可以合并已验证的 PR,也可以直接提交并推送 `main`。操作前必须获取最新 `origin/main`、确认工作区边界、检查提交差异和敏感信息,并执行与风险匹配的本地验证。 3. 不得等待、伪造或手工补写已经取消的 CI status,也不得为了满足旧流程擅自恢复 Gitea Actions。Git 提交、合并、Push 和 Tag 授权只覆盖源码历史变更,永远不等于 publish 或 deploy 授权。 4. 发布只能使用工作区干净、已经提交并属于 `origin/main` 历史的完整 SHA。镜像 Tag 使用完整 SHA,线上只能使用 Registry digest,禁止使用 `latest`。 -5. 本地镜像发布必须由用户明确要求后单独执行: +5. 本地镜像发布必须由用户明确要求后执行: ```bash ./scripts/publish-release-images.sh --components auto ``` - 该命令只能构建、冒烟、推送镜像和生成 `dist/releases/.json`,不得修改生产。 -6. publish 成功后,Agent 必须报告 manifest、组件、digest 和验证结果并停止。只有用户再次明确确认上线,才可单独执行: + 该命令只能构建、冒烟、推送镜像和生成 `dist/releases/.json`,不得修改生产。用户只要求“构建镜像”“推送镜像”或明确限定为 publish 时,执行到此为止。 +6. 用户明确要求“发布”“上线”或“部署到线上”时,该次授权同时覆盖 publish 和 deploy。publish 成功并核验 manifest、组件、digest 和验证结果后,应直接继续执行下列部署命令,无需再次请求确认: ```bash ./scripts/deploy-production-release.sh dist/releases/.json diff --git a/scripts/deploy-production-release.sh b/scripts/deploy-production-release.sh index 97e05b1..232e668 100755 --- a/scripts/deploy-production-release.sh +++ b/scripts/deploy-production-release.sh @@ -3,7 +3,14 @@ set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) production_host=${AI_GATEWAY_PRODUCTION_HOST:-root@110.42.51.33} -remote_helper=${AI_GATEWAY_REMOTE_RELEASE_HELPER:-/usr/local/sbin/easyai-ai-gateway-release} +deploy_mode=${AI_GATEWAY_DEPLOY_MODE:-compose} +case $deploy_mode in + compose) default_remote_helper=/usr/local/sbin/easyai-ai-gateway-release ;; + kubernetes) default_remote_helper=/usr/local/sbin/easyai-ai-gateway-cluster-release ;; + *) echo 'AI_GATEWAY_DEPLOY_MODE must be compose or kubernetes' >&2; exit 1 ;; +esac +remote_helper=${AI_GATEWAY_REMOTE_RELEASE_HELPER:-$default_remote_helper} +production_ssh_key=${AI_GATEWAY_PRODUCTION_SSH_KEY:-} action=deploy value= @@ -14,8 +21,8 @@ Usage: scripts/deploy-production-release.sh --rollback scripts/deploy-production-release.sh --status -This command changes production. Run it only after a separate successful -publish command and explicit user confirmation. +This command changes production. Run it only after a successful publish +command and an explicit user request to release or deploy to production. EOF } @@ -48,9 +55,19 @@ esac echo 'AI_GATEWAY_REMOTE_RELEASE_HELPER must be an absolute safe path' >&2 exit 1 } +ssh_options=(-o BatchMode=yes) +scp_options=(-q -o BatchMode=yes) +if [[ -n $production_ssh_key ]]; then + [[ $production_ssh_key == /* && -f $production_ssh_key && ! -L $production_ssh_key ]] || { + echo 'AI_GATEWAY_PRODUCTION_SSH_KEY must be an absolute regular file' >&2 + exit 1 + } + ssh_options+=(-i "$production_ssh_key") + scp_options+=(-i "$production_ssh_key") +fi if [[ $action == status ]]; then - exec ssh -o BatchMode=yes "$production_host" "$remote_helper status" + exec ssh "${ssh_options[@]}" "$production_host" "$remote_helper status" fi if [[ $action == rollback ]]; then @@ -58,7 +75,8 @@ if [[ $action == rollback ]]; then echo 'rollback release must be a full lowercase Git SHA' >&2 exit 1 } - ssh -o BatchMode=yes "$production_host" "$remote_helper rollback $value" + # shellcheck disable=SC2029 # Both arguments are validated safe tokens above. + ssh "${ssh_options[@]}" "$production_host" "$remote_helper" rollback "$value" echo "production_rollback=PASS release=$value" exit 0 fi @@ -76,11 +94,11 @@ status_file=$(mktemp) cleanup() { rm -f "$status_file" - ssh -o BatchMode=yes "$production_host" "rm -f $remote_manifest" >/dev/null 2>&1 || true + ssh "${ssh_options[@]}" "$production_host" rm -f -- "$remote_manifest" >/dev/null 2>&1 || true } trap cleanup EXIT HUP INT TERM -if ssh -o BatchMode=yes "$production_host" "$remote_helper status" >"$status_file" 2>/dev/null; then +if ssh "${ssh_options[@]}" "$production_host" "$remote_helper" status >"$status_file" 2>/dev/null; then node "$root/scripts/release-manifest.mjs" validate "$status_file" >/dev/null current_sha=$(node "$root/scripts/release-manifest.mjs" get "$status_file" sourceSha) [[ $current_sha == "$base_sha" ]] || { @@ -95,6 +113,7 @@ else } fi -scp -q "$manifest" "$production_host:$remote_manifest" -ssh -o BatchMode=yes "$production_host" "$remote_helper deploy $remote_manifest" +scp "${scp_options[@]}" "$manifest" "$production_host:$remote_manifest" +# shellcheck disable=SC2029 # remote_manifest is derived from a validated full SHA. +ssh "${ssh_options[@]}" "$production_host" "$remote_helper" deploy "$remote_manifest" echo "production_deploy=PASS release=$source_sha" diff --git a/scripts/publish-release-images.sh b/scripts/publish-release-images.sh index 0d1bbf7..4be83ab 100755 --- a/scripts/publish-release-images.sh +++ b/scripts/publish-release-images.sh @@ -6,7 +6,14 @@ cd "$root" components=auto production_host=${AI_GATEWAY_PRODUCTION_HOST:-root@110.42.51.33} -remote_helper=${AI_GATEWAY_REMOTE_RELEASE_HELPER:-/usr/local/sbin/easyai-ai-gateway-release} +deploy_mode=${AI_GATEWAY_DEPLOY_MODE:-compose} +case $deploy_mode in + compose) default_remote_helper=/usr/local/sbin/easyai-ai-gateway-release ;; + kubernetes) default_remote_helper=/usr/local/sbin/easyai-ai-gateway-cluster-release ;; + *) echo 'AI_GATEWAY_DEPLOY_MODE must be compose or kubernetes' >&2; exit 1 ;; +esac +remote_helper=${AI_GATEWAY_REMOTE_RELEASE_HELPER:-$default_remote_helper} +production_ssh_key=${AI_GATEWAY_PRODUCTION_SSH_KEY:-} registry=${AI_GATEWAY_IMAGE_REGISTRY:-registry.cn-shanghai.aliyuncs.com/easyaigc} platform=linux/amd64 platform_probe_image=${AI_GATEWAY_PLATFORM_PROBE_IMAGE:-docker.m.daocloud.io/library/alpine:3.22} @@ -18,8 +25,8 @@ Usage: scripts/publish-release-images.sh [--components auto|api|web|all] This command only builds, smoke-tests, and pushes immutable images. It never -changes production. The generated manifest must be deployed separately with -scripts/deploy-production-release.sh after explicit confirmation. +changes production. When the user requested a production release, continue +with scripts/deploy-production-release.sh without requesting confirmation again. Environment: AI_GATEWAY_PRODUCTION_HOST SSH target used for read-only release status @@ -60,6 +67,14 @@ esac echo 'AI_GATEWAY_REMOTE_RELEASE_HELPER must be an absolute safe path' >&2 exit 1 } +ssh_options=(-o BatchMode=yes -o ConnectTimeout=8) +if [[ -n $production_ssh_key ]]; then + [[ $production_ssh_key == /* && -f $production_ssh_key && ! -L $production_ssh_key ]] || { + echo 'AI_GATEWAY_PRODUCTION_SSH_KEY must be an absolute regular file' >&2 + exit 1 + } + ssh_options+=(-i "$production_ssh_key") +fi [[ $registry =~ ^[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._-]+)+$ ]] || { echo 'AI_GATEWAY_IMAGE_REGISTRY has an invalid format' >&2 exit 1 @@ -114,8 +129,8 @@ if [[ -n $status_file_override ]]; then [[ -f $status_file_override ]] || fail 'AI_GATEWAY_RELEASE_STATUS_FILE does not exist' cp "$status_file_override" "$status_file" has_status=1 -elif ssh -o BatchMode=yes -o ConnectTimeout=8 "$production_host" \ - "$remote_helper status" >"$status_file" 2>/dev/null; then +elif ssh "${ssh_options[@]}" "$production_host" \ + "$remote_helper" status >"$status_file" 2>/dev/null; then has_status=1 fi