Generalize artifact publish runtime lanes
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user