文件
hyb-oa/scripts/qa/check-wecom-hr-master-sync.mjs
T

312 行
9.4 KiB
JavaScript

#!/usr/bin/env node
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const DEFAULT_BASE_URL = "https://oa.hyinda.com";
const DEFAULT_REPORT_ROOT = "artifacts/qa-flow";
function parseArgs(argv) {
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
const options = {
baseUrl: (process.env.LIVE_SMOKE_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, ""),
live: process.env.WECOM_HR_MASTER_SYNC_LIVE === "1",
json: process.env.WECOM_HR_MASTER_SYNC_JSON === "1",
applyApproved: process.env.WECOM_HR_MASTER_SYNC_APPLY_APPROVED === "1",
limit: Number(process.env.WECOM_HR_MASTER_SYNC_LIMIT || 10),
userids: csvValues(process.env.WECOM_HR_MASTER_SYNC_USERIDS || ""),
reportDir: process.env.WECOM_HR_MASTER_SYNC_REPORT_DIR || join(DEFAULT_REPORT_ROOT, `wecom-hr-master-sync-${stamp}`),
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--live") options.live = true;
if (arg === "--json") options.json = true;
if (arg === "--apply-approved") options.applyApproved = true;
if (arg === "--limit") {
options.limit = Number(argv[index + 1] || options.limit);
index += 1;
} else if (arg.startsWith("--limit=")) {
options.limit = Number(arg.slice("--limit=".length));
} else if (arg === "--userids") {
options.userids = csvValues(argv[index + 1] || "");
index += 1;
} else if (arg.startsWith("--userids=")) {
options.userids = csvValues(arg.slice("--userids=".length));
} else if (arg.startsWith("--report-dir=")) {
options.reportDir = arg.slice("--report-dir=".length);
}
}
options.limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(50, Math.floor(options.limit))) : 10;
return options;
}
function csvValues(value) {
return String(value || "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
function maskId(value) {
const raw = String(value || "").trim();
if (!raw) return null;
if (raw.length <= 6) return `${raw.slice(0, 2)}***`;
return `${raw.slice(0, Math.min(8, raw.length))}***`;
}
function getCredential() {
const identifier = process.env.LIVE_SMOKE_ADMIN_IDENTIFIER?.trim();
const password = process.env.LIVE_SMOKE_ADMIN_PASSWORD?.trim();
return identifier && password ? { identifier, password } : null;
}
async function fetchJson(url, init = {}) {
try {
const response = await fetch(url, {
...init,
headers: {
...(init.headers || {}),
...(init.body ? { "content-type": "application/json" } : {}),
},
signal: AbortSignal.timeout(30_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 };
} catch (error) {
return {
ok: false,
status: 0,
json: null,
error: error instanceof Error ? error.message : String(error),
};
}
}
function summarizeError(json) {
return {
code: json?.error?.code ?? null,
message: String(json?.error?.message ?? "").replace(/[A-Za-z0-9_-]{24,}/g, "***").slice(0, 160),
details: json?.error?.details ?? null,
};
}
async function login(baseUrl, credential) {
const result = await fetchJson(`${baseUrl}/api/auth/login`, {
method: "POST",
headers: {
"x-device-fingerprint": "whmslive-hr-master-sync",
},
body: JSON.stringify({
identifier: credential.identifier,
password: credential.password,
}),
});
if (!result.ok || !result.json?.data?.accessToken) {
throw new Error(`admin login failed: HTTP ${result.status} ${summarizeError(result.json).code ?? ""}`.trim());
}
return result.json.data.accessToken;
}
async function callApi(baseUrl, token, method, path, body) {
const result = await fetchJson(`${baseUrl}${path}`, {
method,
headers: { authorization: `Bearer ${token}` },
body: body == null ? undefined : JSON.stringify(body),
});
return result;
}
function step(status, name, details = {}) {
return { status, name, details };
}
function buildPayload(options, mode) {
return {
mode,
limit: options.limit,
...(options.userids.length ? { userids: options.userids } : {}),
};
}
function sanitizeRun(run) {
if (!run) return null;
return {
id: run.id,
mode: run.mode,
status: run.status,
summary: run.summary ?? null,
errorCode: run.errorCode ?? null,
requestedUserids: Array.isArray(run.requestedUserids) ? run.requestedUserids.map(maskId) : null,
itemCount: run.itemCount ?? run.items?.length ?? null,
};
}
function isAcceptableRun(run) {
return run?.status === "success" || run?.status === "partial";
}
function isSkippedRun(run) {
return run?.status === "skipped";
}
async function execute(options) {
const startedAt = new Date().toISOString();
const steps = [];
if (!options.live) {
return {
status: "SKIP",
conclusion: "LIVE_DISABLED",
startedAt,
finishedAt: new Date().toISOString(),
steps: [step("SKIP", "live-disabled", { runWith: "WECOM_HR_MASTER_SYNC_LIVE=1" })],
inputs: {
userids: options.userids.map(maskId),
limit: options.limit,
},
};
}
const credential = getCredential();
if (!credential) {
return {
status: "SKIP",
conclusion: "MISSING_LIVE_SMOKE_ADMIN",
startedAt,
finishedAt: new Date().toISOString(),
steps: [step("SKIP", "missing-admin-credential")],
};
}
const token = await login(options.baseUrl, credential);
steps.push(step("PASS", "admin-login"));
const list = await callApi(options.baseUrl, token, "GET", "/api/employees/wecom-hr-master-sync/runs?pageSize=1");
if (!list.ok) {
const error = summarizeError(list.json);
const featureFlag = error.details?.featureFlag;
return {
status: featureFlag ? "SKIP" : "FAIL",
conclusion: featureFlag ? "FEATURE_DISABLED_OR_UNAVAILABLE" : "RUN_LIST_FAILED",
startedAt,
finishedAt: new Date().toISOString(),
steps: [...steps, step(featureFlag ? "SKIP" : "FAIL", "list-runs", { status: list.status, error })],
};
}
steps.push(step("PASS", "list-runs", { latest: sanitizeRun(list.json?.data?.items?.[0]) }));
const dryRun = await callApi(
options.baseUrl,
token,
"POST",
"/api/employees/wecom-hr-master-sync/runs",
buildPayload(options, "dry_run"),
);
if (!dryRun.ok) {
return {
status: "FAIL",
conclusion: "DRY_RUN_FAILED",
startedAt,
finishedAt: new Date().toISOString(),
steps: [...steps, step("FAIL", "dry-run", { status: dryRun.status, error: summarizeError(dryRun.json) })],
};
}
const dryRunSummary = sanitizeRun(dryRun.json?.data);
if (!isAcceptableRun(dryRunSummary)) {
const status = isSkippedRun(dryRunSummary) ? "SKIP" : "FAIL";
return {
status,
conclusion: isSkippedRun(dryRunSummary) ? "DRY_RUN_SKIPPED" : "DRY_RUN_FAILED",
startedAt,
finishedAt: new Date().toISOString(),
steps: [...steps, step(status, "dry-run", dryRunSummary)],
};
}
steps.push(step("PASS", "dry-run", dryRunSummary));
if (!options.applyApproved) {
return {
status: "PASS",
conclusion: "DRY_RUN_ONLY",
startedAt,
finishedAt: new Date().toISOString(),
steps: [...steps, step("SKIP", "apply", { runWith: "WECOM_HR_MASTER_SYNC_APPLY_APPROVED=1" })],
};
}
if (!options.userids.length) {
return {
status: "FAIL",
conclusion: "APPLY_REQUIRES_EXPLICIT_USERIDS",
startedAt,
finishedAt: new Date().toISOString(),
steps: [...steps, step("FAIL", "apply-guard", { reason: "missing_explicit_userids" })],
};
}
const apply = await callApi(
options.baseUrl,
token,
"POST",
"/api/employees/wecom-hr-master-sync/runs",
buildPayload(options, "apply"),
);
if (!apply.ok) {
return {
status: "FAIL",
conclusion: "APPLY_FAILED",
startedAt,
finishedAt: new Date().toISOString(),
steps: [...steps, step("FAIL", "apply", { status: apply.status, error: summarizeError(apply.json) })],
};
}
const applySummary = sanitizeRun(apply.json?.data);
if (!isAcceptableRun(applySummary)) {
const status = isSkippedRun(applySummary) ? "SKIP" : "FAIL";
return {
status,
conclusion: isSkippedRun(applySummary) ? "APPLY_SKIPPED" : "APPLY_FAILED",
startedAt,
finishedAt: new Date().toISOString(),
steps: [...steps, step(status, "apply", applySummary)],
};
}
return {
status: "PASS",
conclusion: "APPLY_DONE",
startedAt,
finishedAt: new Date().toISOString(),
steps: [...steps, step("PASS", "apply", applySummary)],
};
}
async function main() {
const options = parseArgs(process.argv.slice(2));
mkdirSync(options.reportDir, { recursive: true });
const summary = await execute(options);
const reportPath = join(options.reportDir, "summary.json");
writeFileSync(reportPath, `${JSON.stringify(summary, null, 2)}\n`);
if (options.json) {
console.log(JSON.stringify(summary, null, 2));
} else {
console.log(`[wecom-hr-master-sync] status=${summary.status} conclusion=${summary.conclusion}`);
console.log(`[wecom-hr-master-sync] report=${reportPath}`);
}
if (summary.status === "FAIL") process.exit(1);
}
main().catch((error) => {
console.error(`[wecom-hr-master-sync][ERROR] ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});