fix: rebaseline dev base image evidence (#76)

Co-authored-by: HWLAB Code Queue <code-queue@pikastech.local>
This commit is contained in:
Lyon
2026-05-22 14:20:37 +08:00
committed by GitHub
parent 4f93bc07bf
commit 1af787f63f
10 changed files with 672 additions and 354 deletions
@@ -8,6 +8,8 @@ import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/proto
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const deprecatedLegacyPublicPort = 6000 + 667;
const deprecatedLegacyPublicEndpoint = `http://74.48.78.17:${deprecatedLegacyPublicPort}`;
const levels = Object.freeze(["SOURCE", "LOCAL", "DRY-RUN", "DEV-LIVE", "BLOCKED"]);
const statusRank = Object.freeze({
pass: 0,
@@ -263,9 +265,7 @@ function isEdgeOrFrpBlocker(blocker) {
blocker.scope.includes("ingress") ||
blocker.scope.includes("frp") ||
blocker.summary.includes(":16667") ||
blocker.summary.includes("16667") ||
blocker.summary.includes(":6667") ||
blocker.summary.includes("6667");
blocker.summary.includes("16667");
}
function sourceEntry(entry) {
@@ -519,7 +519,7 @@ function hasCurrentLiveEdgeEvidence(edgeReport) {
function isStaleLegacyIngressBlocker(blocker, sourceReport, reports) {
const text = `${blocker.scope ?? ""} ${blocker.summary ?? ""}`;
const staleLegacyPort = text.includes("74.48.78.17:6667");
const staleLegacyPort = text.includes(deprecatedLegacyPublicEndpoint);
const stalePreflightEdgeProbe = sourceReport.path === reportPaths.devPreflight &&
blocker.type === "network_blocker" &&
(blocker.scope === "dev-edge" || blocker.scope === "dev-edge-health") &&
+200 -24
View File
@@ -17,9 +17,9 @@ const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const defaultReportPath = "reports/dev-gate/dev-preflight-report.json";
const issue = "pikasTech/HWLAB#34";
const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#39", "#49", "#66"].map(
const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#35", "#39", "#43", "#48", "#49", "#57", "#66"].map(
(id) => `pikasTech/HWLAB${id}`
).concat(["pikasTech/HWLAB#33", "pikasTech/HWLAB#35", "pikasTech/HWLAB#48"]);
);
const forbiddenActions = [
"prod-deploy",
"secret-material-read",
@@ -39,6 +39,20 @@ const blockerTypes = new Set([
"safety_blocker"
]);
const digestPattern = /^sha256:[a-f0-9]{64}$/;
const artifactBuildInputPrefixes = [
"cmd/",
"internal/",
"skills/",
"tools/",
"web/"
];
const artifactBuildInputFiles = new Set([
"package.json",
"package-lock.json",
"npm-shrinkwrap.json",
"scripts/dev-artifact-publish.mjs",
"scripts/src/dev-artifact-services.mjs"
]);
function parseArgs(argv) {
const args = {
@@ -210,6 +224,125 @@ function matchesTargetCommit(value, targetCommit, targetShortCommit) {
return value === targetCommit || value === targetShortCommit;
}
function isArtifactBuildInputPath(relativePath) {
return artifactBuildInputFiles.has(relativePath) ||
artifactBuildInputPrefixes.some((prefix) => relativePath.startsWith(prefix));
}
async function resolveCommit(ref) {
if (!ref) return null;
const result = await run("git", ["rev-parse", `${ref}^{commit}`]);
if (!result.ok || !/^[a-f0-9]{40}$/u.test(result.stdout)) return null;
return result.stdout;
}
async function sourceCoverageFor({ catalog, artifactReport, targetCommit, targetShortCommit, targetRef }) {
const reportSource = artifactReport?.artifactPublish?.sourceCommitId ?? artifactReport?.commitId ?? null;
const sourceRef = reportSource ?? catalog.commitId;
const sourceKind = reportSource ? "artifact-publish-report" : "artifact-catalog";
const sourceCommit = await resolveCommit(sourceRef);
if (!sourceCommit) {
return {
sourceKind,
sourceRef,
sourceCommitId: null,
sourceShortCommitId: null,
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "unresolved-source",
covered: false,
artifactBuildInputChanged: true,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: `artifact source ${sourceRef ?? "unknown"} does not resolve to a local git commit`
};
}
if (matchesTargetCommit(sourceCommit, targetCommit, targetShortCommit)) {
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "exact-target",
covered: true,
artifactBuildInputChanged: false,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: "artifact source commit exactly matches the target ref"
};
}
const ancestor = await run("git", ["merge-base", "--is-ancestor", sourceCommit, targetCommit]);
if (!ancestor.ok) {
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "non-ancestor-source",
covered: false,
artifactBuildInputChanged: true,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: `artifact source ${sourceCommit.slice(0, 12)} is not an ancestor of ${targetRef} ${targetShortCommit}`
};
}
const diff = await run("git", ["diff", "--name-only", `${sourceCommit}..${targetCommit}`]);
if (!diff.ok) {
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "diff-unavailable",
covered: false,
artifactBuildInputChanged: true,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: diff.stderr || diff.error || "git diff failed"
};
}
const changedPaths = diff.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
const changedArtifactInputPaths = changedPaths.filter(isArtifactBuildInputPath);
const covered = changedArtifactInputPaths.length === 0;
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: covered ? "report-only-target" : "artifact-input-drift",
covered,
artifactBuildInputChanged: changedArtifactInputPaths.length > 0,
changedPathCount: changedPaths.length,
changedArtifactInputPaths,
changedNonArtifactPathCount: changedPaths.length - changedArtifactInputPaths.length,
reason: covered
? "target changes since the artifact source do not touch artifact build inputs"
: "target changes since the artifact source touch artifact build inputs"
};
}
function uniqueSorted(values) {
return [...new Set(values)].sort();
}
@@ -238,15 +371,24 @@ function disabledCatalogServices(catalog) {
return (catalog.services ?? []).filter((service) => service.artifactRequired === false);
}
function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit, targetRef }) {
async function artifactIdentityFor({ deploy, catalog, artifactReport, targetCommit, targetShortCommit, targetRef }) {
const targetCoverage = await sourceCoverageFor({
catalog,
artifactReport,
targetCommit,
targetShortCommit,
targetRef
});
const artifactSourceCommit = targetCoverage.sourceCommitId ?? targetCommit;
const artifactSourceShortCommit = targetCoverage.sourceShortCommitId ?? targetShortCommit;
const serviceCommitIds = uniqueSorted((catalog.services ?? []).map((service) => service.commitId));
const digestCounts = catalogDigestCounts(catalog);
const requiredServices = requiredCatalogServices(catalog);
const disabledServices = disabledCatalogServices(catalog);
const catalogMatchesSource = matchesTargetCommit(catalog.commitId, targetCommit, targetShortCommit);
const deployMatchesSource = matchesTargetCommit(deploy.commitId, targetCommit, targetShortCommit);
const catalogMatchesSource = matchesTargetCommit(catalog.commitId, artifactSourceCommit, artifactSourceShortCommit);
const deployMatchesSource = matchesTargetCommit(deploy.commitId, artifactSourceCommit, artifactSourceShortCommit);
const servicesMatchSource = serviceCommitIds.length === 1 &&
matchesTargetCommit(serviceCommitIds[0], targetCommit, targetShortCommit);
matchesTargetCommit(serviceCommitIds[0], artifactSourceCommit, artifactSourceShortCommit);
const allRequiredDigestsPublished = requiredServices.length > 0 &&
requiredServices.every((service) => digestPattern.test(service.digest) && service.publishState === "published") &&
disabledServices.every((service) => service.digest === "not_published" && service.publishState === "skeleton-only") &&
@@ -262,10 +404,18 @@ function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit,
commitId: targetCommit,
shortCommitId: targetShortCommit
},
artifactSource: {
source: targetCoverage.sourceKind,
ref: targetCoverage.sourceRef,
commitId: targetCoverage.sourceCommitId,
shortCommitId: targetCoverage.sourceShortCommitId
},
targetCoverage,
deployManifest: {
path: "deploy/deploy.json",
commitId: deploy.commitId,
matchesSource: deployMatchesSource
matchesSource: deployMatchesSource,
matchesTarget: matchesTargetCommit(deploy.commitId, targetCommit, targetShortCommit)
},
artifactCatalog: {
path: "deploy/artifact-catalog.dev.json",
@@ -277,12 +427,14 @@ function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit,
digestCounts,
requiredServiceCount: requiredServices.length,
disabledServiceCount: disabledServices.length,
matchesSource: catalogMatchesSource
matchesSource: catalogMatchesSource,
matchesTarget: matchesTargetCommit(catalog.commitId, targetCommit, targetShortCommit)
},
services: (catalog.services ?? []).map((service) => ({
serviceId: service.serviceId,
commitId: service.commitId,
matchesSource: matchesTargetCommit(service.commitId, targetCommit, targetShortCommit),
matchesSource: matchesTargetCommit(service.commitId, artifactSourceCommit, artifactSourceShortCommit),
matchesTarget: matchesTargetCommit(service.commitId, targetCommit, targetShortCommit),
image: service.image,
imageTag: service.imageTag,
digest: service.digest,
@@ -291,7 +443,7 @@ function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit,
notPublishedReason: service.notPublishedReason ?? null
})),
serviceCommitIds,
matchesSource: deployMatchesSource && catalogMatchesSource && servicesMatchSource,
matchesSource: targetCoverage.covered && deployMatchesSource && catalogMatchesSource && servicesMatchSource,
publishVerified,
refreshCommands: {
blocked: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --blocked`,
@@ -399,9 +551,16 @@ function makeReporter() {
};
}
function validateLocalContracts(reporter, contracts, targetShortCommit, targetCommit, targetRef) {
async function validateLocalContracts(reporter, contracts, optionalReports, targetShortCommit, targetCommit, targetRef) {
const [deploy, catalog, namespace, workloads, services, devKustomization, healthContract, masterEdge] = contracts;
const artifactIdentity = artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit, targetRef });
const artifactIdentity = await artifactIdentityFor({
deploy,
catalog,
artifactReport: optionalReports.artifactPublish,
targetCommit,
targetShortCommit,
targetRef
});
try {
assertStaticContract(deploy, catalog);
@@ -417,25 +576,30 @@ function validateLocalContracts(reporter, contracts, targetShortCommit, targetCo
}
if (artifactIdentity.matchesSource) {
const coverage = artifactIdentity.targetCoverage;
const coverageSuffix = coverage.mode === "exact-target"
? "exact target commit"
: `${coverage.mode}; ${coverage.changedPathCount} changed path(s), ${coverage.changedArtifactInputPaths.length} artifact input change(s)`;
reporter.check(
"target-commit-pinning",
"contract",
"pass",
`Source ${targetRef} ${targetShortCommit} matches deploy/catalog artifact commit ${artifactIdentity.artifactCatalog.commitId}.`,
`Source ${targetRef} ${targetShortCommit} is covered by artifact source ${artifactIdentity.artifactSource.shortCommitId}; ${coverageSuffix}.`,
[artifactIdentity]
);
} else {
const coverage = artifactIdentity.targetCoverage;
reporter.check(
"target-commit-pinning",
"contract",
"blocked",
`Source ${targetRef} ${targetShortCommit} is not covered by deploy commit ${deploy.commitId}, catalog commit ${catalog.commitId}, service commits ${artifactIdentity.serviceCommitIds.join(", ")}.`,
`Source ${targetRef} ${targetShortCommit} is not covered by artifact source ${artifactIdentity.artifactSource.shortCommitId ?? "unknown"}: ${coverage.reason}.`,
[artifactIdentity]
);
reporter.block({
type: "contract_blocker",
scope: "artifact-source-commit",
summary: `source commit ${targetRef} ${targetShortCommit} does not match deploy/catalog artifact identity: deploy=${deploy.commitId}, catalog=${catalog.commitId}, services=${artifactIdentity.serviceCommitIds.join(", ")}.`,
summary: `source commit ${targetRef} ${targetShortCommit} is not covered by artifact source ${artifactIdentity.artifactSource.shortCommitId ?? "unknown"}; ${coverage.reason}.`,
nextTask: `Refresh without fake digests using \`${artifactIdentity.refreshCommands.blocked}\`, or after a successful publish run \`${artifactIdentity.refreshCommands.published}\`.`
});
}
@@ -480,7 +644,7 @@ function validateLocalContracts(reporter, contracts, targetShortCommit, targetCo
return { deploy, catalog, masterEdge, artifactIdentity };
}
function validateArtifactPublishReport(reporter, artifactReport, targetShortCommit, targetCommit, targetRef) {
function validateArtifactPublishReport(reporter, artifactReport, artifactIdentity, targetShortCommit, targetCommit, targetRef) {
if (!artifactReport) {
reporter.check("dev-artifact-publish-report", "registry", "blocked", "No DEV artifact publish report is present.");
reporter.block({
@@ -510,15 +674,26 @@ function validateArtifactPublishReport(reporter, artifactReport, targetShortComm
);
const sourceMatchesTarget = [targetCommit, targetShortCommit].includes(artifactPublish?.sourceCommitId) ||
[targetCommit, targetShortCommit].includes(artifactReport.commitId);
const sourceCoversTarget = artifactIdentity?.targetCoverage?.covered === true &&
artifactIdentity?.artifactSource?.commitId &&
(
artifactPublish?.sourceCommitId === artifactIdentity.artifactSource.commitId ||
artifactPublish?.sourceCommitId === artifactIdentity.artifactSource.shortCommitId ||
artifactReport.commitId === artifactIdentity.artifactSource.commitId ||
artifactReport.commitId === artifactIdentity.artifactSource.shortCommitId
);
const publishReady = artifactPublish?.status === "published" &&
artifactPublish.publishedCount === requiredServices.length &&
allServicesAccountedFor &&
allRequiredServicesPublished &&
disabledServicesNotPublished &&
sourceMatchesTarget;
(sourceMatchesTarget || sourceCoversTarget);
if (publishReady) {
reporter.check("dev-artifact-publish-report", "registry", "pass", "DEV artifact publish report covers all required enabled service IDs with immutable digests for the target commit, and records disabled service reasons.");
const summary = sourceMatchesTarget
? "DEV artifact publish report covers all required enabled service IDs with immutable digests for the target commit, and records disabled service reasons."
: `DEV artifact publish report covers artifact source ${artifactIdentity.artifactSource.shortCommitId}; target ${targetRef} ${targetShortCommit} has no artifact build input changes since that source.`;
reporter.check("dev-artifact-publish-report", "registry", "pass", summary, [artifactIdentity?.targetCoverage].filter(Boolean));
return;
}
@@ -537,7 +712,7 @@ function validateArtifactPublishReport(reporter, artifactReport, targetShortComm
type: "runtime_blocker",
scope: "dev-artifact-publish",
summary: `reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for ${targetRef} ${targetShortCommit}; current status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services published and ${sourceSummary}.`,
nextTask: "Complete DEV artifact publishing for every enabled required HWLAB service at the current origin/main commit and record immutable registry digests; keep disabled services marked not_published with reasons."
nextTask: "Complete DEV artifact publishing for every enabled required HWLAB service at the artifact source commit, or prove the target commit has no artifact build input changes; keep disabled services marked not_published with reasons."
});
}
@@ -868,9 +1043,9 @@ async function validateLiveProbes(reporter, catalog, timeoutMs) {
if (registryStatus === "blocked") {
reporter.block({
type: "runtime_blocker",
scope: "catalog-image-manifest-read",
summary: "This preflight could not verify registry manifests for the required DEV catalog images without reading credentials.",
nextTask: "Publish required DEV images or provide a non-secret registry evidence artifact with immutable digests for each required HWLAB service."
scope: "registry-manifest-read",
summary: "This preflight could not verify catalog image manifests through the runner process path; this is a #66 registry reachability dimension, not evidence that publish digests are missing.",
nextTask: "Resolve the #66 registry reachability path or provide a non-secret registry reachability artifact; do not rerun heavy publish solely for this manifest-read probe."
});
}
}
@@ -987,9 +1162,10 @@ export async function runPreflight(argv) {
registryPrefix,
timeoutMs: args.timeoutMs
});
const { deploy, catalog, masterEdge, artifactIdentity } = validateLocalContracts(
const { deploy, catalog, masterEdge, artifactIdentity } = await validateLocalContracts(
reporter,
contracts,
optionalReports,
targetShortCommit,
targetCommit,
args.targetRef
@@ -997,7 +1173,7 @@ export async function runPreflight(argv) {
validateRegistryCapabilities(reporter, registryCapabilities);
validateCloudApiDbContract(reporter, deploy, contracts[3]);
validateArtifactPublishReport(reporter, optionalReports.artifactPublish, targetShortCommit, targetCommit, args.targetRef);
validateArtifactPublishReport(reporter, optionalReports.artifactPublish, artifactIdentity, targetShortCommit, targetCommit, args.targetRef);
validateEdgeHealthReport(reporter, optionalReports.edgeHealth);
await validateEdgeContracts(reporter, masterEdge);
validateRuntimeBoundary(reporter, deploy);