文件
hyb-oa/scripts/qa/run-warch3-ops-daily.sh
wangsli 3ce183ddcf WOPS1 调整巡检告警降噪
Issue: WOPS1, WROAD2C, WREAL1P
2026-05-20 15:17:07 +08:00

242 行
8.0 KiB
Bash

#!/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