From 7fc6603c30326f6e07d24d1c77f4282def431370 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 4 Jun 2026 15:19:38 +0800 Subject: [PATCH] fix(v02): remove stale endpoint contract freeze --- scripts/artifact-publish.mjs | 17 +++++- scripts/refresh-artifact-catalog.mjs | 29 +++++++--- scripts/refresh-artifact-catalog.test.mjs | 66 +++++++++++++++++++++++ 3 files changed, 103 insertions(+), 9 deletions(-) diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs index a4bc4216..d98c3e1f 100644 --- a/scripts/artifact-publish.mjs +++ b/scripts/artifact-publish.mjs @@ -584,14 +584,25 @@ function laneDeployConfig(deployManifest, lane) { return null; } +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 assertV02Contracts(catalog, deployManifest) { const lane = laneDeployConfig(deployManifest, "v02"); assert.ok(lane && typeof lane === "object", "deploy.lanes.v02 must exist for v02 artifact publish"); assert.equal(lane.namespace, "hwlab-v02", "deploy.lanes.v02.namespace must be hwlab-v02"); - assert.equal(lane.endpoint, "http://74.48.78.17:19667", "deploy.lanes.v02.endpoint must be the v02 API endpoint"); + assert.ok(isHttpEndpoint(lane.endpoint), "deploy.lanes.v02.endpoint must be a non-empty http(s) URL"); assert.equal(catalog.environment, "v02", "catalog.environment must be v02"); assert.equal(catalog.profile, "v02", "catalog.profile must be v02"); assert.equal(catalog.namespace, "hwlab-v02", "catalog.namespace must be hwlab-v02"); + if (catalog.endpoint != null) assert.ok(isHttpEndpoint(catalog.endpoint), "catalog.endpoint must be a non-empty http(s) URL"); assert.deepEqual(catalog.allowedProfiles, ["v02"], "catalog.allowedProfiles must only allow v02"); assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog.forbiddenProfiles must forbid dev/prod"); const catalogIds = (catalog.services ?? []).map((service) => service.serviceId); @@ -618,13 +629,15 @@ function artifactEnvironment(args) { function artifactCatalogSkeleton({ args, deployManifest, commitId }) { const tag = imageTagForCommit(args, commitId); + const laneEndpoint = args.lane === "v02" ? deployManifest?.lanes?.v02?.endpoint : "http://74.48.78.17:16667"; + assert.ok(isHttpEndpoint(laneEndpoint), "deploy lane endpoint must be a non-empty http(s) URL"); return { catalogVersion: "v1", kind: "hwlab-artifact-catalog", environment: artifactEnvironment(args), profile: artifactEnvironment(args), namespace: args.lane === "v02" ? "hwlab-v02" : "hwlab-dev", - endpoint: args.lane === "v02" ? "http://74.48.78.17:19667" : "http://74.48.78.17:16667", + endpoint: laneEndpoint, commitId: tag, artifactState: "contract-skeleton", publish: { diff --git a/scripts/refresh-artifact-catalog.mjs b/scripts/refresh-artifact-catalog.mjs index 6f654d1a..4acf0fba 100644 --- a/scripts/refresh-artifact-catalog.mjs +++ b/scripts/refresh-artifact-catalog.mjs @@ -172,11 +172,24 @@ function artifactNamespace(args) { return args.lane === "v02" ? "hwlab-v02" : "hwlab-dev"; } -function artifactEndpoint(args) { - return args.lane === "v02" ? "http://74.48.78.17:19667" : DEV_ENDPOINT; +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 artifactCatalogSkeleton({ args, target, registryPrefix = defaultRegistryPrefix }) { +function artifactEndpoint(args, deploy) { + if (args.lane !== "v02") return DEV_ENDPOINT; + const endpoint = deploy?.lanes?.v02?.endpoint; + assert.ok(isHttpEndpoint(endpoint), "deploy.lanes.v02.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); return { @@ -185,7 +198,7 @@ function artifactCatalogSkeleton({ args, target, registryPrefix = defaultRegistr environment, profile: environment, namespace, - endpoint: artifactEndpoint(args), + endpoint: artifactEndpoint(args, deploy), commitId: target.imageTag, artifactState: "contract-skeleton", publish: { @@ -303,13 +316,13 @@ function assertV02DeployAndCatalog(deploy, catalog) { const lane = deploy?.lanes?.v02; assert.ok(lane && typeof lane === "object", "deploy.lanes.v02 must exist for v02 catalog refresh"); assert.equal(lane.namespace, "hwlab-v02", "deploy.lanes.v02.namespace must be hwlab-v02"); - assert.equal(lane.endpoint, "http://74.48.78.17:19667", "deploy.lanes.v02.endpoint must be the v02 API endpoint"); + assert.ok(isHttpEndpoint(lane.endpoint), "deploy.lanes.v02.endpoint must be a non-empty http(s) URL"); assert.deepEqual((deploy.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "deploy services must cover frozen service IDs in order"); assert.equal(catalog.environment, "v02", "catalog environment must be v02"); assert.equal(catalog.profile, "v02", "catalog profile must be v02"); assert.equal(catalog.namespace, "hwlab-v02", "catalog namespace must be hwlab-v02"); - assert.equal(catalog.endpoint, "http://74.48.78.17:19667", "catalog endpoint must be the v02 API endpoint"); + if (catalog.endpoint != null) assert.ok(isHttpEndpoint(catalog.endpoint), "catalog endpoint must be a non-empty http(s) URL"); assert.deepEqual(catalog.allowedProfiles, ["v02"], "catalog allowedProfiles must only allow v02"); assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog forbiddenProfiles must forbid dev/prod"); assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), serviceIdsForLane("v02"), "catalog services must cover retained v0.2 service IDs in order"); @@ -425,6 +438,7 @@ function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInve const nextCatalogServices = []; catalog.commitId = target.imageTag; + if (catalog.profile === "v02") catalog.endpoint = artifactEndpoint({ lane: "v02" }, deploy); catalog.artifactState = published ? "published" : "contract-skeleton"; catalog.serviceInventory = serviceInventory; catalog.publish = { @@ -545,7 +559,7 @@ async function main() { args.publishReportPath ? readJson(args.publishReportPath) : Promise.resolve(null) ]); let catalog = await readJsonIfPresent(args.catalogPath, null); - if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, target, registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix }); + if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, deploy, target, registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix }); catalog = normalizeCatalogForLane(catalog, args.lane); assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} catalog refresh`); assertLaneDeployAndCatalog(args, deploy, catalog); @@ -581,6 +595,7 @@ async function main() { artifactCommitId: target.imageTag, lane: args.lane, catalogPath: args.catalogPath, + endpoint: catalog.endpoint ?? null, imageTagMode: args.imageTagMode, wrote: args.write ? [args.catalogPath] : [], deployTruthPolicy: "deploy.json is human-authored runtime config; artifact promotion must not rewrite it", diff --git a/scripts/refresh-artifact-catalog.test.mjs b/scripts/refresh-artifact-catalog.test.mjs index 9f8e2b29..356e80e1 100644 --- a/scripts/refresh-artifact-catalog.test.mjs +++ b/scripts/refresh-artifact-catalog.test.mjs @@ -353,6 +353,72 @@ test("v02 refresh accepts env-reuse device-pod image tag from environment hash", assert.equal(devicePod.bootSh, "deploy/runtime/boot/hwlab-device-pod.sh"); }); +test("v02 refresh follows the current deploy endpoint instead of the retired 19667 freeze", async () => { + const commitId = await git(["rev-parse", "HEAD"]); + const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-v02-https-endpoint-")); + const deployPath = path.join(tempDir, "deploy.json"); + const catalogPath = path.join(tempDir, "artifact-catalog.v02.json"); + await writeFile(deployPath, `${JSON.stringify({ + environment: "dev", + namespace: "hwlab-dev", + endpoint: "http://74.48.78.17:16667", + profiles: { dev: { enabled: true }, prod: { enabled: false } }, + lanes: { + v02: { + namespace: "hwlab-v02", + endpoint: "https://hwlab.74-48-78-17.nip.io" + } + }, + services: SERVICE_IDS.map((serviceId) => ({ serviceId })) + }, null, 2)}\n`); + await writeFile(catalogPath, `${JSON.stringify({ + catalogVersion: "v1", + kind: "hwlab-artifact-catalog", + environment: "v02", + profile: "v02", + namespace: "hwlab-v02", + endpoint: "http://74.48.78.17:19667", + commitId, + artifactState: "contract-skeleton", + publish: { registryPrefix: "127.0.0.1:5000/hwlab" }, + allowedProfiles: ["v02"], + forbiddenProfiles: ["dev", "prod"], + services: V02_SERVICE_IDS.map((serviceId) => ({ + serviceId, + profile: "v02", + namespace: "hwlab-v02", + commitId, + sourceCommitId: commitId, + image: `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`, + imageTag: commitId, + digest: "not_published" + })) + }, null, 2)}\n`); + + const result = await execFileAsync(process.execPath, [ + "scripts/refresh-artifact-catalog.mjs", + "--lane", + "v02", + "--image-tag-mode", + "full", + "--target-ref", + "HEAD", + "--deploy-json", + deployPath, + "--catalog-path", + catalogPath, + "--blocked", + "--no-write" + ], { + cwd: repoRoot, + timeout: 15000, + maxBuffer: 10 * 1024 * 1024 + }); + const payload = JSON.parse(result.stdout); + assert.equal(payload.status, "blocked"); + assert.equal(payload.endpoint, "https://hwlab.74-48-78-17.nip.io"); +}); + test("v02 refresh reports code-only env-reuse records as reused", async () => { const commitId = await git(["rev-parse", "HEAD"]); const previousCommitId = "1234567890abcdef1234567890abcdef12345678";