From 66be27f6daf88f9bdff27009209dd82e776ed271 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Thu, 12 Feb 2026 15:20:11 +0100 Subject: [PATCH] prevent duplicate parallel builds --- .../docker-ci-fix-compose-build-cache.py | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/.github/workflows/scripts/docker-ci-fix-compose-build-cache.py b/.github/workflows/scripts/docker-ci-fix-compose-build-cache.py index 74b591ef2e..5099dc4a44 100644 --- a/.github/workflows/scripts/docker-ci-fix-compose-build-cache.py +++ b/.github/workflows/scripts/docker-ci-fix-compose-build-cache.py @@ -46,17 +46,66 @@ def main(): # Get project name from compose file or default project_name = compose.get("name", "autogpt_platform") + def get_image_name(dockerfile: str, target: str) -> str: + """Generate image name based on Dockerfile folder and build target.""" + dockerfile_parts = dockerfile.replace("\\", "/").split("/") + if len(dockerfile_parts) >= 2: + folder_name = dockerfile_parts[-2] # e.g., "backend" or "frontend" + else: + folder_name = "app" + return f"{project_name}-{folder_name}:{target}" + + def get_build_key(dockerfile: str, target: str) -> str: + """Generate a unique key for a Dockerfile+target combination.""" + return f"{dockerfile}:{target}" + + # First pass: collect all services with build configs and identify duplicates + # Track which (dockerfile, target) combinations we've seen + build_key_to_first_service: dict[str, str] = {} + services_to_build: list[str] = [] + services_to_dedupe: list[str] = [] + + for service_name, service_config in compose.get("services", {}).items(): + if "build" not in service_config: + continue + + build_config = service_config["build"] + dockerfile = build_config.get("dockerfile", "Dockerfile") + target = build_config.get("target", "default") + build_key = get_build_key(dockerfile, target) + + if build_key not in build_key_to_first_service: + # First service with this build config - it will do the actual build + build_key_to_first_service[build_key] = service_name + services_to_build.append(service_name) + else: + # Duplicate - will just use the image from the first service + services_to_dedupe.append(service_name) + + # Second pass: configure builds and deduplicate modified_services = [] for service_name, service_config in compose.get("services", {}).items(): if "build" not in service_config: continue build_config = service_config["build"] + dockerfile = build_config.get("dockerfile", "Dockerfile") + target = build_config.get("target", "default") + image_name = get_image_name(dockerfile, target) + + # Set image name for all services (needed for both builders and deduped) + service_config["image"] = image_name + + if service_name in services_to_dedupe: + # Remove build config - this service will use the pre-built image + del service_config["build"] + continue + + # This service will do the actual build - add cache config cache_from = args.cache_from cache_to = args.cache_to # Determine scope based on Dockerfile path - dockerfile = build_config.get("dockerfile", "Dockerfile") if "type=gha" in args.cache_from or "type=gha" in args.cache_to: if "frontend" in dockerfile: scope = args.frontend_scope @@ -74,20 +123,6 @@ def main(): build_config["cache_from"] = [cache_from] build_config["cache_to"] = [cache_to] - - # Set image name based on Dockerfile folder and build target - # This ensures services with the same Dockerfile+target share an image - if "image" not in service_config: - # Extract folder name from dockerfile path (e.g., "backend" from "autogpt_platform/backend/Dockerfile") - dockerfile_parts = dockerfile.replace("\\", "/").split("/") - if len(dockerfile_parts) >= 2: - folder_name = dockerfile_parts[-2] # e.g., "backend" or "frontend" - else: - folder_name = "app" - - target = build_config.get("target", "default") - service_config["image"] = f"{project_name}-{folder_name}:{target}" - modified_services.append(service_name) # Write back to the same file @@ -97,6 +132,12 @@ def main(): print(f"Added cache config to {len(modified_services)} services in {args.source}:") for svc in modified_services: print(f" - {svc}") + if services_to_dedupe: + print( + f"Deduplicated {len(services_to_dedupe)} services (will use pre-built images):" + ) + for svc in services_to_dedupe: + print(f" - {svc}") if __name__ == "__main__":