WARCH3/WCHECK1/WEXT1 完成受控上线后续自动化
Issue: WARCH3, WCHECK1, WEXT1
这个提交包含在:
@@ -0,0 +1,241 @@
|
||||
#!/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: "finding_or_failure",
|
||||
recipient: "wangshunli/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=wangshunli --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
|
||||
在新工单中引用
屏蔽一个用户