feat: default G14 artifact publish to component reuse

This commit is contained in:
Codex
2026-05-26 10:51:01 +08:00
parent 986d8f5c07
commit 7a14c6bfb3
4 changed files with 243 additions and 25 deletions
+127 -12
View File
@@ -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
+41 -13
View File
@@ -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
+74
View File
@@ -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}`);
});
+1
View File
@@ -159,6 +159,7 @@ export async function createG14CiPlan(options = {}) {
plannerMutatesCiJson: false,
plannerMutatesDeployJson: false,
serviceIdSource: serviceIdResolution.source,
defaultComponentLazyBuild: true,
legacyFullBuildFallback: true
},
inputs: {