diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs index 07958429..fca5611f 100644 --- a/scripts/artifact-publish.mjs +++ b/scripts/artifact-publish.mjs @@ -1846,7 +1846,8 @@ async function readExternalServiceArtifacts({ dir, services, commitId }) { const artifacts = new Map(); const blockers = []; for (const service of services) { - const buildRequired = service.buildRequired === true || service.ciBuildRequired === true; + const buildRequired = serviceRequiresCurrentBuildArtifact(service); + if (!buildRequired) continue; const reportPath = externalServiceReportPath(dir, service.serviceId); let report; try { @@ -1897,6 +1898,10 @@ async function readExternalServiceArtifacts({ dir, services, commitId }) { return { artifacts, blockers }; } +function serviceRequiresCurrentBuildArtifact(service) { + return service?.buildRequired === true || service?.ciBuildRequired === true; +} + function artifactRecordFromServiceResultsEnv({ service, env }) { const status = serviceResultValue(env, service.serviceId, "STATUS"); if (!status) return null; @@ -2354,6 +2359,7 @@ async function main() { const buildServiceIds = new Set(plannedBuildServices ?? services .filter((service) => service.artifactRequired && (service.envReuse ? service.envChanged === true : affectedServiceIds.has(service.serviceId))) .map((service) => service.serviceId)); + const currentBuildArtifactServices = services.filter((service) => buildServiceIds.has(service.serviceId) && serviceRequiresCurrentBuildArtifact(service)); const preflightBuildServiceIds = args.externalServiceReportDir || args.externalServiceResultsEnv ? new Set() : buildServiceIds; let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight, registryCapabilities, buildServiceIds: preflightBuildServiceIds }); @@ -2366,10 +2372,10 @@ async function main() { })); let externalArtifacts = new Map(); if (args.mode === "build" || args.mode === "publish") { - if (args.externalServiceReportDir && buildServiceIds.size > 0) { + if (args.externalServiceReportDir && currentBuildArtifactServices.length > 0) { const externalResult = await readExternalServiceArtifacts({ dir: args.externalServiceReportDir, - services: services.filter((service) => buildServiceIds.has(service.serviceId)), + services: currentBuildArtifactServices, commitId }); externalArtifacts = externalResult.artifacts; @@ -2387,7 +2393,7 @@ async function main() { if (externalArtifacts.has(service.serviceId)) { return externalArtifacts.get(service.serviceId); } - if (!service.artifactRequired || buildServiceIds.has(service.serviceId)) { + if (!service.artifactRequired || (buildServiceIds.has(service.serviceId) && serviceRequiresCurrentBuildArtifact(service))) { return artifactRecord({ args, service, @@ -2426,7 +2432,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) => buildServiceIds.has(item.serviceId) && !externalArtifacts.has(item.serviceId)), args.concurrency, async (service) => { + await mapWithConcurrency(services.filter((item) => buildServiceIds.has(item.serviceId) && serviceRequiresCurrentBuildArtifact(item) && !externalArtifacts.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); diff --git a/scripts/ci-plan.test.mjs b/scripts/ci-plan.test.mjs index e04823f5..866ca28f 100644 --- a/scripts/ci-plan.test.mjs +++ b/scripts/ci-plan.test.mjs @@ -281,6 +281,114 @@ test("artifact reuse paths preserve catalog provenance instead of current planne assert.doesNotMatch(gitopsRender, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u); }); +test("artifact publish ignores stale external reports for envreuse services that do not need current build artifacts", async () => { + const currentCommit = (await git(process.cwd(), ["rev-parse", "HEAD"])).stdout.trim(); + const oldCommit = "0".repeat(40); + const digest = `sha256:${"3".repeat(64)}`; + const tmp = await mkdtemp(path.join(os.tmpdir(), "hwlab-artifact-stale-report-")); + const reportDir = path.join(tmp, "service-results"); + await mkdir(reportDir, { recursive: true }); + const image = "127.0.0.1:5000/hwlab/hwlab-cloud-web-env:env-test"; + const deployPath = path.join(tmp, "deploy.yaml"); + const catalogPath = path.join(tmp, "artifact-catalog.v03.json"); + const ciPlanPath = path.join(tmp, "ci-plan.json"); + const deploy = createDeployFixture({ serviceIds: ["hwlab-cloud-web"] }); + deploy.lanes.v03.namespace = "hwlab-v03"; + deploy.lanes.v03.endpoint = "https://hwlab.pikapython.com"; + await writeStructuredFile(process.cwd(), deployPath, deploy); + + const catalog = createCatalogFixture("ready", { + serviceIds: ["hwlab-cloud-web"], + sourceCommitId: currentCommit, + bootCommits: { "hwlab-cloud-web": currentCommit }, + environmentInputHashes: { "hwlab-cloud-web": "e".repeat(64) }, + codeInputHashes: { "hwlab-cloud-web": "c".repeat(64) } + }); + catalog.environment = "v03"; + catalog.profile = "v03"; + catalog.namespace = "hwlab-v03"; + catalog.allowedProfiles = ["v03"]; + catalog.forbiddenProfiles = ["dev", "prod"]; + for (const service of catalog.services) { + service.profile = "v03"; + service.namespace = "hwlab-v03"; + } + await writeFile(catalogPath, JSON.stringify(catalog, null, 2)); + await writeFile(ciPlanPath, JSON.stringify({ + planVersion: "v1", + sourceCommitId: currentCommit, + shortCommitId: currentCommit.slice(0, 12), + baseRef: "HEAD~1", + targetRef: "HEAD", + compatibility: { lane: "v03" }, + changedPathSummary: {}, + imageBuildRequired: false, + affectedServices: [], + buildServices: ["hwlab-cloud-web"], + rolloutServices: [], + reusedServices: ["hwlab-cloud-web"], + buildSkippedCount: 1, + ciCdPlan: {}, + services: [{ + serviceId: "hwlab-cloud-web", + runtimeMode: "env-reuse-git-mirror-checkout", + envReuse: true, + affected: false, + buildRequired: false, + envChanged: false, + codeChanged: false, + environmentImage: image, + environmentDigest: digest, + environmentInputHash: "e".repeat(64), + codeInputHash: "c".repeat(64), + bootRepo: "git@github.com:pikasTech/HWLAB.git", + bootCommit: currentCommit, + bootSh: "deploy/runtime/boot/hwlab-cloud-web.sh", + reason: ["component-inputs-unchanged"], + reuse: { status: "ready", image, digest, reusedFrom: "e".repeat(64) } + }] + }, null, 2)); + await writeFile(path.join(reportDir, "hwlab-cloud-web.json"), JSON.stringify({ + artifactPublish: { + services: [{ + serviceId: "hwlab-cloud-web", + status: "reused", + commitId: oldCommit, + sourceCommitId: oldCommit, + image: "127.0.0.1:5000/hwlab/hwlab-cloud-web-env:env-old", + imageTag: "env-old", + digest, + repositoryDigest: `127.0.0.1:5000/hwlab/hwlab-cloud-web-env@${digest}`, + runtimeMode: "env-reuse-git-mirror-checkout", + envReuse: true + }] + } + }, null, 2)); + + const { stdout } = await execFileAsync(process.execPath, [ + "scripts/artifact-publish.mjs", + "--build", + "--lane", "v03", + "--services", "hwlab-cloud-web", + "--deploy-config", deployPath, + "--catalog-path", catalogPath, + "--ci-plan-path", ciPlanPath, + "--external-service-report-dir", reportDir, + "--registry-prefix", "127.0.0.1:5000/hwlab", + "--no-report" + ], { + cwd: process.cwd(), + env: { ...process.env, HWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY: "false" } + }); + const result = JSON.parse(stdout); + const blockerText = JSON.stringify(result.blockers ?? []); + assert.equal(blockerText.includes(oldCommit), false); + assert.equal(result.status, "built"); + assert.equal(result.services[0].serviceId, "hwlab-cloud-web"); + assert.equal(result.services[0].status, "reused"); + assert.equal(result.services[0].sourceCommitId, currentCommit); +}); + test("component model uses service paths declared in deploy config", () => { const deploy = createDeployFixture(); const models = componentModelsForServices(["hwlab-cloud-api"], deploy.lanes.v02);