Generalize artifact publish runtime lanes

This commit is contained in:
Codex Agent
2026-06-08 15:14:14 +08:00
parent 5e59a75ff8
commit e63e2afbe4
+53 -45
View File
@@ -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.<lane>.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();