@@ -197,37 +197,36 @@ export function filterGhcrServices(services: Service[]): Service[] {
197197}
198198
199199/**
200- * Filters out service aliases that share the same image with another service.
200+ * Builds a mapping from service names (including aliases) to their canonical service names .
201201 *
202202 * Service aliases are docker-compose services that use the same Docker image as another
203- * service but with different runtime configuration (environment variables, volumes, etc.).
204- * These should not be built separately since they share the same image .
203+ * service but with different runtime configuration. They should resolve to the canonical
204+ * service name for build purposes .
205205 *
206- * A service is considered an alias if:
207- * 1. Its image name doesn't match its service name
208- * 2. Another service exists whose service name matches the image name
206+ * A canonical service is one where the service name matches the image name.
207+ * An alias is any other service using that same image.
209208 *
210- * @param services Array of services to filter
211- * @returns Filtered array with service aliases removed
209+ * @param services Array of all services
210+ * @returns Map from service name to canonical service name
212211 *
213212 * @example
214213 * ```typescript
215214 * const services = [
216215 * { service_name: 'historian', image: 'ghcr.io/groupsky/homy/historian:latest', ... },
217216 * { service_name: 'historian-secondary', image: 'ghcr.io/groupsky/homy/historian:latest', ... }
218217 * ];
219- * const filtered = filterServiceAliases(services);
220- * console.log(filtered.length); // 1 (only historian, historian-secondary is filtered out)
218+ * const mapping = buildServiceAliasMapping(services);
219+ * console.log(mapping.get('historian')); // 'historian'
220+ * console.log(mapping.get('historian-secondary')); // 'historian'
221221 * ```
222222 */
223- export function filterServiceAliases ( services : Service [ ] ) : Service [ ] {
224- // Build a map of image names to service names for canonical services
225- // A canonical service is one where the service name matches the image name
223+ export function buildServiceAliasMapping ( services : Service [ ] ) : Map < string , string > {
224+ // First pass: find canonical services (service name matches image name)
226225 const imageToCanonicalService = new Map < string , string > ( ) ;
227226
228227 for ( const service of services ) {
229228 if ( ! service . image ) {
230- // Services without images (local builds) are always canonical
229+ // Services without images (local builds) map to themselves
231230 imageToCanonicalService . set ( service . service_name , service . service_name ) ;
232231 continue ;
233232 }
@@ -243,26 +242,69 @@ export function filterServiceAliases(services: Service[]): Service[] {
243242 }
244243 }
245244
246- // Filter out service aliases
247- return services . filter ( ( service ) => {
248- // Services without image field are always kept (local builds)
245+ // Second pass: build alias mapping
246+ const aliasMapping = new Map < string , string > ( ) ;
247+
248+ for ( const service of services ) {
249249 if ( ! service . image ) {
250- return true ;
250+ // Local builds map to themselves
251+ aliasMapping . set ( service . service_name , service . service_name ) ;
252+ continue ;
251253 }
252254
253- // Check if this service's image has a canonical service
255+ // Look up canonical service for this image
254256 const canonicalService = imageToCanonicalService . get ( service . image ) ;
255257
256- // Keep service if:
257- // 1. No canonical service exists (orphan image), OR
258- // 2. This IS the canonical service
259- if ( ! canonicalService || canonicalService === service . service_name ) {
260- return true ;
258+ if ( canonicalService ) {
259+ // Map to canonical service
260+ aliasMapping . set ( service . service_name , canonicalService ) ;
261+ } else {
262+ // No canonical service found - map to itself (orphan image)
263+ aliasMapping . set ( service . service_name , service . service_name ) ;
261264 }
265+ }
262266
263- // This is a service alias - filter it out
264- return false ;
265- } ) ;
267+ return aliasMapping ;
268+ }
269+
270+ /**
271+ * Resolves a list of service names to their canonical names, removing duplicates.
272+ *
273+ * Service aliases are resolved to their canonical service names based on the
274+ * alias mapping. Duplicates are removed to ensure each canonical service appears
275+ * only once in the result.
276+ *
277+ * @param serviceNames Array of service names (may include aliases)
278+ * @param aliasMapping Map from service name to canonical service name
279+ * @returns Array of unique canonical service names, sorted
280+ *
281+ * @example
282+ * ```typescript
283+ * const aliasMapping = new Map([
284+ * ['historian', 'historian'],
285+ * ['historian-secondary', 'historian'],
286+ * ]);
287+ * const resolved = resolveServiceAliases(['historian', 'historian-secondary'], aliasMapping);
288+ * console.log(resolved); // ['historian']
289+ * ```
290+ */
291+ export function resolveServiceAliases (
292+ serviceNames : string [ ] ,
293+ aliasMapping : Map < string , string >
294+ ) : string [ ] {
295+ const canonicalServices = new Set < string > ( ) ;
296+
297+ for ( const serviceName of serviceNames ) {
298+ const canonical = aliasMapping . get ( serviceName ) ;
299+ if ( canonical ) {
300+ canonicalServices . add ( canonical ) ;
301+ } else {
302+ // No mapping found - use service name as-is
303+ canonicalServices . add ( serviceName ) ;
304+ }
305+ }
306+
307+ return Array . from ( canonicalServices ) . sort ( ) ;
266308}
267309
268310/**
0 commit comments