fix: share env reuse image across ts services

This commit is contained in:
UniDesk Codex
2026-07-06 14:24:19 +08:00
parent fac4b7ead9
commit 4ae933eeef
5 changed files with 3673 additions and 8 deletions
+97 -4
View File
@@ -72,7 +72,8 @@ export async function createCiPlan(options = {}) {
const artifactCatalogPath = options.artifactCatalogPath ?? defaultArtifactCatalogPath(laneConfig, lane);
const artifactCatalog = await readStructuredFileIfPresent(repoRoot, artifactCatalogPath, null);
const runtimeReuseConfigPath = options.runtimeReuseConfigPath ?? "gitops/reuse.ymal";
const runtimeReuseByService = runtimeReuseServices(await readStructuredFileIfPresent(repoRoot, runtimeReuseConfigPath, null));
const runtimeReuseConfig = await readStructuredFileIfPresent(repoRoot, runtimeReuseConfigPath, null);
const runtimeReuseByService = runtimeReuseServices(runtimeReuseConfig);
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);
@@ -82,6 +83,7 @@ export async function createCiPlan(options = {}) {
const envReuseRecipe = envReuseRecipeForLane(deployJson, lane);
const componentModels = componentModelsForServices(serviceIdResolution.serviceIds, laneConfig, lane);
const envReuseServices = enabledEnvReuseServices(deployJson, lane, serviceIdResolution.serviceIds);
const envArtifactGroupByService = envArtifactGroupsByService(runtimeReuseConfig, serviceIdResolution.serviceIds);
const globalChange = classifyGlobalChange(normalizedChangedPaths);
const hasGoService = componentModels.some((model) => model.runtimeKind === "go-service");
const dockerfileHash = await hashGitPaths(repoRoot, targetRef, [
@@ -99,6 +101,7 @@ export async function createCiPlan(options = {}) {
const runtimeDepMatches = rawRuntimeDepMatches.filter((item) => semanticRuntimeDepPaths.has(item));
const buildSystemMatches = matchingPaths(normalizedChangedPaths, model.buildSystemPaths);
const envReuse = envReuseServices.has(model.serviceId);
const envArtifactGroup = envReuse ? envArtifactGroupByService.get(model.serviceId) ?? null : null;
const bootSh = envReuse ? bootShForService(deployJson, model.serviceId, lane) : null;
const serviceEnvReuseRecipe = envReuse ? envReuseRecipeForService(envReuseRecipe.publicRecipe, model) : null;
const runtimeConfigChanged = envReuse ? await serviceRuntimeConfigChanged(repoRoot, deployConfigPath, baseRef, targetRef, lane, model.serviceId) : null;
@@ -132,7 +135,7 @@ export async function createCiPlan(options = {}) {
const codeInputHash = envReuse ? await hashGitPaths(repoRoot, targetRef, codeInputPaths, {
skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath)
}) : null;
const currentEnvironmentImage = envReuse ? envReuseImageRef(registryPrefix, model.serviceId, environmentInputHash) : null;
const currentEnvironmentImage = envReuse ? envReuseImageRef(registryPrefix, model.serviceId, environmentInputHash, envArtifactGroup) : null;
const catalogEnvironmentImage = envReuse ? environmentImageFromCatalog(model.serviceId, catalogRecord) : null;
const catalogEnvironmentDigest = envReuse ? environmentDigestFromCatalog(catalogRecord) : null;
const catalogEnvironmentSourceCommitId = envReuse ? text(catalogRecord?.sourceCommitId) : "";
@@ -262,6 +265,17 @@ export async function createCiPlan(options = {}) {
runtimeDeps: model.runtimeDeps,
buildSystemPaths: model.buildSystemPaths,
envReuseRecipe: serviceEnvReuseRecipe,
envArtifactGroup: envArtifactGroup ? {
id: envArtifactGroup.id,
buildService: envArtifactGroup.buildService,
imageRepository: envArtifactGroup.imageRepository,
cacheRepository: envArtifactGroup.cacheRepository,
services: envArtifactGroup.services
} : null,
envArtifactGroupId: envArtifactGroup?.id ?? null,
envArtifactBuildServiceId: envArtifactGroup?.buildService ?? null,
envArtifactSourceServiceId: envArtifactGroup?.buildService ?? model.serviceId,
envArtifactCacheRef: envArtifactGroup ? envReuseCacheRef(registryPrefix, envArtifactGroup) : null,
componentCommitId,
componentInputHash,
catalogSourceCommitId: catalogSourceCommitId || null,
@@ -305,6 +319,8 @@ export async function createCiPlan(options = {}) {
});
}
const envArtifactGroupPlans = applyEnvArtifactGroupBuildPlan(services);
const affectedServices = services.filter((service) => service.affected).map((service) => service.serviceId);
const buildServices = services.filter((service) => service.buildRequired).map((service) => service.serviceId);
const reusedServices = services.filter((service) => !service.buildRequired).map((service) => service.serviceId);
@@ -326,6 +342,7 @@ export async function createCiPlan(options = {}) {
serviceDeclarationSource: `deploy.lanes.${lane}.serviceDeclarations`,
envRecipeSource: `deploy.lanes.${lane}.envRecipe`,
envReuseServiceIds: [...envReuseServices],
envArtifactGroups: envArtifactGroupPlans,
v02EnvReuseServiceIds: lane === "v02" ? [...envReuseServices] : [],
defaultComponentLazyBuild: true,
envReuseCodeOnlyFastLane: true,
@@ -345,11 +362,13 @@ export async function createCiPlan(options = {}) {
buildServices,
reusedServices,
buildSkippedCount: services.length - buildServices.length,
envArtifactGroups: envArtifactGroupPlans,
services,
ciCdPlan: {
willBuild: buildServices,
willRollout: affectedServices,
willReuse: reusedServices,
envArtifactGroups: envArtifactGroupPlans,
willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0,
noImageBuildReason: buildServices.length === 0
? (affectedServices.length > 0 ? "env-reuse-code-only-rollout" : globalChange.summary)
@@ -683,6 +702,75 @@ function runtimeReuseServices(config) {
}));
}
function envArtifactGroupsByService(config, allowedServiceIds) {
const allowed = new Set(allowedServiceIds);
const raw = config?.spec?.envArtifactGroups ?? config?.envArtifactGroups;
const entries = Array.isArray(raw)
? raw.map((value, index) => [String(value?.id ?? index), value])
: Object.entries(raw ?? {});
const byService = new Map();
for (const [id, value] of entries) {
if (value?.enabled === false) continue;
const groupId = String(value?.id ?? id ?? "").trim();
const services = uniqueSorted(normalizeStringList(value?.services, [])).filter((serviceId) => allowed.has(serviceId));
if (!groupId || services.length === 0) continue;
const configuredBuildService = String(value?.buildService ?? value?.sourceService ?? services[0] ?? "").trim();
const buildService = services.includes(configuredBuildService) ? configuredBuildService : services[0];
const group = {
id: groupId,
buildService,
services,
imageRepository: normalizeRepositoryPath(value?.imageRepository) ?? groupId,
cacheRepository: normalizeRepositoryPath(value?.cacheRepository) ?? `cache/${groupId}`
};
for (const serviceId of services) byService.set(serviceId, group);
}
return byService;
}
function normalizeRepositoryPath(value) {
const text = String(value ?? "").trim();
return text && /^[a-z0-9][a-z0-9._/-]*$/u.test(text) && !text.includes("..") ? text : null;
}
function applyEnvArtifactGroupBuildPlan(services) {
const groups = new Map();
for (const service of services) {
const group = service.envArtifactGroup;
if (!service.envReuse || !group?.id) continue;
if (!groups.has(group.id)) groups.set(group.id, { ...group, services: [] });
groups.get(group.id).services.push(service);
}
const plans = [];
for (const group of groups.values()) {
const members = group.services;
const buildNeeded = members.some((service) => service.buildRequired === true);
const source = members.find((service) => service.serviceId === group.buildService) ?? members.find((service) => service.buildRequired === true) ?? members[0];
for (const service of members) {
service.envArtifactSourceServiceId = source.serviceId;
service.envArtifactBuildServiceId = source.serviceId;
service.envArtifactGroupBuildRequired = buildNeeded && service.serviceId === source.serviceId;
if (buildNeeded && service.serviceId !== source.serviceId && service.buildRequired === true) {
service.buildRequired = false;
service.skipImageBuild = true;
service.sharedEnvBuildSkipped = true;
service.reason = uniqueSorted([...(Array.isArray(service.reason) ? service.reason : []), "shared-env-artifact-consumer"]);
}
}
plans.push({
id: group.id,
buildService: source.serviceId,
imageRepository: group.imageRepository,
cacheRepository: group.cacheRepository,
buildRequired: buildNeeded,
services: members.map((service) => service.serviceId),
buildServices: buildNeeded ? [source.serviceId] : [],
consumerServices: buildNeeded ? members.filter((service) => service.serviceId !== source.serviceId).map((service) => service.serviceId) : members.map((service) => service.serviceId)
});
}
return plans.sort((a, b) => a.id.localeCompare(b.id));
}
async function runtimeReuseDecisionForService({ repoRoot, baseRef, targetRef, serviceId, config }) {
const runtimeReuse = config?.runtimeReuse ?? null;
const envReuse = config?.envReuse ?? null;
@@ -1032,9 +1120,14 @@ function environmentDigestFromCatalogForHash(catalogRecord, environmentInputHash
return catalogRecord?.environmentInputHash === environmentInputHash ? environmentDigestFromCatalog(catalogRecord) : null;
}
function envReuseImageRef(registryPrefix, serviceId, environmentInputHash) {
function envReuseImageRef(registryPrefix, serviceId, environmentInputHash, group = null) {
const tag = String(environmentInputHash || "unknown").slice(0, 12);
return `${registryPrefix}/${serviceId}-env:env-${tag}`;
const repository = group?.imageRepository ?? `${serviceId}-env`;
return `${registryPrefix}/${repository}:env-${tag}`;
}
function envReuseCacheRef(registryPrefix, group) {
return `${registryPrefix}/${group.cacheRepository}`;
}
function digestFromImageReference(image) {