From e63e2afbe47c6499b75d8fb67338761271e42798 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 8 Jun 2026 15:14:14 +0800 Subject: [PATCH] Generalize artifact publish runtime lanes --- scripts/artifact-publish.mjs | 98 +++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs index 803e7820..0f8a833f 100644 --- a/scripts/artifact-publish.mjs +++ b/scripts/artifact-publish.mjs @@ -51,6 +51,7 @@ const sourceStates = new Set([ "source-present", "intentionally-disabled" ]); +const runtimeArtifactLanePattern = /^v\d{2,}$/u; const ciArtifactIdentityEnvNames = [ "HWLAB_CI_ARTIFACT_RUN_ID", @@ -120,9 +121,8 @@ function parseArgs(argv) { else throw new Error(`unknown argument ${arg}`); } - assert.ok(["g14", "v02"].includes(args.lane), `unknown artifact lane ${args.lane}`); + assert.ok(args.lane === "g14" || isRuntimeArtifactLane(args.lane), `unknown artifact lane ${args.lane}`); assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`); - if (!args.servicesExplicit && args.lane !== "v02") args.services = [...SERVICE_IDS]; return args; } @@ -197,13 +197,13 @@ function printHelp() { "Commit-pinned artifact build/publish helper for controlled CI execution.", "", "options:", - " --lane LANE g14 or v02; default: g14", + " --lane LANE g14 or runtime lane vNN; default: g14", ` --catalog-path PATH default: ${defaultCatalogPath}`, ` --deploy-config PATH default: ${defaultDeployPath}`, " --image-tag-mode MODE short or full; v02 uses full source commit IDs", " --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; v02 default comes from deploy.lanes.v02.envReuseServices", + " --services LIST comma-separated service IDs; runtime lane default comes from deploy.lanes..envReuseServices", " --affected-only default and only supported scope: build/publish affected services and reuse unchanged catalog digests", " --buildkit-command PATH buildctl-daemonless.sh/buildctl command for buildkit backend", " --buildkit-addr ADDR optional buildctl --addr endpoint for a sidecar BuildKit daemon", @@ -563,8 +563,11 @@ function assertDevOnlyContracts(catalog, deployManifest) { } function laneDeployConfig(deployManifest, lane) { - if (lane === "v02") return deployManifest?.lanes?.v02 ?? null; - return null; + return deployManifest?.lanes?.[lane] ?? null; +} + +function isRuntimeArtifactLane(lane) { + return runtimeArtifactLanePattern.test(String(lane ?? "")); } function isHttpEndpoint(value) { @@ -577,33 +580,33 @@ function isHttpEndpoint(value) { } } -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.ok(isHttpEndpoint(lane.endpoint), "deploy.lanes.v02.endpoint must be a non-empty http(s) URL"); - const declaredServiceIds = new Set(v02ServiceIdsFromDeclarations(deployManifest)); +function assertRuntimeLaneContracts(catalog, deployManifest, laneName) { + const lane = laneDeployConfig(deployManifest, laneName); + assert.ok(lane && typeof lane === "object", `deploy.lanes.${laneName} must exist for ${laneName} artifact publish`); + assert.ok(String(lane.namespace ?? "").trim(), `deploy.lanes.${laneName}.namespace must not be empty`); + assert.ok(isHttpEndpoint(lane.endpoint), `deploy.lanes.${laneName}.endpoint must be a non-empty http(s) URL`); + const declaredServiceIds = new Set(runtimeLaneServiceIdsFromDeclarations(deployManifest, laneName)); assert.ok( - v02ServiceIdsFromDeploy(deployManifest).every((serviceId) => declaredServiceIds.has(serviceId)), - "deploy.lanes.v02.serviceDeclarations must cover envReuseServices" + runtimeLaneServiceIdsFromDeploy(deployManifest, laneName).every((serviceId) => declaredServiceIds.has(serviceId)), + `deploy.lanes.${laneName}.serviceDeclarations must cover envReuseServices` ); - 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.environment, laneName, `catalog.environment must be ${laneName}`); + assert.equal(catalog.profile, laneName, `catalog.profile must be ${laneName}`); + assert.equal(catalog.namespace, lane.namespace, `catalog.namespace must be ${lane.namespace}`); 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.allowedProfiles, [laneName], `catalog.allowedProfiles must only allow ${laneName}`); assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog.forbiddenProfiles must forbid dev/prod"); const catalogIds = (catalog.services ?? []).map((service) => service.serviceId); - assert.deepEqual(catalogIds, v02ServiceIdsFromDeploy(deployManifest), "v02 catalog services must match deploy.lanes.v02.envReuseServices"); + assert.deepEqual(catalogIds, runtimeLaneServiceIdsFromDeploy(deployManifest, laneName), `${laneName} catalog services must match deploy.lanes.${laneName}.envReuseServices`); for (const service of catalog.services ?? []) { - assert.equal(service.profile, "v02", `${service.serviceId}.profile must be v02`); - assert.equal(service.namespace, "hwlab-v02", `${service.serviceId}.namespace must be hwlab-v02`); + assert.equal(service.profile, laneName, `${service.serviceId}.profile must be ${laneName}`); + assert.equal(service.namespace, lane.namespace, `${service.serviceId}.namespace must be ${lane.namespace}`); assert.ok(!String(service.image).includes("prod"), `${service.serviceId}.image must not target prod`); } } function assertArtifactContracts(args, catalog, deployManifest) { - if (args.lane === "v02") return assertV02Contracts(catalog, deployManifest); + if (isRuntimeArtifactLane(args.lane)) return assertRuntimeLaneContracts(catalog, deployManifest, args.lane); return assertDevOnlyContracts(catalog, deployManifest); } @@ -612,19 +615,22 @@ function imageTagForCommit(args, commitId) { } function artifactEnvironment(args) { - return args.lane === "v02" ? "v02" : ENVIRONMENT_DEV; + return isRuntimeArtifactLane(args.lane) ? args.lane : ENVIRONMENT_DEV; } 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"; + const laneConfig = laneDeployConfig(deployManifest, args.lane); + const laneEndpoint = isRuntimeArtifactLane(args.lane) ? laneConfig?.endpoint : "http://74.48.78.17:16667"; + const namespace = isRuntimeArtifactLane(args.lane) ? laneConfig?.namespace : "hwlab-dev"; assert.ok(isHttpEndpoint(laneEndpoint), "deploy lane endpoint must be a non-empty http(s) URL"); + assert.ok(String(namespace ?? "").trim(), "deploy lane namespace must not be empty"); return { catalogVersion: "v1", kind: "hwlab-artifact-catalog", environment: artifactEnvironment(args), profile: artifactEnvironment(args), - namespace: args.lane === "v02" ? "hwlab-v02" : "hwlab-dev", + namespace, endpoint: laneEndpoint, commitId: tag, artifactState: "contract-skeleton", @@ -635,12 +641,14 @@ function artifactCatalogSkeleton({ args, deployManifest, commitId }) { note: `${artifactEnvironment(args)} artifact catalog skeleton initialized without DEV/G14 fallback.` }, allowedProfiles: [artifactEnvironment(args)], - forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"], + forbiddenProfiles: isRuntimeArtifactLane(args.lane) ? ["dev", "prod"] : ["prod"], services: serviceIdsForArtifactLane(args, deployManifest).map((serviceId) => artifactCatalogSkeletonService({ args, deployManifest, serviceId, tag, commitId })) }; } function artifactCatalogSkeletonService({ args, deployManifest, serviceId, tag, commitId }) { + const laneConfig = laneDeployConfig(deployManifest, args.lane); + const namespace = isRuntimeArtifactLane(args.lane) ? laneConfig?.namespace : "hwlab-dev"; const base = { serviceId, commitId: tag, @@ -650,7 +658,7 @@ function artifactCatalogSkeletonService({ args, deployManifest, serviceId, tag, digest: "not_published", publishState: "skeleton-only", profile: artifactEnvironment(args), - namespace: args.lane === "v02" ? "hwlab-v02" : "hwlab-dev", + namespace, healthPath: "/health/live", sourceState: "source-present", publishEnabled: true, @@ -660,7 +668,7 @@ function artifactCatalogSkeletonService({ args, deployManifest, serviceId, tag, }; const envReuseServiceIds = envReuseServiceIdsForLane(deployManifest, args.lane); if (!envReuseServiceIds.has(serviceId)) return base; - const bootSh = bootShForService(deployManifest, serviceId); + const bootSh = bootShForService(deployManifest, serviceId, args.lane); const environmentImage = envReuseImageRef(args.registryPrefix, serviceId, commitId); return { ...base, @@ -680,14 +688,14 @@ function artifactCatalogSkeletonService({ args, deployManifest, serviceId, tag, } function envReuseServiceIdsForLane(deployManifest, lane) { - if (lane !== "v02") return new Set(); - const configured = deployManifest?.lanes?.v02?.envReuseServices; + if (!isRuntimeArtifactLane(lane)) return new Set(); + const configured = deployManifest?.lanes?.[lane]?.envReuseServices; return new Set(normalizeServiceIdList(configured)); } function serviceIdsForArtifactLane(args, deployManifest) { - if (args.lane !== "v02") return [...SERVICE_IDS]; - return v02ServiceIdsFromDeploy(deployManifest); + if (!isRuntimeArtifactLane(args.lane)) return [...SERVICE_IDS]; + return runtimeLaneServiceIdsFromDeploy(deployManifest, args.lane); } function applyDeployDefaultServices(args, deployManifest) { @@ -695,14 +703,14 @@ function applyDeployDefaultServices(args, deployManifest) { args.services = serviceIdsForArtifactLane(args, deployManifest); } -function v02ServiceIdsFromDeploy(deployManifest) { - const serviceIds = normalizeServiceIdList(deployManifest?.lanes?.v02?.envReuseServices); - assert.ok(serviceIds.length > 0, "deploy.lanes.v02.envReuseServices must not be empty"); +function runtimeLaneServiceIdsFromDeploy(deployManifest, lane) { + const serviceIds = normalizeServiceIdList(deployManifest?.lanes?.[lane]?.envReuseServices); + assert.ok(serviceIds.length > 0, `deploy.lanes.${lane}.envReuseServices must not be empty`); return serviceIds; } -function v02ServiceIdsFromDeclarations(deployManifest) { - return normalizeServiceIdList(Object.keys(deployManifest?.lanes?.v02?.serviceDeclarations ?? {})); +function runtimeLaneServiceIdsFromDeclarations(deployManifest, lane) { + return normalizeServiceIdList(Object.keys(deployManifest?.lanes?.[lane]?.serviceDeclarations ?? {})); } function normalizeServiceIdList(value) { @@ -720,17 +728,17 @@ function uniquePreserveOrder(values) { return result; } -function bootShForService(deployManifest, serviceId) { - const value = deployManifest?.lanes?.v02?.bootScripts?.[serviceId]; - assert.ok(value, `deploy.lanes.v02.bootScripts.${serviceId} is required`); +function bootShForService(deployManifest, serviceId, lane) { + const value = deployManifest?.lanes?.[lane]?.bootScripts?.[serviceId]; + assert.ok(value, `deploy.lanes.${lane}.bootScripts.${serviceId} is required`); const text = String(value).replaceAll("\\", "/").replace(/^\.\//u, ""); assert.ok(text && !text.startsWith("/") && !text.split("/").includes(".."), `${serviceId} boot script must be repo-relative`); return text; } function normalizeCatalogForLane(catalog, lane, deployManifest) { - if (!catalog || lane !== "v02") return catalog; - const allowed = new Set(v02ServiceIdsFromDeploy(deployManifest)); + if (!catalog || !isRuntimeArtifactLane(lane)) return catalog; + const allowed = new Set(runtimeLaneServiceIdsFromDeploy(deployManifest, lane)); return { ...catalog, services: (catalog.services ?? []).filter((service) => allowed.has(service.serviceId)) @@ -2154,13 +2162,13 @@ async function main() { applyDeployDefaultServices(args, deployManifest); let catalog = await readConfigIfPresent(args.catalogPath, null); const catalogWasMissing = !catalog; - if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, deployManifest, commitId }); + if (!catalog && isRuntimeArtifactLane(args.lane)) catalog = artifactCatalogSkeleton({ args, deployManifest, commitId }); catalog = normalizeCatalogForLane(catalog, args.lane, deployManifest); assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} artifact publish`); const shortCommit = imageTagForCommit(args, commitId); let effectiveCatalogPath = args.catalogPath; - if (catalogWasMissing && args.lane === "v02") { - effectiveCatalogPath = path.join(os.tmpdir(), `hwlab-v02-artifact-catalog-${process.pid}.json`); + if (catalogWasMissing && isRuntimeArtifactLane(args.lane)) { + effectiveCatalogPath = path.join(os.tmpdir(), `hwlab-${args.lane}-artifact-catalog-${process.pid}.json`); await writeFile(effectiveCatalogPath, `${JSON.stringify(catalog, null, 2)}\n`); } const buildCreatedAt = new Date().toISOString();