242 行
8.7 KiB
Bash
可执行文件
242 行
8.7 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
cd "$ROOT_DIR"
|
||
ARTIFACT_ROOT="${RELEASE_GATE_ARTIFACT_DIR:-${ROOT_DIR}/artifacts/release-gate/$(date +%Y%m%d-%H%M%S)}"
|
||
MODE="${1:-all}"
|
||
CURRENT_STAGE=""
|
||
|
||
usage() {
|
||
cat <<USAGE
|
||
Usage: bash scripts/qa/run-release-gate.sh <verify-candidate|all|static|smoke-local|full-local|project-wide|deploy|ci-release|r66-trialrun|help>
|
||
|
||
说明:
|
||
verify-candidate 仅 static + 完整受管 full-local,生成共享回执;绝不部署
|
||
static 运行部署契约、server Prisma 校验、client typecheck/build
|
||
smoke-local 运行本地网页 smoke 门禁
|
||
full-local 运行本地全量门禁(server regression + client full E2E)
|
||
project-wide 项目级门禁:static + 覆盖扫描 + UI/移动契约 + 迁移漂移诊断 + 业务流 + 全页面视觉 + live readiness
|
||
deploy 只消费显式回执,经生产身份引导器发布;不隐式运行测试
|
||
all smoke-local -> 单次 verify-candidate(或重验显式回执)-> 生产发布链
|
||
ci-release 单次 verify-candidate(或重验显式回执)-> 生产发布链
|
||
r66-trialrun R66 小团队试点门禁:static -> server regression-all -> smoke-local
|
||
|
||
可选环境变量:
|
||
RELEASE_GATE_ARTIFACT_DIR 失败产物输出目录
|
||
RELEASE_GATE_RECEIPT_DIR 显式消费的完整候选回执目录;不自动选择历史回执
|
||
QA_* 透传给 scripts/qa/run-web-gates.sh
|
||
LIVE_SMOKE_* 透传给 deploy.sh 与 live smoke
|
||
PROJECT_WIDE_SKIP_BUSINESS_FLOW=1 跳过业务流 E2E
|
||
PROJECT_WIDE_SKIP_VISUAL=1 跳过项目级视觉 E2E
|
||
PROJECT_WIDE_SKIP_REASON / PROJECT_WIDE_SKIP_OPERATOR / PROJECT_WIDE_SKIP_APPROVAL_REF
|
||
skip 仅限有批准记录的 project-wide 诊断,PARTIAL/退出2
|
||
DEPLOY_SERVER / REMOTE_DIR / REMOTE_ENV_FILE / SYNC_ENV
|
||
COMPOSE_PROJECT_NAME / POSTGRES_DATA_DIR / STORAGE_HOST_DIR / DEPLOY_DATA_MODE
|
||
USAGE
|
||
}
|
||
|
||
collect_failure_artifacts() {
|
||
local stage_name="${CURRENT_STAGE:-unknown}"
|
||
local target_dir="${ARTIFACT_ROOT}/${stage_name}"
|
||
mkdir -p "$target_dir"
|
||
bash "$ROOT_DIR/scripts/qa/collect-failure-artifacts.sh" "$target_dir" >/dev/null 2>&1 || true
|
||
echo "[release-gate] 已收集失败产物:$target_dir" >&2
|
||
}
|
||
|
||
handle_error() {
|
||
local exit_code="$?"
|
||
echo "[release-gate][FAIL] 阶段 ${CURRENT_STAGE:-unknown} 失败(exit=${exit_code})" >&2
|
||
# Rejected publication must not trigger diagnostic reads of real configuration.
|
||
case "$MODE" in
|
||
ci-release|all|deploy) ;;
|
||
*) collect_failure_artifacts ;;
|
||
esac
|
||
exit "$exit_code"
|
||
}
|
||
trap handle_error ERR
|
||
|
||
run_stage() {
|
||
local stage_name="$1"
|
||
shift
|
||
CURRENT_STAGE="$stage_name"
|
||
echo ">>> [release-gate] ${stage_name}"
|
||
if [[ "$MODE" == "verify-candidate" ]]; then
|
||
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" stage-start "$ARTIFACT_ROOT" "$stage_name"
|
||
fi
|
||
"$@"
|
||
if [[ "$MODE" == "verify-candidate" ]]; then
|
||
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" stage-end "$ARTIFACT_ROOT" "$stage_name" 0
|
||
fi
|
||
}
|
||
|
||
stage_static() {
|
||
node "$ROOT_DIR/deploy/trialrun/validate-env-contract.js"
|
||
(
|
||
cd "$ROOT_DIR/server"
|
||
npm test -- tests/unit/scripts/deployment-db-lifecycle.test.js tests/unit/scripts/deploy-init.test.js
|
||
npm run prisma:validate
|
||
)
|
||
(
|
||
cd "$ROOT_DIR/client"
|
||
npm run typecheck
|
||
npm run build
|
||
)
|
||
}
|
||
|
||
stage_smoke_local() {
|
||
bash "$ROOT_DIR/scripts/qa/run-web-gates.sh" smoke-local
|
||
}
|
||
|
||
stage_full_local() {
|
||
bash "$ROOT_DIR/scripts/qa/run-web-gates.sh" full-local
|
||
}
|
||
|
||
stage_server_regression_all() {
|
||
(
|
||
cd "$ROOT_DIR/server"
|
||
./scripts/regression-all.sh
|
||
)
|
||
}
|
||
|
||
stage_project_wide_static() {
|
||
local issue_csv="$ROOT_DIR/issues/2026-05-08_23-38-37-project-wide-qa-second-pass.csv"
|
||
python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py "$issue_csv"
|
||
node "$ROOT_DIR/scripts/qa/check-ui-design-rules.mjs" --strict
|
||
node "$ROOT_DIR/scripts/qa/check-error-reason-contract.mjs" --strict
|
||
node "$ROOT_DIR/scripts/qa/check-error-reason-contract.mjs" --self-test
|
||
node "$ROOT_DIR/scripts/qa/check-mobile-route-contract.mjs"
|
||
node "$ROOT_DIR/scripts/qa/check-flow-coverage.mjs" --json > "$ARTIFACT_ROOT/flow-coverage.json"
|
||
node "$ROOT_DIR/scripts/qa/check-flow-coverage.mjs" --markdown "$ARTIFACT_ROOT/flow-coverage.md"
|
||
node "$ROOT_DIR/scripts/qa/check-live-smoke-env.mjs" --json > "$ARTIFACT_ROOT/live-smoke-env.json"
|
||
bash "$ROOT_DIR/scripts/qa/check-local-migration-drift.sh"
|
||
}
|
||
|
||
stage_project_wide_business_flow() {
|
||
if [[ "${PROJECT_WIDE_SKIP_BUSINESS_FLOW:-0}" == "1" ]]; then
|
||
echo "[release-gate] 跳过业务流 E2E(PROJECT_WIDE_SKIP_BUSINESS_FLOW=1)"
|
||
return 0
|
||
fi
|
||
|
||
bash "$ROOT_DIR/scripts/qa/run-business-flow-gates.sh" local
|
||
}
|
||
|
||
stage_project_wide_visual() {
|
||
if [[ "${PROJECT_WIDE_SKIP_VISUAL:-0}" == "1" ]]; then
|
||
echo "[release-gate] 跳过项目级视觉 E2E(PROJECT_WIDE_SKIP_VISUAL=1)"
|
||
return 0
|
||
fi
|
||
|
||
bash "$ROOT_DIR/scripts/qa/run-web-gates.sh" project-wide-visual-local
|
||
}
|
||
|
||
stage_project_wide_live_readiness() {
|
||
LIVE_SMOKE_BASE_URL="${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}" bash "$ROOT_DIR/scripts/qa/run-live-smoke-readiness.sh"
|
||
}
|
||
|
||
stage_deploy() {
|
||
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
|
||
export RELEASE_GATE_EXPECTED_COMMIT
|
||
node "$ROOT_DIR/deploy/trialrun/production-identity-bootstrap.mjs"
|
||
}
|
||
|
||
prepare_release_receipt() {
|
||
if [[ -z "${RELEASE_GATE_RECEIPT_DIR+x}" ]]; then
|
||
# This verification-only child cannot call back into the deployment chain.
|
||
RELEASE_GATE_ARTIFACT_DIR="$ARTIFACT_ROOT" bash "$ROOT_DIR/scripts/qa/run-release-gate.sh" verify-candidate
|
||
RELEASE_GATE_RECEIPT_DIR="$ARTIFACT_ROOT"
|
||
export RELEASE_GATE_RECEIPT_DIR
|
||
fi
|
||
}
|
||
|
||
# The verification-only mode records its own original exit and never deploys.
|
||
finish_candidate() {
|
||
local original_status=$? receipt_status=0
|
||
trap - EXIT ERR INT TERM
|
||
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" finish "$ARTIFACT_ROOT" "$original_status" || receipt_status=$?
|
||
if (( original_status )); then exit "$original_status"; fi
|
||
exit "$receipt_status"
|
||
}
|
||
|
||
if [[ "$MODE" != "help" && "$MODE" != "-h" && "$MODE" != "--help" ]]; then
|
||
# Validate before static, remote readiness or deployment side effects.
|
||
trap - ERR
|
||
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" skip "$ARTIFACT_ROOT" "$MODE"
|
||
case "$MODE" in
|
||
ci-release|all|deploy)
|
||
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" write-mode
|
||
# Invalid explicit receipts fail before smoke or other side effects.
|
||
if [[ -n "${RELEASE_GATE_RECEIPT_DIR+x}" || "$MODE" == "deploy" ]]; then
|
||
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
|
||
export RELEASE_GATE_EXPECTED_COMMIT
|
||
fi
|
||
;;
|
||
esac
|
||
if [[ "$MODE" != "verify-candidate" ]]; then trap handle_error ERR; fi
|
||
fi
|
||
|
||
if [[ "$MODE" != "verify-candidate" ]]; then mkdir -p "$ARTIFACT_ROOT"; fi
|
||
|
||
case "$MODE" in
|
||
help|-h|--help)
|
||
usage
|
||
;;
|
||
verify-candidate)
|
||
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" init "$ARTIFACT_ROOT"
|
||
trap finish_candidate EXIT
|
||
trap 'exit 130' INT
|
||
trap 'exit 143' TERM
|
||
unset QA_ENVIRONMENT_CONTEXT QA_DIAGNOSTIC_REPORT_DIR QA_DIAGNOSTIC_STAGE
|
||
QA_FULL_REPORT_DIR="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" full-path "$ARTIFACT_ROOT")"
|
||
export QA_FULL_REPORT_DIR
|
||
run_stage static stage_static
|
||
run_stage full-local stage_full_local
|
||
exit 0
|
||
;;
|
||
static)
|
||
run_stage static stage_static
|
||
;;
|
||
smoke-local)
|
||
run_stage smoke-local stage_smoke_local
|
||
;;
|
||
full-local)
|
||
run_stage full-local stage_full_local
|
||
;;
|
||
project-wide)
|
||
run_stage static stage_static
|
||
run_stage project-wide-static stage_project_wide_static
|
||
run_stage project-wide-business-flow stage_project_wide_business_flow
|
||
run_stage project-wide-visual stage_project_wide_visual
|
||
run_stage project-wide-live-readiness stage_project_wide_live_readiness
|
||
;;
|
||
deploy)
|
||
run_stage deploy stage_deploy
|
||
;;
|
||
ci-release)
|
||
prepare_release_receipt
|
||
run_stage deploy stage_deploy
|
||
;;
|
||
r66-trialrun)
|
||
run_stage static stage_static
|
||
run_stage server-regression-all stage_server_regression_all
|
||
run_stage smoke-local stage_smoke_local
|
||
;;
|
||
all)
|
||
run_stage smoke-local stage_smoke_local
|
||
prepare_release_receipt
|
||
run_stage deploy stage_deploy
|
||
;;
|
||
*)
|
||
usage >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
if [[ "$MODE" == "project-wide" && ( "${PROJECT_WIDE_SKIP_BUSINESS_FLOW:-0}" == "1" || "${PROJECT_WIDE_SKIP_VISUAL:-0}" == "1" ) ]]; then
|
||
echo "[release-gate][PARTIAL] 诊断跳过不具备发布资格"
|
||
exit 2
|
||
fi
|
||
|
||
echo "[release-gate][PASS] 模式 ${MODE} 通过"
|
||
echo "[release-gate] 产物目录:${ARTIFACT_ROOT}"
|