feat: default G14 artifact publish to component reuse
This commit is contained in:
+127
-12
@@ -32,6 +32,7 @@ const catalogPath = "deploy/artifact-catalog.dev.json";
|
||||
const deployPath = "deploy/deploy.json";
|
||||
|
||||
const mutableTags = new Set(["latest", "dev", "main", "master", "prod", "production"]);
|
||||
const shaDigestPattern = /^sha256:[a-f0-9]{64}$/u;
|
||||
const fatalBlockerTypes = new Set([
|
||||
"contract_blocker",
|
||||
"environment_blocker",
|
||||
@@ -75,6 +76,7 @@ function parseArgs(argv) {
|
||||
reportPath: defaultReportPath,
|
||||
services: [...SERVICE_IDS],
|
||||
servicesExplicit: false,
|
||||
affectedOnly: true,
|
||||
emitReport: true,
|
||||
quietBuild: false,
|
||||
concurrency: parseConcurrency(process.env.HWLAB_DEV_ARTIFACT_CONCURRENCY, 4)
|
||||
@@ -92,6 +94,8 @@ function parseArgs(argv) {
|
||||
args.services = readOption(argv, ++index, arg).split(",").map((item) => item.trim()).filter(Boolean);
|
||||
args.servicesExplicit = true;
|
||||
}
|
||||
else if (arg === "--affected-only") args.affectedOnly = true;
|
||||
else if (arg === "--full-build") args.affectedOnly = false;
|
||||
else if (arg === "--no-report") args.emitReport = false;
|
||||
else if (arg === "--quiet-build") args.quietBuild = true;
|
||||
else if (arg === "--concurrency") args.concurrency = parseConcurrency(readOption(argv, ++index, arg), 4);
|
||||
@@ -175,6 +179,8 @@ function printHelp() {
|
||||
" --registry-prefix PREFIX default: 127.0.0.1:5000/hwlab",
|
||||
" --base-image IMAGE override HWLAB_DEV_BASE_IMAGE for this run; must already be local",
|
||||
" --services LIST comma-separated service IDs; default: all frozen DEV services",
|
||||
" --affected-only default: build/publish only g14-ci-plan affected services and reuse unchanged catalog digests",
|
||||
" --full-build compatibility fallback: build/publish every required service",
|
||||
` --report PATH default: ${defaultReportPath}`,
|
||||
" --no-report print JSON without updating the temporary artifact JSON",
|
||||
" --quiet-build keep docker build quiet; default keeps build output visible",
|
||||
@@ -923,13 +929,14 @@ function sourceStateBlocker(service) {
|
||||
});
|
||||
}
|
||||
|
||||
async function preflight({ args, services, catalog, deployManifest, baseImagePreflight, registryCapabilities }) {
|
||||
async function preflight({ args, services, catalog, deployManifest, baseImagePreflight, registryCapabilities, buildServiceIds = new Set() }) {
|
||||
const blockers = [];
|
||||
let registryPrefix = args.registryPrefix;
|
||||
const buildRequired = buildServiceIds.size > 0 || !args.affectedOnly;
|
||||
|
||||
if (baseImagePreflight.publishUsable) {
|
||||
args.baseImage = baseImagePreflight.localTag;
|
||||
} else {
|
||||
} else if (buildRequired) {
|
||||
blockers.push(baseImagePreflightBlocker(baseImagePreflight));
|
||||
}
|
||||
|
||||
@@ -941,7 +948,7 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre
|
||||
type: "safety_blocker",
|
||||
scope: "registry",
|
||||
summary: error.message,
|
||||
next: "Use the D601 local/internal registry prefix, for example 127.0.0.1:5000/hwlab."
|
||||
next: "Use the G14 local/internal registry prefix, for example 127.0.0.1:5000/hwlab."
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -973,13 +980,13 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre
|
||||
}
|
||||
}
|
||||
|
||||
if (registryCapabilities?.dimensions?.dockerDaemonPushAccess?.status === "blocked") {
|
||||
if (buildRequired && registryCapabilities?.dimensions?.dockerDaemonPushAccess?.status === "blocked") {
|
||||
blockers.push(
|
||||
blocker({
|
||||
type: "network_blocker",
|
||||
scope: "docker-daemon-push-access",
|
||||
summary: registryCapabilities.dimensions.dockerDaemonPushAccess.summary,
|
||||
next: "Restore Docker daemon access to the D601 local/internal registry before running --publish."
|
||||
next: "Restore Docker daemon access to the G14 local/internal registry before running --publish."
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1217,7 +1224,7 @@ async function publishService(artifact) {
|
||||
type: "network_blocker",
|
||||
scope: artifact.serviceId,
|
||||
summary: `docker push failed for ${artifact.image}`,
|
||||
next: "Verify the D601 local/internal registry is reachable from the Docker daemon and rerun --publish."
|
||||
next: "Verify the G14 local/internal registry is reachable from the Docker daemon and rerun --publish."
|
||||
}),
|
||||
logTail: tailText(`${result.stdout}\n${result.stderr}`),
|
||||
publishDurationMs: result.durationMs
|
||||
@@ -1256,13 +1263,16 @@ function repositoryFromImageRef(image) {
|
||||
return lastColon === -1 ? image : image.slice(0, lastColon);
|
||||
}
|
||||
|
||||
const publishReadyStatuses = new Set(["published", "reused"]);
|
||||
const buildReadyStatuses = new Set(["built", "published", "published_unverified_digest", "reused"]);
|
||||
|
||||
function reportStatus(mode, artifacts, blockers) {
|
||||
const requiredArtifacts = artifacts.filter((artifact) => artifact.artifactRequired);
|
||||
if (mode === "preflight") return blockers.length ? "blocked" : "pass";
|
||||
if (requiredArtifacts.some((artifact) => artifact.status === "publish_failed" || artifact.status === "build_failed")) return "failed";
|
||||
if (requiredArtifacts.some((artifact) => artifact.status.startsWith("blocked_"))) return "blocked";
|
||||
if (mode === "publish" && requiredArtifacts.length > 0 && requiredArtifacts.every((artifact) => artifact.status === "published")) return "published";
|
||||
if (mode === "build" && requiredArtifacts.length > 0 && requiredArtifacts.every((artifact) => artifact.status === "built")) return "built";
|
||||
if (mode === "publish" && requiredArtifacts.length > 0 && requiredArtifacts.every((artifact) => publishReadyStatuses.has(artifact.status))) return "published";
|
||||
if (mode === "build" && requiredArtifacts.length > 0 && requiredArtifacts.every((artifact) => buildReadyStatuses.has(artifact.status))) return "built";
|
||||
return blockers.length ? "blocked" : "pass";
|
||||
}
|
||||
|
||||
@@ -1294,9 +1304,21 @@ function artifactNotPublishedReason(artifact, mode, fatalBlocked) {
|
||||
return artifact.notPublishedReason ?? "publish_not_run";
|
||||
}
|
||||
|
||||
function imageTagFromReference(image) {
|
||||
const value = String(image ?? "");
|
||||
const slashIndex = value.lastIndexOf("/");
|
||||
const colonIndex = value.lastIndexOf(":");
|
||||
return colonIndex > slashIndex ? value.slice(colonIndex + 1) : null;
|
||||
}
|
||||
|
||||
function repositoryDigestFor(image, digest) {
|
||||
return image && shaDigestPattern.test(digest ?? "") ? `${repositoryFromImageRef(image)}@${digest}` : null;
|
||||
}
|
||||
|
||||
function artifactRecord({ args, service, commitId, shortCommit, buildCreatedAt = null, buildSource = null, status, digest = "not_published" }) {
|
||||
return {
|
||||
...service,
|
||||
commitId: shortCommit,
|
||||
sourceCommitId: commitId,
|
||||
status,
|
||||
image: imageRef(args.registryPrefix, service.serviceId, shortCommit),
|
||||
@@ -1308,6 +1330,43 @@ function artifactRecord({ args, service, commitId, shortCommit, buildCreatedAt =
|
||||
};
|
||||
}
|
||||
|
||||
function artifactRecordForCatalogReuse({ service, catalogService, commitId }) {
|
||||
const image = catalogService?.image ?? null;
|
||||
const digest = catalogService?.digest ?? "not_published";
|
||||
const imageTag = catalogService?.imageTag ?? imageTagFromReference(image);
|
||||
const catalogCommitId = catalogService?.commitId ?? imageTag ?? null;
|
||||
if (!image || !imageTag || !shaDigestPattern.test(digest)) {
|
||||
return {
|
||||
...service,
|
||||
commitId: catalogCommitId,
|
||||
sourceCommitId: catalogService?.sourceCommitId ?? catalogCommitId ?? commitId,
|
||||
status: "blocked_reuse_unavailable",
|
||||
image: image ?? null,
|
||||
imageTag,
|
||||
buildCreatedAt: catalogService?.buildCreatedAt ?? null,
|
||||
buildSource: catalogService?.buildSource ?? null,
|
||||
digest,
|
||||
repositoryDigest: repositoryDigestFor(image, digest),
|
||||
reusedFrom: null,
|
||||
notPublishedReason: "reuse_catalog_digest_unavailable"
|
||||
};
|
||||
}
|
||||
return {
|
||||
...service,
|
||||
commitId: catalogCommitId,
|
||||
sourceCommitId: catalogService?.sourceCommitId ?? catalogCommitId ?? commitId,
|
||||
status: "reused",
|
||||
image,
|
||||
imageTag,
|
||||
buildCreatedAt: catalogService?.buildCreatedAt ?? null,
|
||||
buildSource: catalogService?.buildSource ?? null,
|
||||
digest,
|
||||
repositoryDigest: catalogService?.repositoryDigest ?? repositoryDigestFor(image, digest),
|
||||
reusedFrom: service.reuse?.reusedFrom ?? catalogService?.componentInputHash ?? catalogCommitId,
|
||||
notPublishedReason: null
|
||||
};
|
||||
}
|
||||
|
||||
function publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlocked, registryCapabilities }) {
|
||||
return {
|
||||
version: "v2",
|
||||
@@ -1325,7 +1384,8 @@ function publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlo
|
||||
enabled: artifact.publishEnabled,
|
||||
required: artifact.artifactRequired,
|
||||
artifactScope: artifact.artifactScope,
|
||||
sourceCommitId: commitId,
|
||||
commitId: artifact.commitId ?? shortCommit,
|
||||
sourceCommitId: artifact.sourceCommitId ?? commitId,
|
||||
registryTarget: imageRepository(args.registryPrefix, artifact.serviceId),
|
||||
image,
|
||||
imageTag: artifact.imageTag ?? shortCommit,
|
||||
@@ -1347,6 +1407,7 @@ function publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlo
|
||||
ciAffected: artifact.ciAffected ?? null,
|
||||
ciReason: artifact.ciReason ?? [],
|
||||
reuse: artifact.reuse ?? null,
|
||||
reusedFrom: artifact.reusedFrom ?? null,
|
||||
notPublishedReason: artifactNotPublishedReason(artifact, mode, fatalBlocked)
|
||||
};
|
||||
})
|
||||
@@ -1419,7 +1480,8 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
|
||||
const serviceInventory = serviceInventoryFromServices(services);
|
||||
const publishPlan = publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlocked, registryCapabilities });
|
||||
const publishedCount = requiredArtifacts.filter((artifact) => artifact.status === "published").length;
|
||||
const builtCount = requiredArtifacts.filter((artifact) => ["built", "published", "published_unverified_digest"].includes(artifact.status)).length;
|
||||
const reusedCount = requiredArtifacts.filter((artifact) => artifact.status === "reused").length;
|
||||
const builtCount = requiredArtifacts.filter((artifact) => buildReadyStatuses.has(artifact.status)).length;
|
||||
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/artifact-publish.schema.json",
|
||||
@@ -1502,6 +1564,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
|
||||
registryCapabilities: summarizeRegistryCapabilities(registryCapabilities),
|
||||
baseImage: args.baseImage ?? null,
|
||||
selectedServiceIds: args.services,
|
||||
affectedOnly: args.affectedOnly,
|
||||
quietBuild: args.quietBuild,
|
||||
concurrency: args.concurrency,
|
||||
baseImagePreflight: summarizeBaseImagePreflight(baseImagePreflight),
|
||||
@@ -1515,6 +1578,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
|
||||
disabledServiceCount: serviceInventory.disabledServiceCount,
|
||||
builtCount,
|
||||
publishedCount,
|
||||
reusedCount,
|
||||
timings: {
|
||||
dockerBuildDurationMs: sumDurations(artifacts, "dockerBuildDurationMs"),
|
||||
cloudWebBuildDurationMs: sumDurations(artifacts, "cloudWebBuildDurationMs"),
|
||||
@@ -1524,6 +1588,8 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
|
||||
services: artifacts.map((artifact) => ({
|
||||
serviceId: artifact.serviceId,
|
||||
status: artifact.status,
|
||||
commitId: artifact.commitId ?? shortCommit,
|
||||
sourceCommitId: artifact.sourceCommitId ?? commitId,
|
||||
image: artifact.image ?? imageRef(args.registryPrefix, artifact.serviceId, shortCommit),
|
||||
imageTag: artifact.imageTag ?? shortCommit,
|
||||
buildCreatedAt: artifact.buildCreatedAt ?? null,
|
||||
@@ -1552,6 +1618,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
|
||||
ciAffected: artifact.ciAffected ?? null,
|
||||
ciReason: artifact.ciReason ?? [],
|
||||
reuse: artifact.reuse ?? null,
|
||||
reusedFrom: artifact.reusedFrom ?? null,
|
||||
dockerBuildLogTail: artifact.dockerBuildLogTail ?? null,
|
||||
logTail: artifact.logTail ?? null,
|
||||
pushLogTail: artifact.pushLogTail ?? null
|
||||
@@ -1616,8 +1683,15 @@ async function main() {
|
||||
...(args.servicesExplicit ? { services: args.services } : {})
|
||||
});
|
||||
const services = enrichServicesWithCiPlan(await resolveServices(args.services, catalog), ciPlan);
|
||||
const catalogByServiceId = new Map((catalog?.services ?? []).map((service) => [service.serviceId, service]));
|
||||
const affectedServiceIds = new Set(ciPlan?.affectedServices ?? []);
|
||||
const buildServiceIds = new Set(
|
||||
args.affectedOnly
|
||||
? services.filter((service) => service.artifactRequired && affectedServiceIds.has(service.serviceId)).map((service) => service.serviceId)
|
||||
: services.filter((service) => service.artifactRequired).map((service) => service.serviceId)
|
||||
);
|
||||
|
||||
let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight, registryCapabilities });
|
||||
let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight, registryCapabilities, buildServiceIds });
|
||||
let artifacts = services.map((service) => artifactRecord({
|
||||
args,
|
||||
service,
|
||||
@@ -1625,6 +1699,40 @@ async function main() {
|
||||
shortCommit,
|
||||
status: service.artifactRequired ? "preflight_only" : "not_required"
|
||||
}));
|
||||
if (args.affectedOnly && (args.mode === "build" || args.mode === "publish")) {
|
||||
artifacts = services.map((service) => {
|
||||
if (!service.artifactRequired || buildServiceIds.has(service.serviceId)) {
|
||||
return artifactRecord({
|
||||
args,
|
||||
service,
|
||||
commitId,
|
||||
shortCommit,
|
||||
status: service.artifactRequired ? "preflight_only" : "not_required"
|
||||
});
|
||||
}
|
||||
const reused = artifactRecordForCatalogReuse({
|
||||
service,
|
||||
catalogService: catalogByServiceId.get(service.serviceId),
|
||||
commitId
|
||||
});
|
||||
emit("artifact_reuse", {
|
||||
serviceId: service.serviceId,
|
||||
status: reused.status,
|
||||
image: reused.image,
|
||||
digest: reused.digest,
|
||||
reusedFrom: reused.reusedFrom ?? null
|
||||
});
|
||||
if (reused.status.startsWith("blocked_")) {
|
||||
blockers.push(blocker({
|
||||
type: "environment_blocker",
|
||||
scope: service.serviceId,
|
||||
summary: `${service.serviceId} cannot be reused because deploy/artifact-catalog.dev.json has no verified digest`,
|
||||
next: "Run the default full publish once, or refresh the service catalog from a successful publish report before using --affected-only."
|
||||
}));
|
||||
}
|
||||
return reused;
|
||||
});
|
||||
}
|
||||
|
||||
if ((args.mode === "build" || args.mode === "publish") && hasFatalBlocker(blockers)) {
|
||||
emit("publish_blocked", {
|
||||
@@ -1632,7 +1740,7 @@ async function main() {
|
||||
});
|
||||
} else if (args.mode === "build" || args.mode === "publish") {
|
||||
const artifactByServiceId = new Map(artifacts.map((artifact) => [artifact.serviceId, artifact]));
|
||||
await mapWithConcurrency(services.filter((item) => item.artifactRequired), args.concurrency, async (service) => {
|
||||
await mapWithConcurrency(services.filter((item) => buildServiceIds.has(item.serviceId)), args.concurrency, async (service) => {
|
||||
emit("build_start", { serviceId: service.serviceId, image: imageRef(args.registryPrefix, service.serviceId, shortCommit) });
|
||||
const artifact = await buildService({ args, repo, commitId, shortCommit, service, buildCreatedAt });
|
||||
artifactByServiceId.set(service.serviceId, artifact);
|
||||
@@ -1676,13 +1784,19 @@ async function main() {
|
||||
registryCapabilities: summarizeRegistryCapabilities(registryCapabilities),
|
||||
baseImagePreflight: summarizeBaseImagePreflight(baseImagePreflight),
|
||||
selectedServiceIds: report.artifactPublish.selectedServiceIds,
|
||||
affectedOnly: report.artifactPublish.affectedOnly,
|
||||
ciPlan: report.artifactPublish.ciPlan,
|
||||
concurrency: report.artifactPublish.concurrency,
|
||||
builtCount: report.artifactPublish.builtCount,
|
||||
publishedCount: report.artifactPublish.publishedCount,
|
||||
reusedCount: report.artifactPublish.reusedCount,
|
||||
timings: report.artifactPublish.timings,
|
||||
publishPlan: report.artifactPublish.publishPlan,
|
||||
services: report.artifactPublish.services.map((service) => ({
|
||||
serviceId: service.serviceId,
|
||||
status: service.status,
|
||||
commitId: service.commitId,
|
||||
sourceCommitId: service.sourceCommitId,
|
||||
image: service.image,
|
||||
imageTag: service.imageTag,
|
||||
buildCreatedAt: service.buildCreatedAt,
|
||||
@@ -1691,6 +1805,7 @@ async function main() {
|
||||
dockerBuildDurationMs: service.dockerBuildDurationMs,
|
||||
cloudWebBuildDurationMs: service.cloudWebBuildDurationMs,
|
||||
publishDurationMs: service.publishDurationMs,
|
||||
reusedFrom: service.reusedFrom,
|
||||
logTail: service.logTail,
|
||||
pushLogTail: service.pushLogTail,
|
||||
notPublishedReason: service.notPublishedReason
|
||||
|
||||
@@ -23,6 +23,8 @@ const defaultRegistryPrefix = process.env.HWLAB_DEV_REGISTRY_PREFIX || process.e
|
||||
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
||||
const commitPattern = /^[a-f0-9]{7,40}$/;
|
||||
|
||||
const publishReadyStatuses = new Set(["published", "reused"]);
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
targetRef: "HEAD",
|
||||
@@ -122,6 +124,19 @@ 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 value.slice(0, 7);
|
||||
if (typeof record?.sourceCommitId === "string" && commitPattern.test(record.sourceCommitId)) return record.sourceCommitId.slice(0, 7);
|
||||
return target.shortCommitId;
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -160,16 +175,15 @@ function publishRecordsFromReport(report, target) {
|
||||
"publish plan must cover every frozen service"
|
||||
);
|
||||
assert.equal(
|
||||
artifactPublish.publishedCount,
|
||||
(artifactPublish.publishedCount ?? 0) + (artifactPublish.reusedCount ?? 0),
|
||||
requiredServiceIds.length,
|
||||
"publish report must publish every required enabled service"
|
||||
"publish report must publish or reuse every required enabled service"
|
||||
);
|
||||
|
||||
const records = new Map();
|
||||
for (const service of artifactPublish.services ?? []) {
|
||||
assert.ok(SERVICE_IDS.includes(service.serviceId), `unknown service ${service.serviceId} in publish report`);
|
||||
const image = parseTaggedImage(service.image, service.serviceId);
|
||||
assert.equal(image.tag, target.shortCommitId, `${service.serviceId} image tag must match target short commit`);
|
||||
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`);
|
||||
@@ -177,11 +191,17 @@ function publishRecordsFromReport(report, target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert.equal(service.status, "published", `${service.serviceId} status must be published`);
|
||||
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.shortCommitId, `${service.serviceId} newly published image tag must match target short commit`);
|
||||
}
|
||||
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,
|
||||
@@ -196,7 +216,8 @@ function publishRecordsFromReport(report, target) {
|
||||
buildArgsHash: service.buildArgsHash ?? null,
|
||||
ciAffected: service.ciAffected ?? null,
|
||||
ciReason: service.ciReason ?? [],
|
||||
reuse: service.reuse ?? null
|
||||
reuse: service.reuse ?? null,
|
||||
reusedFrom: service.reusedFrom ?? null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -206,13 +227,15 @@ function publishRecordsFromReport(report, target) {
|
||||
|
||||
function updateEnvObject(env, service, target, publishRecord) {
|
||||
if (!env || typeof env !== "object" || Array.isArray(env)) return;
|
||||
if (Object.hasOwn(env, "HWLAB_COMMIT_ID")) env.HWLAB_COMMIT_ID = target.shortCommitId;
|
||||
const shortCommitId = shortCommitForRecord(publishRecord, target);
|
||||
const revision = revisionForRecord(publishRecord, target);
|
||||
if (Object.hasOwn(env, "HWLAB_COMMIT_ID")) env.HWLAB_COMMIT_ID = shortCommitId;
|
||||
if (Object.hasOwn(env, "HWLAB_IMAGE")) env.HWLAB_IMAGE = service.image;
|
||||
if (Object.hasOwn(env, "HWLAB_IMAGE_TAG")) env.HWLAB_IMAGE_TAG = service.imageTag;
|
||||
if (Object.hasOwn(env, "HWLAB_REVISION")) env.HWLAB_REVISION = target.commitId;
|
||||
if (Object.hasOwn(env, "HWLAB_REVISION")) env.HWLAB_REVISION = revision;
|
||||
if (Object.hasOwn(env, "HWLAB_BUILD_CREATED_AT")) env.HWLAB_BUILD_CREATED_AT = publishRecord?.buildCreatedAt ?? "";
|
||||
if (Object.hasOwn(env, "HWLAB_BUILD_SOURCE")) env.HWLAB_BUILD_SOURCE = publishRecord?.buildSource ?? "";
|
||||
if (Object.hasOwn(env, "HWLAB_SKILLS_COMMIT_ID")) env.HWLAB_SKILLS_COMMIT_ID = target.shortCommitId;
|
||||
if (Object.hasOwn(env, "HWLAB_SKILLS_COMMIT_ID")) env.HWLAB_SKILLS_COMMIT_ID = shortCommitId;
|
||||
if (Object.hasOwn(env, "HWLAB_IMAGE_DIGEST")) env.HWLAB_IMAGE_DIGEST = publishRecord?.digest ?? "not_published";
|
||||
}
|
||||
|
||||
@@ -226,14 +249,16 @@ function serviceIdForWorkload(item, container) {
|
||||
|
||||
function updateEnvList(envList, service, target, publishRecord) {
|
||||
if (!Array.isArray(envList)) return;
|
||||
const shortCommitId = shortCommitForRecord(publishRecord, target);
|
||||
const revision = revisionForRecord(publishRecord, target);
|
||||
for (const entry of envList) {
|
||||
if (entry.name === "HWLAB_COMMIT_ID") entry.value = target.shortCommitId;
|
||||
if (entry.name === "HWLAB_COMMIT_ID") entry.value = shortCommitId;
|
||||
if (entry.name === "HWLAB_IMAGE") entry.value = service.image;
|
||||
if (entry.name === "HWLAB_IMAGE_TAG") entry.value = service.imageTag;
|
||||
if (entry.name === "HWLAB_REVISION") entry.value = target.commitId;
|
||||
if (entry.name === "HWLAB_REVISION") entry.value = revision;
|
||||
if (entry.name === "HWLAB_BUILD_CREATED_AT") entry.value = publishRecord?.buildCreatedAt ?? "";
|
||||
if (entry.name === "HWLAB_BUILD_SOURCE") entry.value = publishRecord?.buildSource ?? "";
|
||||
if (entry.name === "HWLAB_SKILLS_COMMIT_ID") entry.value = target.shortCommitId;
|
||||
if (entry.name === "HWLAB_SKILLS_COMMIT_ID") entry.value = shortCommitId;
|
||||
if (entry.name === "HWLAB_IMAGE_DIGEST") entry.value = publishRecord?.digest ?? "not_published";
|
||||
}
|
||||
}
|
||||
@@ -276,11 +301,12 @@ function refreshDocuments({ deploy, catalog, workloads, target, publishRecords,
|
||||
const publishRecord = records?.get(serviceId) ?? null;
|
||||
const image = publishRecord?.image ?? targetImage(serviceId, target.shortCommitId, registryPrefix);
|
||||
const imageTag = publishRecord?.imageTag ?? target.shortCommitId;
|
||||
const serviceCommitId = shortCommitForRecord(publishRecord, target);
|
||||
|
||||
deployService.image = image;
|
||||
updateEnvObject(deployService.env, { image, imageTag }, target, publishRecord);
|
||||
|
||||
catalogService.commitId = target.shortCommitId;
|
||||
catalogService.commitId = serviceCommitId;
|
||||
catalogService.image = image;
|
||||
catalogService.imageTag = imageTag;
|
||||
catalogService.buildCreatedAt = publishRecord?.buildCreatedAt ?? null;
|
||||
@@ -295,7 +321,8 @@ function refreshDocuments({ deploy, catalog, workloads, target, publishRecords,
|
||||
catalogService.ciAffected = publishRecord?.ciAffected ?? catalogService.ciAffected ?? null;
|
||||
catalogService.ciReason = publishRecord?.ciReason ?? catalogService.ciReason ?? [];
|
||||
catalogService.reuse = publishRecord?.reuse ?? catalogService.reuse ?? null;
|
||||
catalogService.publishState = publishRecord ? "published" : "skeleton-only";
|
||||
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";
|
||||
@@ -313,6 +340,7 @@ function refreshDocuments({ deploy, catalog, workloads, target, publishRecords,
|
||||
dockerfileHash: catalogService.dockerfileHash,
|
||||
ciAffected: catalogService.ciAffected,
|
||||
ciReason: catalogService.ciReason,
|
||||
reusedFrom: catalogService.reusedFrom ?? null,
|
||||
publishState: catalogService.publishState,
|
||||
artifactRequired: catalogService.artifactRequired,
|
||||
notPublishedReason: catalogService.notPublishedReason
|
||||
|
||||
@@ -92,3 +92,77 @@ test("refresh accepts an absolute publish report path", async () => {
|
||||
assert.equal(payload.services[0].ciAffected, true);
|
||||
assert.equal(payload.services[0].componentInputHash, `${String(1).padStart(64, "a")}`);
|
||||
});
|
||||
|
||||
test("refresh keeps reused service image tags from the publish report", async () => {
|
||||
const commitId = await git(["rev-parse", "HEAD"]);
|
||||
const shortCommitId = await git(["rev-parse", "--short=7", "HEAD"]);
|
||||
const reusedTag = "d42c77d";
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-reused-"));
|
||||
const reportPath = path.join(tempDir, "dev-artifacts.json");
|
||||
await writeFile(reportPath, `${JSON.stringify({
|
||||
reportVersion: "v1",
|
||||
taskId: "g14-artifact-publish",
|
||||
commitId: shortCommitId,
|
||||
artifactPublish: {
|
||||
status: "published",
|
||||
mode: "publish",
|
||||
sourceCommitId: commitId,
|
||||
registryPrefix: "127.0.0.1:5000/hwlab",
|
||||
serviceCount: SERVICE_IDS.length,
|
||||
requiredServiceCount: SERVICE_IDS.length,
|
||||
disabledServiceCount: 0,
|
||||
publishedCount: 1,
|
||||
reusedCount: SERVICE_IDS.length - 1,
|
||||
publishPlan: {
|
||||
version: "v2",
|
||||
services: SERVICE_IDS.map((serviceId, index) => ({
|
||||
serviceId,
|
||||
required: true,
|
||||
status: index === 0 ? "published" : "reused",
|
||||
digest: digestFor(index),
|
||||
image: `127.0.0.1:5000/hwlab/${serviceId}:${index === 0 ? shortCommitId : reusedTag}`,
|
||||
imageTag: index === 0 ? shortCommitId : reusedTag
|
||||
}))
|
||||
},
|
||||
services: SERVICE_IDS.map((serviceId, index) => ({
|
||||
serviceId,
|
||||
status: index === 0 ? "published" : "reused",
|
||||
artifactRequired: true,
|
||||
commitId: index === 0 ? shortCommitId : reusedTag,
|
||||
sourceCommitId: index === 0 ? commitId : reusedTag,
|
||||
image: `127.0.0.1:5000/hwlab/${serviceId}:${index === 0 ? shortCommitId : reusedTag}`,
|
||||
imageTag: index === 0 ? shortCommitId : reusedTag,
|
||||
digest: digestFor(index),
|
||||
repositoryDigest: `127.0.0.1:5000/hwlab/${serviceId}@${digestFor(index)}`,
|
||||
buildCreatedAt: "2026-05-24T00:00:00.000Z",
|
||||
buildSource: index === 0 ? `pikasTech/HWLAB@${commitId}` : `pikasTech/HWLAB@${reusedTag}`,
|
||||
componentCommitId: index === 0 ? commitId : reusedTag,
|
||||
componentInputHash: `${String(index + 1).padStart(64, "d")}`,
|
||||
dockerfileHash: `${String(index + 1).padStart(64, "e")}`,
|
||||
buildArgsHash: `${String(index + 1).padStart(64, "f")}`,
|
||||
ciAffected: index === 0,
|
||||
ciReason: index === 0 ? ["component-path-changed"] : ["component-inputs-unchanged"],
|
||||
reusedFrom: index === 0 ? null : `catalog:${reusedTag}`
|
||||
}))
|
||||
}
|
||||
}, null, 2)}\n`);
|
||||
|
||||
const result = await execFileAsync(process.execPath, [
|
||||
"scripts/refresh-artifact-catalog.mjs",
|
||||
"--target-ref",
|
||||
"HEAD",
|
||||
"--publish-report",
|
||||
reportPath,
|
||||
"--no-write"
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
timeout: 15000,
|
||||
maxBuffer: 10 * 1024 * 1024
|
||||
});
|
||||
const payload = JSON.parse(result.stdout);
|
||||
assert.equal(payload.status, "published");
|
||||
assert.equal(payload.services[0].imageTag, shortCommitId);
|
||||
assert.equal(payload.services[1].imageTag, reusedTag);
|
||||
assert.equal(payload.services[1].publishState, "reused");
|
||||
assert.equal(payload.services[1].reusedFrom, `catalog:${reusedTag}`);
|
||||
});
|
||||
|
||||
@@ -159,6 +159,7 @@ export async function createG14CiPlan(options = {}) {
|
||||
plannerMutatesCiJson: false,
|
||||
plannerMutatesDeployJson: false,
|
||||
serviceIdSource: serviceIdResolution.source,
|
||||
defaultComponentLazyBuild: true,
|
||||
legacyFullBuildFallback: true
|
||||
},
|
||||
inputs: {
|
||||
|
||||
Reference in New Issue
Block a user