R30-1 R30-9: 建立零人工发布门禁并补齐高频回归
这个提交包含在:
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
DEST_DIR="${1:-${ROOT_DIR}/artifacts/qa/latest}"
|
||||
|
||||
mkdir -p "$DEST_DIR"
|
||||
|
||||
copy_path() {
|
||||
local source_path="$1"
|
||||
local target_name="${2:-$(basename "$source_path")}"
|
||||
if [[ -e "$source_path" ]]; then
|
||||
rm -rf "$DEST_DIR/$target_name"
|
||||
cp -R "$source_path" "$DEST_DIR/$target_name"
|
||||
fi
|
||||
}
|
||||
|
||||
copy_tail() {
|
||||
local source_path="$1"
|
||||
local target_name="$2"
|
||||
if [[ -f "$source_path" ]]; then
|
||||
tail -n 200 "$source_path" > "$DEST_DIR/$target_name"
|
||||
fi
|
||||
}
|
||||
|
||||
copy_path "$ROOT_DIR/client/playwright-report" "playwright-report"
|
||||
copy_path "$ROOT_DIR/client/test-results" "test-results"
|
||||
copy_path "/tmp/hyb-oa-regression/report.json" "regression-report.json"
|
||||
copy_path "/tmp/hyb-oa-web-gates-server.log" "hyb-oa-web-gates-server.log"
|
||||
copy_path "/tmp/hyb-oa-server.log" "hyb-oa-server.log"
|
||||
copy_tail "/tmp/hyb-oa-web-gates-server.log" "hyb-oa-web-gates-server.tail.log"
|
||||
copy_tail "/tmp/hyb-oa-server.log" "hyb-oa-server.tail.log"
|
||||
|
||||
git -C "$ROOT_DIR" status --short > "$DEST_DIR/git-status.txt" || true
|
||||
|
||||
echo "$DEST_DIR"
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
ARTIFACT_ROOT="${RELEASE_GATE_ARTIFACT_DIR:-${ROOT_DIR}/artifacts/release-gate/$(date +%Y%m%d-%H%M%S)}"
|
||||
MODE="${1:-all}"
|
||||
CURRENT_STAGE=""
|
||||
|
||||
mkdir -p "$ARTIFACT_ROOT"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: bash scripts/qa/run-release-gate.sh <all|static|smoke-local|full-local|deploy|ci-release|help>
|
||||
|
||||
说明:
|
||||
static 运行部署契约、server Prisma 校验、client typecheck/build
|
||||
smoke-local 运行本地网页 smoke 门禁
|
||||
full-local 运行本地全量门禁(server regression + client full E2E)
|
||||
deploy 调用 deploy/trialrun/deploy.sh,内部会串联 /ready、external deps、runtime smoke 与 live smoke
|
||||
all 依次执行 static -> smoke-local -> full-local -> deploy
|
||||
ci-release 依次执行 static -> full-local -> deploy
|
||||
|
||||
可选环境变量:
|
||||
RELEASE_GATE_ARTIFACT_DIR 失败产物输出目录
|
||||
QA_* 透传给 scripts/qa/run-web-gates.sh
|
||||
LIVE_SMOKE_* 透传给 deploy.sh 与 live smoke
|
||||
DEPLOY_SERVER / REMOTE_DIR / REMOTE_ENV_FILE / SYNC_ENV
|
||||
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
|
||||
collect_failure_artifacts
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap handle_error ERR
|
||||
|
||||
run_stage() {
|
||||
local stage_name="$1"
|
||||
shift
|
||||
CURRENT_STAGE="$stage_name"
|
||||
echo ">>> [release-gate] ${stage_name}"
|
||||
"$@"
|
||||
}
|
||||
|
||||
stage_static() {
|
||||
node "$ROOT_DIR/deploy/trialrun/validate-env-contract.js"
|
||||
(
|
||||
cd "$ROOT_DIR/server"
|
||||
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_deploy() {
|
||||
bash "$ROOT_DIR/deploy/trialrun/deploy.sh"
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
help|-h|--help)
|
||||
usage
|
||||
;;
|
||||
static)
|
||||
run_stage static stage_static
|
||||
;;
|
||||
smoke-local)
|
||||
run_stage smoke-local stage_smoke_local
|
||||
;;
|
||||
full-local)
|
||||
run_stage full-local stage_full_local
|
||||
;;
|
||||
deploy)
|
||||
run_stage deploy stage_deploy
|
||||
;;
|
||||
ci-release)
|
||||
run_stage static stage_static
|
||||
run_stage full-local stage_full_local
|
||||
run_stage deploy stage_deploy
|
||||
;;
|
||||
all)
|
||||
run_stage static stage_static
|
||||
run_stage smoke-local stage_smoke_local
|
||||
run_stage full-local stage_full_local
|
||||
run_stage deploy stage_deploy
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[release-gate][PASS] 模式 ${MODE} 通过"
|
||||
echo "[release-gate] 产物目录:${ARTIFACT_ROOT}"
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SERVER_DIR="$ROOT_DIR/server"
|
||||
CLIENT_DIR="$ROOT_DIR/client"
|
||||
|
||||
MODE="${1:-}"
|
||||
QA_HOST="${QA_HOST:-127.0.0.1}"
|
||||
QA_PORT_INPUT="${QA_PORT:-}"
|
||||
QA_PORT="${QA_PORT_INPUT:-3000}"
|
||||
QA_DB_HOST="${QA_DB_HOST:-127.0.0.1}"
|
||||
QA_DB_PORT="${QA_DB_PORT:-5432}"
|
||||
QA_DB_USER="${QA_DB_USER:-hyboa_user}"
|
||||
QA_DB_PASSWORD="${QA_DB_PASSWORD:-hyboa_password}"
|
||||
QA_DB_ADMIN_DB="${QA_DB_ADMIN_DB:-postgres}"
|
||||
QA_DB_PREFIX="${QA_DB_PREFIX:-hyb_oa_webgate}"
|
||||
QA_KEEP_DB="${QA_KEEP_DB:-0}"
|
||||
QA_SERVER_LOG="${QA_SERVER_LOG:-/tmp/hyb-oa-web-gates-server.log}"
|
||||
QA_SERVER_PID_FILE="${QA_SERVER_PID_FILE:-/tmp/hyb-oa-web-gates-server.pid}"
|
||||
QA_READY_TIMEOUT_SECONDS="${QA_READY_TIMEOUT_SECONDS:-90}"
|
||||
QA_BASE_URL=""
|
||||
QA_DB_NAME="${QA_DB_NAME:-${QA_DB_PREFIX}_$(date +%Y%m%d%H%M%S)_$RANDOM}"
|
||||
DATABASE_URL="postgresql://${QA_DB_USER}:${QA_DB_PASSWORD}@${QA_DB_HOST}:${QA_DB_PORT}/${QA_DB_NAME}?schema=public"
|
||||
TEST_FIELD_ENCRYPTION_KEY_DEFAULT="${FIELD_ENCRYPTION_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=}"
|
||||
|
||||
qa_require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "[ERROR] 缺少命令: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
qa_usage() {
|
||||
cat <<USAGE
|
||||
Usage: bash scripts/qa/run-web-gates.sh <smoke-local|full-local|smoke-live>
|
||||
|
||||
可选环境变量:
|
||||
QA_PORT / QA_HOST
|
||||
QA_DB_HOST / QA_DB_PORT / QA_DB_USER / QA_DB_PASSWORD / QA_DB_ADMIN_DB
|
||||
QA_DB_PREFIX / QA_DB_NAME / QA_KEEP_DB
|
||||
LIVE_SMOKE_BASE_URL / LIVE_SMOKE_ADMIN_* / LIVE_SMOKE_EMPLOYEE_* / LIVE_EXPECT_WECOM_STATUS
|
||||
USAGE
|
||||
}
|
||||
|
||||
qa_port_in_use() {
|
||||
python3 - "$QA_HOST" "$QA_PORT" <<'PY'
|
||||
import socket, sys
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(0.5)
|
||||
try:
|
||||
sock.connect((host, port))
|
||||
except OSError:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
PY
|
||||
}
|
||||
|
||||
qa_refresh_base_url() {
|
||||
QA_BASE_URL="http://${QA_HOST}:${QA_PORT}"
|
||||
}
|
||||
|
||||
qa_find_available_port() {
|
||||
python3 - "$QA_HOST" "$QA_PORT" <<'PY'
|
||||
import socket, sys
|
||||
host = sys.argv[1]
|
||||
start = int(sys.argv[2])
|
||||
for port in range(start, start + 200):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind((host, port))
|
||||
except OSError:
|
||||
continue
|
||||
print(port)
|
||||
sys.exit(0)
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
qa_select_port() {
|
||||
if qa_port_in_use; then
|
||||
if [[ -n "$QA_PORT_INPUT" ]]; then
|
||||
echo "[ERROR] 端口 ${QA_PORT} 已被占用,请改用 QA_PORT=<port> 后重试。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local next_port
|
||||
next_port="$(qa_find_available_port)"
|
||||
if [[ -z "$next_port" ]]; then
|
||||
echo "[ERROR] 无法为网页门禁分配空闲端口,请手工设置 QA_PORT。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[INFO] 默认端口 ${QA_PORT} 已被占用,自动切换到 ${next_port}"
|
||||
QA_PORT="$next_port"
|
||||
fi
|
||||
|
||||
qa_refresh_base_url
|
||||
}
|
||||
|
||||
qa_wait_port_free() {
|
||||
local timeout="${1:-15}"
|
||||
local elapsed=0
|
||||
while [[ "$elapsed" -lt "$timeout" ]]; do
|
||||
if ! qa_port_in_use; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
qa_wait_ready() {
|
||||
local timeout="$1"
|
||||
local elapsed=0
|
||||
while [[ "$elapsed" -lt "$timeout" ]]; do
|
||||
if curl -fsS "$QA_BASE_URL/ready" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
echo "[ERROR] /ready 在 ${timeout}s 内未通过:$QA_BASE_URL/ready" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
qa_psql() {
|
||||
PGPASSWORD="$QA_DB_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$QA_DB_HOST" -p "$QA_DB_PORT" -U "$QA_DB_USER" -d "$QA_DB_ADMIN_DB" "$@"
|
||||
}
|
||||
|
||||
qa_createdb() {
|
||||
PGPASSWORD="$QA_DB_PASSWORD" createdb -h "$QA_DB_HOST" -p "$QA_DB_PORT" -U "$QA_DB_USER" "$QA_DB_NAME"
|
||||
}
|
||||
|
||||
qa_dropdb() {
|
||||
if [[ "$QA_KEEP_DB" == "1" ]]; then
|
||||
echo "[INFO] QA_KEEP_DB=1,保留临时数据库:$QA_DB_NAME"
|
||||
return 0
|
||||
fi
|
||||
|
||||
qa_psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${QA_DB_NAME}' AND pid <> pg_backend_pid();" >/dev/null || true
|
||||
PGPASSWORD="$QA_DB_PASSWORD" dropdb --if-exists -h "$QA_DB_HOST" -p "$QA_DB_PORT" -U "$QA_DB_USER" "$QA_DB_NAME" >/dev/null || true
|
||||
}
|
||||
|
||||
qa_cleanup_server() {
|
||||
if [[ -f "$QA_SERVER_PID_FILE" ]]; then
|
||||
local pid
|
||||
pid="$(cat "$QA_SERVER_PID_FILE")"
|
||||
if [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1; then
|
||||
kill "$pid" >/dev/null 2>&1 || true
|
||||
for _ in $(seq 1 15); do
|
||||
if ! kill -0 "$pid" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if kill -0 "$pid" >/dev/null 2>&1; then
|
||||
kill -9 "$pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
wait "$pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -f "$QA_SERVER_PID_FILE"
|
||||
fi
|
||||
|
||||
if qa_wait_port_free 15; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[WARN] 端口 ${QA_PORT} 在 server 关闭后仍未释放,后续启动可能失败。" >&2
|
||||
}
|
||||
|
||||
qa_cleanup() {
|
||||
qa_cleanup_server
|
||||
if [[ -n "${QA_DB_NAME:-}" && "$MODE" != "smoke-live" ]]; then
|
||||
qa_dropdb
|
||||
fi
|
||||
}
|
||||
|
||||
qa_export_app_env() {
|
||||
export NODE_ENV=test
|
||||
export HOST="$QA_HOST"
|
||||
export PORT="$QA_PORT"
|
||||
export APP_ORIGIN="$QA_BASE_URL"
|
||||
export E2E_BASE_URL="$QA_BASE_URL"
|
||||
export BASE_URL="$QA_BASE_URL"
|
||||
export DATABASE_URL="$DATABASE_URL"
|
||||
export JWT_SECRET="${JWT_SECRET:-qa_web_gates_secret_key}"
|
||||
export OCR_PROVIDER="${OCR_PROVIDER:-mock}"
|
||||
export OCR_MAX_ATTEMPTS="${OCR_MAX_ATTEMPTS:-3}"
|
||||
export OCR_POLL_CRON="${OCR_POLL_CRON:-*/5 * * * * *}"
|
||||
export DELEGATION_REVOKE_CRON="${DELEGATION_REVOKE_CRON:-0 * * * *}"
|
||||
export SERVE_CLIENT_DIST="true"
|
||||
export CLIENT_DIST_DIR="../client/dist"
|
||||
export PORTAL_HOME_ENABLED="true"
|
||||
export GLOBAL_SEARCH_ENABLED="true"
|
||||
export ANNOUNCEMENTS_ENABLED="true"
|
||||
export PRESENCE_BOARD_ENABLED="true"
|
||||
export WEEKLY_REPORT_ENABLED="true"
|
||||
export EMPLOYEE_LIFECYCLE_ENABLED="true"
|
||||
export TODO_BOARD_ENABLED="true"
|
||||
export PROJECT_DOMAIN_ENABLED="true"
|
||||
export PROJECT_LINKING_ENABLED="true"
|
||||
export PROJECT_COMMUNICATION_LOG_ENABLED="true"
|
||||
export PROJECT_CLOSURE_CHECKLIST_ENABLED="true"
|
||||
export CONFIDENTIAL_DOC_REFS_ENABLED="true"
|
||||
export TRAVEL_SYNC_ENABLED="true"
|
||||
export IM_NOTIFIER_ENABLED="true"
|
||||
export FIELD_ENCRYPTION_KEY="$TEST_FIELD_ENCRYPTION_KEY_DEFAULT"
|
||||
}
|
||||
|
||||
qa_prepare_db() {
|
||||
echo ">>> 创建临时数据库: $QA_DB_NAME"
|
||||
qa_createdb
|
||||
|
||||
echo ">>> 执行 Prisma migrate"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
npm run prisma:migrate:deploy
|
||||
)
|
||||
|
||||
echo ">>> 写入开发种子数据"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
npm run seed:dev
|
||||
)
|
||||
}
|
||||
|
||||
qa_start_server() {
|
||||
if ! qa_wait_port_free 15; then
|
||||
echo "[ERROR] 端口 ${QA_PORT} 已被占用,请改用 QA_PORT=<port> 后重试。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$QA_SERVER_LOG" "$QA_SERVER_PID_FILE"
|
||||
|
||||
echo ">>> 启动本地 server: $QA_BASE_URL"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
nohup node src/index.js > "$QA_SERVER_LOG" 2>&1 &
|
||||
echo $! > "$QA_SERVER_PID_FILE"
|
||||
)
|
||||
|
||||
qa_wait_ready "$QA_READY_TIMEOUT_SECONDS" || {
|
||||
echo "[ERROR] server 未就绪,最近日志如下:" >&2
|
||||
tail -n 200 "$QA_SERVER_LOG" >&2 || true
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
qa_build_client() {
|
||||
echo ">>> 构建 client dist"
|
||||
(
|
||||
cd "$CLIENT_DIR"
|
||||
npm run build
|
||||
)
|
||||
}
|
||||
|
||||
qa_run_smoke_local() {
|
||||
qa_select_port
|
||||
export JOBS_ENABLED="${JOBS_ENABLED:-false}"
|
||||
qa_export_app_env
|
||||
qa_prepare_db
|
||||
qa_build_client
|
||||
qa_start_server
|
||||
|
||||
echo ">>> 执行 Playwright smoke 门禁"
|
||||
(
|
||||
cd "$CLIENT_DIR"
|
||||
rm -rf test-results playwright-report
|
||||
E2E_MODE=local npm run test:e2e:smoke
|
||||
)
|
||||
}
|
||||
|
||||
qa_run_full_local() {
|
||||
qa_select_port
|
||||
export JOBS_ENABLED="${JOBS_ENABLED:-false}"
|
||||
qa_export_app_env
|
||||
qa_prepare_db
|
||||
|
||||
echo ">>> 执行 server Prisma 校验"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
npm run prisma:validate
|
||||
)
|
||||
|
||||
echo ">>> 执行 server 单元测试"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
npm test
|
||||
)
|
||||
|
||||
echo ">>> 执行 client typecheck"
|
||||
(
|
||||
cd "$CLIENT_DIR"
|
||||
npm run typecheck
|
||||
)
|
||||
|
||||
qa_build_client
|
||||
qa_start_server
|
||||
|
||||
echo ">>> 执行 workflow-actions 回归"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
./scripts/regression-workflow-actions.sh
|
||||
)
|
||||
|
||||
echo ">>> 执行 server 全量回归"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
./scripts/regression-all.sh
|
||||
)
|
||||
|
||||
echo ">>> 切换 server 到 Playwright E2E 模式(启用后台 job)"
|
||||
qa_cleanup_server
|
||||
export JOBS_ENABLED="${JOBS_ENABLED_FOR_E2E:-true}"
|
||||
qa_start_server
|
||||
|
||||
echo ">>> 执行 Playwright full E2E"
|
||||
(
|
||||
cd "$CLIENT_DIR"
|
||||
rm -rf test-results playwright-report
|
||||
E2E_MODE=local npm run test:e2e:full
|
||||
)
|
||||
}
|
||||
|
||||
qa_run_live_smoke() {
|
||||
local live_base_url="${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}"
|
||||
echo ">>> 探活 live /ready: ${live_base_url}/ready"
|
||||
curl -fsS "${live_base_url}/ready" >/dev/null
|
||||
|
||||
echo ">>> 执行 live smoke Playwright"
|
||||
(
|
||||
cd "$CLIENT_DIR"
|
||||
rm -rf test-results playwright-report
|
||||
E2E_MODE=live npm run test:e2e:live
|
||||
)
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
smoke-local)
|
||||
qa_require_cmd curl
|
||||
qa_require_cmd psql
|
||||
qa_require_cmd createdb
|
||||
qa_require_cmd dropdb
|
||||
qa_require_cmd node
|
||||
qa_require_cmd npm
|
||||
trap qa_cleanup EXIT
|
||||
qa_run_smoke_local
|
||||
;;
|
||||
full-local)
|
||||
qa_require_cmd curl
|
||||
qa_require_cmd psql
|
||||
qa_require_cmd createdb
|
||||
qa_require_cmd dropdb
|
||||
qa_require_cmd node
|
||||
qa_require_cmd npm
|
||||
trap qa_cleanup EXIT
|
||||
qa_run_full_local
|
||||
;;
|
||||
smoke-live)
|
||||
qa_require_cmd curl
|
||||
qa_require_cmd node
|
||||
qa_require_cmd npm
|
||||
qa_run_live_smoke
|
||||
;;
|
||||
*)
|
||||
qa_usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
在新工单中引用
屏蔽一个用户