完善 readiness、发布追溯与敏感治理(07-15-production-trust-closeout-cleanup)

这个提交包含在:
2026-07-16 05:17:38 +08:00
父节点 abb6a22f5c
当前提交 28e8e746b7
共修改 29 个文件,包含 1849 行新增和 233 行删除
+46 -6
查看文件
@@ -4,11 +4,21 @@ import process from "node:process";
const baseUrl = normalizeBaseUrl(process.argv[2] ?? process.env.PUBLIC_BASE_URL ?? process.env.LIVE_SMOKE_BASE_URL ?? "https://oa.hyinda.com");
const focusedAssetPattern = /(MobileReimbursementDetailPage|ReimbursementDetailPage|page-mobile|page-assets|page-travel)/;
const forbiddenTerms = ["绑定整单", "不绑定明细", "识别完成后可在", "发票池", "OCR"];
const expectedBuildSha = normalizeBuildSha(process.env.PUBLIC_EXPECTED_BUILD_SHA);
const forbiddenFullBuildSha = normalizeBuildSha(process.env.PUBLIC_FORBIDDEN_BUILD_SHA, { keepFull: true });
function normalizeBaseUrl(value) {
return String(value).replace(/\/+$/, "");
}
function normalizeBuildSha(value, { keepFull = false } = {}) {
const normalized = String(value ?? "").trim().toLowerCase();
if (!/^[0-9a-f]{7,64}$/.test(normalized)) {
return null;
}
return keepFull ? normalized : normalized.slice(0, 12);
}
function toAssetPath(value) {
if (value.startsWith("/")) {
return value;
@@ -27,6 +37,7 @@ async function fetchText(path) {
async function collectAssets() {
const seen = new Set();
const queue = [];
const sources = new Map();
const html = await fetchText("/");
for (const match of html.matchAll(/(?:src|href)=["'](\/assets\/[^"']+\.js)["']/g)) {
@@ -40,6 +51,7 @@ async function collectAssets() {
for (let index = 0; index < queue.length; index += 1) {
const assetPath = queue[index];
const source = await fetchText(assetPath);
sources.set(assetPath, source);
for (const match of source.matchAll(/\.\/([A-Za-z0-9_./-]+\.js)/g)) {
const nextPath = toAssetPath(match[1]);
if (!seen.has(nextPath)) {
@@ -49,28 +61,56 @@ async function collectAssets() {
}
}
return Array.from(seen).sort();
return {
assets: Array.from(seen).sort(),
sources,
};
}
const assets = await collectAssets();
const { assets, sources } = await collectAssets();
const findings = [];
let expectedBuildShaFound = expectedBuildSha === null;
let forbiddenFullBuildShaFound = false;
for (const assetPath of assets) {
const source = sources.get(assetPath) ?? "";
if (expectedBuildSha && source.toLowerCase().includes(expectedBuildSha)) {
expectedBuildShaFound = true;
}
if (
forbiddenFullBuildSha
&& forbiddenFullBuildSha !== expectedBuildSha
&& source.toLowerCase().includes(forbiddenFullBuildSha)
) {
forbiddenFullBuildShaFound = true;
}
if (!focusedAssetPattern.test(assetPath)) {
continue;
}
const source = await fetchText(assetPath);
for (const term of forbiddenTerms) {
if (source.includes(term)) {
findings.push({ assetPath, term });
findings.push({ assetPath, category: "forbidden_copy", term });
}
}
}
console.log(`[public-bundle-copy] base=${baseUrl} scanned=${assets.length} focused=${assets.filter((item) => focusedAssetPattern.test(item)).length} findings=${findings.length}`);
if (!expectedBuildShaFound) {
findings.push({ assetPath: "public-bundle", category: "build_sha_missing" });
}
if (forbiddenFullBuildShaFound) {
findings.push({ assetPath: "public-bundle", category: "full_build_sha_exposed" });
}
const buildMarkerStatus = expectedBuildSha === null
? "NOT_CHECKED"
: expectedBuildShaFound && !forbiddenFullBuildShaFound
? "PASS"
: "FAIL";
console.log(`[public-bundle-copy] base=${baseUrl} scanned=${assets.length} focused=${assets.filter((item) => focusedAssetPattern.test(item)).length} buildMarker=${buildMarkerStatus} findings=${findings.length}`);
for (const finding of findings) {
console.log(`ERROR ${finding.assetPath} contains ${finding.term}`);
console.log(`ERROR ${finding.assetPath} category=${finding.category}`);
}
if (findings.length) {