test: lock backend-core artifact readiness contract
This commit is contained in:
@@ -1333,7 +1333,7 @@ function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: Artifact
|
||||
providerId: options.providerId,
|
||||
reason: spec.prodLiveBlockReason ?? `${spec.serviceId} does not yet satisfy the artifact consumer runtime verification contract.`,
|
||||
requiredBeforeLiveApply: [
|
||||
"CI can publish a commit-pinned image with matching service id, source commit, and Dockerfile labels",
|
||||
"CI can publish a commit-pinned image with matching service id, source repo, source commit, and Dockerfile labels",
|
||||
"runtime Compose env injects deploy commit/requestedCommit metadata",
|
||||
"service health reports deploy.commit and deploy.requestedCommit for strict verification",
|
||||
],
|
||||
@@ -1823,25 +1823,29 @@ function verifyLocalArtifactLabels(
|
||||
localLoadedImage: string,
|
||||
spec: ArtifactConsumerSpec,
|
||||
commit: string,
|
||||
sourceRepo: string,
|
||||
): Record<string, unknown> | null {
|
||||
const inspectPulled = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{json .Config.Labels}}"], repoRoot);
|
||||
const labelCommit = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/source-commit\" }}"], repoRoot);
|
||||
const labelService = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/service-id\" }}"], repoRoot);
|
||||
const labelRepo = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/source-repo\" }}"], repoRoot);
|
||||
const labelDockerfile = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/dockerfile\" }}"], repoRoot);
|
||||
const observed = {
|
||||
commit: labelCommit.stdout.trim(),
|
||||
serviceId: labelService.stdout.trim(),
|
||||
sourceRepo: labelRepo.stdout.trim(),
|
||||
dockerfile: labelDockerfile.stdout.trim(),
|
||||
labels: inspectPulled.stdout.trim(),
|
||||
};
|
||||
const ok = observed.commit === commit
|
||||
&& observed.serviceId === spec.serviceId
|
||||
&& observed.sourceRepo === sourceRepo
|
||||
&& observed.dockerfile === spec.dockerfile;
|
||||
if (ok) return null;
|
||||
return {
|
||||
ok: false,
|
||||
step: "image-label-verify",
|
||||
expected: { commit, serviceId: spec.serviceId, dockerfile: spec.dockerfile },
|
||||
expected: { commit, serviceId: spec.serviceId, sourceRepo, dockerfile: spec.dockerfile },
|
||||
observed,
|
||||
};
|
||||
}
|
||||
@@ -1937,7 +1941,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
};
|
||||
}
|
||||
const localLoadedImage = sourceImage;
|
||||
const labelFailure = verifyLocalArtifactLabels(localLoadedImage, spec, commit);
|
||||
const labelFailure = verifyLocalArtifactLabels(localLoadedImage, spec, commit, sourceRepoFor(options, spec));
|
||||
if (labelFailure !== null) return { ...labelFailure, registryProbe: commandTail(registryProbe) };
|
||||
const tag = runCommand(["docker", "tag", localLoadedImage, composeImage], repoRoot);
|
||||
if (tag.exitCode !== 0 || tag.timedOut) {
|
||||
@@ -2100,9 +2104,11 @@ function d601ComposeArtifactDeployScript(options: ArtifactRegistryOptions, spec:
|
||||
"docker pull -q \"$registry_image\" >/dev/null",
|
||||
"label_commit=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}')",
|
||||
"label_service=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/service-id\" }}')",
|
||||
"label_repo=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/source-repo\" }}')",
|
||||
"label_dockerfile=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/dockerfile\" }}')",
|
||||
"test \"$label_commit\" = \"$commit\"",
|
||||
"test \"$label_service\" = \"$service_id\"",
|
||||
"test \"$label_repo\" = \"$source_repo\"",
|
||||
"test \"$label_dockerfile\" = \"$dockerfile\"",
|
||||
"test -d \"$work_dir\"",
|
||||
"test -f \"$work_dir/$compose_file\"",
|
||||
@@ -2290,6 +2296,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
},
|
||||
requiredLabels: {
|
||||
"unidesk.ai/service-id": spec.serviceId,
|
||||
"unidesk.ai/source-repo": sourceRepoFor(options, spec),
|
||||
"unidesk.ai/source-commit": commit,
|
||||
"unidesk.ai/dockerfile": spec.dockerfile,
|
||||
},
|
||||
@@ -2338,8 +2345,8 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
validation: [
|
||||
"D601 registry /v2 manifest exists for the commit tag",
|
||||
spec.kind === "d601-compose"
|
||||
? "D601-pulled image labels match service id, source commit, and Dockerfile"
|
||||
: "loaded image labels match service id, source commit, and Dockerfile",
|
||||
? "D601-pulled image labels match service id, source repo, source commit, and Dockerfile"
|
||||
: "loaded image labels match service id, source repo, source commit, and Dockerfile",
|
||||
spec.kind === "d601-compose"
|
||||
? "running D601 Compose container is recreated with a no-build override that points the service image at the artifact"
|
||||
: "running Compose container image label matches the requested commit",
|
||||
@@ -2380,7 +2387,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
},
|
||||
validation: [
|
||||
"D601 registry /v2 manifest exists for the commit tag before mutation",
|
||||
"D601 Docker-pulled image labels match service id, source commit, and Dockerfile",
|
||||
"D601 Docker-pulled image labels match service id, source repo, source commit, and Dockerfile",
|
||||
"native k3s containerd has the commit image and stable runtime image tag",
|
||||
"Deployment annotation and pod image id label match the requested commit",
|
||||
"service health via Kubernetes API service proxy returns the same deploy.commit and deploy.requestedCommit",
|
||||
@@ -2485,9 +2492,11 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
|
||||
"docker pull -q \"$registry_image\" >/dev/null",
|
||||
"label_commit=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}')",
|
||||
"label_service=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/service-id\" }}')",
|
||||
"label_repo=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/source-repo\" }}')",
|
||||
"label_dockerfile=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/dockerfile\" }}')",
|
||||
"test \"$label_commit\" = \"$commit\"",
|
||||
"test \"$label_service\" = \"$service_id\"",
|
||||
"test \"$label_repo\" = \"$source_repo\"",
|
||||
"test \"$label_dockerfile\" = \"$dockerfile\"",
|
||||
"docker tag \"$registry_image\" \"$stable_image\"",
|
||||
"docker tag \"$registry_image\" \"$commit_image\"",
|
||||
|
||||
@@ -567,6 +567,7 @@ function backendCoreDevApplyPath(options: CiPublishBackendCoreOptions, plannedAr
|
||||
environment: "dev",
|
||||
pullOnly: true,
|
||||
apply: `bun scripts/cli.ts deploy apply --env dev --service backend-core --commit ${options.commit}`,
|
||||
dryRun: `bun scripts/cli.ts deploy apply --env dev --service backend-core --commit ${options.commit} --dry-run`,
|
||||
sourceImage: plannedArtifact.imageRef,
|
||||
forbidden: ["--env prod", "cargo build", "docker build", "docker compose build", "server rebuild backend-core"],
|
||||
note: "PROD is intentionally not part of backend-core publish preflight; dev apply still needs explicit authorization after artifact publication.",
|
||||
|
||||
@@ -982,7 +982,7 @@ function artifactConsumerPlanValidation(service: UniDeskMicroserviceConfig, envi
|
||||
const base = [
|
||||
"reads origin/master:deploy.json for commit intent",
|
||||
"requires a commit-pinned artifact in 127.0.0.1:5000",
|
||||
"verifies image labels for service id, source commit, and Dockerfile",
|
||||
"verifies image labels for service id, source repo, source commit, and Dockerfile",
|
||||
"does not build source on the runtime target",
|
||||
];
|
||||
if (kind === "d601-k3s-managed") {
|
||||
@@ -2997,6 +2997,7 @@ function environmentDryRunPlan(
|
||||
tag: service.commitId,
|
||||
imageRef: `127.0.0.1:5000/unidesk/${service.id}:${service.commitId}`,
|
||||
digest: null,
|
||||
digestHeader: "Docker-Content-Digest",
|
||||
digestSource: "planned registry manifest HEAD; live apply records Docker-Content-Digest before mutation",
|
||||
},
|
||||
source: unsupported ? null : {
|
||||
@@ -3011,6 +3012,12 @@ function environmentDryRunPlan(
|
||||
willRunDockerComposeBuild: false,
|
||||
producerBoundary: service.id === "backend-core" ? "ci publish-backend-core" : "ci publish-user-service",
|
||||
},
|
||||
requiredLabels: unsupported ? null : {
|
||||
"unidesk.ai/service-id": service.id,
|
||||
"unidesk.ai/source-repo": service.repo,
|
||||
"unidesk.ai/source-commit": service.commitId,
|
||||
"unidesk.ai/dockerfile": serviceConfig.repository.dockerfile,
|
||||
},
|
||||
noRuntimeSourceBuild: unsupported || planKind !== "d601-dev-target-side-build",
|
||||
dryRunOnly: unsupported || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null,
|
||||
blockedReason: unsupportedReason ?? dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null),
|
||||
|
||||
Reference in New Issue
Block a user