321 行
11 KiB
Bash
可执行文件
321 行
11 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="${WAUTO1_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/wecom-controlled-trial-evidence-$(date +%Y%m%d-%H%M%S)}"
|
|
SUMMARY_TXT="$REPORT_DIR/summary.txt"
|
|
STATUS=0
|
|
FINDING_COUNT=0
|
|
|
|
mkdir -p "$REPORT_DIR"
|
|
: >"$SUMMARY_TXT"
|
|
|
|
log() {
|
|
printf '%s\n' "$*" | tee -a "$SUMMARY_TXT"
|
|
}
|
|
|
|
latest_summary() {
|
|
local pattern="$1"
|
|
find "$ROOT_DIR/artifacts/qa-flow" -maxdepth 3 -path "$pattern" -type f 2>/dev/null \
|
|
| sort \
|
|
| tail -n 1
|
|
}
|
|
|
|
run_step() {
|
|
local name="$1"
|
|
shift
|
|
local log_file="$REPORT_DIR/${name}.log"
|
|
|
|
log ">>> [wauto1] $name"
|
|
if "$@" > >(tee "$log_file") 2>&1; then
|
|
log "[wauto1][PASS] $name"
|
|
else
|
|
local code="$?"
|
|
log "[wauto1][FAIL] $name exit=$code"
|
|
STATUS=1
|
|
fi
|
|
}
|
|
|
|
run_finding_step() {
|
|
local name="$1"
|
|
shift
|
|
local log_file="$REPORT_DIR/${name}.log"
|
|
|
|
log ">>> [wauto1] $name"
|
|
if "$@" > >(tee "$log_file") 2>&1; then
|
|
log "[wauto1][PASS] $name"
|
|
else
|
|
local code="$?"
|
|
log "[wauto1][FINDING] $name exit=$code"
|
|
FINDING_COUNT=$((FINDING_COUNT + 1))
|
|
fi
|
|
}
|
|
|
|
run_server_finding_step() {
|
|
local name="$1"
|
|
shift
|
|
local log_file="$REPORT_DIR/${name}.log"
|
|
|
|
log ">>> [wauto1] $name"
|
|
if (cd "$SERVER_DIR" && "$@") > >(tee "$log_file") 2>&1; then
|
|
log "[wauto1][PASS] $name"
|
|
else
|
|
local code="$?"
|
|
log "[wauto1][FINDING] $name exit=$code"
|
|
FINDING_COUNT=$((FINDING_COUNT + 1))
|
|
fi
|
|
}
|
|
|
|
run_ready() {
|
|
run_step "public-ready" curl -fsS "$PUBLIC_BASE_URL/ready"
|
|
}
|
|
|
|
run_warch3_summary_only() {
|
|
local wroad2_summary
|
|
wroad2_summary="$(latest_summary '*/wroad2-ops-*/summary.json')"
|
|
if [[ -z "$wroad2_summary" ]]; then
|
|
log "[wauto1][FINDING] warch3-summary-only missing latest WROAD2 summary"
|
|
FINDING_COUNT=$((FINDING_COUNT + 1))
|
|
return 0
|
|
fi
|
|
|
|
run_finding_step "warch3-summary-only" env \
|
|
WARCH3_SKIP_WROAD2=1 \
|
|
WARCH3_SEND_ALERT=0 \
|
|
WARCH3_WROAD2_SUMMARY_FILE="$wroad2_summary" \
|
|
WARCH3_OPS_REPORT_DIR="$REPORT_DIR/warch3-summary" \
|
|
bash "$ROOT_DIR/scripts/qa/run-warch3-ops-daily.sh"
|
|
}
|
|
|
|
run_readonly_runners() {
|
|
run_server_finding_step "approval-compensation-readonly" npm run check:wecom-approval-compensation-poc -- --live --format=json --discover-template-ids --report-dir="$REPORT_DIR/wecom-approval-compensation-poc"
|
|
run_server_finding_step "approval-archive-offline" npm run check:wecom-approval-archive -- --offline --format=json --report-dir="$REPORT_DIR/wecom-approval-archive"
|
|
run_server_finding_step "checkin-summary-offline" npm run check:wecom-checkin-summary-archive -- --offline --format=json --report-dir="$REPORT_DIR/wecom-checkin-summary-archive"
|
|
run_server_finding_step "outing-overtime-dryrun" npm run check:wecom-approval-fixtures -- --format=json --target=outing --target=overtime --report-dir="$REPORT_DIR/wecom-approval-fixtures-dryrun"
|
|
}
|
|
|
|
write_manual_checklist() {
|
|
cat >"$REPORT_DIR/wappr1-manual-checklist.md" <<'EOF'
|
|
# WAPPR1 人工确认清单
|
|
|
|
请分别检查请假、补卡、外出、加班模板,并按每个模板给出一个选项:
|
|
|
|
- A:审批人自动带出,申请人不能任意选择。
|
|
- B:审批人默认带出,但申请人仍可改人。
|
|
- C:必须申请人手选审批人。
|
|
- D:当前看不到或模板未配置。
|
|
|
|
小团队默认规则:公司不到 10 人,按一层直属主管或指定审批角色自动带出审批人;主管链缺失、审批人不可见/停用/未绑定或命中自审时,由管理员兜底或阻断。当前目标按 A 执行,B/C/D 均视为阻断 WEXT1-LIVE 和 WREAL1 的 finding。
|
|
|
|
需要记录的字段:模板入口、适用范围、审批人规则、是否允许申请人手选、是否可能自审、主管链缺失时表现、审批人不可见或停用时表现。
|
|
|
|
边界:不要记录 Secret、token、EncodingAESKey、完整 userid、完整审批单号、完整 template_id、表单值、附件、评论或原始 payload。
|
|
EOF
|
|
}
|
|
|
|
run_privacy_smoke() {
|
|
local sensitive_pattern='access_token=[A-Za-z0-9._-]{8,}|1[3-9][0-9]{9}|[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}|-----BEGIN|BEGIN [A-Z ]*KEY|"((token|secret|password|EncodingAESKey|encodingAESKey|sp_no|spNo|template_id|templateId|userid|userId|external_userid|externalUserId|external_user_id))"[[:space:]]*:[[:space:]]*"[^"*]{8,}"|\b20[0-9]{12,}\b|"(raw|rawPayload|raw_payload|payload|xml|ciphertext|Encrypt|formValue|formValues|attachments|comments)"[[:space:]]*:'
|
|
local matches
|
|
matches="$(grep -RInE "$sensitive_pattern" "$REPORT_DIR" | grep -vE 'HRWDTEST-[0-9]{14}' || true)"
|
|
if [[ -n "$matches" ]]; then
|
|
printf '%s\n' "$matches"
|
|
log "[wauto1][FAIL] privacy smoke matched sensitive pattern"
|
|
STATUS=1
|
|
else
|
|
log "[wauto1][PASS] report-privacy-smoke"
|
|
fi
|
|
}
|
|
|
|
write_summary_json() {
|
|
node - "$ROOT_DIR" "$REPORT_DIR" "$PUBLIC_BASE_URL" "$STATUS" "$FINDING_COUNT" <<'NODE' || return 1
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { execSync } = require("node:child_process");
|
|
|
|
const [rootDir, reportDir, baseURL, statusInput, findingCountInput] = 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 findLatestSummary(prefix) {
|
|
const qaRoot = path.join(rootDir, "artifacts/qa-flow");
|
|
let files = [];
|
|
try {
|
|
files = fs.readdirSync(qaRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix))
|
|
.map((entry) => path.join(qaRoot, entry.name, "summary.json"))
|
|
.filter((file) => fs.existsSync(file))
|
|
.sort();
|
|
} catch {}
|
|
return files.at(-1) || null;
|
|
}
|
|
|
|
function childReport(relativePath) {
|
|
const file = path.join(reportDir, relativePath, "summary.json");
|
|
const json = readJson(file);
|
|
return {
|
|
status: json?.status || json?.conclusion || "MISSING",
|
|
conclusion: json?.conclusion || null,
|
|
reportPath: fs.existsSync(file) ? path.resolve(file) : null,
|
|
};
|
|
}
|
|
|
|
function safeStatus(value) {
|
|
return String(value || "UNKNOWN").slice(0, 80);
|
|
}
|
|
|
|
function isBlocked(status) {
|
|
return /SKIP|BLOCKED|FAIL|ERROR/i.test(status);
|
|
}
|
|
|
|
function isPartial(status) {
|
|
return /PARTIAL|NEEDS_CONFIG|NEEDS_DATA|MISSING/i.test(status);
|
|
}
|
|
|
|
function isFinding(status) {
|
|
return /FINDING/i.test(status);
|
|
}
|
|
|
|
let commit = "unknown";
|
|
try {
|
|
commit = execSync("git rev-parse --short HEAD", { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
} catch {}
|
|
|
|
const childReports = {
|
|
publicReady: {
|
|
status: Number(statusInput || 0) === 0 ? "PASS" : "FAIL",
|
|
reportPath: path.resolve(path.join(reportDir, "public-ready.log")),
|
|
},
|
|
warch3Summary: childReport("warch3-summary"),
|
|
approvalCompensationReadonly: childReport("wecom-approval-compensation-poc"),
|
|
approvalArchiveOffline: childReport("wecom-approval-archive"),
|
|
checkinSummaryOffline: childReport("wecom-checkin-summary-archive"),
|
|
outingOvertimeDryRun: childReport("wecom-approval-fixtures-dryrun"),
|
|
};
|
|
|
|
const sourceReports = {
|
|
latestWroad2: findLatestSummary("wroad2-ops-"),
|
|
latestWarch3: findLatestSummary("warch3-ops-"),
|
|
latestCheckin: findLatestSummary("wecom-checkin-summary-archive-"),
|
|
latestOutingOvertimeDryRun: findLatestSummary("wecom-approval-fixtures-"),
|
|
};
|
|
|
|
const statuses = Object.fromEntries(Object.entries(childReports).map(([key, value]) => [key, safeStatus(value.status)]));
|
|
const statusValues = Object.values(statuses);
|
|
const findings = [];
|
|
for (const [key, value] of Object.entries(statuses)) {
|
|
if (isBlocked(value)) {
|
|
findings.push({ source: key, status: value, severity: "blocker" });
|
|
} else if (isPartial(value)) {
|
|
findings.push({ source: key, status: value, severity: "manual_or_data_gap" });
|
|
} else if (isFinding(value)) {
|
|
findings.push({ source: key, status: value, severity: "finding" });
|
|
}
|
|
}
|
|
|
|
const manualGates = [
|
|
{
|
|
id: "WROAD2-BIZ1",
|
|
status: "PENDING_MANUAL_CONFIRMATION",
|
|
owner: "SMOKE002/SMOKE101/HYD000006",
|
|
requiredEvidence: "三天打卡、请假、补卡、详情后审批和 OA 历史查看结果",
|
|
},
|
|
{
|
|
id: "WAPPR1",
|
|
status: "PENDING_MANUAL_CONFIRMATION",
|
|
owner: "HYD000006",
|
|
requiredEvidence: "请假、补卡、外出、加班逐模板均达到 A;小团队采用一层主管/指定审批角色 + 管理员兜底",
|
|
},
|
|
{
|
|
id: "WEXT1-LIVE",
|
|
status: "PENDING_USER_APPROVAL",
|
|
owner: "human_lead",
|
|
requiredEvidence: "外出/加班 dry-run 可解释通过后,是否允许单次 live 创建 OA-POC 样件",
|
|
},
|
|
];
|
|
|
|
let status = "PASS_AUTO_EVIDENCE_READY";
|
|
if (statusValues.some(isBlocked) || findings.some((item) => item.severity === "blocker")) {
|
|
status = "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
|
|
} else if (statusValues.some(isPartial)) {
|
|
status = "PAUSED_MANUAL_CONFIRMATION";
|
|
} else if (Number(findingCountInput || 0) > 0 || statusValues.some(isFinding)) {
|
|
status = "PASS_WITH_FINDINGS";
|
|
} else if (manualGates.some((gate) => gate.status.includes("PENDING"))) {
|
|
status = "PAUSED_MANUAL_CONFIRMATION";
|
|
}
|
|
|
|
const summary = {
|
|
issueId: "WAUTO1",
|
|
mode: "wecom-controlled-trial-auto-evidence",
|
|
status,
|
|
conclusion: status,
|
|
baseURL,
|
|
commit,
|
|
checkedAt: new Date().toISOString(),
|
|
reportDir: path.resolve(reportDir),
|
|
scope: {
|
|
accounts: {
|
|
applicant: "SMOKE002",
|
|
approver: "SMOKE101",
|
|
adminObserverEmployeeNo: "HYD000006",
|
|
},
|
|
secretSource: "WECOM_AGENT_SECRET for WeCom approval/checkin read paths; no runtime Secret fallback",
|
|
noRealEmployees: true,
|
|
},
|
|
boundaries: {
|
|
noWeComWrites: true,
|
|
noWeComTemplateCreation: true,
|
|
noWeComApprovalSubmission: true,
|
|
noOaWritesFromThisScript: true,
|
|
noFixtureLiveFlags: true,
|
|
dryRunOnlyForOutingOvertime: true,
|
|
},
|
|
childReports,
|
|
sourceReports,
|
|
statuses,
|
|
findingCount: findings.length,
|
|
findings,
|
|
manualGates,
|
|
nextQuestions: {
|
|
wext1Live: [
|
|
"A:批准一次 live 创建外出+加班 OA-POC 样件,保留审计。",
|
|
"B:只保留 dry-run,不创建样件。",
|
|
"C:外出/加班移出 V1。",
|
|
],
|
|
threeDayCloseout: [
|
|
"A:接受 PASS_AUTO_EVIDENCE_READY,进入 WEXT1-LIVE live 问答。",
|
|
"B:接受 PASS_WITH_FINDINGS,但先处理指定 finding。",
|
|
"C:暂停,不进入外出/加班 live 或真实试用。",
|
|
],
|
|
},
|
|
privacy: "status/count/report-path summaries only; no phone/email/token/Secret/EncodingAESKey/full userid/full template_id/full sp_no/form values/attachments/comments/raw payload/XML/ciphertext",
|
|
};
|
|
|
|
fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
|
console.log(`[wauto1] summary=${path.join(reportDir, "summary.json")}`);
|
|
NODE
|
|
}
|
|
|
|
finish() {
|
|
write_manual_checklist
|
|
write_summary_json || STATUS=1
|
|
run_privacy_smoke
|
|
log "[wauto1] report=$REPORT_DIR"
|
|
exit "$STATUS"
|
|
}
|
|
|
|
trap finish EXIT
|
|
|
|
run_ready
|
|
run_warch3_summary_only
|
|
run_readonly_runners
|