test: cover artifact consumer dry-run matrix
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { rootPath } from "./src/config";
|
||||
import { runArtifactRegistryCommand } from "./src/artifact-registry";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
interface ServiceCase {
|
||||
serviceId: string;
|
||||
sourceRepo: string;
|
||||
dockerfile: string;
|
||||
registryRepository: string;
|
||||
composeService: string;
|
||||
containerName: string;
|
||||
targetImage: string;
|
||||
deployEnvPrefix: string;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown, label: string): JsonRecord {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error(`${label} must be an object: ${JSON.stringify(value)}`);
|
||||
}
|
||||
return value as JsonRecord;
|
||||
}
|
||||
|
||||
function asArray(value: unknown, label: string): unknown[] {
|
||||
if (!Array.isArray(value)) throw new Error(`${label} must be an array: ${JSON.stringify(value)}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
||||
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
||||
}
|
||||
|
||||
const commit = "0123456789abcdef0123456789abcdef01234567";
|
||||
|
||||
const serviceCases: ServiceCase[] = [
|
||||
{
|
||||
serviceId: "project-manager",
|
||||
sourceRepo: "https://github.com/pikasTech/unidesk",
|
||||
dockerfile: "src/components/microservices/project-manager/Dockerfile",
|
||||
registryRepository: "unidesk/project-manager",
|
||||
composeService: "project-manager",
|
||||
containerName: "project-manager-backend",
|
||||
targetImage: "project-manager",
|
||||
deployEnvPrefix: "UNIDESK_PROJECT_MANAGER_DEPLOY",
|
||||
},
|
||||
{
|
||||
serviceId: "oa-event-flow",
|
||||
sourceRepo: "https://github.com/pikasTech/unidesk",
|
||||
dockerfile: "src/components/microservices/oa-event-flow/Dockerfile",
|
||||
registryRepository: "unidesk/oa-event-flow",
|
||||
composeService: "oa-event-flow",
|
||||
containerName: "oa-event-flow-backend",
|
||||
targetImage: "oa-event-flow",
|
||||
deployEnvPrefix: "UNIDESK_OA_EVENT_FLOW_DEPLOY",
|
||||
},
|
||||
{
|
||||
serviceId: "todo-note",
|
||||
sourceRepo: "https://gitee.com/Lyon1998/todo_note",
|
||||
dockerfile: "Dockerfile",
|
||||
registryRepository: "unidesk/todo-note",
|
||||
composeService: "todo-note",
|
||||
containerName: "todo-note-backend",
|
||||
targetImage: "todo-note",
|
||||
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
|
||||
},
|
||||
{
|
||||
serviceId: "baidu-netdisk",
|
||||
sourceRepo: "https://github.com/pikasTech/unidesk",
|
||||
dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile",
|
||||
registryRepository: "unidesk/baidu-netdisk",
|
||||
composeService: "baidu-netdisk",
|
||||
containerName: "baidu-netdisk-backend",
|
||||
targetImage: "baidu-netdisk",
|
||||
deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY",
|
||||
},
|
||||
{
|
||||
serviceId: "frontend",
|
||||
sourceRepo: "https://github.com/pikasTech/unidesk",
|
||||
dockerfile: "src/components/frontend/Dockerfile",
|
||||
registryRepository: "unidesk/frontend",
|
||||
composeService: "frontend",
|
||||
containerName: "unidesk-frontend",
|
||||
targetImage: "unidesk-frontend",
|
||||
deployEnvPrefix: "UNIDESK_FRONTEND_DEPLOY",
|
||||
},
|
||||
];
|
||||
|
||||
const ciCatalog = asRecord(JSON.parse(readFileSync(rootPath("CI.json"), "utf8")) as unknown, "CI.json");
|
||||
const artifacts = asArray(ciCatalog.artifacts, "CI.json.artifacts").map((item, index) => asRecord(item, `CI.json.artifacts[${index}]`));
|
||||
const artifactByService = new Map(artifacts.map((item) => [String(item.serviceId), item]));
|
||||
|
||||
for (const item of serviceCases) {
|
||||
const artifact = asRecord(artifactByService.get(item.serviceId), `CI.json artifact ${item.serviceId}`);
|
||||
const source = asRecord(artifact.source, `CI.json artifact ${item.serviceId}.source`);
|
||||
const image = asRecord(artifact.image, `CI.json artifact ${item.serviceId}.image`);
|
||||
assertCondition(artifact.kind === "source-build", `${item.serviceId} producer must be source-build`, artifact);
|
||||
assertCondition(artifact.status === "supported", `${item.serviceId} producer must be supported`, artifact);
|
||||
assertCondition(artifact.producer === "ci publish-user-service", `${item.serviceId} producer command must be publish-user-service`, artifact);
|
||||
assertCondition(source.repo === item.sourceRepo, `${item.serviceId} producer source repo mismatch`, source);
|
||||
assertCondition(source.dockerfile === item.dockerfile, `${item.serviceId} producer dockerfile mismatch`, source);
|
||||
assertCondition(image.repository === item.registryRepository, `${item.serviceId} registry repository mismatch`, image);
|
||||
}
|
||||
|
||||
const plans: JsonRecord[] = [];
|
||||
|
||||
for (const item of serviceCases) {
|
||||
const plan = asRecord(await runArtifactRegistryCommand([
|
||||
"deploy-service",
|
||||
"--env",
|
||||
"prod",
|
||||
"--service",
|
||||
item.serviceId,
|
||||
"--commit",
|
||||
commit,
|
||||
"--dry-run",
|
||||
]), `${item.serviceId} dry-run plan`);
|
||||
const target = asRecord(plan.target, `${item.serviceId} dry-run target`);
|
||||
const labels = asRecord(plan.requiredLabels, `${item.serviceId} dry-run labels`);
|
||||
const registryProbe = asRecord(plan.registryProbe, `${item.serviceId} dry-run registry probe`);
|
||||
const validation = asArray(plan.validation, `${item.serviceId} dry-run validation`).map(String);
|
||||
|
||||
assertCondition(plan.ok === true, `${item.serviceId} dry-run must be ok`, plan);
|
||||
assertCondition(plan.dryRun === true && plan.mutation === false, `${item.serviceId} dry-run must be non-mutating`, plan);
|
||||
assertCondition(plan.environment === "prod", `${item.serviceId} dry-run must target prod`, plan);
|
||||
assertCondition(plan.serviceId === item.serviceId, `${item.serviceId} dry-run service id mismatch`, plan);
|
||||
assertCondition(plan.sourceImage === `127.0.0.1:5000/${item.registryRepository}:${commit}`, `${item.serviceId} source image mismatch`, plan);
|
||||
assertCondition(labels["unidesk.ai/service-id"] === item.serviceId, `${item.serviceId} required service label missing`, labels);
|
||||
assertCondition(labels["unidesk.ai/source-commit"] === commit, `${item.serviceId} required commit label missing`, labels);
|
||||
assertCondition(labels["unidesk.ai/dockerfile"] === item.dockerfile, `${item.serviceId} required dockerfile label missing`, labels);
|
||||
assertCondition(registryProbe.method === "HEAD", `${item.serviceId} dry-run must only plan registry HEAD probe`, registryProbe);
|
||||
|
||||
assertCondition(target.kind === "compose", `${item.serviceId} target kind must be main-server Compose`, target);
|
||||
assertCondition(target.runtimeHost === "main-server", `${item.serviceId} runtime host must be main-server`, target);
|
||||
assertCondition(target.composeService === item.composeService, `${item.serviceId} compose service mismatch`, target);
|
||||
assertCondition(target.containerName === item.containerName, `${item.serviceId} container mismatch`, target);
|
||||
assertCondition(target.targetImage === item.targetImage, `${item.serviceId} stable target image mismatch`, target);
|
||||
assertCondition(target.runtimeImage === `${item.targetImage}:${commit}`, `${item.serviceId} runtime image mismatch`, target);
|
||||
assertCondition(target.deployEnvPrefix === item.deployEnvPrefix, `${item.serviceId} deploy env prefix mismatch`, target);
|
||||
assertCondition(
|
||||
target.deployCommandShape === `docker compose up -d --no-build --no-deps --force-recreate ${item.composeService}`,
|
||||
`${item.serviceId} dry-run must narrow Compose recreate command`,
|
||||
target,
|
||||
);
|
||||
|
||||
assertCondition(validation.some((line) => line.includes("registry /v2 manifest")), `${item.serviceId} must plan registry manifest validation`, validation);
|
||||
assertCondition(validation.some((line) => line.includes("image labels match service id, source commit, and Dockerfile")), `${item.serviceId} must plan image label validation`, validation);
|
||||
assertCondition(validation.some((line) => line.includes("health probe succeeds") && line.includes("deploy.commit/deploy.requestedCommit")), `${item.serviceId} must plan health commit validation`, validation);
|
||||
assertCondition(!JSON.stringify(plan).includes("docker compose build"), `${item.serviceId} dry-run must not include compose build`, plan);
|
||||
assertCondition(!JSON.stringify(plan).includes("server rebuild"), `${item.serviceId} dry-run must not include server rebuild`, plan);
|
||||
|
||||
plans.push({
|
||||
serviceId: item.serviceId,
|
||||
sourceImage: plan.sourceImage,
|
||||
composeService: target.composeService,
|
||||
containerName: target.containerName,
|
||||
deployCommandShape: target.deployCommandShape,
|
||||
});
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"CI.json producer catalog covers the main-server direct artifact services",
|
||||
"prod dry-run plans are non-mutating and commit-pinned",
|
||||
"dry-run targets only the matching main-server Compose service with --no-build --no-deps --force-recreate",
|
||||
"dry-run plans image-label and health deploy commit validation",
|
||||
],
|
||||
plans,
|
||||
}, null, 2)}\n`);
|
||||
@@ -290,6 +290,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
fileItem("scripts/code-queue-pr-preflight-example.ts"),
|
||||
fileItem("scripts/schedule-cli-contract-test.ts"),
|
||||
fileItem("scripts/src/artifact-registry.ts"),
|
||||
fileItem("scripts/artifact-consumer-dry-run-matrix-test.ts"),
|
||||
fileItem("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"),
|
||||
);
|
||||
} else {
|
||||
@@ -307,6 +308,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(commandItem("code-queue:control-plane-split-brain-diagnostics", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:control-plane-split-brain-diagnostics"], 30_000));
|
||||
items.push(commandItem("code-queue:oa-publisher-degraded-visible", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:oa-publisher-degraded-visible"], 30_000));
|
||||
items.push(commandItem("baidu-netdisk:artifact-guard-contract", ["bun", "scripts/baidu-netdisk-artifact-guard-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("artifact-registry:direct-compose-dry-run-matrix", ["bun", "scripts/artifact-consumer-dry-run-matrix-test.ts"], 30_000));
|
||||
items.push(commandItem("schedule:cli-contract", ["bun", "scripts/schedule-cli-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:issue-guard-contract", ["bun", "scripts/gh-cli-issue-guard-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:pr-contract", ["bun", "scripts/gh-cli-pr-contract-test.ts"], 30_000));
|
||||
@@ -318,6 +320,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(skippedItem("code-queue:pr-preflight-contract", "Code Queue PR preflight contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("code-queue:liveness-diagnostics-fixtures", "Code Queue liveness diagnostics fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("baidu-netdisk:artifact-guard-contract", "Baidu Netdisk artifact guard contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("artifact-registry:direct-compose-dry-run-matrix", "main-server direct artifact consumer dry-run matrix is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("schedule:cli-contract", "Schedule CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("gh:issue-guard-contract", "GitHub issue CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("gh:pr-contract", "GitHub PR CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
|
||||
Reference in New Issue
Block a user