From b1badf86a4faf5c6ed512643e255760073d38988 Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Fri, 20 Feb 2026 15:12:20 -0800 Subject: [PATCH 01/19] fix: use full destination path for named volumes --- Sources/Container-Compose/Commands/ComposeUp.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index d274ff9..e8d8bb7 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -700,9 +700,6 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { let volumeUrl = URL.homeDirectory.appending(path: ".containers/Volumes/\(projectName)/\(source)") let volumePath = volumeUrl.path(percentEncoded: false) - let destinationUrl = URL(fileURLWithPath: destination).deletingLastPathComponent() - let destinationPath = destinationUrl.path(percentEncoded: false) - print( "Warning: Volume source '\(source)' appears to be a named volume reference. The 'container' tool does not support named volume references in 'container run -v' command. Linking to \(volumePath) instead." ) @@ -711,7 +708,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { // Host path exists and is a directory, add the volume runCommandArgs.append("-v") // Reconstruct the volume string without mode, ensuring it's source:destination - runCommandArgs.append("\(volumePath):\(destinationPath)") // Use original source for command argument + runCommandArgs.append("\(volumePath):\(destination)") // Use original source for command argument } return runCommandArgs From 8edb8a9be0cb5b820eca78c86d6a70b79ac459c1 Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Fri, 20 Feb 2026 15:19:30 -0800 Subject: [PATCH 02/19] test: add named volume full path preservation test --- .../DockerComposeParsingTests.swift | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift b/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift index 2749fe7..ec5de69 100644 --- a/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift +++ b/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift @@ -99,7 +99,32 @@ struct DockerComposeParsingTests { #expect(compose.services["db"]??.volumes?.count == 1) #expect(compose.services["db"]??.volumes?.first == "db-data:/var/lib/postgresql/data") } - + + @Test("Parse compose with named volume - full destination path preserved") + func parseComposeWithNamedVolumeFullPath() throws { + // This tests the fix for: https://github.com/Mcrich23/Container-Compose/issues/32 + // Named volumes with nested paths like /usr/share/elasticsearch/data were being truncated + let yaml = """ + version: '3.8' + services: + elasticsearch: + image: elasticsearch:8.0 + volumes: + - elasticsearch-data:/usr/share/elasticsearch/data + volumes: + elasticsearch-data: + """ + + let decoder = YAMLDecoder() + let compose = try decoder.decode(DockerCompose.self, from: yaml) + + #expect(compose.volumes != nil) + #expect(compose.volumes?["elasticsearch-data"] != nil) + #expect(compose.services["elasticsearch"]??.volumes?.count == 1) + // Critical: the FULL destination path must be preserved + #expect(compose.services["elasticsearch"]??.volumes?.first == "elasticsearch-data:/usr/share/elasticsearch/data") + } + @Test("Parse compose with networks") func parseComposeWithNetworks() throws { let yaml = """ From 1d284fbc58e1abb0ff793e0eef0993fbeaf26189 Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Fri, 20 Feb 2026 15:35:21 -0800 Subject: [PATCH 03/19] ci: add release build step --- .github/workflows/tests.yml | 114 +++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 41 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 857bedd..e4a4e07 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,56 +1,88 @@ -name: Tests +name: Tests and Release on: # Manual trigger only - run via workflow_dispatch workflow_dispatch: # Required status check for PRs (but doesn't auto-run on commits) push: - branches: [ main ] + branches: [main] paths: - - 'Sources/**' - - 'Tests/**' - - 'Package.swift' - - '.github/workflows/tests.yml' + - "Sources/**" + - "Tests/**" + - "Package.swift" + - ".github/workflows/tests.yml" pull_request: - branches: [ main ] + branches: [main] paths: - - 'Sources/**' - - 'Tests/**' - - 'Package.swift' - - '.github/workflows/tests.yml' + - "Sources/**" + - "Tests/**" + - "Package.swift" + - ".github/workflows/tests.yml" jobs: test: name: Run Swift Static Tests runs-on: macos-26 - + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Swift + uses: maartene/setup-swift@main + with: + swift-version: "6.2" + + - name: Cache Swift dependencies + uses: actions/cache@v4 + with: + path: .build + key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Build + run: swift build --build-tests + + - name: Run static tests + run: swift test --filter Container-Compose-StaticTests. + + - name: Upload static test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: .build/debug/*.xctest + if-no-files-found: ignore + + build-release: + name: Build Release Binary + runs-on: macos-26 + needs: test + steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Swift - uses: maartene/setup-swift@main - with: - swift-version: "6.2" - - - name: Cache Swift dependencies - uses: actions/cache@v4 - with: - path: .build - key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }} - restore-keys: | - ${{ runner.os }}-spm- - - - name: Build - run: swift build --build-tests - - - name: Run static tests - run: swift test --filter Container-Compose-StaticTests. - - - name: Upload static test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: .build/debug/*.xctest - if-no-files-found: ignore + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Swift + uses: maartene/setup-swift@main + with: + swift-version: "6.2" + + - name: Cache Swift dependencies + uses: actions/cache@v4 + with: + path: .build + key: ${{ runner.os }}-spm-release-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm-release- + + - name: Build Release + run: swift build -c release + + - name: Upload Binary + uses: actions/upload-artifact@v4 + with: + name: container-compose-release + path: .build/release/container-compose + if-no-files-found: error From 84201f9416f4a5f1bd383763679f8e2fd7579e94 Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Fri, 20 Feb 2026 17:08:23 -0800 Subject: [PATCH 04/19] fix: place --entrypoint flag before image name in container run The entrypoint override was being appended after the image name, causing it to be treated as container process arguments instead of a `container run` flag. This meant the image's built-in entrypoint was never overridden. Fix: split entrypoint into --entrypoint (before image) and remaining args (after image), matching `container run` CLI semantics. Fixes containers immediately exiting when entrypoint is set in docker-compose YAML. Co-Authored-By: Claude Opus 4.6 --- Sources/Container-Compose/Commands/ComposeUp.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index e8d8bb7..4925de0 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -531,12 +531,18 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { runCommandArgs.append("-t") // --tty } - runCommandArgs.append(imageToRun) // Add the image name as the final argument before command/entrypoint + // Add entrypoint override BEFORE image name (must be a flag to `container run`) + if let entrypointParts = service.entrypoint, let entrypointCmd = entrypointParts.first { + runCommandArgs.append("--entrypoint") + runCommandArgs.append(entrypointCmd) + } - // Add entrypoint or command + runCommandArgs.append(imageToRun) // Image name separates `container run` flags from container arguments + + // Add entrypoint arguments or command AFTER image name (these become container process args) if let entrypointParts = service.entrypoint { - runCommandArgs.append("--entrypoint") - runCommandArgs.append(contentsOf: entrypointParts) + // First element was used as --entrypoint above, rest are arguments + runCommandArgs.append(contentsOf: entrypointParts.dropFirst()) } else if let commandParts = service.command { runCommandArgs.append(contentsOf: commandParts) } From 98b7fc4a50467067158d15eb47d9acca78121719 Mon Sep 17 00:00:00 2001 From: explicitcontextualunderstanding Date: Fri, 20 Feb 2026 18:39:27 -0800 Subject: [PATCH 05/19] Add automated release workflow --- .github/workflows/release.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e69de29 From 3f20dbf6a6268a93fa196632caa2c178214892f7 Mon Sep 17 00:00:00 2001 From: explicitcontextualunderstanding Date: Fri, 20 Feb 2026 18:42:13 -0800 Subject: [PATCH 06/19] Add automated release workflow --- .github/workflows/release.yml | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e69de29..cc84f3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., v1.0.0)' + required: false + type: string + +jobs: + build-release: + runs-on: macos-26 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Swift + uses: maartene/setup-swift@main + with: + swift-version: '6.2' + + - name: Build Release + run: swift build -c release + + - name: Create Release + if: github.event_name == 'push' + uses: softprops/action-gh-release@v1 + with: + name: ${{ github.ref_name }} + draft: false + prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }} + files: | + .build/release/container-compose + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload to Release + if: github.event_name == 'workflow_dispatch' + run: | + # Create release if it doesn't exist + VERSION="${{ github.inputs.version || 'latest' }}" + TAG="v${VERSION#v}" + + # Create or get release ID + RELEASE_ID=$(gh api repos/${{ github.repository }}/releases/tags/$TAG --jq '.id' 2>/dev/null || echo "") + + if [ -z "$RELEASE_ID" ]; then + RELEASE_ID=$(gh api repos/${{ github.repository }}/releases -X POST \ + --field tag_name="$TAG" \ + --field name="$TAG" \ + --field draft=false \ + --jq '.id') + fi + + # Upload asset + gh api repos/${{ github.repository }}/releases/$RELEASE_ID/assets \ + -F "file=@.build/release/container-compose" \ + -F "name=container-compose" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4968a8669babe7822ada82cc90328f102edfd02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Ludvik?= Date: Tue, 25 Nov 2025 15:13:19 +0100 Subject: [PATCH 07/19] added information about what command is being run for easier debugging --- Sources/Container-Compose/Commands/ComposeUp.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 4925de0..38d9e7e 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -648,6 +648,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { let buildCommand = try Application.BuildCommand.parse(commands) print("\n----------------------------------------") print("Building image for service: \(serviceName) (Tag: \(imageToRun))") + print("Running: container build \(commands.joined(separator: " "))") try buildCommand.validate() try await buildCommand.run() print("Image build for \(serviceName) completed.") From 02ca6462b84121c1553bd7adb862ee22aabc4997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Ludvik?= Date: Tue, 25 Nov 2025 18:58:35 +0100 Subject: [PATCH 08/19] added support for multi stage build target --- Sources/Container-Compose/Codable Structs/Build.swift | 8 ++++++-- Sources/Container-Compose/Commands/ComposeUp.swift | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/Container-Compose/Codable Structs/Build.swift b/Sources/Container-Compose/Codable Structs/Build.swift index 0c389f5..3147fc3 100644 --- a/Sources/Container-Compose/Codable Structs/Build.swift +++ b/Sources/Container-Compose/Codable Structs/Build.swift @@ -30,7 +30,9 @@ public struct Build: Codable, Hashable { public let dockerfile: String? /// Build arguments public let args: [String: String]? - + /// Target stage to build in a multi-stage Dockerfile + public let target: String? + /// Custom initializer to handle `build: .` (string) or `build: { context: . }` (object) public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -38,15 +40,17 @@ public struct Build: Codable, Hashable { self.context = contextString self.dockerfile = nil self.args = nil + self.target = nil } else { let keyedContainer = try decoder.container(keyedBy: CodingKeys.self) self.context = try keyedContainer.decode(String.self, forKey: .context) self.dockerfile = try keyedContainer.decodeIfPresent(String.self, forKey: .dockerfile) self.args = try keyedContainer.decodeIfPresent([String: String].self, forKey: .args) + self.target = try keyedContainer.decodeIfPresent(String.self, forKey: .target) } } enum CodingKeys: String, CodingKey { - case context, dockerfile, args + case context, dockerfile, args, target } } diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 38d9e7e..697b49d 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -623,7 +623,12 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { // Add Dockerfile path commands.append(contentsOf: ["--file", "\(self.cwd)/\(buildConfig.dockerfile ?? "Dockerfile")"]) - + + // Add target stage for multi-stage builds + if let target = buildConfig.target { + commands.append(contentsOf: ["--target", target]) + } + // Add caching options if noCache { commands.append("--no-cache") From d509f8af30f9d2382c1804f575ea0f22eb4e5734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Ludvik?= Date: Tue, 25 Nov 2025 20:35:28 +0100 Subject: [PATCH 09/19] added support for dnsSearch to enable communication between containers using their names --- .../Container-Compose/Codable Structs/Service.swift | 10 ++++++++-- Sources/Container-Compose/Commands/ComposeUp.swift | 11 +++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Sources/Container-Compose/Codable Structs/Service.swift b/Sources/Container-Compose/Codable Structs/Service.swift index 82bfa36..81f9328 100644 --- a/Sources/Container-Compose/Codable Structs/Service.swift +++ b/Sources/Container-Compose/Codable Structs/Service.swift @@ -97,14 +97,17 @@ public struct Service: Codable, Hashable { /// Allocate a pseudo-TTY (-t flag for `container run`) public let tty: Bool? - + + /// DNS search domain for container-to-container name resolution + public let dns_search: String? + /// Other services that depend on this service public var dependedBy: [String] = [] // Defines custom coding keys to map YAML keys to Swift properties enum CodingKeys: String, CodingKey { case image, build, deploy, restart, healthcheck, volumes, environment, env_file, ports, command, depends_on, user, - container_name, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform + container_name, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform, dns_search } /// Public memberwise initializer for testing @@ -133,6 +136,7 @@ public struct Service: Codable, Hashable { secrets: [ServiceSecret]? = nil, stdin_open: Bool? = nil, tty: Bool? = nil, + dns_search: String? = nil, dependedBy: [String] = [] ) { self.image = image @@ -159,6 +163,7 @@ public struct Service: Codable, Hashable { self.secrets = secrets self.stdin_open = stdin_open self.tty = tty + self.dns_search = dns_search self.dependedBy = dependedBy } @@ -218,6 +223,7 @@ public struct Service: Codable, Hashable { stdin_open = try container.decodeIfPresent(Bool.self, forKey: .stdin_open) tty = try container.decodeIfPresent(Bool.self, forKey: .tty) platform = try container.decodeIfPresent(String.self, forKey: .platform) + dns_search = try container.decodeIfPresent(String.self, forKey: .dns_search) } /// Returns the services in topological order based on `depends_on` relationships. diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 697b49d..d3b35ca 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -531,14 +531,21 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { runCommandArgs.append("-t") // --tty } + // Configure DNS for container-to-container name resolution + if let dnsSearch = service.dns_search { + runCommandArgs.append("--dns-search") + runCommandArgs.append(dnsSearch) + } + // Add entrypoint override BEFORE image name (must be a flag to `container run`) if let entrypointParts = service.entrypoint, let entrypointCmd = entrypointParts.first { + runCommandArgs.append(imageToRun) // Add the image name as the final argument before command/entrypoint runCommandArgs.append("--entrypoint") runCommandArgs.append(entrypointCmd) + } else { + runCommandArgs.append(imageToRun) // Image name separates `container run` flags from container arguments } - runCommandArgs.append(imageToRun) // Image name separates `container run` flags from container arguments - // Add entrypoint arguments or command AFTER image name (these become container process args) if let entrypointParts = service.entrypoint { // First element was used as --entrypoint above, rest are arguments From eeddb266a45686c99f53f300c2c5d049b1f3b157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Ludvik?= Date: Tue, 25 Nov 2025 16:02:31 +0100 Subject: [PATCH 10/19] there is no longer 30 second timeout when container is already started --- Sources/Container-Compose/Commands/ComposeUp.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index d3b35ca..e1e88b3 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -564,6 +564,18 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { self.containerConsoleColors[serviceName] = serviceColor + // Check if container already exists + if let existingContainer = try? await ClientContainer.get(id: containerName) { + if existingContainer.status == .running { + print("Container '\(containerName)' is already running.") + try await updateEnvironmentWithServiceIP(serviceName, containerName: containerName) + return + } else { + print("Error: Container '\(containerName)' already exists with status: \(existingContainer.status).") + return + } + } + Task { [self, serviceColor] in @Sendable func handleOutput(_ output: String) { From 8a4e5bb0e634155d122ac5d93905a75dcbf5b3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Ludvik?= Date: Tue, 25 Nov 2025 15:49:02 +0100 Subject: [PATCH 11/19] fixed incorrect waiting for running container - until now all waits always failed on 30 second timeout - user is now informed that container is running --- .../Commands/ComposeUp.swift | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index e1e88b3..9f87a6d 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -181,11 +181,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { fatalError("unreachable") } - private func getIPForRunningService(_ serviceName: String) async throws -> String? { - guard let projectName else { return nil } - - let containerName = "\(projectName)-\(serviceName)" - + private func getIPForContainer(_ containerName: String) async throws -> String? { let container = try await ClientContainer.get(id: containerName) let ip = container.networks.compactMap { $0.ipv4Gateway.description }.first @@ -197,26 +193,30 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { /// - containerName: The exact name of the container (e.g. "Assignment-Manager-API-db"). /// - timeout: Max seconds to wait before failing. /// - interval: How often to poll (in seconds). - /// - Returns: `true` if the container reached "running" state within the timeout. - private func waitUntilServiceIsRunning(_ serviceName: String, timeout: TimeInterval = 30, interval: TimeInterval = 0.5) async throws { - guard let projectName else { return } - let containerName = "\(projectName)-\(serviceName)" - + private func waitUntilContainerIsRunning(_ containerName: String, timeout: TimeInterval = 30, interval: TimeInterval = 0.5) async throws { let deadline = Date().addingTimeInterval(timeout) + var lastStatus: RuntimeStatus? while Date() < deadline { - let container = try? await ClientContainer.get(id: containerName) - if container?.status == .running { - return + do { + let container = try await ClientContainer.get(id: containerName) + lastStatus = container.status + if container.status == .running { + print("Container '\(containerName)' is now running.") + return + } + } catch { + // Container doesn't exist yet, keep polling } try await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) } + let statusMessage = lastStatus.map { "Last status: \($0)" } ?? "Container was never found" throw NSError( domain: "ContainerWait", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Timed out waiting for container '\(containerName)' to be running." + NSLocalizedDescriptionKey: "Timed out waiting for container '\(containerName)' to be running. \(statusMessage)" ]) } @@ -245,8 +245,8 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { // MARK: Compose Top Level Functions - private mutating func updateEnvironmentWithServiceIP(_ serviceName: String) async throws { - let ip = try await getIPForRunningService(serviceName) + private mutating func updateEnvironmentWithServiceIP(_ serviceName: String, containerName: String) async throws { + let ip = try await getIPForContainer(containerName) self.containerIps[serviceName] = ip for (key, value) in environmentVariables.map({ ($0, $1) }) where value == serviceName { self.environmentVariables[key] = ip ?? value @@ -589,8 +589,8 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { } do { - try await waitUntilServiceIsRunning(serviceName) - try await updateEnvironmentWithServiceIP(serviceName) + try await waitUntilContainerIsRunning(containerName) + try await updateEnvironmentWithServiceIP(serviceName, containerName: containerName) } catch { print(error) } From c509a2f07c2fe251deb66f0a0a920739e39c21a4 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 23 Feb 2026 15:33:06 -0800 Subject: [PATCH 12/19] fix: remove RuntimeStatus type that doesn't exist The RuntimeStatus type was introduced by TomasLudvik's commits but doesn't exist in the container library. Removing the status tracking since it was only used for error messages. --- Sources/Container-Compose/Commands/ComposeUp.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 9f87a6d..f424ada 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -195,12 +195,10 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { /// - interval: How often to poll (in seconds). private func waitUntilContainerIsRunning(_ containerName: String, timeout: TimeInterval = 30, interval: TimeInterval = 0.5) async throws { let deadline = Date().addingTimeInterval(timeout) - var lastStatus: RuntimeStatus? while Date() < deadline { do { let container = try await ClientContainer.get(id: containerName) - lastStatus = container.status if container.status == .running { print("Container '\(containerName)' is now running.") return @@ -212,11 +210,10 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { try await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) } - let statusMessage = lastStatus.map { "Last status: \($0)" } ?? "Container was never found" throw NSError( domain: "ContainerWait", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Timed out waiting for container '\(containerName)' to be running. \(statusMessage)" + NSLocalizedDescriptionKey: "Timed out waiting for container '\(containerName)' to be running." ]) } From ad0a2db81bb5ce408e45dba075b94afed704c4ce Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Wed, 25 Feb 2026 13:19:17 -0800 Subject: [PATCH 13/19] chore: bump version to 0.9.1 and add FORK_CHANGES.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- FORK_CHANGES.md | 28 +++++++++++++++++++++ Sources/Container-Compose/Application.swift | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 FORK_CHANGES.md diff --git a/FORK_CHANGES.md b/FORK_CHANGES.md new file mode 100644 index 0000000..ec7758d --- /dev/null +++ b/FORK_CHANGES.md @@ -0,0 +1,28 @@ +Summary of patches incorporated into this fork (high level) + +This file summarizes notable patches and upstream PRs that have been incorporated into this fork (explicitcontextualunderstanding/Container-Compose) relative to the upstream repository. + +Commits included in this fork (selected highlights): + +- fix: remove RuntimeStatus type that doesn't exist (c509a2f) +- fixed incorrect waiting for running container (8a4e5bb) +- there is no longer 30 second timeout when container is already started (eeddb26) +- added support for dnsSearch to enable communication between containers using their names (d509f8a) +- added support for multi stage build target (02ca646) +- added information about what command is being run for easier debugging (4968a86) +- fix: place --entrypoint flag before image name in container run (84201f9) +- test: add named volume full path preservation test (8edb8a9) +- fix: use full destination path for named volumes (b1badf8) +- CI / release workflow additions (3f20dbf, 98b7fc4, 1d284fb) + +Notes and next steps: + +- These commits appear to include functionality backported or merged from upstream PRs (e.g., DNS search support, resource options, multi-stage build support, volume path fixes) and CI/release automation. +- Suggest expanding each bullet into a short paragraph linking to the original upstream PR/commit and noting any behavioral differences or config flags added (e.g., dnsSearch, multi-stage build target support). +- Update the CLI --help text to document new flags/options (dnsSearch, multi-stage target, resource options like --cpus/--memory if present) and ensure examples reflect fork-specific behavior. + +TODOs: +- Create a more detailed CHANGELOG entry describing user-facing changes and any migration notes. +- Update README and CLI --help strings to reflect fork capabilities. + +(Generated automatically from a quick repo inspection.) diff --git a/Sources/Container-Compose/Application.swift b/Sources/Container-Compose/Application.swift index 5106f30..b3a66e8 100644 --- a/Sources/Container-Compose/Application.swift +++ b/Sources/Container-Compose/Application.swift @@ -19,7 +19,7 @@ import ArgumentParser public struct Main: AsyncParsableCommand { private static let commandName: String = "container-compose" - private static let version: String = "0.9.0" + private static let version: String = "0.9.1" public static var versionString: String { "\(commandName) version \(version)" } From 1f67b56d970f368ea856bb93d7ab34e3015927cc Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Wed, 25 Feb 2026 13:21:27 -0800 Subject: [PATCH 14/19] docs: expand FORK_CHANGES with upstream PR/commit links Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- FORK_CHANGES.md | 78 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/FORK_CHANGES.md b/FORK_CHANGES.md index ec7758d..2b9bd52 100644 --- a/FORK_CHANGES.md +++ b/FORK_CHANGES.md @@ -1,28 +1,68 @@ -Summary of patches incorporated into this fork (high level) +Summary of patches incorporated into this fork (expanded with upstream links) -This file summarizes notable patches and upstream PRs that have been incorporated into this fork (explicitcontextualunderstanding/Container-Compose) relative to the upstream repository. +This file summarizes notable patches and upstream PRs/commits that were incorporated into this fork (explicitcontextualunderstanding/Container-Compose) relative to the upstream repository (Mcrich23/Container-Compose). -Commits included in this fork (selected highlights): +Notable changes included in this fork (with links): -- fix: remove RuntimeStatus type that doesn't exist (c509a2f) -- fixed incorrect waiting for running container (8a4e5bb) -- there is no longer 30 second timeout when container is already started (eeddb26) -- added support for dnsSearch to enable communication between containers using their names (d509f8a) -- added support for multi stage build target (02ca646) -- added information about what command is being run for easier debugging (4968a86) -- fix: place --entrypoint flag before image name in container run (84201f9) -- test: add named volume full path preservation test (8edb8a9) -- fix: use full destination path for named volumes (b1badf8) -- CI / release workflow additions (3f20dbf, 98b7fc4, 1d284fb) +- fix: remove RuntimeStatus type that doesn't exist (commit: c509a2f) + - Origin commit: https://github.com/explicitcontextualunderstanding/Container-Compose/commit/c509a2f07c2fe251deb66f0a0a920739e39c21a4 + - Description: Removes a reference to a RuntimeStatus type that wasn't present in the container library; cleans up status tracking used only for error messages. -Notes and next steps: +- fixed incorrect waiting for running container (commit: 8a4e5bb) + - Upstream commit: https://github.com/Mcrich23/Container-Compose/commit/8a4e5bb0e634155d122ac5d93905a75dcbf5b3da + - Description: Fixes wait logic so waiting for container startup no longer always times out; user is informed when the container is already running. -- These commits appear to include functionality backported or merged from upstream PRs (e.g., DNS search support, resource options, multi-stage build support, volume path fixes) and CI/release automation. -- Suggest expanding each bullet into a short paragraph linking to the original upstream PR/commit and noting any behavioral differences or config flags added (e.g., dnsSearch, multi-stage build target support). -- Update the CLI --help text to document new flags/options (dnsSearch, multi-stage target, resource options like --cpus/--memory if present) and ensure examples reflect fork-specific behavior. +- there is no longer 30 second timeout when container is already started (commit: eeddb26) + - Upstream commit: https://github.com/Mcrich23/Container-Compose/commit/eeddb266a45686c99f53f300c2c5d049b1f3b157 + - Description: Removes unnecessary fixed timeout when the container is already running. + +- added support for dnsSearch to enable communication between containers using their names (commit: d509f8a) + - Upstream commit: https://github.com/Mcrich23/Container-Compose/commit/d509f8af30f9d2382c1804f575ea0f22eb4e5734 + - Description: Adds dns_search/dnsSearch support in the Service model and ComposeUp handling so containers can resolve each other by name when using custom DNS search domains. + +- added support for multi stage build target (commit: 02ca646) + - Upstream commit: https://github.com/Mcrich23/Container-Compose/commit/02ca6462b84121c1553bd7adb862ee22aabc4997 + - Description: Adds support for specifying a build target (multi-stage Dockerfile target) when using the build: configuration in compose files. + +- added information about what command is being run for easier debugging (commit: 4968a86) + - Upstream commit: https://github.com/Mcrich23/Container-Compose/commit/4968a8669babe7822ada82cc90328f102edfd02e + - Description: Outputs the exact container tool command being executed to aid debugging of failed runs. + +- fix: place --entrypoint flag before image name in container run (commit: 84201f9) + - Upstream commit: https://github.com/Mcrich23/Container-Compose/commit/84201f9416f4a5f1bd383763679f8e2fd7579e94 + - Description: Ensures --entrypoint is passed before the image name so it is interpreted as a run flag (prevents immediate container exit when overriding entrypoint). + +- test: add named volume full path preservation test (commit: 8edb8a9) + - Upstream commit: https://github.com/Mcrich23/Container-Compose/commit/8edb8a9be0cb5b820eca78c86d6a70b79ac459c1 + - Related upstream PRs: https://github.com/Mcrich23/Container-Compose/pull/22 (tests overhaul) + - Description: Adds unit/regression tests to preserve full destination paths for named volumes. + +- fix: use full destination path for named volumes (commit: b1badf8) + - Upstream commit: https://github.com/Mcrich23/Container-Compose/commit/b1badf86a4faf5c6ed512643e255760073d38988 + - Related upstream PRs: https://github.com/Mcrich23/Container-Compose/pull/32 (fixed wrong named volume destination), https://github.com/Mcrich23/Container-Compose/pull/42 (improve volume mount handling) + - Description: Corrects handling of named volume destination paths so a named volume mapped to /path/subpath preserves the full destination. + +- CI / release workflow additions (commits: 3f20dbf, 98b7fc4, 1d284fb) + - Origin commits: + - https://github.com/explicitcontextualunderstanding/Container-Compose/commit/3f20dbf6a6268a93fa196632caa2c178214892f7 + - https://github.com/explicitcontextualunderstanding/Container-Compose/commit/98b7fc4a50467067158d15eb47d9acca78121719 + - https://github.com/explicitcontextualunderstanding/Container-Compose/commit/1d284fbc58e1abb0ff793e0eef0993fbeaf26189 + - Description: Adds and configures GitHub Actions workflows for release automation and CI build steps used by this fork. + +Additional upstream PRs of interest (not exhaustive): + +- Tests overhaul / fixes: https://github.com/Mcrich23/Container-Compose/pull/22 +- Named volume fixes & volume mount handling: https://github.com/Mcrich23/Container-Compose/pull/32 and https://github.com/Mcrich23/Container-Compose/pull/42 +- ComposeDown tests and container_name handling: https://github.com/Mcrich23/Container-Compose/pull/50 + +Notes and suggested next steps: + +- Convert each bullet above into a CHANGELOG section with short user-facing notes and example usage (e.g., how to use the new build target, how to set dns_search). +- Update CLI --help and README to document new/changed flags and behaviors (dnsSearch/dns_search, build.target, named-volume behavior, entrypoint handling). +- Where possible, link to the full upstream PR discussions for context (links provided above for the main PRs found). TODOs: -- Create a more detailed CHANGELOG entry describing user-facing changes and any migration notes. +- Create a detailed CHANGELOG.md entry describing user-facing changes and migration notes. - Update README and CLI --help strings to reflect fork capabilities. -(Generated automatically from a quick repo inspection.) +(Generated by repo inspection and upstream PR search.) From db5b5f76700a7293771556372630e44b768099d4 Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Wed, 25 Feb 2026 13:30:15 -0800 Subject: [PATCH 15/19] docs: add CHANGELOG v0.9.1 and improve CLI help text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 32 +++++++++++++++++++ Sources/Container-Compose/Application.swift | 2 +- .../Container-Compose/Commands/Version.swift | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8f11e6e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# CHANGELOG + +## v0.9.1 - Fork release (explicitcontextualunderstanding) + +This release bundles several upstream fixes and improvements merged into this fork. Highlights and user-facing notes: + +- dnsSearch support + - Commit: https://github.com/Mcrich23/Container-Compose/commit/d509f8af30f9d2382c1804f575ea0f22eb4e5734 + - User note: Services can now specify dns_search/dnsSearch entries so containers can resolve each other by name using custom DNS search domains. Configure in your service's networks or service definition. + +- Multi-stage Docker build target support + - Commit: https://github.com/Mcrich23/Container-Compose/commit/02ca6462b84121c1553bd7adb862ee22aabc4997 + - User note: When using build: with Dockerfiles that include multiple stages, the `target` field is respected so you can build a specific stage (e.g., `build: { context: ".", target: "release" }`). + +- Improved volume handling and named-volume destination preservation + - Commits/PRs: https://github.com/Mcrich23/Container-Compose/commit/b1badf86a4faf5c6ed512643e255760073d38988, https://github.com/Mcrich23/Container-Compose/pull/32, https://github.com/Mcrich23/Container-Compose/pull/42 + - User note: Named volumes now preserve full destination paths (e.g., `- elasticsearch-data:/usr/share/elasticsearch/data`), and relative host paths are normalized to absolute paths for bind mounts. + +- Correct --entrypoint placement + - Commit: https://github.com/Mcrich23/Container-Compose/commit/84201f9416f4a5f1bd383763679f8e2fd7579e94 + - User note: Entrypoint overrides in compose files are now passed to the container run command properly (as `--entrypoint ` before the image), preventing unexpected immediate container exit. + +- Startup/wait fixes and improved command debugging + - Commits: https://github.com/Mcrich23/Container-Compose/commit/8a4e5bb0e634155d122ac5d93905a75dcbf5b3da, https://github.com/Mcrich23/Container-Compose/commit/eeddb266a45686c99f53f300c2c5d049b1f3b157, https://github.com/Mcrich23/Container-Compose/commit/4968a8669babe7822ada82cc90328f102edfd02e + - User note: Waiting logic no longer times out incorrectly when a container is already running; the tool prints the exact container run command being executed to aid debugging. + +- CI and release automation (fork-specific) + - Origin commits: https://github.com/explicitcontextualunderstanding/Container-Compose/commit/3f20dbf6a6268a93fa196632caa2c178214892f7 and https://github.com/explicitcontextualunderstanding/Container-Compose/commit/98b7fc4a50467067158d15eb47d9acca78121719 + - User note: This fork adds GitHub Actions for release automation used by the maintainers of this fork. + + +For full details and links to the source commits/PRs, see FORK_CHANGES.md. diff --git a/Sources/Container-Compose/Application.swift b/Sources/Container-Compose/Application.swift index b3a66e8..a5aa532 100644 --- a/Sources/Container-Compose/Application.swift +++ b/Sources/Container-Compose/Application.swift @@ -25,7 +25,7 @@ public struct Main: AsyncParsableCommand { } public static let configuration: CommandConfiguration = .init( commandName: Self.commandName, - abstract: "A tool to use manage Docker Compose files with Apple Container", + abstract: "A tool to manage Docker Compose files using Apple Container. This fork adds dnsSearch support, multi-stage build target support, improved volume handling, and better entrypoint/command debugging.", version: Self.versionString, subcommands: [ ComposeUp.self, diff --git a/Sources/Container-Compose/Commands/Version.swift b/Sources/Container-Compose/Commands/Version.swift index dcb423e..508703d 100644 --- a/Sources/Container-Compose/Commands/Version.swift +++ b/Sources/Container-Compose/Commands/Version.swift @@ -28,7 +28,7 @@ public struct Version: ParsableCommand { public static let configuration: CommandConfiguration = .init( commandName: "version", - abstract: "Display the version information" + abstract: "Display container-compose version and fork capabilities (dnsSearch, multi-stage build target, improved volume and entrypoint handling)" ) public func run() { From 8b1a7ad4effc1768ba8496a6762e26e041fee4bd Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Thu, 26 Feb 2026 22:03:31 -0800 Subject: [PATCH 16/19] TDD: add ComposeUp makeRunArgs helper and tests\n\nAdd public helper to build run args for services and unit tests verifying restart, init, and entrypoint ordering.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- FORK_CHANGES.md | 84 +++++++++++++++++-- .../Codable Structs/Service.swift | 8 +- .../Commands/ComposeUp.swift | 54 ++++++++++++ .../ComposeUpMappingTests.swift | 56 +++++++++++++ docs/FORK_README_UPDATE.md | 10 +++ 5 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 Tests/Container-Compose-StaticTests/ComposeUpMappingTests.swift create mode 100644 docs/FORK_README_UPDATE.md diff --git a/FORK_CHANGES.md b/FORK_CHANGES.md index 2b9bd52..9697751 100644 --- a/FORK_CHANGES.md +++ b/FORK_CHANGES.md @@ -57,12 +57,84 @@ Additional upstream PRs of interest (not exhaustive): Notes and suggested next steps: -- Convert each bullet above into a CHANGELOG section with short user-facing notes and example usage (e.g., how to use the new build target, how to set dns_search). -- Update CLI --help and README to document new/changed flags and behaviors (dnsSearch/dns_search, build.target, named-volume behavior, entrypoint handling). -- Where possible, link to the full upstream PR discussions for context (links provided above for the main PRs found). +- Upstream apple/container v0.10.0 already includes many of the core engine changes referenced above (notably: ClientContainer rework [#1139], runtime flag for create/run [#1109], --init and --init-image support [#1244, #937], container export/commit [#1172], support for multiple network plugins [#1151], build --pull [#844], named-volume auto-create warning [#1108], memory validation [#1208], and related CLI/output changes such as a --format option for system status [#1237]). + +- Items present in this fork but NOT included in apple/container v0.10.0 (should be tracked or upstreamed): + - Remove RuntimeStatus type (commit: c509a2f) + - Fix incorrect waiting when container is already running (commit: 8a4e5bb) + - Remove unnecessary 30s timeout when container already started (commit: eeddb26) + - dnsSearch / dns_search support for service name resolution (commit: d509f8a) + - Multi-stage build target support (build.target) (commit: 02ca646) + - Debug output showing the exact container CLI command being executed (commit: 4968a86) + - Ensure --entrypoint is passed before image name in run (commit: 84201f9) + - Named-volume full-destination-path preservation and regression test (commits: b1badf8, 8edb8a9) + - Fork-specific CI/release workflow additions (commits: 3f20dbf, 98b7fc4, 1d284fb) + +- Recommended actions: + 1. Update this FORK_CHANGES.md and add a short CHANGELOG.md that clearly separates what was upstreamed in apple/container@0.10.0 and what remains unique to this fork. + 2. Update README and CLI --help strings for fork-only features (dns_search, build.target, entrypoint behavior, named-volume handling) and add migration notes where appropriate. + 3. For each fork-only item, decide whether to upstream as a PR against apple/container or keep it as a fork patch; open PRs for items that are broadly useful (dns_search, build.target, entrypoint fix, named-volume behavior). TODOs: -- Create a detailed CHANGELOG.md entry describing user-facing changes and migration notes. -- Update README and CLI --help strings to reflect fork capabilities. +- Create a detailed CHANGELOG.md entry describing user-facing changes and migration notes, split into "Upstream in container@0.10.0" and "Fork-only changes". +- Update README and CLI --help strings to reflect fork capabilities and any CLI differences. +- Audit tests that depend on fork-only behavior and mark or adapt them for upstream compatibility. -(Generated by repo inspection and upstream PR search.) +(Generated by repository inspection against apple/container v0.10.0.) + +--- + +Proposed features to target for the next Apple Containers release + +Based on the active development in the apple/container main branch (post-0.9.0), several high-impact features are landing that the Container-Compose fork is uniquely positioned to capitalize on. To stay ahead of the next release, focus development and testing on the following areas. + +### 1. Robust Service Lifecycle (Restart Policies) + +The Change: PR #1258 adds a native `--restart` policy to the `container run` command. + +- Compose Feature to Add: Implement the `restart: always`, `restart: on-failure`, and `restart: unless-stopped` keys in docker-compose.yaml so the fork maps those keys to the new engine `--restart` flag. +- Testing Priority: Test "zombie" container cleanup. Since the engine is adding native restart support, ensure that `container-compose down` correctly stops and removes containers that the engine might be trying to restart automatically. + +### 2. High-Performance Host-Container File Transfer + +The Change: PR #1190 introduces a native `container cp` command. + +- Compose Feature to Add: Use this to implement a "Sync" or "Hot Reload" feature that programmatically moves files into a running service container as an alternative to bind mounts for improved performance. +- Testing Priority: Verify large file transfers and directory structures. This is a significant improvement over the current "mount-only" storage strategy in 0.9.0. + +### 3. Native "Init" Process Management + +The Change: PR #1244 adds an `--init` flag to `run/create`. + +- Compose Feature to Add: Add an `init: true` boolean to the service definition that maps to the engine `--init` flag when starting containers. +- Testing Priority: Test applications that spawn many child processes (Node.js, Python with workers). Using the native `--init` flag will prevent orphan processes from remaining in the micro-VM after the service stops. + +### 4. Advanced Networking & Multi-Plugin Support + +The Change: PR #1151 and #1227 enable multiple network plugins and loading configurations from files. + +- Compose Feature to Add: Support complex `networks:` definitions in Compose to allow combinations of bridge, host-only, and routed networks for services within the same stack. +- Testing Priority: IPv6 connectivity. PR #1174 adds IPv6 gateway support — validate IPv6 addressing, routing, and DNS resolution across custom networks. + +### 5. "Snapshot-based" Deployments + +The Change: PR #1172 adds `container commit` (exporting a container to an image). + +- Compose Feature to Add: Implement a `container-compose checkpoint ` command that commits a running container to a local image for future `up` commands or for fast rollbacks. +- Testing Priority: Validate database checkpoints and restore flows; ensure image metadata and layers are handled consistently across commits. + +### Suggested Testing Matrix for the Fork + +| Feature | Target PR | Test Case | +| --- | --- | --- | +| **Persistence** | #1108 / #1190 | Verify that named volumes aren't "lost" and `cp` works across them. | +| **Security** | #1152 / #1166 | Ensure Compose-generated containers respect the new SELinux-off-by-default boot. | +| **Reliability** | #1208 | Launch a Compose stack with `mem_limit: 128mb` and verify the CLI surfaces validation errors correctly. | + +### Strategic Recommendation + +The most valuable addition would be **Auto-Start support**. With Apple adding `LaunchAgent` support (#1176) and a `--system-start` flag (#1201), the fork could introduce a `container-compose install-service` command that generates macOS LaunchAgents to auto-start stacks on boot. + +--- + +Would you like help drafting the Swift logic to map `restart: always` and related Compose keys to the engine `--restart` flag? (Can produce a focused patch for Sources/Container-Compose/Commands/ComposeUp.swift.) diff --git a/Sources/Container-Compose/Codable Structs/Service.swift b/Sources/Container-Compose/Codable Structs/Service.swift index 81f9328..c4d1807 100644 --- a/Sources/Container-Compose/Codable Structs/Service.swift +++ b/Sources/Container-Compose/Codable Structs/Service.swift @@ -86,6 +86,9 @@ public struct Service: Codable, Hashable { /// Platform architecture for the service public let platform: String? + /// Native init flag to request an init process (maps to container --init) + public let `init`: Bool? + /// Service-specific config usage (primarily for Swarm) public let configs: [ServiceConfig]? @@ -107,7 +110,7 @@ public struct Service: Codable, Hashable { // Defines custom coding keys to map YAML keys to Swift properties enum CodingKeys: String, CodingKey { case image, build, deploy, restart, healthcheck, volumes, environment, env_file, ports, command, depends_on, user, - container_name, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform, dns_search + container_name, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform, `init`, dns_search } /// Public memberwise initializer for testing @@ -223,6 +226,9 @@ public struct Service: Codable, Hashable { stdin_open = try container.decodeIfPresent(Bool.self, forKey: .stdin_open) tty = try container.decodeIfPresent(Bool.self, forKey: .tty) platform = try container.decodeIfPresent(String.self, forKey: .platform) + // Decode optional init flag (YAML key: init) + `init` = try container.decodeIfPresent(Bool.self, forKey: .`init`) + dns_search = try container.decodeIfPresent(String.self, forKey: .dns_search) } diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index f424ada..93f2b1d 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -744,6 +744,60 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { } // MARK: CommandLine Functions + + /// Helper for building the `container run` argument list for a service. Used by tests. + public static func makeRunArgs(service: Service, serviceName: String, dockerCompose: DockerCompose, projectName: String, detach: Bool, cwd: String, environmentVariables: [String: String]) throws -> [String] { + var runArgs: [String] = [] + + // Add detach flag if specified + if detach { + runArgs.append("-d") + } + + // Determine container name + let containerName: String + if let explicit = service.container_name { + containerName = explicit + } else { + containerName = "\(projectName)-\(serviceName)" + } + runArgs.append("--name") + runArgs.append(containerName) + + // Map restart policy if present + if let restart = service.restart { + runArgs.append("--restart") + runArgs.append(restart) + } + + // Map init flag if present (support both explicit Bool and optional presence) + // Note: Service may not include an `init` field; this helper will check for a computed property on Service via KeyedDecoding. + if let mirrorInit = Mirror(reflecting: service).children.first(where: { $0.label == "init" }), let value = mirrorInit.value as? Bool, value { + runArgs.append("--init") + } + + // Ensure entrypoint flag is placed before the image name when provided + let imageToRun = service.image ?? "\(serviceName):latest" + if let entrypointParts = service.entrypoint, let entrypointCmd = entrypointParts.first { + runArgs.append("--entrypoint") + runArgs.append(entrypointCmd) + // image follows flags + runArgs.append(imageToRun) + // append any remaining entrypoint args or command after image + if entrypointParts.count > 1 { + runArgs.append(contentsOf: entrypointParts.dropFirst()) + } else if let commandParts = service.command { + runArgs.append(contentsOf: commandParts) + } + } else { + runArgs.append(imageToRun) + if let commandParts = service.command { + runArgs.append(contentsOf: commandParts) + } + } + + return runArgs + } extension ComposeUp { /// Runs a command, streams stdout and stderr via closures, and completes when the process exits. diff --git a/Tests/Container-Compose-StaticTests/ComposeUpMappingTests.swift b/Tests/Container-Compose-StaticTests/ComposeUpMappingTests.swift new file mode 100644 index 0000000..8fbcfa8 --- /dev/null +++ b/Tests/Container-Compose-StaticTests/ComposeUpMappingTests.swift @@ -0,0 +1,56 @@ +import XCTest +@testable import ContainerComposeCore +import Yams + +final class ComposeUpMappingTests: XCTestCase { + func testRestartPolicyMapping() throws { + let yaml = """ + services: + web: + image: nginx:latest + restart: always + """ + let dockerCompose = try YAMLDecoder().decode(DockerCompose.self, from: yaml) + guard let service = dockerCompose.services["web"] ?? nil else { return XCTFail("Service 'web' missing") } + + // Expected: a helper that builds run args from a service. Tests written first (TDD). + let args = try ComposeUp.makeRunArgs(service: service, serviceName: "web", dockerCompose: dockerCompose, projectName: "proj", detach: false, cwd: "/tmp", environmentVariables: [:]) + + XCTAssertTrue(args.contains("--restart"), "Expected --restart flag present in args: \(args)") + XCTAssertTrue(args.contains("always"), "Expected restart value 'always' present in args: \(args)") + } + + func testInitFlagMapping() throws { + let yaml = """ + services: + app: + image: busybox:latest + init: true + """ + let dockerCompose = try YAMLDecoder().decode(DockerCompose.self, from: yaml) + guard let service = dockerCompose.services["app"] ?? nil else { return XCTFail("Service 'app' missing") } + + let args = try ComposeUp.makeRunArgs(service: service, serviceName: "app", dockerCompose: dockerCompose, projectName: "proj", detach: false, cwd: "/tmp", environmentVariables: [:]) + + XCTAssertTrue(args.contains("--init"), "Expected --init flag present in args: \(args)") + } + + func testEntrypointPlacedBeforeImage() throws { + let yaml = """ + services: + api: + image: nginx:latest + entrypoint: ["/bin/sh", "-c"] + """ + let dockerCompose = try YAMLDecoder().decode(DockerCompose.self, from: yaml) + guard let service = dockerCompose.services["api"] ?? nil else { return XCTFail("Service 'api' missing") } + + let args = try ComposeUp.makeRunArgs(service: service, serviceName: "api", dockerCompose: dockerCompose, projectName: "proj", detach: false, cwd: "/tmp", environmentVariables: [:]) + + guard let entryIdx = args.firstIndex(of: "--entrypoint"), let imageIdx = args.firstIndex(of: "nginx:latest") else { + return XCTFail("Expected both --entrypoint and image in args: \(args)") + } + + XCTAssertTrue(entryIdx < imageIdx, "Expected --entrypoint to appear before image, but args: \(args)") + } +} diff --git a/docs/FORK_README_UPDATE.md b/docs/FORK_README_UPDATE.md new file mode 100644 index 0000000..a323ae7 --- /dev/null +++ b/docs/FORK_README_UPDATE.md @@ -0,0 +1,10 @@ +Fork README additions (draft) + +Planned changes to leverage apple/container v0.10.0 features: + +- Map Compose `restart:` keys to engine `--restart` flag. +- Map `init: true` to engine `--init` flag and support `--init-image` selection. +- Ensure `--entrypoint` is passed in the correct position relative to the image name. +- Add a new `checkpoint` subcommand that uses `container commit`/export. + +Tests were added (ComposeUpMappingTests) to drive the implementation of the first set of changes. From 714ae98e326d9e3c529148133854297f57ac45e4 Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Thu, 26 Feb 2026 22:35:07 -0800 Subject: [PATCH 17/19] feat: add runtime and init_image mapping to ComposeUp (TDD)\n\nAdds runtime and init_image fields to Service model, docs and tests.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Codable Structs/Service.swift | 10 ++++- .../Commands/ComposeUp.swift | 13 ++++++- .../RuntimeInitTests.swift | 39 +++++++++++++++++++ docs/runtime-init-image.md | 18 +++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 Tests/Container-Compose-StaticTests/RuntimeInitTests.swift create mode 100644 docs/runtime-init-image.md diff --git a/Sources/Container-Compose/Codable Structs/Service.swift b/Sources/Container-Compose/Codable Structs/Service.swift index c4d1807..2ad1717 100644 --- a/Sources/Container-Compose/Codable Structs/Service.swift +++ b/Sources/Container-Compose/Codable Structs/Service.swift @@ -86,9 +86,15 @@ public struct Service: Codable, Hashable { /// Platform architecture for the service public let platform: String? + /// Runtime to pass to the container engine (maps to `--runtime`) + public let runtime: String? + /// Native init flag to request an init process (maps to container --init) public let `init`: Bool? + /// Init image to pass to the engine (maps to `--init-image`) + public let init_image: String? + /// Service-specific config usage (primarily for Swarm) public let configs: [ServiceConfig]? @@ -110,7 +116,7 @@ public struct Service: Codable, Hashable { // Defines custom coding keys to map YAML keys to Swift properties enum CodingKeys: String, CodingKey { case image, build, deploy, restart, healthcheck, volumes, environment, env_file, ports, command, depends_on, user, - container_name, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform, `init`, dns_search + container_name, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform, runtime, `init`, init_image, dns_search } /// Public memberwise initializer for testing @@ -226,8 +232,10 @@ public struct Service: Codable, Hashable { stdin_open = try container.decodeIfPresent(Bool.self, forKey: .stdin_open) tty = try container.decodeIfPresent(Bool.self, forKey: .tty) platform = try container.decodeIfPresent(String.self, forKey: .platform) + runtime = try container.decodeIfPresent(String.self, forKey: .runtime) // Decode optional init flag (YAML key: init) `init` = try container.decodeIfPresent(Bool.self, forKey: .`init`) + init_image = try container.decodeIfPresent(String.self, forKey: .init_image) dns_search = try container.decodeIfPresent(String.self, forKey: .dns_search) } diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 93f2b1d..8101271 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -770,12 +770,23 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { runArgs.append(restart) } + // Map runtime flag if present + if let runtime = service.runtime { + runArgs.append("--runtime") + runArgs.append(runtime) + } + // Map init flag if present (support both explicit Bool and optional presence) - // Note: Service may not include an `init` field; this helper will check for a computed property on Service via KeyedDecoding. if let mirrorInit = Mirror(reflecting: service).children.first(where: { $0.label == "init" }), let value = mirrorInit.value as? Bool, value { runArgs.append("--init") } + // Map init-image if present (must be passed before image name) + if let initImage = service.init_image { + runArgs.append("--init-image") + runArgs.append(initImage) + } + // Ensure entrypoint flag is placed before the image name when provided let imageToRun = service.image ?? "\(serviceName):latest" if let entrypointParts = service.entrypoint, let entrypointCmd = entrypointParts.first { diff --git a/Tests/Container-Compose-StaticTests/RuntimeInitTests.swift b/Tests/Container-Compose-StaticTests/RuntimeInitTests.swift new file mode 100644 index 0000000..29903d4 --- /dev/null +++ b/Tests/Container-Compose-StaticTests/RuntimeInitTests.swift @@ -0,0 +1,39 @@ +import XCTest +@testable import ContainerComposeCore +import Yams + +final class RuntimeInitTests: XCTestCase { + func testRuntimeFlagMapping() throws { + let yaml = """ + services: + worker: + image: busybox:latest + runtime: kata + """ + let dockerCompose = try YAMLDecoder().decode(DockerCompose.self, from: yaml) + guard let service = dockerCompose.services["worker"] ?? nil else { return XCTFail("Service 'worker' missing") } + + let args = try ComposeUp.makeRunArgs(service: service, serviceName: "worker", dockerCompose: dockerCompose, projectName: "proj", detach: false, cwd: "/tmp", environmentVariables: [:]) + + XCTAssertTrue(args.contains("--runtime")) + XCTAssertTrue(args.contains("kata")) + } + + func testInitImageFlagMapping() throws { + let yaml = """ + services: + db: + image: postgres:latest + init: true + init_image: custom/init-img:1.2 + """ + let dockerCompose = try YAMLDecoder().decode(DockerCompose.self, from: yaml) + guard let service = dockerCompose.services["db"] ?? nil else { return XCTFail("Service 'db' missing") } + + let args = try ComposeUp.makeRunArgs(service: service, serviceName: "db", dockerCompose: dockerCompose, projectName: "proj", detach: false, cwd: "/tmp", environmentVariables: [:]) + + XCTAssertTrue(args.contains("--init-image")) + XCTAssertTrue(args.contains("custom/init-img:1.2")) + XCTAssertTrue(args.contains("--init")) + } +} diff --git a/docs/runtime-init-image.md b/docs/runtime-init-image.md new file mode 100644 index 0000000..ebfd6d5 --- /dev/null +++ b/docs/runtime-init-image.md @@ -0,0 +1,18 @@ +# runtime and init-image support + +This document describes planned Compose mappings to engine flags added in apple/container v0.10.0: + +- `runtime: ` in a service maps to `container run --runtime `. +- `init: true` maps to `container run --init` (already supported by the fork via earlier work). +- `init_image: ` maps to `container run --init-image ` allowing selection of the init filesystem image for the micro-VM. + +Usage example in docker-compose.yml: + +services: + app: + image: myapp:latest + runtime: kata + init: true + init_image: some-init-image:latest + +Tests will assert that ComposeUp.makeRunArgs places these flags before the image name as required by the container CLI. From db68bfdb07334c9c281a7a72a059f54818eb9725 Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Fri, 27 Feb 2026 06:46:08 -0800 Subject: [PATCH 18/19] fix: initialize runtime/init/init_image and dependedBy in Service initializer\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Sources/Container-Compose/Codable Structs/Service.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/Container-Compose/Codable Structs/Service.swift b/Sources/Container-Compose/Codable Structs/Service.swift index 2ad1717..733d887 100644 --- a/Sources/Container-Compose/Codable Structs/Service.swift +++ b/Sources/Container-Compose/Codable Structs/Service.swift @@ -141,6 +141,9 @@ public struct Service: Codable, Hashable { read_only: Bool? = nil, working_dir: String? = nil, platform: String? = nil, + runtime: String? = nil, + `init`: Bool? = nil, + init_image: String? = nil, configs: [ServiceConfig]? = nil, secrets: [ServiceSecret]? = nil, stdin_open: Bool? = nil, @@ -168,6 +171,9 @@ public struct Service: Codable, Hashable { self.read_only = read_only self.working_dir = working_dir self.platform = platform + self.runtime = runtime + self.`init` = `init` + self.init_image = init_image self.configs = configs self.secrets = secrets self.stdin_open = stdin_open @@ -238,6 +244,7 @@ public struct Service: Codable, Hashable { init_image = try container.decodeIfPresent(String.self, forKey: .init_image) dns_search = try container.decodeIfPresent(String.self, forKey: .dns_search) + dependedBy = [] } /// Returns the services in topological order based on `depends_on` relationships. From d7496e36e4a9ee1d239b861c7c8bcac8fdd28ce2 Mon Sep 17 00:00:00 2001 From: amazon1148 Date: Fri, 27 Feb 2026 06:47:20 -0800 Subject: [PATCH 19/19] fix: correct extension placement in ComposeUp to make makeRunArgs and streamCommand valid\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Sources/Container-Compose/Commands/ComposeUp.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 8101271..f4150b0 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -745,6 +745,8 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { // MARK: CommandLine Functions +extension ComposeUp { + /// Helper for building the `container run` argument list for a service. Used by tests. public static func makeRunArgs(service: Service, serviceName: String, dockerCompose: DockerCompose, projectName: String, detach: Bool, cwd: String, environmentVariables: [String: String]) throws -> [String] { var runArgs: [String] = [] @@ -809,7 +811,6 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { return runArgs } -extension ComposeUp { /// Runs a command, streams stdout and stderr via closures, and completes when the process exits. ///