#!/usr/bin/env bash set -u -o pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" SERVER_DIR="$ROOT_DIR/server" PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}" REPORT_DIR="${WARCH3_OPS_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/warch3-ops-$(date +%Y%m%d-%H%M%S)}" WROAD2_REPORT_DIR="${WARCH3_WROAD2_REPORT_DIR:-$REPORT_DIR/wroad2-ops}" SUMMARY_TXT="$REPORT_DIR/summary.txt" STATUS=0 mkdir -p "$REPORT_DIR" : >"$SUMMARY_TXT" run_wroad2() { if [[ "${WARCH3_SKIP_WROAD2:-0}" == "1" ]]; then echo "[warch3-ops] skip WROAD2 execution by WARCH3_SKIP_WROAD2=1" | tee -a "$SUMMARY_TXT" return 0 fi echo ">>> [warch3-ops] run WROAD2 controlled ops" | tee -a "$SUMMARY_TXT" if WROAD2_OPS_REPORT_DIR="$WROAD2_REPORT_DIR" bash "$ROOT_DIR/scripts/qa/run-wroad2-ops-daily.sh" \ > >(tee "$REPORT_DIR/wroad2.log") 2>&1; then echo "[warch3-ops][PASS] wroad2" | tee -a "$SUMMARY_TXT" else local code="$?" echo "[warch3-ops][FAIL] wroad2 exit=$code" | tee -a "$SUMMARY_TXT" STATUS=1 fi } resolve_wroad2_summary() { if [[ -n "${WARCH3_WROAD2_SUMMARY_FILE:-}" && -f "${WARCH3_WROAD2_SUMMARY_FILE}" ]]; then printf '%s\n' "$WARCH3_WROAD2_SUMMARY_FILE" return 0 fi if [[ -f "$WROAD2_REPORT_DIR/summary.json" ]]; then printf '%s\n' "$WROAD2_REPORT_DIR/summary.json" return 0 fi find "$ROOT_DIR/artifacts/qa-flow" -maxdepth 2 -path '*/wroad2-ops-*/summary.json' -type f 2>/dev/null \ | sort \ | tail -n 1 } write_summary_json() { local wroad2_summary wroad2_summary="$(resolve_wroad2_summary)" node - "$REPORT_DIR" "$PUBLIC_BASE_URL" "$STATUS" "$wroad2_summary" <<'NODE' const fs = require("node:fs"); const path = require("node:path"); const { execSync } = require("node:child_process"); const crypto = require("node:crypto"); const [reportDir, baseURL, statusInput, wroad2SummaryFile] = process.argv.slice(2); function readJson(file) { if (!file || !fs.existsSync(file)) return null; try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } } function hash(value) { return crypto.createHash("sha256").update(String(value ?? "")).digest("hex"); } function safeString(value, maxLength = 240) { return String(value ?? "") .replace(/access_token=[^&\s]+/gi, "access_token=***") .replace(/[A-Za-z0-9+/=_-]{24,}/g, "***") .slice(0, maxLength); } function reportStatus(report) { return report?.status || report?.conclusion || "UNKNOWN"; } function addFinding(findings, source, status, detail, severity = "info") { const text = `${source}:${status}:${detail}`; const evidenceDigest = hash(text).slice(0, 16); if (findings.some((item) => item.evidenceDigest === evidenceDigest)) return; findings.push({ source, status, severity, evidenceDigest, detail: safeString(detail), }); } let commit = "unknown"; try { commit = execSync("git rev-parse --short HEAD", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); } catch {} const wroad2 = readJson(wroad2SummaryFile); const childReports = wroad2?.childReports && typeof wroad2.childReports === "object" ? wroad2.childReports : {}; const findings = []; const observations = []; const requiredChildren = ["wecomRuntimeReady", "approvalCompensation", "approvalArchive"]; for (const key of requiredChildren) { const status = reportStatus(childReports[key]); const passLikeStatuses = new Set([ "PASS", "PASS_RUNTIME_READY", "PASS_WITH_CONFIG_GAPS", "PASS_WITH_FINDINGS", "PASS_COMPENSATION_POC", "PASS_ARCHIVE_HISTORY_READABLE", ]); if (!childReports[key]) { addFinding(findings, key, "MISSING", "required WROAD2 child report missing", "warn"); } else if (!passLikeStatuses.has(status)) { addFinding(findings, key, status, "required WROAD2 child report is not pass-like", "warn"); } } for (const [key, value] of Object.entries(childReports)) { const status = reportStatus(value); if (key === "liveReadiness" && status !== "PASS") { observations.push({ source: key, status, reason: "live smoke credentials or pending OCR subcheck is tracked as WARCH3 observation, not archive blocker", }); continue; } if (status === "PASS_WITH_FINDINGS") { addFinding(findings, key, status, "child report has explainable findings", "info"); } } if (Number(wroad2?.findingCount || 0) > 0) { observations.push({ source: "wroad2", status: reportStatus(wroad2), findingCount: Number(wroad2.findingCount || 0), reason: "WROAD2 finding count kept for trend; known live smoke/OCR and historical callback gap do not block archive ops", }); } if (!wroad2) { addFinding(findings, "wroad2", "MISSING", "WROAD2 summary not found", "error"); } const shellFailed = Number(statusInput || 0) !== 0; let status = "PASS_ARCHIVE_OPS_READY"; if (shellFailed || reportStatus(wroad2) === "SKIP/BLOCKED_BY_EXTERNAL_CONFIG" || findings.some((item) => item.severity === "error")) { status = "SKIP/BLOCKED_BY_EXTERNAL_CONFIG"; } else if (findings.length || observations.length || reportStatus(wroad2) === "PASS_WITH_FINDINGS") { status = "PASS_WITH_FINDINGS"; } const summary = { issueId: "WARCH3", mode: "wecom-archive-stabilization-ops", status, conclusion: status, baseURL, commit, checkedAt: new Date().toISOString(), reportDir: path.resolve(reportDir), sourceReports: { wroad2: wroad2SummaryFile ? path.resolve(wroad2SummaryFile) : null, }, mainChain: { wecomRuntimeReady: reportStatus(childReports.wecomRuntimeReady), approvalCompensation: reportStatus(childReports.approvalCompensation), approvalArchive: reportStatus(childReports.approvalArchive), }, findingCount: findings.length, findings, observations, schedule: { timer: "hyb-oa-warch3-ops.timer", plannedLocalTime: "07:30 Asia/Shanghai", currentRun: process.env.WARCH3_SKIP_WROAD2 === "1" ? "summary_only" : "wroad2_then_summary", }, alertPolicy: { enabledWhen: "blocked_failure_new_finding_or_status_worsening", recipient: "HYD000006", messageBoundary: "stage/status/finding count/report path/next step only", }, boundaries: { noWeComWrites: true, noFixtureCreation: true, archivePersistence: "OA_DB_only_via_WROAD2_compensation_step", liveSmokeAndOcr: "observation_not_archive_blocker", }, privacy: "masked counts/status/report paths only; no phone/email/token/secret/full userid/full template_id/full sp_no/raw XML/ciphertext/form values/attachments/comments/raw payload", }; fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8"); console.log(`[warch3-ops] summary=${path.join(reportDir, "summary.json")}`); NODE } run_privacy_smoke() { if grep -RInE 'access_token=[A-Za-z0-9._-]{8,}|1[3-9][0-9]{9}|[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}|-----BEGIN|BEGIN [A-Z ]*KEY' "$REPORT_DIR"; then echo "[warch3-ops][FAIL] privacy smoke matched sensitive pattern" | tee -a "$SUMMARY_TXT" STATUS=1 fi } maybe_send_alert() { local summary_file="$REPORT_DIR/summary.json" if [[ "${WARCH3_SEND_ALERT:-0}" != "1" || ! -f "$summary_file" ]]; then return 0 fi local should_alert should_alert="$(node -e "const s=require(process.argv[1]); console.log((s.status!=='PASS_ARCHIVE_OPS_READY'||Number(s.findingCount||0)>0)?'1':'0')" "$summary_file" 2>/dev/null || printf '0')" if [[ "$should_alert" != "1" ]]; then echo "[warch3-ops] alert skipped: no finding or failure" | tee -a "$SUMMARY_TXT" return 0 fi echo ">>> [warch3-ops] send dehydrated WeCom alert" | tee -a "$SUMMARY_TXT" if (cd "$SERVER_DIR" && npm run send:wecom-ops-alert -- --summary-file="$summary_file" --recipient-employee-no=HYD000006 --send --format=json) \ > >(tee "$REPORT_DIR/wecom-alert.log") 2>&1; then echo "[warch3-ops][PASS] alert" | tee -a "$SUMMARY_TXT" else local code="$?" echo "[warch3-ops][FINDING] alert exit=$code" | tee -a "$SUMMARY_TXT" fi } finish() { write_summary_json || STATUS=1 run_privacy_smoke maybe_send_alert run_privacy_smoke echo "[warch3-ops] report=$REPORT_DIR" | tee -a "$SUMMARY_TXT" exit "$STATUS" } trap finish EXIT run_wroad2