Generalize CI plan runtime lanes
This commit is contained in:
@@ -8,7 +8,8 @@ import { readStructuredFileIfPresent } from "./structured-config.mjs";
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const G14_CI_PLAN_VERSION = "v1";
|
||||
export const V02_ENV_REUSE_RUNTIME_MODE = "env-reuse-git-mirror-checkout";
|
||||
export const ENV_REUSE_RUNTIME_MODE = "env-reuse-git-mirror-checkout";
|
||||
export const V02_ENV_REUSE_RUNTIME_MODE = ENV_REUSE_RUNTIME_MODE;
|
||||
|
||||
export const DEFAULT_RUNTIME_DEP_PATHS = Object.freeze([
|
||||
"package.json",
|
||||
@@ -51,25 +52,22 @@ export async function createG14CiPlan(options = {}) {
|
||||
const targetRef = options.targetRef ?? "HEAD";
|
||||
const baseRef = options.baseRef ?? await defaultBaseRef(repoRoot, targetRef);
|
||||
const lane = options.lane ?? "v02";
|
||||
if (lane !== "v02") throw new Error(`unsupported CI plan lane ${lane}; v02 env reuse is configured from deploy/deploy.yaml`);
|
||||
const artifactCatalogPath = options.artifactCatalogPath ?? (lane === "v02" ? "deploy/artifact-catalog.v02.json" : "deploy/artifact-catalog.dev.json");
|
||||
const registryPrefix = options.registryPrefix ?? process.env.HWLAB_DEV_REGISTRY_PREFIX ?? process.env.HWLAB_DEV_REGISTRY ?? "127.0.0.1:5000/hwlab";
|
||||
const baseImage = options.baseImage ?? process.env.HWLAB_DEV_BASE_IMAGE ?? "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
|
||||
|
||||
const [deployJson, artifactCatalog] = await Promise.all([
|
||||
readStructuredFileIfPresent(repoRoot, deployConfigPath, {}),
|
||||
readStructuredFileIfPresent(repoRoot, artifactCatalogPath, null)
|
||||
]);
|
||||
const deployJson = await readStructuredFileIfPresent(repoRoot, deployConfigPath, {});
|
||||
const laneConfig = laneDeployConfig(deployJson, lane);
|
||||
assertLaneEnvReuseConfig(laneConfig, lane);
|
||||
const artifactCatalogPath = options.artifactCatalogPath ?? defaultArtifactCatalogPath(laneConfig, lane);
|
||||
const artifactCatalog = await readStructuredFileIfPresent(repoRoot, artifactCatalogPath, null);
|
||||
const sourceCommitId = await gitValue(repoRoot, ["rev-parse", targetRef]);
|
||||
const shortCommitId = await gitValue(repoRoot, ["rev-parse", "--short=7", targetRef]);
|
||||
const changedPaths = await changedPathsBetween(repoRoot, baseRef, targetRef);
|
||||
const normalizedChangedPaths = changedPaths.map(normalizeRepoPath).filter(Boolean);
|
||||
const semanticRuntimeDepPaths = await runtimeDependencyChangedPaths(repoRoot, baseRef, targetRef, normalizedChangedPaths);
|
||||
const laneConfig = laneDeployConfig(deployJson, lane);
|
||||
assertV02EnvReuseConfig(laneConfig, lane);
|
||||
const serviceIdResolution = resolveServiceIds({ deployJson, options, laneConfig, lane });
|
||||
const envReuseRecipe = envReuseRecipeForLane(deployJson, lane);
|
||||
const componentModels = componentModelsForServices(serviceIdResolution.serviceIds, laneConfig);
|
||||
const componentModels = componentModelsForServices(serviceIdResolution.serviceIds, laneConfig, lane);
|
||||
const envReuseServices = enabledEnvReuseServices(deployJson, lane, serviceIdResolution.serviceIds);
|
||||
const globalChange = classifyGlobalChange(normalizedChangedPaths);
|
||||
const dockerfileHash = await hashGitPaths(repoRoot, targetRef, [
|
||||
@@ -85,8 +83,8 @@ export async function createG14CiPlan(options = {}) {
|
||||
const rawRuntimeDepMatches = matchingPaths(normalizedChangedPaths, model.runtimeDeps);
|
||||
const runtimeDepMatches = rawRuntimeDepMatches.filter((item) => semanticRuntimeDepPaths.has(item));
|
||||
const buildSystemMatches = matchingPaths(normalizedChangedPaths, model.buildSystemPaths);
|
||||
const envReuse = lane === "v02" && envReuseServices.has(model.serviceId);
|
||||
const bootSh = envReuse ? bootShForService(deployJson, model.serviceId) : null;
|
||||
const envReuse = envReuseServices.has(model.serviceId);
|
||||
const bootSh = envReuse ? bootShForService(deployJson, model.serviceId, lane) : null;
|
||||
const envInputPaths = envReuse ? uniqueSorted([
|
||||
...model.runtimeDeps,
|
||||
...envReuseRecipe.launcherInputPaths
|
||||
@@ -196,13 +194,13 @@ export async function createG14CiPlan(options = {}) {
|
||||
affected,
|
||||
buildRequired,
|
||||
componentInputChanged,
|
||||
runtimeMode: envReuse ? V02_ENV_REUSE_RUNTIME_MODE : "service-image",
|
||||
runtimeMode: envReuse ? ENV_REUSE_RUNTIME_MODE : "service-image",
|
||||
envReuse,
|
||||
envChanged,
|
||||
codeChanged,
|
||||
environmentInputHash,
|
||||
codeInputHash,
|
||||
bootRepo: envReuse ? canonicalBootRepo(deployJson) : null,
|
||||
bootRepo: envReuse ? canonicalBootRepo(deployJson, lane) : null,
|
||||
bootCommit: envReuse ? sourceCommitId : null,
|
||||
bootSh,
|
||||
environmentImage,
|
||||
@@ -234,9 +232,10 @@ export async function createG14CiPlan(options = {}) {
|
||||
currentRenderCompatible: true,
|
||||
plannerMutatesDeployJson: false,
|
||||
serviceIdSource: serviceIdResolution.source,
|
||||
serviceDeclarationSource: lane === "v02" ? `deploy.lanes.${lane}.serviceDeclarations` : "not-applicable",
|
||||
envRecipeSource: lane === "v02" ? `deploy.lanes.${lane}.envRecipe` : "not-applicable",
|
||||
v02EnvReuseServiceIds: [...envReuseServices],
|
||||
serviceDeclarationSource: `deploy.lanes.${lane}.serviceDeclarations`,
|
||||
envRecipeSource: `deploy.lanes.${lane}.envRecipe`,
|
||||
envReuseServiceIds: [...envReuseServices],
|
||||
v02EnvReuseServiceIds: lane === "v02" ? [...envReuseServices] : [],
|
||||
defaultComponentLazyBuild: true,
|
||||
envReuseCodeOnlyFastLane: true,
|
||||
fullBuildFallback: false
|
||||
@@ -267,9 +266,9 @@ export async function createG14CiPlan(options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export function componentModelsForServices(serviceIds, laneConfig = null) {
|
||||
export function componentModelsForServices(serviceIds, laneConfig = null, lane = "v02") {
|
||||
return serviceIds.map((serviceId) => {
|
||||
const declaration = serviceDeclarationFor(laneConfig, serviceId);
|
||||
const declaration = serviceDeclarationFor(laneConfig, serviceId, lane);
|
||||
return {
|
||||
serviceId,
|
||||
runtimeKind: declaration.runtimeKind,
|
||||
@@ -280,18 +279,18 @@ export function componentModelsForServices(serviceIds, laneConfig = null) {
|
||||
componentPaths: uniqueSorted(declaration.componentPaths.map(normalizeRepoPath)),
|
||||
sharedPaths: uniqueSorted(DEFAULT_SHARED_RUNTIME_PATHS.map(normalizeRepoPath)),
|
||||
runtimeDeps: uniqueSorted(DEFAULT_RUNTIME_DEP_PATHS.map(normalizeRepoPath)),
|
||||
buildSystemPaths: uniqueSorted(envReuseRecipeForLaneConfig(laneConfig).additionalEnvPaths)
|
||||
buildSystemPaths: uniqueSorted(envReuseRecipeForLaneConfig(laneConfig, lane).additionalEnvPaths)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function serviceDeclarationFor(laneConfig, serviceId) {
|
||||
function serviceDeclarationFor(laneConfig, serviceId, lane) {
|
||||
const configured = laneConfig?.serviceDeclarations?.[serviceId];
|
||||
const runtimeKind = requireDeclarationString(configured?.runtimeKind, `serviceDeclarations.${serviceId}.runtimeKind`);
|
||||
const entrypoint = requireDeclarationString(configured?.entrypoint, `serviceDeclarations.${serviceId}.entrypoint`);
|
||||
const runtimeKind = requireDeclarationString(configured?.runtimeKind, lane, `serviceDeclarations.${serviceId}.runtimeKind`);
|
||||
const entrypoint = requireDeclarationString(configured?.entrypoint, lane, `serviceDeclarations.${serviceId}.entrypoint`);
|
||||
const artifactKind = normalizeDeclarationString(configured?.artifactKind) || runtimeKind;
|
||||
if (!Array.isArray(configured?.componentPaths) || configured.componentPaths.length === 0) {
|
||||
throw new Error(`deploy.lanes.v02.serviceDeclarations.${serviceId}.componentPaths is required`);
|
||||
throw new Error(`deploy.lanes.${lane}.serviceDeclarations.${serviceId}.componentPaths is required`);
|
||||
}
|
||||
return {
|
||||
runtimeKind,
|
||||
@@ -303,9 +302,9 @@ function serviceDeclarationFor(laneConfig, serviceId) {
|
||||
};
|
||||
}
|
||||
|
||||
function requireDeclarationString(value, label) {
|
||||
function requireDeclarationString(value, lane, label) {
|
||||
const text = normalizeDeclarationString(value);
|
||||
if (!text) throw new Error(`deploy.lanes.v02.${label} is required`);
|
||||
if (!text) throw new Error(`deploy.lanes.${lane}.${label} is required`);
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -318,27 +317,31 @@ function laneDeployConfig(deployJson, lane) {
|
||||
return deployJson?.lanes?.[lane] ?? null;
|
||||
}
|
||||
|
||||
export function envReuseRecipeForLane(deployJson, lane = "v02") {
|
||||
return envReuseRecipeForLaneConfig(laneDeployConfig(deployJson, lane));
|
||||
function defaultArtifactCatalogPath(laneConfig, lane) {
|
||||
return normalizeDeclarationString(laneConfig?.artifactCatalog) ?? `deploy/artifact-catalog.${lane}.json`;
|
||||
}
|
||||
|
||||
function envReuseRecipeForLaneConfig(laneConfig) {
|
||||
export function envReuseRecipeForLane(deployJson, lane = "v02") {
|
||||
return envReuseRecipeForLaneConfig(laneDeployConfig(deployJson, lane), lane);
|
||||
}
|
||||
|
||||
function envReuseRecipeForLaneConfig(laneConfig, lane = "v02") {
|
||||
const configured = laneConfig?.envRecipe;
|
||||
if (!configured) throw new Error("deploy.lanes.v02.envRecipe is required");
|
||||
if (!configured) throw new Error(`deploy.lanes.${lane}.envRecipe is required`);
|
||||
const osPackages = normalizeStringList(configured.osPackages, []);
|
||||
const bunVersion = requireDeclarationString(configured.bunVersion, "envRecipe.bunVersion");
|
||||
const launcherPath = normalizeRepoPath(requireDeclarationString(configured.launcherPath, "envRecipe.launcherPath"));
|
||||
const bunVersion = requireDeclarationString(configured.bunVersion, lane, "envRecipe.bunVersion");
|
||||
const launcherPath = normalizeRepoPath(requireDeclarationString(configured.launcherPath, lane, "envRecipe.launcherPath"));
|
||||
const runtimeNodeModulesPath = normalizeAbsolutePath(configured.runtimeNodeModulesPath);
|
||||
if (!runtimeNodeModulesPath) throw new Error("deploy.lanes.v02.envRecipe.runtimeNodeModulesPath must be absolute");
|
||||
if (!runtimeNodeModulesPath) throw new Error(`deploy.lanes.${lane}.envRecipe.runtimeNodeModulesPath must be absolute`);
|
||||
const additionalEnvPaths = uniqueSorted(normalizeStringList(configured.additionalEnvPaths, []).map(normalizeRepoPath));
|
||||
const launcherInputPaths = uniqueSorted([
|
||||
launcherPath,
|
||||
...additionalEnvPaths
|
||||
]);
|
||||
const hwpodAliases = normalizeHwpodAliases(configured.hwpodAliases);
|
||||
if (osPackages.length === 0) throw new Error("deploy.lanes.v02.envRecipe.osPackages is required");
|
||||
if (additionalEnvPaths.length === 0) throw new Error("deploy.lanes.v02.envRecipe.additionalEnvPaths is required");
|
||||
if (hwpodAliases.length === 0) throw new Error("deploy.lanes.v02.envRecipe.hwpodAliases is required");
|
||||
if (osPackages.length === 0) throw new Error(`deploy.lanes.${lane}.envRecipe.osPackages is required`);
|
||||
if (additionalEnvPaths.length === 0) throw new Error(`deploy.lanes.${lane}.envRecipe.additionalEnvPaths is required`);
|
||||
if (hwpodAliases.length === 0) throw new Error(`deploy.lanes.${lane}.envRecipe.hwpodAliases is required`);
|
||||
return {
|
||||
osPackages,
|
||||
bunVersion,
|
||||
@@ -376,12 +379,11 @@ function normalizeHwpodAliases(value) {
|
||||
})).filter((item) => item.command && item.script);
|
||||
}
|
||||
|
||||
function assertV02EnvReuseConfig(laneConfig, lane) {
|
||||
if (lane !== "v02") return;
|
||||
function assertLaneEnvReuseConfig(laneConfig, lane) {
|
||||
if (!laneConfig?.serviceDeclarations || Object.keys(laneConfig.serviceDeclarations).length === 0) {
|
||||
throw new Error("deploy.lanes.v02.serviceDeclarations is required for v02 env reuse planning");
|
||||
throw new Error(`deploy.lanes.${lane}.serviceDeclarations is required for ${lane} env reuse planning`);
|
||||
}
|
||||
if (!laneConfig?.envRecipe) throw new Error("deploy.lanes.v02.envRecipe is required for v02 env reuse planning");
|
||||
if (!laneConfig?.envRecipe) throw new Error(`deploy.lanes.${lane}.envRecipe is required for ${lane} env reuse planning`);
|
||||
}
|
||||
|
||||
export async function runtimeDependencyChangedPaths(repoRoot, baseRef, targetRef, changedPaths) {
|
||||
@@ -425,25 +427,24 @@ async function readJsonFromGit(repoRoot, ref, filePath, fallback) {
|
||||
}
|
||||
|
||||
export function enabledEnvReuseServices(deployJson, lane, serviceIds) {
|
||||
if (lane !== "v02") return new Set();
|
||||
const configured = deployJson?.lanes?.v02?.envReuseServices;
|
||||
const configured = deployJson?.lanes?.[lane]?.envReuseServices;
|
||||
const values = Array.isArray(configured) ? configured : [];
|
||||
const allowed = new Set(serviceIds);
|
||||
return new Set(values.filter((serviceId) => allowed.has(serviceId)));
|
||||
}
|
||||
|
||||
export function bootShForService(deployJson, serviceId) {
|
||||
const configured = deployJson?.lanes?.v02?.bootScripts?.[serviceId];
|
||||
if (!configured) throw new Error(`deploy.lanes.v02.bootScripts.${serviceId} is required`);
|
||||
export function bootShForService(deployJson, serviceId, lane = "v02") {
|
||||
const configured = deployJson?.lanes?.[lane]?.bootScripts?.[serviceId];
|
||||
if (!configured) throw new Error(`deploy.lanes.${lane}.bootScripts.${serviceId} is required`);
|
||||
const bootSh = normalizeRepoPath(configured);
|
||||
if (!bootSh || bootSh.startsWith("/") || bootSh.split("/").includes("..")) {
|
||||
throw new Error(`invalid v02 boot script for ${serviceId}: ${configured}`);
|
||||
throw new Error(`invalid ${lane} boot script for ${serviceId}: ${configured}`);
|
||||
}
|
||||
return bootSh;
|
||||
}
|
||||
|
||||
function canonicalBootRepo(deployJson) {
|
||||
return deployJson?.lanes?.v02?.sourceRepo || "git@github.com:pikasTech/HWLAB.git";
|
||||
function canonicalBootRepo(deployJson, lane) {
|
||||
return deployJson?.lanes?.[lane]?.sourceRepo || "git@github.com:pikasTech/HWLAB.git";
|
||||
}
|
||||
|
||||
export function classifyGlobalChange(changedPaths) {
|
||||
@@ -549,12 +550,11 @@ function resolveServiceIds({ options, laneConfig, lane }) {
|
||||
return { source: "cli-services", serviceIds };
|
||||
}
|
||||
const configured = Array.isArray(laneConfig?.envReuseServices) ? laneConfig.envReuseServices.filter(Boolean) : [];
|
||||
if (configured.length === 0) throw new Error("deploy.lanes.v02.envReuseServices is required for v02 CI planning");
|
||||
return { source: "deploy.lanes.v02.envReuseServices", serviceIds: uniquePreserveOrder(configured) };
|
||||
if (configured.length === 0) throw new Error(`deploy.lanes.${lane}.envReuseServices is required for ${lane} CI planning`);
|
||||
return { source: `deploy.lanes.${lane}.envReuseServices`, serviceIds: uniquePreserveOrder(configured) };
|
||||
}
|
||||
|
||||
function knownServiceIds(laneConfig, lane) {
|
||||
if (lane !== "v02") return new Set();
|
||||
const values = new Set(Object.keys(laneConfig?.serviceDeclarations ?? {}));
|
||||
for (const serviceId of laneConfig?.envReuseServices ?? []) values.add(serviceId);
|
||||
return values;
|
||||
|
||||
Reference in New Issue
Block a user