完善 readiness、发布追溯与敏感治理(07-15-production-trust-closeout-cleanup)
这个提交包含在:
+77
-20
@@ -5,7 +5,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
DEPLOY_SERVER="${DEPLOY_SERVER:-deploy@118.89.77.218}"
|
||||
DEPLOY_SERVER="${DEPLOY_SERVER:-}"
|
||||
REMOTE_DIR="${REMOTE_DIR:-/opt/hyb-oa-trialrun}"
|
||||
REMOTE_ENV_FILE="${REMOTE_ENV_FILE:-${REMOTE_DIR}/deploy/trialrun/.env}"
|
||||
ENABLE_WORKER="${ENABLE_WORKER:-0}"
|
||||
@@ -16,6 +16,16 @@ LOCAL_ENV_FILE="deploy/trialrun/.env"
|
||||
ENV_CONTRACT_SCRIPT="deploy/trialrun/validate-env-contract.js"
|
||||
RUNTIME_SMOKE_SCRIPT="server/src/scripts/smoke-trialrun-runtime.js"
|
||||
EXTERNAL_DEPS_SMOKE_SCRIPT="server/src/scripts/smoke-external-deps.js"
|
||||
PROVENANCE_SCRIPT="deploy/trialrun/check-source-provenance.mjs"
|
||||
STAGING_DIR=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$STAGING_DIR" && -d "$STAGING_DIR" ]]; then
|
||||
rm -rf "$STAGING_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
@@ -27,6 +37,34 @@ need_cmd() {
|
||||
need_cmd rsync
|
||||
need_cmd ssh
|
||||
need_cmd node
|
||||
need_cmd git
|
||||
need_cmd tar
|
||||
need_cmd curl
|
||||
need_cmd mktemp
|
||||
|
||||
if [[ -z "$DEPLOY_SERVER" ]]; then
|
||||
echo "[ERROR] 必须显式提供 DEPLOY_SERVER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${ROOT_DIR}/${PROVENANCE_SCRIPT}" ]]; then
|
||||
echo "[ERROR] 缺少 ${PROVENANCE_SCRIPT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ">>> 0. 校验 Git 来源与发布提交"
|
||||
PROVENANCE_JSON="$(node "${ROOT_DIR}/${PROVENANCE_SCRIPT}")"
|
||||
SOURCE_COMMIT="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(p.commit)' "$PROVENANCE_JSON")"
|
||||
SOURCE_SHORT_COMMIT="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(p.shortCommit)' "$PROVENANCE_JSON")"
|
||||
SOURCE_BRANCH="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(p.branch)' "$PROVENANCE_JSON")"
|
||||
SOURCE_PUBLISHED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.published))' "$PROVENANCE_JSON")"
|
||||
SOURCE_REMOTE_VERIFIED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.remoteVerified))' "$PROVENANCE_JSON")"
|
||||
SOURCE_OVERRIDE_USED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.overrideUsed))' "$PROVENANCE_JSON")"
|
||||
SOURCE_OVERRIDE_REASON="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.overrideReason || ""))' "$PROVENANCE_JSON")"
|
||||
echo "[deploy-source] commit=${SOURCE_COMMIT} branch=${SOURCE_BRANCH} published=${SOURCE_PUBLISHED} remoteVerified=${SOURCE_REMOTE_VERIFIED}"
|
||||
if [[ "$SOURCE_OVERRIDE_USED" == "true" || "$SOURCE_REMOTE_VERIFIED" != "true" ]]; then
|
||||
echo "[deploy-source][UNPUBLISHED-OVERRIDE] reason=${SOURCE_OVERRIDE_REASON} remoteVerified=${SOURCE_REMOTE_VERIFIED}" >&2
|
||||
fi
|
||||
|
||||
if [[ ! -f "${ROOT_DIR}/${COMPOSE_FILE}" ]]; then
|
||||
echo "[ERROR] 缺少 ${COMPOSE_FILE}" >&2
|
||||
@@ -53,13 +91,17 @@ if [[ "${SYNC_ENV}" == "1" && ! -f "${ROOT_DIR}/${LOCAL_ENV_FILE}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ">>> 0. 校验部署环境契约"
|
||||
echo ">>> 1. 校验部署环境契约"
|
||||
node "${ROOT_DIR}/${ENV_CONTRACT_SCRIPT}"
|
||||
|
||||
echo ">>> 1. 创建远端目录"
|
||||
echo ">>> 2. 从已验证提交生成不可变同步目录"
|
||||
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/hyb-oa-deploy.XXXXXX")"
|
||||
git -C "$ROOT_DIR" archive --format=tar "$SOURCE_COMMIT" | tar -xf - -C "$STAGING_DIR"
|
||||
|
||||
echo ">>> 3. 创建远端目录"
|
||||
ssh "${DEPLOY_SERVER}" "mkdir -p '${REMOTE_DIR}'"
|
||||
|
||||
echo ">>> 2. 同步 OA 仓库到服务器"
|
||||
echo ">>> 4. 同步已验证提交到服务器"
|
||||
rsync -az --delete \
|
||||
--exclude '.git' \
|
||||
--exclude '.github' \
|
||||
@@ -67,6 +109,7 @@ rsync -az --delete \
|
||||
--exclude '.agents' \
|
||||
--exclude '.codex' \
|
||||
--exclude '.claude' \
|
||||
--exclude '.trellis' \
|
||||
--exclude 'node_modules' \
|
||||
--exclude 'client/node_modules' \
|
||||
--exclude 'server/node_modules' \
|
||||
@@ -81,20 +124,20 @@ rsync -az --delete \
|
||||
--exclude 'coverage' \
|
||||
--exclude 'dist' \
|
||||
--exclude '*.log' \
|
||||
"${ROOT_DIR}/" "${DEPLOY_SERVER}:${REMOTE_DIR}/"
|
||||
"${STAGING_DIR}/" "${DEPLOY_SERVER}:${REMOTE_DIR}/"
|
||||
|
||||
if [[ "${SYNC_ENV}" == "1" ]]; then
|
||||
echo ">>> 3. 同步部署环境变量"
|
||||
echo ">>> 5. 同步部署环境变量"
|
||||
ssh "${DEPLOY_SERVER}" "mkdir -p '$(dirname "${REMOTE_ENV_FILE}")'"
|
||||
rsync -az "${ROOT_DIR}/${LOCAL_ENV_FILE}" "${DEPLOY_SERVER}:${REMOTE_ENV_FILE}"
|
||||
else
|
||||
echo ">>> 3. 跳过 .env 同步(默认安全模式)"
|
||||
echo ">>> 5. 跳过 .env 同步(默认安全模式)"
|
||||
echo " 如果你确认本地 deploy/trialrun/.env 可直接上服务器,可用:SYNC_ENV=1 bash deploy/trialrun/deploy.sh"
|
||||
fi
|
||||
|
||||
echo ">>> 4. 在服务器执行部署与 smoke"
|
||||
echo ">>> 6. 在服务器执行部署与 smoke"
|
||||
ssh "${DEPLOY_SERVER}" \
|
||||
"REMOTE_DIR='${REMOTE_DIR}' REMOTE_ENV_FILE='${REMOTE_ENV_FILE}' COMPOSE_FILE='${COMPOSE_FILE}' ENABLE_WORKER='${ENABLE_WORKER}' ENABLE_MINIO='${ENABLE_MINIO}' bash -s" <<'REMOTE'
|
||||
"REMOTE_DIR='${REMOTE_DIR}' REMOTE_ENV_FILE='${REMOTE_ENV_FILE}' COMPOSE_FILE='${COMPOSE_FILE}' ENABLE_WORKER='${ENABLE_WORKER}' ENABLE_MINIO='${ENABLE_MINIO}' BUILD_SHA='${SOURCE_COMMIT}' VITE_BUILD_SHA='${SOURCE_SHORT_COMMIT}' bash -s" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
cd "$REMOTE_DIR"
|
||||
mkdir -p deploy/trialrun/data/postgres deploy/trialrun/data/storage deploy/trialrun/data/minio
|
||||
@@ -147,12 +190,17 @@ if ! "${compose_cmd[@]}" exec -T server sh -lc 'cd /app/server && npx prisma mig
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ">>> 4.6 等待 /ready"
|
||||
echo ">>> 4.6 等待 /ready 并核对构建标识"
|
||||
ready_ok=0
|
||||
for i in $(seq 1 60); do
|
||||
if curl -fsS http://127.0.0.1:3000/ready >/dev/null; then
|
||||
ready_ok=1
|
||||
break
|
||||
if ready_payload="$(curl -fsS http://127.0.0.1:3000/ready 2>/dev/null)"; then
|
||||
if READY_PAYLOAD="$ready_payload" EXPECTED_BUILD_SHA="${BUILD_SHA:0:12}" node -e '
|
||||
const payload = JSON.parse(process.env.READY_PAYLOAD || "{}");
|
||||
if (payload?.data?.buildSha !== process.env.EXPECTED_BUILD_SHA) process.exit(1);
|
||||
'; then
|
||||
ready_ok=1
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
@@ -190,18 +238,27 @@ for required_key in LIVE_SMOKE_ADMIN_IDENTIFIER LIVE_SMOKE_ADMIN_PASSWORD
|
||||
done
|
||||
|
||||
if [[ "$LIVE_SMOKE_READY" == "1" ]]; then
|
||||
echo ">>> 5. 执行部署后 live smoke"
|
||||
echo ">>> 7. 执行部署后 live smoke"
|
||||
bash "${ROOT_DIR}/scripts/qa/run-web-gates.sh" smoke-live
|
||||
else
|
||||
echo ">>> 5. 跳过部署后 live smoke(未提供完整 LIVE_SMOKE_* 凭据)"
|
||||
echo ">>> 7. 跳过部署后 live smoke(未提供完整凭据)"
|
||||
fi
|
||||
|
||||
echo ">>> 6. 执行公网前端资源文案扫描"
|
||||
node "${ROOT_DIR}/scripts/qa/check-public-bundle-copy.mjs" "${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}"
|
||||
PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}"
|
||||
echo ">>> 8. 核对公网构建标识"
|
||||
PUBLIC_READY_PAYLOAD="$(curl -fsS "${PUBLIC_BASE_URL}/ready")"
|
||||
READY_PAYLOAD="$PUBLIC_READY_PAYLOAD" EXPECTED_BUILD_SHA="$SOURCE_SHORT_COMMIT" node -e '
|
||||
const payload = JSON.parse(process.env.READY_PAYLOAD || "{}");
|
||||
if (payload?.data?.buildSha !== process.env.EXPECTED_BUILD_SHA) process.exit(1);
|
||||
'
|
||||
|
||||
echo ">>> 7. 部署完成"
|
||||
echo " 远端目录: ${REMOTE_DIR}"
|
||||
echo " 环境文件: ${REMOTE_ENV_FILE}"
|
||||
echo ">>> 9. 执行公网前端资源文案扫描"
|
||||
PUBLIC_EXPECTED_BUILD_SHA="$SOURCE_SHORT_COMMIT" \
|
||||
PUBLIC_FORBIDDEN_BUILD_SHA="$SOURCE_COMMIT" \
|
||||
node "${ROOT_DIR}/scripts/qa/check-public-bundle-copy.mjs" "$PUBLIC_BASE_URL"
|
||||
|
||||
echo ">>> 10. 部署完成"
|
||||
echo " 来源提交: ${SOURCE_COMMIT}"
|
||||
echo " 已自动执行:契约校验 + /ready + external deps smoke + runtime smoke + 公网前端资源文案扫描"
|
||||
echo " 若本地已配置 LIVE_SMOKE_*,部署后会自动补跑网页 live smoke"
|
||||
echo " 请确认 Caddy 或等效反向代理已把 OA 域名反代到 127.0.0.1:3000"
|
||||
|
||||
在新工单中引用
屏蔽一个用户