LAUTO 收口正式启用后自动化巡检
这个提交包含在:
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const DEFAULT_BASE_URL = "https://oa.hyinda.com";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
baseUrl: process.env.LIVE_SMOKE_BASE_URL || DEFAULT_BASE_URL,
|
||||
json: false,
|
||||
month: new Date().toISOString().slice(0, 7),
|
||||
};
|
||||
for (const arg of argv) {
|
||||
if (arg === "--json") {
|
||||
options.json = true;
|
||||
} else if (arg.startsWith("--base-url=")) {
|
||||
options.baseUrl = arg.slice("--base-url=".length);
|
||||
} else if (arg.startsWith("--month=")) {
|
||||
options.month = arg.slice("--month=".length);
|
||||
}
|
||||
}
|
||||
options.baseUrl = options.baseUrl.trim().replace(/\/$/, "");
|
||||
if (!/^\d{4}-\d{2}$/.test(options.month)) {
|
||||
throw new Error(`invalid month: ${options.month}`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function today() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function getCredential(role) {
|
||||
const prefixByRole = {
|
||||
admin: "LIVE_SMOKE_ADMIN",
|
||||
employee: "LIVE_SMOKE_EMPLOYEE",
|
||||
};
|
||||
const prefix = prefixByRole[role];
|
||||
const identifier = process.env[`${prefix}_IDENTIFIER`]?.trim();
|
||||
const password = process.env[`${prefix}_PASSWORD`]?.trim();
|
||||
if (!identifier || !password) {
|
||||
return null;
|
||||
}
|
||||
return { identifier, password };
|
||||
}
|
||||
|
||||
async function fetchJson(url, init = {}) {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
signal: AbortSignal.timeout(25_000),
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
return { ok: response.ok, status: response.status, json };
|
||||
}
|
||||
|
||||
function errorSummary(json) {
|
||||
const error = json?.error ?? {};
|
||||
const details = error.details ?? {};
|
||||
return {
|
||||
code: typeof error.code === "string" ? error.code : null,
|
||||
featureFlag: typeof details.featureFlag === "string" ? details.featureFlag : null,
|
||||
reason: typeof details.reason === "string" ? details.reason : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function login(baseUrl, role) {
|
||||
const credential = getCredential(role);
|
||||
if (!credential) {
|
||||
return { ok: false, status: "SKIP", role, reason: "missing_credentials" };
|
||||
}
|
||||
const result = await fetchJson(`${baseUrl}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-device-fingerprint": `launch-auto-attendance-${role}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifier: credential.identifier,
|
||||
password: credential.password,
|
||||
}),
|
||||
});
|
||||
const accessToken = result.json?.data?.accessToken;
|
||||
if (!result.ok || typeof accessToken !== "string" || !accessToken) {
|
||||
return {
|
||||
ok: false,
|
||||
status: "FAIL",
|
||||
role,
|
||||
httpStatus: result.status,
|
||||
error: errorSummary(result.json),
|
||||
};
|
||||
}
|
||||
return { ok: true, status: "PASS", role, accessToken };
|
||||
}
|
||||
|
||||
async function callApi(baseUrl, token, method, pathName, body) {
|
||||
return fetchJson(`${baseUrl}${pathName}`, {
|
||||
method,
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
accept: "application/json",
|
||||
...(body ? { "content-type": "application/json" } : {}),
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function passStep(name, details = {}) {
|
||||
return { name, status: "PASS", ...details };
|
||||
}
|
||||
|
||||
function failStep(name, details = {}) {
|
||||
return { name, status: "FAIL", ...details };
|
||||
}
|
||||
|
||||
function skipStep(name, details = {}) {
|
||||
return { name, status: "SKIP", ...details };
|
||||
}
|
||||
|
||||
function printHuman(summary) {
|
||||
console.log(`[live-attendance-unified] status=${summary.status} base=${summary.baseUrl} month=${summary.month}`);
|
||||
for (const step of summary.steps) {
|
||||
const detail = step.httpStatus ? ` http=${step.httpStatus}` : "";
|
||||
const code = step.code ? ` code=${step.code}` : "";
|
||||
console.log(`[live-attendance-unified] ${step.name} status=${step.status}${detail}${code}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const employee = await login(options.baseUrl, "employee");
|
||||
const admin = await login(options.baseUrl, "admin");
|
||||
const steps = [];
|
||||
|
||||
if (!employee.ok) {
|
||||
steps.push(employee.status === "SKIP" ? skipStep("employee-login", employee) : failStep("employee-login", employee));
|
||||
} else {
|
||||
steps.push(passStep("employee-login"));
|
||||
|
||||
const clockIn = await callApi(options.baseUrl, employee.accessToken, "POST", "/api/attendance/clock-in", {});
|
||||
const clockError = errorSummary(clockIn.json);
|
||||
if (clockIn.status === 409 && clockError.featureFlag === "attendance_v1_disabled") {
|
||||
steps.push(passStep("legacy-clock-in-disabled", {
|
||||
httpStatus: clockIn.status,
|
||||
featureFlag: clockError.featureFlag,
|
||||
reason: clockError.reason,
|
||||
}));
|
||||
} else {
|
||||
steps.push(failStep("legacy-clock-in-disabled", {
|
||||
httpStatus: clockIn.status,
|
||||
error: clockError,
|
||||
}));
|
||||
}
|
||||
|
||||
const monthly = await callApi(
|
||||
options.baseUrl,
|
||||
employee.accessToken,
|
||||
"GET",
|
||||
`/api/attendance/monthly?month=${encodeURIComponent(options.month)}`,
|
||||
);
|
||||
if (monthly.ok) {
|
||||
const data = monthly.json?.data ?? {};
|
||||
steps.push(passStep("attendance-monthly", {
|
||||
httpStatus: monthly.status,
|
||||
employeeCount: data.summary?.employeeCount ?? null,
|
||||
workDays: data.summary?.workDays ?? null,
|
||||
checkinDays: data.summary?.checkinDays ?? null,
|
||||
missingDays: data.summary?.missingDays ?? null,
|
||||
}));
|
||||
} else {
|
||||
steps.push(failStep("attendance-monthly", {
|
||||
httpStatus: monthly.status,
|
||||
error: errorSummary(monthly.json),
|
||||
}));
|
||||
}
|
||||
|
||||
const corrections = await callApi(
|
||||
options.baseUrl,
|
||||
employee.accessToken,
|
||||
"GET",
|
||||
`/api/attendance-corrections?month=${encodeURIComponent(options.month)}&page=1&pageSize=5`,
|
||||
);
|
||||
if (corrections.ok) {
|
||||
const data = corrections.json?.data ?? {};
|
||||
steps.push(passStep("attendance-corrections-list", {
|
||||
httpStatus: corrections.status,
|
||||
total: data.total ?? null,
|
||||
itemCount: Array.isArray(data.items) ? data.items.length : null,
|
||||
}));
|
||||
} else {
|
||||
steps.push(failStep("attendance-corrections-list", {
|
||||
httpStatus: corrections.status,
|
||||
error: errorSummary(corrections.json),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (!admin.ok) {
|
||||
steps.push(admin.status === "SKIP" ? skipStep("admin-login", admin) : failStep("admin-login", admin));
|
||||
} else {
|
||||
steps.push(passStep("admin-login"));
|
||||
const syncRuns = await callApi(options.baseUrl, admin.accessToken, "GET", "/api/attendance/wecom/sync-runs?page=1&pageSize=5");
|
||||
if (syncRuns.ok) {
|
||||
const data = syncRuns.json?.data ?? {};
|
||||
steps.push(passStep("wecom-sync-runs", {
|
||||
httpStatus: syncRuns.status,
|
||||
total: data.total ?? null,
|
||||
itemCount: Array.isArray(data.items) ? data.items.length : null,
|
||||
}));
|
||||
} else {
|
||||
steps.push(failStep("wecom-sync-runs", {
|
||||
httpStatus: syncRuns.status,
|
||||
error: errorSummary(syncRuns.json),
|
||||
}));
|
||||
}
|
||||
|
||||
if (process.env.LAUNCH_AUTO_RUN_WECOM_SYNC === "1") {
|
||||
const sync = await callApi(options.baseUrl, admin.accessToken, "POST", "/api/attendance/wecom/sync", {
|
||||
startDate: today(),
|
||||
endDate: today(),
|
||||
});
|
||||
if (sync.ok && sync.json?.data?.skipped) {
|
||||
steps.push(skipStep("wecom-checkin-sync", {
|
||||
httpStatus: sync.status,
|
||||
code: sync.json?.data?.code ?? null,
|
||||
employeeCount: sync.json?.data?.employeeCount ?? null,
|
||||
boundUserCount: sync.json?.data?.boundUserCount ?? null,
|
||||
recordCount: sync.json?.data?.recordCount ?? null,
|
||||
}));
|
||||
} else if (sync.ok) {
|
||||
steps.push(passStep("wecom-checkin-sync", {
|
||||
httpStatus: sync.status,
|
||||
code: sync.json?.data?.code ?? null,
|
||||
employeeCount: sync.json?.data?.employeeCount ?? null,
|
||||
boundUserCount: sync.json?.data?.boundUserCount ?? null,
|
||||
recordCount: sync.json?.data?.recordCount ?? null,
|
||||
}));
|
||||
} else {
|
||||
steps.push(failStep("wecom-checkin-sync", {
|
||||
httpStatus: sync.status,
|
||||
error: errorSummary(sync.json),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
steps.push(skipStep("wecom-checkin-sync", { reason: "LAUNCH_AUTO_RUN_WECOM_SYNC not set" }));
|
||||
}
|
||||
}
|
||||
|
||||
const failed = steps.some((step) => step.status === "FAIL");
|
||||
const summary = {
|
||||
checkedAt: new Date().toISOString(),
|
||||
baseUrl: options.baseUrl,
|
||||
month: options.month,
|
||||
status: failed ? "FAIL" : "PASS",
|
||||
steps,
|
||||
};
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
} else {
|
||||
printHuman(summary);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[live-attendance-unified][ERROR] ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const DEFAULT_BASE_URL = "https://oa.hyinda.com";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
baseUrl: process.env.LIVE_SMOKE_BASE_URL || DEFAULT_BASE_URL,
|
||||
json: false,
|
||||
expectations: [],
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--json") {
|
||||
options.json = true;
|
||||
} else if (arg === "--expect") {
|
||||
const value = argv[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("--expect requires key=value");
|
||||
}
|
||||
options.expectations.push(value);
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--expect=")) {
|
||||
options.expectations.push(arg.slice("--expect=".length));
|
||||
} else if (arg.startsWith("--base-url=")) {
|
||||
options.baseUrl = arg.slice("--base-url=".length);
|
||||
}
|
||||
}
|
||||
|
||||
options.baseUrl = options.baseUrl.trim().replace(/\/$/, "");
|
||||
return options;
|
||||
}
|
||||
|
||||
function parseExpectation(value) {
|
||||
const [flagKey, expectedState] = value.split("=");
|
||||
if (!flagKey || !expectedState) {
|
||||
throw new Error(`invalid expectation: ${value}`);
|
||||
}
|
||||
const allowed = new Set(["force_on", "force_off", "enabled", "disabled"]);
|
||||
if (!allowed.has(expectedState)) {
|
||||
throw new Error(`invalid expected state for ${flagKey}: ${expectedState}`);
|
||||
}
|
||||
return { flagKey, expectedState };
|
||||
}
|
||||
|
||||
function getAdminCredential() {
|
||||
const identifier = process.env.LIVE_SMOKE_ADMIN_IDENTIFIER?.trim();
|
||||
const password = process.env.LIVE_SMOKE_ADMIN_PASSWORD?.trim();
|
||||
if (!identifier || !password) {
|
||||
return null;
|
||||
}
|
||||
return { identifier, password };
|
||||
}
|
||||
|
||||
async function fetchJson(url, init = {}) {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
signal: AbortSignal.timeout(20_000),
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
return { ok: response.ok, status: response.status, json };
|
||||
}
|
||||
|
||||
function summarizeError(json) {
|
||||
const error = json?.error ?? {};
|
||||
return {
|
||||
code: typeof error.code === "string" ? error.code : null,
|
||||
message: typeof error.message === "string" ? error.message.slice(0, 120) : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function login(baseUrl, credential) {
|
||||
const result = await fetchJson(`${baseUrl}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-device-fingerprint": "launch-auto-feature-flags",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifier: credential.identifier,
|
||||
password: credential.password,
|
||||
}),
|
||||
});
|
||||
const accessToken = result.json?.data?.accessToken;
|
||||
if (!result.ok || typeof accessToken !== "string" || !accessToken) {
|
||||
throw new Error(`admin login failed: HTTP ${result.status} ${summarizeError(result.json).code ?? ""}`.trim());
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
function checkExpectation(item, expectedState) {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (expectedState === "force_on") {
|
||||
return item.overrideMode === "force_on" && item.effectiveEnabled === true;
|
||||
}
|
||||
if (expectedState === "force_off") {
|
||||
return item.overrideMode === "force_off" && item.effectiveEnabled === false;
|
||||
}
|
||||
if (expectedState === "enabled") {
|
||||
return item.effectiveEnabled === true;
|
||||
}
|
||||
return item.effectiveEnabled === false;
|
||||
}
|
||||
|
||||
function buildFlagSummary(item) {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
flagKey: item.flagKey,
|
||||
overrideMode: item.overrideMode,
|
||||
rolloutType: item.rolloutType,
|
||||
version: item.version,
|
||||
envEnabled: item.envEnabled,
|
||||
effectiveEnabled: item.effectiveEnabled,
|
||||
effectiveReason: item.effectiveReason,
|
||||
};
|
||||
}
|
||||
|
||||
function printHuman(summary) {
|
||||
console.log(`[live-feature-flags] status=${summary.status} base=${summary.baseUrl}`);
|
||||
for (const result of summary.results) {
|
||||
const item = result.item;
|
||||
const detail = item
|
||||
? `override=${item.overrideMode} effective=${item.effectiveEnabled} reason=${item.effectiveReason}`
|
||||
: "missing";
|
||||
console.log(
|
||||
`[live-feature-flags] ${result.flagKey} expected=${result.expectedState} status=${result.status} ${detail}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const expectations = options.expectations.map(parseExpectation);
|
||||
if (!expectations.length) {
|
||||
throw new Error("at least one --expect key=value is required");
|
||||
}
|
||||
const credential = getAdminCredential();
|
||||
if (!credential) {
|
||||
throw new Error("missing LIVE_SMOKE_ADMIN_* credentials");
|
||||
}
|
||||
|
||||
const accessToken = await login(options.baseUrl, credential);
|
||||
const flags = await fetchJson(`${options.baseUrl}/api/system/feature-flags`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
accept: "application/json",
|
||||
},
|
||||
});
|
||||
if (!flags.ok) {
|
||||
throw new Error(`feature flag list failed: HTTP ${flags.status} ${summarizeError(flags.json).code ?? ""}`.trim());
|
||||
}
|
||||
|
||||
const items = flags.json?.data?.items ?? [];
|
||||
const results = expectations.map((expectation) => {
|
||||
const item = items.find((candidate) => candidate.flagKey === expectation.flagKey);
|
||||
const passed = checkExpectation(item, expectation.expectedState);
|
||||
return {
|
||||
...expectation,
|
||||
status: passed ? "PASS" : "FAIL",
|
||||
item: buildFlagSummary(item),
|
||||
};
|
||||
});
|
||||
const status = results.every((result) => result.status === "PASS") ? "PASS" : "FAIL";
|
||||
const summary = {
|
||||
checkedAt: new Date().toISOString(),
|
||||
baseUrl: options.baseUrl,
|
||||
status,
|
||||
readOnly: true,
|
||||
results,
|
||||
};
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
} else {
|
||||
printHuman(summary);
|
||||
}
|
||||
|
||||
if (status !== "PASS") {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[live-feature-flags][ERROR] ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u -o pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
CLIENT_DIR="$ROOT_DIR/client"
|
||||
SERVER_DIR="$ROOT_DIR/server"
|
||||
PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}"
|
||||
REPORT_DIR="${LAUNCH_AUTO_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/launch-auto-ops-$(date +%Y%m%d-%H%M%S)}"
|
||||
SUMMARY_TXT="$REPORT_DIR/summary.txt"
|
||||
STATUS=0
|
||||
|
||||
mkdir -p "$REPORT_DIR"
|
||||
: >"$SUMMARY_TXT"
|
||||
|
||||
load_default_env() {
|
||||
local env_file="${LAUNCH_AUTO_ENV_FILE:-$HOME/.hyb-oa/live-smoke.env}"
|
||||
if [[ "${LAUNCH_AUTO_SKIP_ENV_FILE:-0}" == "1" || ! -f "$env_file" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$env_file"
|
||||
set +a
|
||||
PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-$PUBLIC_BASE_URL}"
|
||||
echo "[launch-auto-ops] loaded masked env file" | tee -a "$SUMMARY_TXT"
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local name="$1"
|
||||
shift
|
||||
local log_file="$REPORT_DIR/${name}.log"
|
||||
|
||||
echo ">>> [launch-auto-ops] $name" | tee -a "$SUMMARY_TXT"
|
||||
if "$@" > >(tee "$log_file") 2>&1; then
|
||||
echo "[launch-auto-ops][PASS] $name" | tee -a "$SUMMARY_TXT"
|
||||
else
|
||||
local code="$?"
|
||||
echo "[launch-auto-ops][FAIL] $name exit=$code" | tee -a "$SUMMARY_TXT"
|
||||
STATUS=1
|
||||
fi
|
||||
}
|
||||
|
||||
run_shell_step() {
|
||||
local name="$1"
|
||||
local command="$2"
|
||||
run_step "$name" bash -lc "$command"
|
||||
}
|
||||
|
||||
write_summary_json() {
|
||||
local qa_status="PASS"
|
||||
local notes="LAUTO GO_TO_OPERATE:正式启用后自动化巡检通过;payment_batch_v1 保持 force_on,erp_sync 保持 force_off;企微打卡同步缺外部配置时按 SKIP 记录。"
|
||||
if [[ "$STATUS" -ne 0 ]]; then
|
||||
qa_status="FAIL"
|
||||
notes="LAUTO PAUSE_FOR_FIX:正式启用后自动化巡检存在失败;未执行生产恢复、数据库重置、真实支付或 ERP 操作。"
|
||||
fi
|
||||
|
||||
QA_REPORT_DIR="$REPORT_DIR" \
|
||||
QA_ISSUE_ID="${QA_ISSUE_ID:-LAUTO}" \
|
||||
QA_BASE_URL="$PUBLIC_BASE_URL" \
|
||||
QA_MODE="launch-auto-ops" \
|
||||
QA_STATUS="$qa_status" \
|
||||
QA_SUMMARY_NOTES="$notes" \
|
||||
node "$ROOT_DIR/scripts/qa/write-qa-summary.mjs" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
finish() {
|
||||
write_summary_json
|
||||
echo "[launch-auto-ops] report=$REPORT_DIR" | tee -a "$SUMMARY_TXT"
|
||||
exit "$STATUS"
|
||||
}
|
||||
|
||||
trap finish EXIT
|
||||
|
||||
load_default_env
|
||||
|
||||
run_shell_step "backend-prisma" "cd '$SERVER_DIR' && npm run prisma:validate"
|
||||
run_shell_step "backend-node-check" "cd '$SERVER_DIR' && node --check src/routes/attendance.js src/routes/attendanceCorrections.js src/routes/paymentBatches.js src/routes/exports.js src/routes/wecomAuth.js"
|
||||
run_shell_step "backend-focused-tests" "cd '$SERVER_DIR' && npm test -- tests/unit/routes/attendance.test.js tests/unit/routes/attendanceCorrections.test.js tests/unit/routes/paymentBatches.reimbursement.test.js tests/unit/routes/exports.apOperations.test.js tests/unit/routes/wecomAuth.test.js tests/unit/routes/notifications.test.js"
|
||||
|
||||
if [[ "${LAUNCH_AUTO_SKIP_BUILD:-0}" == "1" ]]; then
|
||||
echo "[launch-auto-ops][SKIP] frontend-build LAUNCH_AUTO_SKIP_BUILD=1" | tee -a "$SUMMARY_TXT"
|
||||
else
|
||||
run_shell_step "frontend-typecheck" "cd '$CLIENT_DIR' && npm run typecheck"
|
||||
run_shell_step "frontend-build" "cd '$CLIENT_DIR' && npm run build"
|
||||
fi
|
||||
|
||||
if [[ "${LAUNCH_AUTO_SKIP_LIVE:-0}" == "1" ]]; then
|
||||
echo "[launch-auto-ops][SKIP] live checks LAUNCH_AUTO_SKIP_LIVE=1" | tee -a "$SUMMARY_TXT"
|
||||
else
|
||||
run_shell_step "live-env-strict" "LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' node '$ROOT_DIR/scripts/qa/check-live-smoke-env.mjs' --strict"
|
||||
run_shell_step "live-readiness" "QA_ISSUE_ID='${QA_ISSUE_ID:-LAUTO}' LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' bash '$ROOT_DIR/scripts/qa/run-live-smoke-readiness.sh'"
|
||||
run_shell_step "live-feature-flags" "LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' node '$ROOT_DIR/scripts/qa/check-live-feature-flags.mjs' --expect payment_batch_v1=force_on --expect erp_sync=force_off --expect attendance_v1=disabled --expect attendance_unified_v1=enabled"
|
||||
run_shell_step "live-attendance-unified" "LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' node '$ROOT_DIR/scripts/qa/check-live-attendance-unified.mjs' --json"
|
||||
|
||||
if [[ "$STATUS" -ne 0 ]]; then
|
||||
echo "[launch-auto-ops][SKIP] payment-batch-force-on-flow skipped because previous live gate failed" | tee -a "$SUMMARY_TXT"
|
||||
elif [[ "${LAUNCH_AUTO_ALLOW_PUBLIC_WRITES:-0}" != "1" ]]; then
|
||||
echo "[launch-auto-ops][FAIL] payment-batch-live-write requires LAUNCH_AUTO_ALLOW_PUBLIC_WRITES=1" | tee -a "$SUMMARY_TXT"
|
||||
STATUS=1
|
||||
else
|
||||
run_shell_step "payment-batch-force-on-flow" "cd '$CLIENT_DIR' && PLAYWRIGHT_MANUAL=1 E2E_MODE=live P2_FIN_LIVE_APPROVED=1 P2_FIN_LIVE_ALREADY_ON=1 P2_FIN_LIVE_FINAL_STATE=force_on QA_ISSUE_ID='${QA_ISSUE_ID:-LAUTO}' P2_FIN_LIVE_MARKER_PREFIX='${LAUNCH_AUTO_MARKER_PREFIX:-LAUNCHAUTO}' P2_FIN_LIVE_REPORT_DIR='$REPORT_DIR' LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' npx playwright test tests/e2e/manual/p2-fin-live-payment-batch.spec.ts --workers=1 --reporter=list"
|
||||
fi
|
||||
|
||||
run_shell_step "public-mobile-matrix" "cd '$CLIENT_DIR' && E2E_BASE_URL='$PUBLIC_BASE_URL' npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list"
|
||||
run_shell_step "wecom-rendering-matrix" "cd '$CLIENT_DIR' && E2E_BASE_URL='$PUBLIC_BASE_URL' npx playwright test tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list"
|
||||
fi
|
||||
在新工单中引用
屏蔽一个用户