582 lines
27 KiB
JavaScript
582 lines
27 KiB
JavaScript
#!/usr/bin/env node
|
|
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
|
|
import {
|
|
resolveDevArtifactServices,
|
|
serviceInventoryFromServices
|
|
} from "./src/dev-artifact-services.mjs";
|
|
import { tempReportPath } from "./src/report-paths.mjs";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const defaultLane = process.env.HWLAB_ARTIFACT_LANE || process.env.HWLAB_GITOPS_LANE || "g14";
|
|
const defaultCatalogPath = process.env.HWLAB_ARTIFACT_CATALOG_PATH || "deploy/artifact-catalog.dev.json";
|
|
const defaultDeployPath = process.env.HWLAB_DEPLOY_MANIFEST_PATH || "deploy/deploy.json";
|
|
const defaultPublishReportPath = tempReportPath("dev-artifacts.json");
|
|
const defaultRegistryPrefix = process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
|
|
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
|
const commitPattern = /^[a-f0-9]{7,40}$/;
|
|
|
|
const publishReadyStatuses = new Set(["published", "reused"]);
|
|
const v02EnvReuseRuntimeMode = "env-reuse-git-mirror-checkout";
|
|
|
|
const v02RuntimeServiceIds = Object.freeze([
|
|
"hwlab-cloud-api",
|
|
"hwlab-cloud-web",
|
|
"hwlab-agent-mgr",
|
|
"hwlab-agent-worker",
|
|
"hwlab-device-pod",
|
|
"hwlab-gateway",
|
|
"hwlab-edge-proxy",
|
|
"hwlab-cli",
|
|
"hwlab-agent-skills"
|
|
]);
|
|
|
|
function serviceIdsForLane(lane) {
|
|
return lane === "v02" ? v02RuntimeServiceIds : SERVICE_IDS;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {
|
|
lane: defaultLane,
|
|
catalogPath: defaultCatalogPath,
|
|
deployPath: defaultDeployPath,
|
|
imageTagMode: process.env.HWLAB_ARTIFACT_IMAGE_TAG_MODE || "short",
|
|
targetRef: "HEAD",
|
|
publishReportPath: null,
|
|
blocked: false,
|
|
write: false
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === "--target-ref") {
|
|
args.targetRef = readOption(argv, ++index, arg);
|
|
} else if (arg === "--lane") {
|
|
args.lane = readOption(argv, ++index, arg);
|
|
} else if (arg === "--catalog-path") {
|
|
args.catalogPath = readOption(argv, ++index, arg);
|
|
} else if (arg === "--deploy-json") {
|
|
args.deployPath = readOption(argv, ++index, arg);
|
|
} else if (arg === "--image-tag-mode") {
|
|
args.imageTagMode = readOption(argv, ++index, arg);
|
|
} else if (arg === "--publish-report") {
|
|
args.publishReportPath = readOption(argv, ++index, arg);
|
|
} else if (arg === "--blocked") {
|
|
args.blocked = true;
|
|
} else if (arg === "--write") {
|
|
args.write = true;
|
|
} else if (arg === "--no-write") {
|
|
args.write = false;
|
|
} else if (arg === "--help" || arg === "-h") {
|
|
args.help = true;
|
|
} else {
|
|
throw new Error(`unknown argument ${arg}`);
|
|
}
|
|
}
|
|
|
|
if (!args.help) {
|
|
assert.ok(["g14", "v02"].includes(args.lane), `unknown artifact lane ${args.lane}`);
|
|
assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`);
|
|
assert.notEqual(args.blocked && Boolean(args.publishReportPath), true, "--blocked and --publish-report are mutually exclusive");
|
|
assert.ok(args.blocked || args.publishReportPath, `choose --blocked or --publish-report ${defaultPublishReportPath}`);
|
|
assert.ok(!args.write || args.publishReportPath, "--write requires --publish-report; blocked skeleton refresh must stay preview-only");
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
function readOption(argv, index, name) {
|
|
const value = argv[index];
|
|
if (!value || value.startsWith("--")) {
|
|
throw new Error(`${name} requires a value`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function usage() {
|
|
return [
|
|
"usage: node scripts/refresh-artifact-catalog.mjs --target-ref REF (--blocked|--publish-report PATH) [--write|--no-write]",
|
|
"",
|
|
"Preview or explicitly write DEV artifact catalog identity without faking digest evidence.",
|
|
"Default mode is read-only. --write is reserved for G14 Tekton GitOps promotion and never writes deploy/deploy.json.",
|
|
"",
|
|
"examples:",
|
|
" node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --blocked --no-write",
|
|
` node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --publish-report ${defaultPublishReportPath} --no-write`,
|
|
` node scripts/refresh-artifact-catalog.mjs --lane v02 --catalog-path deploy/artifact-catalog.v02.json --image-tag-mode full --target-ref HEAD --publish-report ${defaultPublishReportPath} --write`
|
|
].join("\n");
|
|
}
|
|
|
|
async function readJson(relativePath) {
|
|
const filePath = path.isAbsolute(relativePath) ? relativePath : path.join(repoRoot, relativePath);
|
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
}
|
|
|
|
async function readJsonIfPresent(relativePath, fallback = null) {
|
|
try {
|
|
return await readJson(relativePath);
|
|
} catch (error) {
|
|
if (error?.code === "ENOENT") return fallback;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function writeJson(relativePath, value) {
|
|
await writeFile(path.join(repoRoot, relativePath), `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
async function gitValue(args) {
|
|
const result = await execFileAsync("git", args, {
|
|
cwd: repoRoot,
|
|
timeout: 5000,
|
|
maxBuffer: 1024 * 1024
|
|
});
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
async function resolveTarget(ref, imageTagMode = "short") {
|
|
const commitId = await gitValue(["rev-parse", ref]);
|
|
const shortCommitId = await gitValue(["rev-parse", "--short=7", ref]);
|
|
assert.match(commitId, /^[a-f0-9]{40}$/, `target ref ${ref} must resolve to a full SHA`);
|
|
return { ref, commitId, shortCommitId, imageTag: imageTagMode === "full" ? commitId : shortCommitId };
|
|
}
|
|
|
|
function parseTaggedImage(image, context) {
|
|
assert.equal(typeof image, "string", `${context} image must be a string`);
|
|
assert.equal(image.includes("@"), false, `${context} image must not include a digest suffix`);
|
|
assert.ok(!/prod|production/iu.test(image), `${context} image must not target prod`);
|
|
const slashIndex = image.lastIndexOf("/");
|
|
const colonIndex = image.lastIndexOf(":");
|
|
assert.ok(colonIndex > slashIndex, `${context} image must be a tagged image reference`);
|
|
return {
|
|
repository: image.slice(0, colonIndex),
|
|
tag: image.slice(colonIndex + 1)
|
|
};
|
|
}
|
|
|
|
function targetImage(serviceId, shortCommitId, registryPrefix = defaultRegistryPrefix) {
|
|
return `${registryPrefix.replace(/\/+$/u, "")}/${serviceId}:${shortCommitId}`;
|
|
}
|
|
|
|
function artifactEnvironment(args) {
|
|
return args.lane === "v02" ? "v02" : ENVIRONMENT_DEV;
|
|
}
|
|
|
|
function artifactNamespace(args) {
|
|
return args.lane === "v02" ? "hwlab-v02" : "hwlab-dev";
|
|
}
|
|
|
|
function artifactEndpoint(args) {
|
|
return args.lane === "v02" ? "http://74.48.78.17:19667" : DEV_ENDPOINT;
|
|
}
|
|
|
|
function artifactCatalogSkeleton({ args, target, registryPrefix = defaultRegistryPrefix }) {
|
|
const environment = artifactEnvironment(args);
|
|
const namespace = artifactNamespace(args);
|
|
return {
|
|
catalogVersion: "v1",
|
|
kind: "hwlab-artifact-catalog",
|
|
environment,
|
|
profile: environment,
|
|
namespace,
|
|
endpoint: artifactEndpoint(args),
|
|
commitId: target.imageTag,
|
|
artifactState: "contract-skeleton",
|
|
publish: {
|
|
ciPublished: false,
|
|
registryVerified: false,
|
|
provenance: "not_available_until_publish",
|
|
note: `${environment} artifact catalog skeleton initialized without DEV/G14 fallback.`
|
|
},
|
|
healthContract: {
|
|
method: "GET",
|
|
path: "/health/live",
|
|
responseFormat: "json",
|
|
requiredFields: ["serviceId", "environment", "status"]
|
|
},
|
|
allowedProfiles: [environment],
|
|
forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"],
|
|
services: serviceIdsForLane(args.lane).map((serviceId) => catalogSkeletonService({ args, serviceId, target, registryPrefix, environment, namespace }))
|
|
};
|
|
}
|
|
|
|
function catalogSkeletonService({ args, serviceId, target, registryPrefix, environment, namespace }) {
|
|
const base = {
|
|
serviceId,
|
|
commitId: target.imageTag,
|
|
sourceCommitId: target.commitId,
|
|
image: targetImage(serviceId, target.imageTag, registryPrefix),
|
|
imageTag: target.imageTag,
|
|
digest: "not_published",
|
|
publishState: "skeleton-only",
|
|
profile: environment,
|
|
namespace,
|
|
healthPath: "/health/live",
|
|
sourceState: "source-present",
|
|
publishEnabled: true,
|
|
artifactRequired: true,
|
|
artifactScope: "required",
|
|
notPublishedReason: "publish_not_run"
|
|
};
|
|
if (args.lane !== "v02" || serviceId !== "hwlab-device-pod") return base;
|
|
const environmentImage = targetImage(`${serviceId}-env`, `env-${String(target.commitId).slice(0, 12)}`, registryPrefix);
|
|
return {
|
|
...base,
|
|
image: environmentImage,
|
|
imageTag: parseTaggedImage(environmentImage, `${serviceId} environment image`).tag,
|
|
runtimeMode: v02EnvReuseRuntimeMode,
|
|
envReuse: true,
|
|
environmentImage,
|
|
environmentDigest: "not_published",
|
|
environmentInputHash: null,
|
|
bootRepo: "git@github.com:pikasTech/HWLAB.git",
|
|
bootCommit: target.commitId,
|
|
bootSh: `deploy/runtime/boot/${serviceId}.sh`,
|
|
codeInputHash: null,
|
|
bootEnvEvidence: {
|
|
env: {
|
|
HWLAB_BOOT_REPO: "git@github.com:pikasTech/HWLAB.git",
|
|
HWLAB_BOOT_COMMIT: target.commitId,
|
|
HWLAB_BOOT_SH: `deploy/runtime/boot/${serviceId}.sh`
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function commitMatchesTarget(value, target) {
|
|
return value === target.commitId || value === target.shortCommitId;
|
|
}
|
|
|
|
function shortCommitForRecord(record, target) {
|
|
const value = record?.commitId ?? record?.imageTag ?? null;
|
|
if (typeof value === "string" && commitPattern.test(value)) return target.imageTag.length === 40 ? value : value.slice(0, 7);
|
|
if (typeof record?.sourceCommitId === "string" && commitPattern.test(record.sourceCommitId)) return target.imageTag.length === 40 ? record.sourceCommitId : record.sourceCommitId.slice(0, 7);
|
|
return target.imageTag;
|
|
}
|
|
|
|
function revisionForRecord(record, target) {
|
|
if (typeof record?.sourceCommitId === "string" && commitPattern.test(record.sourceCommitId)) return record.sourceCommitId;
|
|
if (typeof record?.commitId === "string" && commitPattern.test(record.commitId)) return record.commitId;
|
|
return target.commitId;
|
|
}
|
|
|
|
function assertDevOnlyDeployAndCatalog(deploy, catalog) {
|
|
assert.equal(deploy.environment, ENVIRONMENT_DEV, "deploy environment must be dev");
|
|
assert.equal(deploy.namespace, "hwlab-dev", "deploy namespace must be hwlab-dev");
|
|
assert.equal(deploy.endpoint, DEV_ENDPOINT, "deploy endpoint must stay frozen");
|
|
assert.equal(deploy.profiles?.dev?.enabled, true, "deploy dev profile must be enabled");
|
|
assert.equal(deploy.profiles?.prod?.enabled, false, "deploy prod profile must stay disabled");
|
|
assert.deepEqual((deploy.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "deploy services must cover frozen service IDs in order");
|
|
|
|
assert.equal(catalog.environment, ENVIRONMENT_DEV, "catalog environment must be dev");
|
|
assert.equal(catalog.profile, ENVIRONMENT_DEV, "catalog profile must be dev");
|
|
assert.equal(catalog.namespace, "hwlab-dev", "catalog namespace must be hwlab-dev");
|
|
assert.equal(catalog.endpoint, DEV_ENDPOINT, "catalog endpoint must stay frozen");
|
|
assert.deepEqual(catalog.allowedProfiles, [ENVIRONMENT_DEV], "catalog allowedProfiles must only allow dev");
|
|
assert.deepEqual(catalog.forbiddenProfiles, ["prod"], "catalog forbiddenProfiles must forbid prod");
|
|
assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "catalog services must cover frozen service IDs in order");
|
|
}
|
|
|
|
function assertV02DeployAndCatalog(deploy, catalog) {
|
|
const lane = deploy?.lanes?.v02;
|
|
assert.ok(lane && typeof lane === "object", "deploy.lanes.v02 must exist for v02 catalog refresh");
|
|
assert.equal(lane.namespace, "hwlab-v02", "deploy.lanes.v02.namespace must be hwlab-v02");
|
|
assert.equal(lane.endpoint, "http://74.48.78.17:19667", "deploy.lanes.v02.endpoint must be the v02 API endpoint");
|
|
assert.deepEqual((deploy.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "deploy services must cover frozen service IDs in order");
|
|
|
|
assert.equal(catalog.environment, "v02", "catalog environment must be v02");
|
|
assert.equal(catalog.profile, "v02", "catalog profile must be v02");
|
|
assert.equal(catalog.namespace, "hwlab-v02", "catalog namespace must be hwlab-v02");
|
|
assert.equal(catalog.endpoint, "http://74.48.78.17:19667", "catalog endpoint must be the v02 API endpoint");
|
|
assert.deepEqual(catalog.allowedProfiles, ["v02"], "catalog allowedProfiles must only allow v02");
|
|
assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog forbiddenProfiles must forbid dev/prod");
|
|
assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), serviceIdsForLane("v02"), "catalog services must cover retained v0.2 service IDs in order");
|
|
}
|
|
|
|
function normalizeCatalogForLane(catalog, lane) {
|
|
if (!catalog || lane !== "v02") return catalog;
|
|
const allowed = new Set(serviceIdsForLane(lane));
|
|
return {
|
|
...catalog,
|
|
services: (catalog.services ?? []).filter((service) => allowed.has(service.serviceId))
|
|
};
|
|
}
|
|
|
|
function assertLaneDeployAndCatalog(args, deploy, catalog) {
|
|
if (args.lane === "v02") return assertV02DeployAndCatalog(deploy, catalog);
|
|
return assertDevOnlyDeployAndCatalog(deploy, catalog);
|
|
}
|
|
|
|
function publishRecordsFromReport(report, target, serviceIds = SERVICE_IDS) {
|
|
const artifactPublish = report.artifactPublish;
|
|
assert.ok(artifactPublish && typeof artifactPublish === "object", "publish report missing artifactPublish");
|
|
assert.equal(artifactPublish.status, "published", "publish report artifactPublish.status must be published");
|
|
assert.ok(commitMatchesTarget(artifactPublish.sourceCommitId, target), "publish report sourceCommitId must match target ref");
|
|
assert.equal(artifactPublish.serviceCount, serviceIds.length, "publish report must cover every service for the selected lane");
|
|
assert.ok(artifactPublish.publishPlan && typeof artifactPublish.publishPlan === "object", "publish report missing artifactPublish.publishPlan");
|
|
assert.equal(artifactPublish.publishPlan.version, "v2", "publish report plan version must be v2");
|
|
|
|
const requiredServiceIds = artifactPublish.publishPlan.services
|
|
.filter((service) => service.required)
|
|
.map((service) => service.serviceId);
|
|
const disabledServiceIds = artifactPublish.publishPlan.services
|
|
.filter((service) => !service.required)
|
|
.map((service) => service.serviceId);
|
|
assert.deepEqual(
|
|
[...requiredServiceIds, ...disabledServiceIds].sort(),
|
|
[...serviceIds].sort(),
|
|
"publish plan must cover every service for the selected lane"
|
|
);
|
|
assert.equal(
|
|
(artifactPublish.publishedCount ?? 0) + (artifactPublish.reusedCount ?? 0),
|
|
requiredServiceIds.length,
|
|
"publish report must publish or reuse every required enabled service"
|
|
);
|
|
|
|
const records = new Map();
|
|
const allowed = new Set(serviceIds);
|
|
for (const service of artifactPublish.services ?? []) {
|
|
assert.ok(allowed.has(service.serviceId), `unknown service ${service.serviceId} in publish report`);
|
|
const image = parseTaggedImage(service.image, service.serviceId);
|
|
if (!requiredServiceIds.includes(service.serviceId)) {
|
|
assert.equal(service.artifactRequired, false, `${service.serviceId} disabled service must not be required`);
|
|
assert.equal(service.digest, "not_published", `${service.serviceId} disabled digest must stay not_published`);
|
|
assert.equal(service.notPublishedReason?.startsWith("disabled_"), true, `${service.serviceId} disabled service must state a disabled reason`);
|
|
continue;
|
|
}
|
|
|
|
assert.equal(publishReadyStatuses.has(service.status), true, `${service.serviceId} status must be published or reused`);
|
|
assert.equal(service.artifactRequired, true, `${service.serviceId} required service must state artifactRequired=true`);
|
|
if (service.status === "published") {
|
|
assert.equal(image.tag, target.imageTag, `${service.serviceId} newly published image tag must match target image tag`);
|
|
}
|
|
assert.match(service.digest, digestPattern, `${service.serviceId} digest must be a sha256 registry digest`);
|
|
records.set(service.serviceId, {
|
|
serviceId: service.serviceId,
|
|
status: service.status,
|
|
commitId: service.commitId ?? image.tag,
|
|
sourceCommitId: service.sourceCommitId ?? null,
|
|
image: service.image,
|
|
imageTag: image.tag,
|
|
buildCreatedAt: service.buildCreatedAt ?? null,
|
|
buildSource: service.buildSource ?? null,
|
|
digest: service.digest,
|
|
repositoryDigest: service.repositoryDigest ?? `${image.repository}@${service.digest}`,
|
|
componentCommitId: service.componentCommitId ?? null,
|
|
componentInputHash: service.componentInputHash ?? null,
|
|
runtimeMode: service.runtimeMode ?? "service-image",
|
|
envReuse: service.envReuse === true,
|
|
envChanged: service.envChanged ?? null,
|
|
codeChanged: service.codeChanged ?? null,
|
|
environmentImage: service.environmentImage ?? null,
|
|
environmentDigest: service.environmentDigest ?? null,
|
|
environmentInputHash: service.environmentInputHash ?? null,
|
|
codeInputHash: service.codeInputHash ?? null,
|
|
bootRepo: service.bootRepo ?? null,
|
|
bootCommit: service.bootCommit ?? null,
|
|
bootSh: service.bootSh ?? null,
|
|
bootEnvEvidence: service.bootEnvEvidence ?? null,
|
|
dockerfileHash: service.dockerfileHash ?? null,
|
|
baseImageReference: service.baseImageReference ?? null,
|
|
baseImageDigest: service.baseImageDigest ?? null,
|
|
buildArgsHash: service.buildArgsHash ?? null,
|
|
ciAffected: service.ciAffected ?? null,
|
|
ciReason: service.ciReason ?? [],
|
|
reuse: service.reuse ?? null,
|
|
reusedFrom: service.reusedFrom ?? null
|
|
});
|
|
}
|
|
|
|
assert.deepEqual([...records.keys()], requiredServiceIds, "publish report services must publish required service IDs in order");
|
|
return { records, requiredServiceIds, disabledServiceIds };
|
|
}
|
|
|
|
function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath, registryPrefix, serviceIds = SERVICE_IDS }) {
|
|
const published = Boolean(publishRecords);
|
|
const records = publishRecords?.records ?? null;
|
|
const requiredIds = new Set(publishRecords?.requiredServiceIds ?? serviceInventory.requiredServiceIds);
|
|
const catalogByService = new Map(catalog.services.map((service) => [service.serviceId, service]));
|
|
const deployByService = new Map(deploy.services.map((service) => [service.serviceId, service]));
|
|
const inventoryByService = new Map(serviceInventory.services.map((service) => [service.serviceId, service]));
|
|
const refreshedServices = [];
|
|
const nextCatalogServices = [];
|
|
|
|
catalog.commitId = target.imageTag;
|
|
catalog.artifactState = published ? "published" : "contract-skeleton";
|
|
catalog.serviceInventory = serviceInventory;
|
|
catalog.publish = {
|
|
...catalog.publish,
|
|
ciPublished: published,
|
|
registryVerified: published,
|
|
provenance: published ? provenancePath : "not_available_until_publish",
|
|
note: published
|
|
? "Digest fields were copied from a successful DEV artifact publish report for this source commit."
|
|
: "Artifact identity was refreshed to this source commit, but no publish report proved registry digests."
|
|
};
|
|
|
|
for (const serviceId of serviceIds) {
|
|
const deployService = deployByService.get(serviceId);
|
|
const catalogService = catalogByService.get(serviceId);
|
|
assert.ok(deployService, `${serviceId} missing from deploy manifest`);
|
|
assert.ok(catalogService, `${serviceId} missing from catalog`);
|
|
|
|
const inventory = inventoryByService.get(serviceId);
|
|
const required = requiredIds.has(serviceId);
|
|
const publishRecord = records?.get(serviceId) ?? null;
|
|
const envReuse = publishRecord?.runtimeMode === v02EnvReuseRuntimeMode
|
|
|| publishRecord?.envReuse === true
|
|
|| catalogService.runtimeMode === v02EnvReuseRuntimeMode
|
|
|| catalogService.envReuse === true;
|
|
const fallbackEnvTag = `env-${String(publishRecord?.environmentInputHash ?? catalogService.environmentInputHash ?? target.commitId).slice(0, 12)}`;
|
|
const fallbackImage = envReuse ? targetImage(`${serviceId}-env`, fallbackEnvTag, registryPrefix) : targetImage(serviceId, target.imageTag, registryPrefix);
|
|
const image = publishRecord?.image ?? publishRecord?.environmentImage ?? fallbackImage;
|
|
const imageTag = publishRecord?.imageTag ?? parseTaggedImage(image, `${serviceId} catalog image`).tag;
|
|
const serviceCommitId = shortCommitForRecord(publishRecord, target);
|
|
|
|
catalogService.commitId = serviceCommitId;
|
|
catalogService.image = image;
|
|
catalogService.imageTag = imageTag;
|
|
catalogService.buildCreatedAt = publishRecord?.buildCreatedAt ?? null;
|
|
catalogService.buildSource = publishRecord?.buildSource ?? null;
|
|
catalogService.digest = publishRecord?.digest ?? "not_published";
|
|
catalogService.componentCommitId = publishRecord?.componentCommitId ?? catalogService.componentCommitId ?? null;
|
|
catalogService.componentInputHash = publishRecord?.componentInputHash ?? catalogService.componentInputHash ?? null;
|
|
catalogService.runtimeMode = publishRecord?.runtimeMode ?? catalogService.runtimeMode ?? "service-image";
|
|
catalogService.envReuse = envReuse;
|
|
catalogService.envChanged = publishRecord?.envChanged ?? catalogService.envChanged ?? null;
|
|
catalogService.codeChanged = publishRecord?.codeChanged ?? catalogService.codeChanged ?? null;
|
|
catalogService.environmentImage = publishRecord?.environmentImage ?? (envReuse ? image : catalogService.environmentImage ?? null);
|
|
catalogService.environmentDigest = publishRecord?.environmentDigest ?? catalogService.environmentDigest ?? null;
|
|
catalogService.environmentInputHash = publishRecord?.environmentInputHash ?? catalogService.environmentInputHash ?? null;
|
|
catalogService.codeInputHash = publishRecord?.codeInputHash ?? catalogService.codeInputHash ?? null;
|
|
catalogService.bootRepo = publishRecord?.bootRepo ?? catalogService.bootRepo ?? null;
|
|
catalogService.bootCommit = publishRecord?.bootCommit ?? target.commitId;
|
|
catalogService.bootSh = publishRecord?.bootSh ?? catalogService.bootSh ?? null;
|
|
catalogService.bootEnvEvidence = publishRecord?.bootEnvEvidence ?? catalogService.bootEnvEvidence ?? null;
|
|
catalogService.dockerfileHash = publishRecord?.dockerfileHash ?? catalogService.dockerfileHash ?? null;
|
|
catalogService.baseImageReference = publishRecord?.baseImageReference ?? catalogService.baseImageReference ?? null;
|
|
catalogService.baseImageDigest = publishRecord?.baseImageDigest ?? catalogService.baseImageDigest ?? null;
|
|
catalogService.buildArgsHash = publishRecord?.buildArgsHash ?? catalogService.buildArgsHash ?? null;
|
|
catalogService.ciAffected = publishRecord?.ciAffected ?? catalogService.ciAffected ?? null;
|
|
catalogService.ciReason = publishRecord?.ciReason ?? catalogService.ciReason ?? [];
|
|
catalogService.reuse = publishRecord?.reuse ?? catalogService.reuse ?? null;
|
|
catalogService.reusedFrom = publishRecord?.reusedFrom ?? catalogService.reusedFrom ?? null;
|
|
catalogService.publishState = publishRecord?.status === "reused" ? "reused" : publishRecord ? "published" : "skeleton-only";
|
|
catalogService.publishEnabled = inventory?.publishEnabled ?? required;
|
|
catalogService.artifactRequired = required;
|
|
catalogService.artifactScope = required ? "required" : "disabled";
|
|
catalogService.notPublishedReason = publishRecord
|
|
? null
|
|
: inventory?.disabledReason ?? (published ? "not_required" : "publish_not_run");
|
|
refreshedServices.push({
|
|
serviceId,
|
|
image,
|
|
imageTag,
|
|
buildCreatedAt: catalogService.buildCreatedAt,
|
|
digest: catalogService.digest,
|
|
componentCommitId: catalogService.componentCommitId,
|
|
componentInputHash: catalogService.componentInputHash,
|
|
runtimeMode: catalogService.runtimeMode,
|
|
environmentDigest: catalogService.environmentDigest ?? null,
|
|
environmentInputHash: catalogService.environmentInputHash ?? null,
|
|
codeInputHash: catalogService.codeInputHash ?? null,
|
|
bootCommit: catalogService.bootCommit ?? null,
|
|
bootSh: catalogService.bootSh ?? null,
|
|
dockerfileHash: catalogService.dockerfileHash,
|
|
ciAffected: catalogService.ciAffected,
|
|
ciReason: catalogService.ciReason,
|
|
reusedFrom: catalogService.reusedFrom ?? null,
|
|
publishState: catalogService.publishState,
|
|
artifactRequired: catalogService.artifactRequired,
|
|
notPublishedReason: catalogService.notPublishedReason
|
|
});
|
|
nextCatalogServices.push(catalogService);
|
|
}
|
|
|
|
catalog.services = nextCatalogServices;
|
|
|
|
return refreshedServices;
|
|
}
|
|
|
|
function publishReportProvenance(publishReport, fallbackPath) {
|
|
const producer = publishReport?.artifactPublish?.producer;
|
|
if (producer?.kind && producer?.runId) return `${producer.kind}:${producer.runId}`;
|
|
return fallbackPath;
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
console.log(usage());
|
|
return;
|
|
}
|
|
|
|
const target = await resolveTarget(args.targetRef, args.imageTagMode);
|
|
const [deploy, publishReport] = await Promise.all([
|
|
readJson(args.deployPath),
|
|
args.publishReportPath ? readJson(args.publishReportPath) : Promise.resolve(null)
|
|
]);
|
|
let catalog = await readJsonIfPresent(args.catalogPath, null);
|
|
if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, target, registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix });
|
|
catalog = normalizeCatalogForLane(catalog, args.lane);
|
|
assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} catalog refresh`);
|
|
assertLaneDeployAndCatalog(args, deploy, catalog);
|
|
|
|
const serviceIds = serviceIdsForLane(args.lane);
|
|
const publishRecords = publishReport ? publishRecordsFromReport(publishReport, target, serviceIds) : null;
|
|
const serviceInventory = publishReport?.artifactPublish?.serviceInventory ?? serviceInventoryFromServices(
|
|
await resolveDevArtifactServices(repoRoot, serviceIds, SERVICE_IDS)
|
|
);
|
|
const services = refreshDocuments({
|
|
deploy,
|
|
catalog,
|
|
target,
|
|
publishRecords,
|
|
serviceInventory,
|
|
provenancePath: publishReportProvenance(publishReport, args.publishReportPath ?? defaultPublishReportPath),
|
|
registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix,
|
|
serviceIds
|
|
});
|
|
|
|
if (args.write) {
|
|
await writeJson(args.catalogPath, catalog);
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
status: publishRecords ? "published" : "blocked",
|
|
targetRef: target.ref,
|
|
sourceCommitId: target.commitId,
|
|
artifactCommitId: target.imageTag,
|
|
lane: args.lane,
|
|
catalogPath: args.catalogPath,
|
|
imageTagMode: args.imageTagMode,
|
|
wrote: args.write ? [args.catalogPath] : [],
|
|
deployTruthPolicy: "deploy.json is human-authored runtime config; artifact promotion must not rewrite it",
|
|
ciPublished: Boolean(publishRecords),
|
|
registryVerified: Boolean(publishRecords),
|
|
publishedCount: publishRecords?.records.size ?? 0,
|
|
requiredServiceCount: serviceInventory.requiredServiceCount,
|
|
disabledServiceCount: serviceInventory.disabledServiceCount,
|
|
notPublishedCount: services.filter((service) => service.digest === "not_published").length,
|
|
digestPolicy: publishRecords
|
|
? "catalog digests copied only for required services from a published DEV artifact report; disabled services stay not_published"
|
|
: "all catalog digests remain not_published",
|
|
services
|
|
}, null, 2));
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(JSON.stringify({
|
|
status: "failed",
|
|
error: error instanceof Error ? error.message : String(error)
|
|
}, null, 2));
|
|
process.exitCode = 1;
|
|
});
|