#!/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"; import { readStructuredFile } from "./src/structured-config.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 || "node"; const defaultCatalogPath = process.env.HWLAB_ARTIFACT_CATALOG_PATH || "deploy/artifact-catalog.dev.json"; const defaultDeployPath = process.env.HWLAB_DEPLOY_MANIFEST_PATH || "deploy/deploy.yaml"; 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 runtimeArtifactLanePattern = /^[a-z][a-z0-9-]*$/u; const publishReadyStatuses = new Set(["published", "reused"]); const v02EnvReuseRuntimeMode = "env-reuse-gitea-checkout"; function isRuntimeArtifactLane(lane) { return runtimeArtifactLanePattern.test(String(lane ?? "")); } function laneDeployConfig(deploy, lane) { return deploy?.lanes?.[lane] ?? null; } function serviceIdsForLane(lane, deploy = null) { return isRuntimeArtifactLane(lane) ? runtimeServiceIdsFromDeploy(deploy, lane) : SERVICE_IDS; } function runtimeServiceIdsFromDeploy(deploy, lane) { const serviceIds = uniquePreserveOrder((Array.isArray(deploy?.lanes?.[lane]?.envReuseServices) ? deploy.lanes[lane].envReuseServices : []) .map((item) => String(item ?? "").trim()) .filter(Boolean)); assert.ok(serviceIds.length > 0, `deploy.lanes.${lane}.envReuseServices must not be empty`); return serviceIds; } function uniquePreserveOrder(values) { const seen = new Set(); const result = []; for (const value of values) { if (seen.has(value)) continue; seen.add(value); result.push(value); } return result; } 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-config") { 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(args.lane === "node" || isRuntimeArtifactLane(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 node Tekton GitOps promotion and never writes deploy/deploy.yaml.", "", "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 v03 --catalog-path deploy/artifact-catalog.v03.json --image-tag-mode full --target-ref HEAD --publish-report ${defaultPublishReportPath} --write` ].join("\n"); } async function readConfig(relativePath) { return readStructuredFile(repoRoot, relativePath); } async function readConfigIfPresent(relativePath, fallback = null) { try { return await readConfig(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 isRuntimeArtifactLane(args.lane) ? args.lane : ENVIRONMENT_DEV; } function artifactNamespace(args, deploy) { if (!isRuntimeArtifactLane(args.lane)) return "hwlab-dev"; const namespace = laneDeployConfig(deploy, args.lane)?.namespace; assert.ok(String(namespace ?? "").trim(), `deploy.lanes.${args.lane}.namespace must not be empty`); return namespace; } function isHttpEndpoint(value) { if (typeof value !== "string" || value.trim().length === 0) return false; try { const url = new URL(value); return url.protocol === "http:" || url.protocol === "https:"; } catch { return false; } } function artifactEndpoint(args, deploy) { if (!isRuntimeArtifactLane(args.lane)) return DEV_ENDPOINT; const endpoint = laneDeployConfig(deploy, args.lane)?.endpoint; assert.ok(isHttpEndpoint(endpoint), `deploy.lanes.${args.lane}.endpoint must be a non-empty http(s) URL`); return endpoint; } function artifactCatalogSkeleton({ args, deploy, target, registryPrefix = defaultRegistryPrefix }) { const environment = artifactEnvironment(args); const namespace = artifactNamespace(args, deploy); return { catalogVersion: "v1", kind: "hwlab-artifact-catalog", environment, profile: environment, namespace, endpoint: artifactEndpoint(args, deploy), commitId: target.imageTag, artifactState: "contract-skeleton", publish: { ciPublished: false, registryVerified: false, provenance: "not_available_until_publish", note: `${environment} artifact catalog skeleton initialized without DEV/node fallback.` }, healthContract: { method: "GET", path: "/health/live", responseFormat: "json", requiredFields: ["serviceId", "environment", "status"] }, allowedProfiles: [environment], forbiddenProfiles: isRuntimeArtifactLane(args.lane) ? ["dev", "prod"] : ["prod"], services: serviceIdsForLane(args.lane, deploy).map((serviceId) => catalogSkeletonService({ args, serviceId, target, registryPrefix, environment, namespace })) }; } function catalogSkeletonService({ args, serviceId, target, registryPrefix, environment, namespace }) { return { 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" }; } 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 isEnvReusePublishRecord(service) { return service?.runtimeMode === v02EnvReuseRuntimeMode || service?.envReuse === true; } function assertPublishedImageTag(service, image, target) { if (!isEnvReusePublishRecord(service)) { assert.equal(image.tag, target.imageTag, `${service.serviceId} newly published image tag must match target image tag`); return; } assert.equal(service.sourceCommitId, target.commitId, `${service.serviceId} env-reuse sourceCommitId must match target commit`); assert.equal(service.bootCommit, target.commitId, `${service.serviceId} env-reuse bootCommit must match target commit`); assert.match(service.environmentInputHash ?? "", /^[a-f0-9]{64}$/u, `${service.serviceId} env-reuse environmentInputHash must be a sha256 hex input hash`); assert.equal(image.tag, `env-${service.environmentInputHash.slice(0, 12)}`, `${service.serviceId} env-reuse image tag must match environment input hash`); } 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 assertRuntimeLaneDeployAndCatalog(deploy, catalog, laneName) { const lane = laneDeployConfig(deploy, laneName); assert.ok(lane && typeof lane === "object", `deploy.lanes.${laneName} must exist for ${laneName} catalog refresh`); assert.ok(String(lane.namespace ?? "").trim(), `deploy.lanes.${laneName}.namespace must not be empty`); assert.ok(isHttpEndpoint(lane.endpoint), `deploy.lanes.${laneName}.endpoint must be a non-empty http(s) URL`); const serviceIds = serviceIdsForLane(laneName, deploy); assert.ok(serviceIds.every((serviceId) => lane.serviceDeclarations?.[serviceId]), `deploy.lanes.${laneName}.serviceDeclarations must cover envReuseServices`); assert.ok(serviceIds.every((serviceId) => lane.bootScripts?.[serviceId]), `deploy.lanes.${laneName}.bootScripts must cover envReuseServices`); assert.equal(catalog.environment, laneName, `catalog environment must be ${laneName}`); assert.equal(catalog.profile, laneName, `catalog profile must be ${laneName}`); assert.equal(catalog.namespace, lane.namespace, `catalog namespace must be ${lane.namespace}`); if (catalog.endpoint != null) assert.ok(isHttpEndpoint(catalog.endpoint), "catalog endpoint must be a non-empty http(s) URL"); assert.deepEqual(catalog.allowedProfiles, [laneName], `catalog allowedProfiles must only allow ${laneName}`); assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog forbiddenProfiles must forbid dev/prod"); assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), serviceIds, `catalog services must cover deploy.lanes.${laneName}.envReuseServices in order`); } function normalizeCatalogForLane(catalog, args, deploy, target, registryPrefix) { if (!catalog) return catalog; const serviceIds = isRuntimeArtifactLane(args.lane) ? serviceIdsForLane(args.lane, deploy) : SERVICE_IDS; const allowed = new Set(serviceIds); const environment = artifactEnvironment(args); const namespace = artifactNamespace(args, deploy); const existingById = new Map((catalog.services ?? []) .filter((service) => allowed.has(service.serviceId)) .map((service) => [service.serviceId, service])); return { ...catalog, services: serviceIds.map((serviceId) => existingById.get(serviceId) ?? catalogSkeletonService({ args, serviceId, target, registryPrefix, environment, namespace })) }; } function assertLaneDeployAndCatalog(args, deploy, catalog) { if (isRuntimeArtifactLane(args.lane)) return assertRuntimeLaneDeployAndCatalog(deploy, catalog, args.lane); 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") { assertPublishedImageTag(service, image, target); } 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, buildBackend: service.buildBackend ?? 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, lane = "node", 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 = deployServiceMapForLane(deploy, lane, serviceIds); const inventoryByService = new Map(serviceInventory.services.map((service) => [service.serviceId, service])); const refreshedServices = []; const nextCatalogServices = []; catalog.commitId = target.imageTag; if (isRuntimeArtifactLane(lane)) catalog.endpoint = artifactEndpoint({ lane }, deploy); 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); const sourceCommitId = revisionForRecord(publishRecord, target); catalogService.commitId = serviceCommitId; catalogService.sourceCommitId = sourceCommitId; catalogService.image = image; catalogService.imageTag = imageTag; catalogService.buildCreatedAt = publishRecord?.buildCreatedAt ?? null; catalogService.buildSource = publishRecord?.buildSource ?? null; catalogService.buildBackend = publishRecord ? publishRecord.buildBackend ?? null : catalogService.buildBackend ?? 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, sourceCommitId: catalogService.sourceCommitId, buildBackend: catalogService.buildBackend, 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 deployServiceMapForLane(deploy, lane, serviceIds) { const base = new Map((deploy.services ?? []).map((service) => [service.serviceId, service])); if (!isRuntimeArtifactLane(lane)) return base; const overlay = new Map((laneDeployConfig(deploy, lane)?.services ?? []).map((service) => [service.serviceId, service])); return new Map(serviceIds.map((serviceId) => [serviceId, { serviceId, ...(base.get(serviceId) ?? {}), ...(overlay.get(serviceId) ?? {}) }])); } 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([ readConfig(args.deployPath), args.publishReportPath ? readConfig(args.publishReportPath) : Promise.resolve(null) ]); let catalog = await readConfigIfPresent(args.catalogPath, null); if (!catalog && isRuntimeArtifactLane(args.lane)) catalog = artifactCatalogSkeleton({ args, deploy, target, registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix }); catalog = normalizeCatalogForLane( catalog, args, deploy, target, publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix ); assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} catalog refresh`); assertLaneDeployAndCatalog(args, deploy, catalog); const serviceIds = serviceIdsForLane(args.lane, deploy); const publishRecords = publishReport ? publishRecordsFromReport(publishReport, target, serviceIds) : null; const serviceInventory = publishReport?.artifactPublish?.serviceInventory ?? serviceInventoryFromServices( await resolveDevArtifactServices(repoRoot, serviceIds, serviceIds) ); const services = refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath: publishReportProvenance(publishReport, args.publishReportPath ?? defaultPublishReportPath), registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix, lane: args.lane, serviceIds }); if (args.write) { await writeJson(args.catalogPath, catalog); } const publishedRecords = [...(publishRecords?.records.values() ?? [])]; const publishedCount = publishedRecords.filter((record) => record.status === "published").length; const reusedCount = publishedRecords.filter((record) => record.status === "reused").length; console.log(JSON.stringify({ status: publishRecords ? "published" : "blocked", targetRef: target.ref, sourceCommitId: target.commitId, artifactCommitId: target.imageTag, lane: args.lane, catalogPath: args.catalogPath, endpoint: catalog.endpoint ?? null, imageTagMode: args.imageTagMode, wrote: args.write ? [args.catalogPath] : [], deployTruthPolicy: "deploy.yaml is human-authored runtime config; artifact promotion must not rewrite it", ciPublished: Boolean(publishRecords), registryVerified: Boolean(publishRecords), publishedCount, reusedCount, 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; });