feat: add v02 device-pod env reuse boot path
This commit is contained in:
@@ -9,6 +9,7 @@ import { SERVICE_IDS } from "../../internal/protocol/index.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 DEFAULT_BUILD_SYSTEM_PATHS = Object.freeze([
|
||||
"scripts/artifact-publish.mjs",
|
||||
@@ -16,6 +17,14 @@ export const DEFAULT_BUILD_SYSTEM_PATHS = Object.freeze([
|
||||
"scripts/src/dev-artifact-services.mjs"
|
||||
]);
|
||||
|
||||
export const DEFAULT_ENV_REUSE_LAUNCHER_PATHS = Object.freeze([
|
||||
"deploy/runtime/launcher/",
|
||||
"scripts/artifact-publish.mjs",
|
||||
"scripts/g14-artifact-publish.mjs",
|
||||
"scripts/src/dev-artifact-services.mjs",
|
||||
"scripts/src/g14-ci-plan-lib.mjs"
|
||||
]);
|
||||
|
||||
export const DEFAULT_RUNTIME_DEP_PATHS = Object.freeze([
|
||||
"package.json",
|
||||
"package-lock.json"
|
||||
@@ -73,9 +82,10 @@ const bunCommandServices = new Set([
|
||||
export async function createG14CiPlan(options = {}) {
|
||||
const repoRoot = options.repoRoot ?? process.cwd();
|
||||
const deployJsonPath = options.deployJsonPath ?? "deploy/deploy.json";
|
||||
const artifactCatalogPath = options.artifactCatalogPath ?? "deploy/artifact-catalog.dev.json";
|
||||
const targetRef = options.targetRef ?? "HEAD";
|
||||
const baseRef = options.baseRef ?? await defaultBaseRef(repoRoot, targetRef);
|
||||
const lane = options.lane ?? (options.artifactCatalogPath?.includes(".v02.") ? "v02" : "g14");
|
||||
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";
|
||||
|
||||
@@ -89,6 +99,7 @@ export async function createG14CiPlan(options = {}) {
|
||||
const normalizedChangedPaths = changedPaths.map(normalizeRepoPath).filter(Boolean);
|
||||
const serviceIdResolution = resolveServiceIds({ deployJson, options });
|
||||
const componentModels = componentModelsForServices(serviceIdResolution.serviceIds);
|
||||
const envReuseServices = enabledEnvReuseServices(deployJson, lane, serviceIdResolution.serviceIds);
|
||||
const globalChange = classifyGlobalChange(normalizedChangedPaths);
|
||||
const dockerfileHash = await hashGitPaths(repoRoot, targetRef, [
|
||||
...DEFAULT_BUILD_SYSTEM_PATHS,
|
||||
@@ -102,6 +113,21 @@ export async function createG14CiPlan(options = {}) {
|
||||
const sharedMatches = matchingPaths(normalizedChangedPaths, model.sharedPaths);
|
||||
const runtimeDepMatches = matchingPaths(normalizedChangedPaths, model.runtimeDeps);
|
||||
const buildSystemMatches = matchingPaths(normalizedChangedPaths, model.buildSystemPaths);
|
||||
const envReuse = lane === "v02" && envReuseServices.has(model.serviceId);
|
||||
const bootSh = envReuse ? bootShForService(deployJson, model.serviceId) : null;
|
||||
const envInputPaths = envReuse ? uniqueSorted([
|
||||
...model.runtimeDeps,
|
||||
...DEFAULT_ENV_REUSE_LAUNCHER_PATHS.map(normalizeRepoPath)
|
||||
]) : [];
|
||||
const codeInputPaths = envReuse ? uniqueSorted([
|
||||
...model.componentPaths,
|
||||
...model.sharedPaths,
|
||||
bootSh
|
||||
]) : [];
|
||||
const envMatches = envReuse ? matchingPaths(normalizedChangedPaths, envInputPaths) : [];
|
||||
const codeMatches = envReuse ? matchingPaths(normalizedChangedPaths, codeInputPaths) : [];
|
||||
const relevantEnvMatches = envMatches.filter((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item));
|
||||
const relevantCodeMatches = codeMatches.filter((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item));
|
||||
const imageRelevantChangedPaths = uniqueSorted([
|
||||
...directMatches,
|
||||
...sharedMatches,
|
||||
@@ -109,8 +135,21 @@ export async function createG14CiPlan(options = {}) {
|
||||
...buildSystemMatches
|
||||
].filter((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item)));
|
||||
const catalogRecord = catalogByServiceId.get(model.serviceId) ?? null;
|
||||
const catalogReuse = reuseCandidate(catalogRecord, imageRelevantChangedPaths.length > 0);
|
||||
const affected = imageRelevantChangedPaths.length > 0 || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest";
|
||||
const environmentInputHash = envReuse ? await hashGitPaths(repoRoot, targetRef, envInputPaths, {
|
||||
skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath)
|
||||
}) : null;
|
||||
const codeInputHash = envReuse ? await hashGitPaths(repoRoot, targetRef, codeInputPaths, {
|
||||
skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath)
|
||||
}) : null;
|
||||
const environmentImage = envReuse ? environmentImageFromCatalog(model.serviceId, catalogRecord) : null;
|
||||
const environmentDigest = envReuse ? environmentDigestFromCatalog(catalogRecord) : null;
|
||||
const environmentReady = envReuse && /^sha256:[a-f0-9]{64}$/u.test(environmentDigest ?? "") && Boolean(environmentImage);
|
||||
const envChanged = envReuse ? relevantEnvMatches.length > 0 || !environmentReady || catalogRecord?.environmentInputHash !== environmentInputHash : null;
|
||||
const codeChanged = envReuse ? relevantCodeMatches.length > 0 || catalogRecord?.codeInputHash !== codeInputHash || catalogRecord?.bootCommit !== sourceCommitId : null;
|
||||
const catalogReuse = envReuse ? envReuseCandidate(catalogRecord, envChanged) : reuseCandidate(catalogRecord, imageRelevantChangedPaths.length > 0);
|
||||
const affected = envReuse
|
||||
? Boolean(envChanged || codeChanged)
|
||||
: imageRelevantChangedPaths.length > 0 || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest";
|
||||
const componentInputPaths = uniqueSorted([
|
||||
...model.componentPaths,
|
||||
...model.sharedPaths,
|
||||
@@ -144,10 +183,23 @@ export async function createG14CiPlan(options = {}) {
|
||||
buildArgsHash,
|
||||
changedPaths: imageRelevantChangedPaths,
|
||||
affected,
|
||||
runtimeMode: envReuse ? V02_ENV_REUSE_RUNTIME_MODE : "service-image",
|
||||
envReuse,
|
||||
envChanged,
|
||||
codeChanged,
|
||||
environmentInputHash,
|
||||
codeInputHash,
|
||||
bootRepo: envReuse ? canonicalBootRepo(deployJson) : null,
|
||||
bootCommit: envReuse ? sourceCommitId : null,
|
||||
bootSh,
|
||||
environmentImage,
|
||||
environmentDigest,
|
||||
envChangedPaths: relevantEnvMatches,
|
||||
codeChangedPaths: relevantCodeMatches,
|
||||
reason: affected
|
||||
? reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse })
|
||||
? reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse, envReuse, envChanged, codeChanged })
|
||||
: reasonForUnchanged(globalChange),
|
||||
reuse: reuseCandidate(catalogRecord, affected)
|
||||
reuse: catalogReuse
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,11 +214,13 @@ export async function createG14CiPlan(options = {}) {
|
||||
compatibility: {
|
||||
deployJson: deployJsonPath,
|
||||
artifactCatalog: artifactCatalog ? artifactCatalogPath : null,
|
||||
lane,
|
||||
ciContractSource: "scripts/g14-gitops-render.mjs",
|
||||
mode: "advisory-read-only",
|
||||
currentRenderCompatible: true,
|
||||
plannerMutatesDeployJson: false,
|
||||
serviceIdSource: serviceIdResolution.source,
|
||||
v02EnvReuseServiceIds: [...envReuseServices],
|
||||
defaultComponentLazyBuild: true,
|
||||
fullBuildFallback: false
|
||||
},
|
||||
@@ -205,6 +259,27 @@ export function componentModelsForServices(serviceIds) {
|
||||
});
|
||||
}
|
||||
|
||||
export function enabledEnvReuseServices(deployJson, lane, serviceIds) {
|
||||
if (lane !== "v02") return new Set();
|
||||
const configured = deployJson?.lanes?.v02?.envReuseServices;
|
||||
const values = Array.isArray(configured) ? configured : ["hwlab-device-pod"];
|
||||
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];
|
||||
const bootSh = normalizeRepoPath(configured || `deploy/runtime/boot/${serviceId}.sh`);
|
||||
if (!bootSh || bootSh.startsWith("/") || bootSh.split("/").includes("..")) {
|
||||
throw new Error(`invalid v02 boot script for ${serviceId}: ${configured}`);
|
||||
}
|
||||
return bootSh;
|
||||
}
|
||||
|
||||
function canonicalBootRepo(deployJson) {
|
||||
return deployJson?.lanes?.v02?.sourceRepo || "git@github.com:pikasTech/HWLAB.git";
|
||||
}
|
||||
|
||||
export function classifyGlobalChange(changedPaths) {
|
||||
const relevant = changedPaths.filter(Boolean);
|
||||
const testOnly = relevant.length > 0 && relevant.every(isTestOnlyPath);
|
||||
@@ -325,8 +400,10 @@ async function gitValue(repoRoot, args) {
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse = null }) {
|
||||
function reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse = null, envReuse = false, envChanged = null, codeChanged = null }) {
|
||||
const reasons = [];
|
||||
if (envReuse && envChanged) reasons.push("environment-input-changed");
|
||||
if (envReuse && codeChanged) reasons.push("code-input-changed");
|
||||
if (directMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("component-path-changed");
|
||||
if (sharedMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("shared-runtime-changed");
|
||||
if (runtimeDepMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("runtime-deps-changed");
|
||||
@@ -358,6 +435,33 @@ function reuseCandidate(catalogRecord, affected) {
|
||||
};
|
||||
}
|
||||
|
||||
function envReuseCandidate(catalogRecord, envChanged) {
|
||||
if (envChanged) return { status: "not-reused", reason: "environment-affected" };
|
||||
if (!catalogRecord) return { status: "candidate-no-catalog", reason: "no-previous-artifact-record" };
|
||||
const image = environmentImageFromCatalog(catalogRecord.serviceId, catalogRecord);
|
||||
const digest = environmentDigestFromCatalog(catalogRecord);
|
||||
if (!/^sha256:[a-f0-9]{64}$/u.test(digest ?? "")) {
|
||||
return { status: "candidate-unverified-digest", image, digest };
|
||||
}
|
||||
return {
|
||||
status: catalogRecord.environmentInputHash ? "ready" : "candidate-without-environment-metadata",
|
||||
image,
|
||||
digest,
|
||||
reusedFrom: catalogRecord.environmentInputHash ?? catalogRecord.commitId ?? catalogRecord.imageTag ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function environmentImageFromCatalog(serviceId, catalogRecord) {
|
||||
const image = catalogRecord?.environmentImage ?? null;
|
||||
if (image) return image;
|
||||
const fallback = catalogRecord?.image ?? null;
|
||||
return typeof fallback === "string" && fallback.includes(`/${serviceId}-env:`) ? fallback : null;
|
||||
}
|
||||
|
||||
function environmentDigestFromCatalog(catalogRecord) {
|
||||
return catalogRecord?.environmentDigest ?? (catalogRecord?.environmentImage ? catalogRecord?.digest : null) ?? null;
|
||||
}
|
||||
|
||||
function runtimeKindForService(serviceId) {
|
||||
if (bunCommandServices.has(serviceId)) return "bun-command";
|
||||
if (serviceId === "hwlab-cloud-web") return "cloud-web";
|
||||
|
||||
Reference in New Issue
Block a user