176 lines
9.0 KiB
JavaScript
176 lines
9.0 KiB
JavaScript
#!/usr/bin/env node
|
|
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
const catalogPath = "deploy/artifact-catalog.dev.json";
|
|
const deployPath = "deploy/deploy.json";
|
|
const ciPath = "CI.json";
|
|
const healthContractPath = "deploy/k8s/dev/health-contract.yaml";
|
|
const commitPattern = /^[a-f0-9]{7,40}$/;
|
|
const mutableTags = new Set(["latest", "dev", "main", "master", "prod", "production"]);
|
|
const requiredForbiddenItems = [
|
|
"prod-deploy",
|
|
"prod-profile-enabled",
|
|
"prod-namespace",
|
|
"real-ci-publish-claim",
|
|
"real-dev-deploy",
|
|
"secret-material",
|
|
"unidesk-runtime-substitute",
|
|
"force-push"
|
|
];
|
|
|
|
async function readJSON(relativePath) {
|
|
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
function assertUnique(name, values) {
|
|
assert.equal(new Set(values).size, values.length, `${name} must be unique`);
|
|
}
|
|
|
|
function assertString(value, context) {
|
|
assert.equal(typeof value, "string", `${context} must be a string`);
|
|
assert.ok(value.length > 0, `${context} must not be empty`);
|
|
}
|
|
|
|
function assertCommitId(value, context) {
|
|
assertString(value, context);
|
|
assert.match(value, commitPattern, `${context} must be a short or full lowercase Git SHA`);
|
|
}
|
|
|
|
function imageParts(image, context) {
|
|
assertString(image, context);
|
|
const match = image.match(/^(ghcr\.io\/pikastech\/[^:@]+):([^:@]+)$/);
|
|
assert.ok(match, `${context} must be a tagged ghcr.io/pikastech image`);
|
|
return {
|
|
repository: match[1],
|
|
tag: match[2]
|
|
};
|
|
}
|
|
|
|
function assertNoMutableTag(tag, context) {
|
|
assert.equal(mutableTags.has(tag), false, `${context} must not use mutable tag ${tag}`);
|
|
}
|
|
|
|
function assertDevOnlyCatalog(catalog) {
|
|
assert.equal(catalog.catalogVersion, "v1", "catalogVersion");
|
|
assert.equal(catalog.kind, "hwlab-artifact-catalog", "catalog kind");
|
|
assert.equal(catalog.environment, ENVIRONMENT_DEV, "catalog environment");
|
|
assert.equal(catalog.profile, ENVIRONMENT_DEV, "catalog profile");
|
|
assert.equal(catalog.namespace, "hwlab-dev", "catalog namespace");
|
|
assert.equal(catalog.endpoint, DEV_ENDPOINT, "catalog endpoint");
|
|
assertCommitId(catalog.commitId, "catalog commitId");
|
|
assert.equal(catalog.artifactState, "contract-skeleton", "catalog artifactState");
|
|
|
|
assert.deepEqual(catalog.allowedProfiles, [ENVIRONMENT_DEV], "only dev profile is allowed");
|
|
assert.deepEqual(catalog.forbiddenProfiles, ["prod"], "prod profile must be forbidden");
|
|
assert.equal(catalog.publish.ciPublished, false, "catalog must not claim CI publish");
|
|
assert.equal(catalog.publish.registryVerified, false, "catalog must not claim registry verification");
|
|
assert.equal(catalog.publish.provenance, "not_available_in_mvp_skeleton", "catalog provenance");
|
|
|
|
assert.equal(catalog.healthContract.method, "GET", "health method");
|
|
assert.equal(catalog.healthContract.path, "/health/live", "health path");
|
|
assert.equal(catalog.healthContract.responseFormat, "json", "health response format");
|
|
assert.ok(catalog.healthContract.requiredFields.includes("serviceId"), "health requires serviceId");
|
|
assert.ok(catalog.healthContract.requiredFields.includes("environment"), "health requires environment");
|
|
assert.ok(catalog.healthContract.requiredFields.includes("status"), "health requires status");
|
|
|
|
const forbiddenIds = catalog.forbiddenItems.map((item) => item.id);
|
|
assertUnique("catalog forbidden item ids", forbiddenIds);
|
|
for (const required of requiredForbiddenItems) {
|
|
assert.ok(forbiddenIds.includes(required), `catalog forbiddenItems missing ${required}`);
|
|
}
|
|
}
|
|
|
|
function assertDeployManifest(deployManifest, catalog) {
|
|
assert.equal(deployManifest.manifestVersion, "v1", "deploy manifestVersion");
|
|
assert.equal(deployManifest.environment, ENVIRONMENT_DEV, "deploy environment");
|
|
assert.equal(deployManifest.commitId, catalog.commitId, "deploy commitId must match catalog");
|
|
assert.equal(deployManifest.namespace, catalog.namespace, "deploy namespace must match catalog");
|
|
assert.equal(deployManifest.endpoint, catalog.endpoint, "deploy endpoint must match catalog");
|
|
assert.equal(deployManifest.profiles.dev.enabled, true, "deploy dev profile must be enabled");
|
|
assert.equal(deployManifest.profiles.dev.namespace, catalog.namespace, "deploy dev namespace must match catalog");
|
|
assert.equal(deployManifest.profiles.dev.endpoint, catalog.endpoint, "deploy dev endpoint must match catalog");
|
|
assert.ok(deployManifest.profiles.prod, "deploy may keep disabled prod placeholder");
|
|
assert.equal(deployManifest.profiles.prod.enabled, false, "deploy prod profile must stay disabled");
|
|
}
|
|
|
|
function assertCatalogServices(catalog, deployManifest) {
|
|
assert.ok(Array.isArray(catalog.services), "catalog services must be an array");
|
|
assert.ok(Array.isArray(deployManifest.services), "deploy services must be an array");
|
|
|
|
const catalogServiceIds = catalog.services.map((service) => service.serviceId);
|
|
const deployServiceIds = deployManifest.services.map((service) => service.serviceId);
|
|
assert.deepEqual(catalogServiceIds, SERVICE_IDS, "catalog must cover frozen service ids in order");
|
|
assert.deepEqual(deployServiceIds, SERVICE_IDS, "deploy manifest must cover frozen service ids in order");
|
|
assertUnique("catalog service ids", catalogServiceIds);
|
|
assertUnique("deploy service ids", deployServiceIds);
|
|
|
|
const deployByServiceId = new Map(deployManifest.services.map((service) => [service.serviceId, service]));
|
|
for (const service of catalog.services) {
|
|
const context = `service ${service.serviceId}`;
|
|
const deployService = deployByServiceId.get(service.serviceId);
|
|
assert.ok(deployService, `${context} missing from deploy manifest`);
|
|
|
|
assertCommitId(service.commitId, `${context} commitId`);
|
|
assert.equal(service.commitId, catalog.commitId, `${context} commitId must match catalog`);
|
|
assert.equal(service.imageTag, service.commitId.slice(0, 7), `${context} imageTag must be the short commit`);
|
|
assertNoMutableTag(service.imageTag, `${context} imageTag`);
|
|
|
|
const image = imageParts(service.image, `${context} image`);
|
|
assert.equal(image.repository, `ghcr.io/pikastech/${service.serviceId}`, `${context} image repository`);
|
|
assert.equal(image.tag, service.imageTag, `${context} image tag`);
|
|
assert.equal(service.image, deployService.image, `${context} image must match deploy manifest`);
|
|
|
|
assert.equal(service.profile, ENVIRONMENT_DEV, `${context} profile`);
|
|
assert.equal(service.profile, deployService.profile, `${context} profile must match deploy manifest`);
|
|
assert.equal(service.namespace, catalog.namespace, `${context} namespace`);
|
|
assert.equal(service.namespace, deployService.namespace, `${context} namespace must match deploy manifest`);
|
|
assert.equal(service.healthPath, catalog.healthContract.path, `${context} healthPath`);
|
|
assert.equal(service.healthPath, deployService.healthPath, `${context} healthPath must match deploy manifest`);
|
|
assert.equal(service.publishState, "skeleton-only", `${context} publishState`);
|
|
assert.equal(service.digest, "not_published", `${context} digest`);
|
|
}
|
|
}
|
|
|
|
function assertCIForbidden(ci, catalog) {
|
|
assert.equal(ci.profile, ENVIRONMENT_DEV, "CI skeleton profile must be dev");
|
|
assert.ok(Array.isArray(ci.forbidden), "CI forbidden must be an array");
|
|
for (const required of ["prod-deploy", "secret-material", "unidesk-runtime-substitute", "force-push"]) {
|
|
assert.ok(ci.forbidden.includes(required), `CI forbidden missing ${required}`);
|
|
assert.ok(catalog.forbiddenItems.some((item) => item.id === required), `catalog forbiddenItems missing CI item ${required}`);
|
|
}
|
|
}
|
|
|
|
function assertHealthContract(healthContract, catalog) {
|
|
assert.equal(healthContract.kind, "ConfigMap", "health contract kind");
|
|
assert.equal(healthContract.metadata.namespace, catalog.namespace, "health contract namespace");
|
|
assert.equal(healthContract.metadata.labels["hwlab.pikastech.local/profile"], ENVIRONMENT_DEV, "health contract profile label");
|
|
assert.equal(healthContract.data.endpoint, catalog.endpoint, "health contract endpoint");
|
|
assert.ok(healthContract.data["cloud-api"].includes(catalog.healthContract.path), "cloud-api health contract path");
|
|
assert.ok(healthContract.data["edge-proxy"].includes(catalog.healthContract.path), "edge-proxy health contract path");
|
|
assert.ok(
|
|
healthContract.data["runtime-substitute-policy"].includes("Do not replace HWLAB runtime"),
|
|
"health contract must include runtime substitute policy"
|
|
);
|
|
}
|
|
|
|
const catalog = await readJSON(catalogPath);
|
|
const deployManifest = await readJSON(deployPath);
|
|
const ci = await readJSON(ciPath);
|
|
const healthContract = await readJSON(healthContractPath);
|
|
|
|
assertDevOnlyCatalog(catalog);
|
|
assertDeployManifest(deployManifest, catalog);
|
|
assertCatalogServices(catalog, deployManifest);
|
|
assertCIForbidden(ci, catalog);
|
|
assertHealthContract(healthContract, catalog);
|
|
|
|
console.log(`validated ${catalog.services.length} DEV artifact catalog services at ${catalog.commitId}`);
|