Generalize CI plan runtime lanes
This commit is contained in:
@@ -38,13 +38,14 @@ function readOption(argv, index, name) {
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
"usage: node scripts/g14-ci-plan.mjs [--lane v02] [--base-ref REF] [--target-ref REF] [--pretty]",
|
||||
"usage: node scripts/g14-ci-plan.mjs [--lane v02|v03] [--base-ref REF] [--target-ref REF] [--pretty]",
|
||||
"",
|
||||
"Plan G14 monorepo image builds without mutating CI/CD state.",
|
||||
"For v02 env reuse, service component boundaries and env recipe must come from deploy/deploy.yaml.",
|
||||
"For runtime lane env reuse, service component boundaries and env recipe must come from deploy/deploy.yaml.",
|
||||
"",
|
||||
"examples:",
|
||||
" node scripts/g14-ci-plan.mjs --lane v02 --base-ref HEAD~1 --target-ref HEAD --pretty",
|
||||
" node scripts/g14-ci-plan.mjs --lane v03 --base-ref HEAD~1 --target-ref HEAD --pretty",
|
||||
" node scripts/g14-ci-plan.mjs --services hwlab-cloud-api,hwlab-cloud-web --pretty"
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -610,6 +610,31 @@ test("v02 planner reads env reuse declarations and recipe from deploy config", a
|
||||
assert.equal(recipe.launcherInputPaths.includes("custom/env-tool.mjs"), true);
|
||||
});
|
||||
|
||||
test("v03 planner reads env reuse declarations and catalog from v03 lane config", async () => {
|
||||
const repo = await createFixtureRepo();
|
||||
|
||||
const plan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v03",
|
||||
baseRef: "HEAD",
|
||||
targetRef: "HEAD",
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web"]
|
||||
});
|
||||
|
||||
assert.equal(plan.compatibility.lane, "v03");
|
||||
assert.equal(plan.compatibility.artifactCatalog, "deploy/artifact-catalog.v03.json");
|
||||
assert.equal(plan.compatibility.serviceIdSource, "cli-services");
|
||||
assert.equal(plan.compatibility.serviceDeclarationSource, "deploy.lanes.v03.serviceDeclarations");
|
||||
assert.equal(plan.compatibility.envRecipeSource, "deploy.lanes.v03.envRecipe");
|
||||
assert.deepEqual(plan.compatibility.envReuseServiceIds, ["hwlab-cloud-api", "hwlab-cloud-web"]);
|
||||
for (const service of plan.services) {
|
||||
assert.equal(service.runtimeMode, "env-reuse-git-mirror-checkout", service.serviceId);
|
||||
assert.equal(service.envReuse, true, service.serviceId);
|
||||
assert.equal(service.bootRepo, "git@github.com:pikasTech/HWLAB.git", service.serviceId);
|
||||
assert.equal(service.bootSh, `deploy/runtime/boot/${service.serviceId}.sh`, service.serviceId);
|
||||
}
|
||||
});
|
||||
|
||||
async function createFixtureRepo(options = {}) {
|
||||
const serviceIds = options.serviceIds ?? ["hwlab-cloud-api", "hwlab-cloud-web"];
|
||||
const laneServiceIds = options.laneServiceIds ?? V02_RUNTIME_SERVICE_IDS;
|
||||
@@ -684,7 +709,8 @@ async function createFixtureRepo(options = {}) {
|
||||
const catalog = createCatalogFixture(catalogMode, catalogOptionsFromPlan(initialPlan, { sourceCommitId: initialSha, serviceIds }));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.dev.json"), JSON.stringify(catalog, null, 2));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(catalog, null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.dev.json", "deploy/artifact-catalog.v02.json"]);
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v03.json"), JSON.stringify(catalog, null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.dev.json", "deploy/artifact-catalog.v02.json", "deploy/artifact-catalog.v03.json"]);
|
||||
await git(repo, ["commit", "-m", "seed artifact catalogs"]);
|
||||
}
|
||||
return repo;
|
||||
@@ -704,7 +730,8 @@ async function refreshFixtureCatalogForHead(repo) {
|
||||
const catalog = createCatalogFixture("ready", catalogOptionsFromPlan(plan, { sourceCommitId, serviceIds: services }));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.dev.json"), JSON.stringify(catalog, null, 2));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(catalog, null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.dev.json", "deploy/artifact-catalog.v02.json"]);
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v03.json"), JSON.stringify(catalog, null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.dev.json", "deploy/artifact-catalog.v02.json", "deploy/artifact-catalog.v03.json"]);
|
||||
await git(repo, ["commit", "-m", "refresh fixture artifact catalogs"]);
|
||||
}
|
||||
|
||||
@@ -719,10 +746,9 @@ function planV02CoreServices(repo, options) {
|
||||
}
|
||||
|
||||
function createDeployFixture({ serviceIds = V02_RUNTIME_SERVICE_IDS } = {}) {
|
||||
const deploy = {
|
||||
lanes: {
|
||||
v02: {
|
||||
const v02 = {
|
||||
sourceRepo: "git@github.com:pikasTech/HWLAB.git",
|
||||
artifactCatalog: "deploy/artifact-catalog.v02.json",
|
||||
envReuseServices: [...serviceIds],
|
||||
bootScripts: {
|
||||
"hwlab-cloud-api": "deploy/runtime/boot/hwlab-cloud-api.sh",
|
||||
@@ -822,7 +848,13 @@ function createDeployFixture({ serviceIds = V02_RUNTIME_SERVICE_IDS } = {}) {
|
||||
template: "default",
|
||||
overrides: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
const v03 = JSON.parse(JSON.stringify(v02));
|
||||
v03.artifactCatalog = "deploy/artifact-catalog.v03.json";
|
||||
const deploy = {
|
||||
lanes: {
|
||||
v02,
|
||||
v03
|
||||
}
|
||||
};
|
||||
return deploy;
|
||||
|
||||
@@ -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