diff --git a/scripts/refresh-artifact-catalog.mjs b/scripts/refresh-artifact-catalog.mjs index 4b16c4aa..78b05488 100644 --- a/scripts/refresh-artifact-catalog.mjs +++ b/scripts/refresh-artifact-catalog.mjs @@ -267,6 +267,22 @@ function revisionForRecord(record, target) { 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"); @@ -355,7 +371,7 @@ function publishRecordsFromReport(report, target, serviceIds = SERVICE_IDS) { assert.equal(publishReadyStatuses.has(service.status), true, `${service.serviceId} status must be published or reused`); assert.equal(service.artifactRequired, true, `${service.serviceId} required service must state artifactRequired=true`); if (service.status === "published") { - assert.equal(image.tag, target.imageTag, `${service.serviceId} newly published image tag must match target image tag`); + assertPublishedImageTag(service, image, target); } assert.match(service.digest, digestPattern, `${service.serviceId} digest must be a sha256 registry digest`); records.set(service.serviceId, { diff --git a/scripts/refresh-artifact-catalog.test.mjs b/scripts/refresh-artifact-catalog.test.mjs index 0baeb9d0..c5769f36 100644 --- a/scripts/refresh-artifact-catalog.test.mjs +++ b/scripts/refresh-artifact-catalog.test.mjs @@ -10,6 +10,17 @@ import { SERVICE_IDS } from "../internal/protocol/index.mjs"; const execFileAsync = promisify(execFile); const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const V02_SERVICE_IDS = Object.freeze([ + "hwlab-cloud-api", + "hwlab-cloud-web", + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-device-pod", + "hwlab-gateway", + "hwlab-edge-proxy", + "hwlab-cli", + "hwlab-agent-skills" +]); async function git(args) { const result = await execFileAsync("git", args, { @@ -223,3 +234,120 @@ test("refresh keeps reused service image tags from the publish report", async () assert.equal(payload.services[1].publishState, "reused"); assert.equal(payload.services[1].reusedFrom, `catalog:${reusedTag}`); }); + +test("v02 refresh accepts env-reuse device-pod image tag from environment hash", async () => { + const commitId = await git(["rev-parse", "HEAD"]); + const environmentInputHash = "14847b0fe65d3d99a8bad2553c67c8660e24fba7e065eb122dba0de83e471823"; + const envTag = `env-${environmentInputHash.slice(0, 12)}`; + const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-v02-env-reuse-")); + const reportPath = path.join(tempDir, "v02-artifacts.json"); + await writeFile(reportPath, `${JSON.stringify({ + reportVersion: "v1", + taskId: "v02-artifact-publish", + commitId, + artifactPublish: { + status: "published", + mode: "publish", + sourceCommitId: commitId, + registryPrefix: "127.0.0.1:5000/hwlab", + serviceCount: V02_SERVICE_IDS.length, + requiredServiceCount: V02_SERVICE_IDS.length, + disabledServiceCount: 0, + publishedCount: V02_SERVICE_IDS.length, + reusedCount: 0, + serviceInventory: { + version: "v2", + serviceCount: V02_SERVICE_IDS.length, + requiredServiceCount: V02_SERVICE_IDS.length, + disabledServiceCount: 0, + requiredServiceIds: V02_SERVICE_IDS, + disabledServiceIds: [], + services: V02_SERVICE_IDS.map((serviceId) => ({ + serviceId, + publishEnabled: true, + artifactRequired: true, + artifactScope: "required", + runtimeKind: serviceId === "hwlab-device-pod" ? "env-reuse" : "node-command", + implementationState: "repo-entrypoint", + sourceState: "source-present", + entrypoint: `cmd/${serviceId}/main.ts`, + disabledReason: null + })) + }, + publishPlan: { + version: "v2", + services: V02_SERVICE_IDS.map((serviceId, index) => ({ + serviceId, + required: true, + digest: digestFor(index), + image: serviceId === "hwlab-device-pod" + ? `127.0.0.1:5000/hwlab/hwlab-device-pod-env:${envTag}` + : `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`, + imageTag: serviceId === "hwlab-device-pod" ? envTag : commitId + })) + }, + services: V02_SERVICE_IDS.map((serviceId, index) => { + if (serviceId !== "hwlab-device-pod") { + return { + serviceId, + status: "published", + artifactRequired: true, + commitId, + sourceCommitId: commitId, + image: `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`, + imageTag: commitId, + digest: digestFor(index), + repositoryDigest: `127.0.0.1:5000/hwlab/${serviceId}@${digestFor(index)}` + }; + } + return { + serviceId, + status: "published", + artifactRequired: true, + commitId, + sourceCommitId: commitId, + image: `127.0.0.1:5000/hwlab/hwlab-device-pod-env:${envTag}`, + imageTag: envTag, + digest: digestFor(index), + repositoryDigest: `127.0.0.1:5000/hwlab/hwlab-device-pod-env@${digestFor(index)}`, + runtimeMode: "env-reuse-git-mirror-checkout", + envReuse: true, + environmentImage: `127.0.0.1:5000/hwlab/hwlab-device-pod-env:${envTag}`, + environmentDigest: digestFor(index), + environmentInputHash, + codeInputHash: "96cd701e01ab9fab8d4daafd30b2206f65f5e72d23adb72dc4ad23fd9310f97e", + bootRepo: "git@github.com:pikasTech/HWLAB.git", + bootCommit: commitId, + bootSh: "deploy/runtime/boot/hwlab-device-pod.sh" + }; + }) + } + }, null, 2)}\n`); + + const result = await execFileAsync(process.execPath, [ + "scripts/refresh-artifact-catalog.mjs", + "--lane", + "v02", + "--image-tag-mode", + "full", + "--target-ref", + "HEAD", + "--catalog-path", + path.join(tempDir, "artifact-catalog.v02.json"), + "--publish-report", + reportPath, + "--no-write" + ], { + cwd: repoRoot, + timeout: 15000, + maxBuffer: 10 * 1024 * 1024 + }); + const payload = JSON.parse(result.stdout); + const devicePod = payload.services.find((service) => service.serviceId === "hwlab-device-pod"); + assert.equal(payload.status, "published"); + assert.equal(devicePod.imageTag, envTag); + assert.equal(devicePod.runtimeMode, "env-reuse-git-mirror-checkout"); + assert.equal(devicePod.environmentInputHash, environmentInputHash); + assert.equal(devicePod.bootCommit, commitId); + assert.equal(devicePod.bootSh, "deploy/runtime/boot/hwlab-device-pod.sh"); +});