From 9cc29ec675845e147a3a1cc906e683585331a837 Mon Sep 17 00:00:00 2001 From: lucasvdiepen <49072755+lucasvdiepen@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:41:34 +0200 Subject: [PATCH 1/3] fix: resolve image tags from container OS, not host OS --- .../unity/runner/runner-image-tag.test.ts | 70 +++++++++++++++++++ src/model/unity/runner/runner-image-tag.ts | 23 +++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/model/unity/runner/runner-image-tag.test.ts b/src/model/unity/runner/runner-image-tag.test.ts index 7e6f8cf..14b0ea0 100644 --- a/src/model/unity/runner/runner-image-tag.test.ts +++ b/src/model/unity/runner/runner-image-tag.test.ts @@ -132,4 +132,74 @@ describe('RunnerImageTag', () => { expect(image.imageRollingVersion).toStrictEqual(5); }); }); + + describe('cross-OS container (Linux containers on Windows host)', () => { + it('uses ubuntu prefix when hostOS is linux, even on win32 hostPlatform', () => { + const image = new RunnerImageTag({ + engineVersion: '6000.3.2f1', + targetPlatform: 'StandaloneWindows64', + hostPlatform: 'win32', + hostOS: 'linux', + }); + + expect(image.toString()).toContain('ubuntu-'); + expect(image.toString()).not.toContain('windows-6000'); + }); + + it('selects windows-mono (not windows-il2cpp) for StandaloneWindows64 in a Linux container', () => { + const image = new RunnerImageTag({ + engineVersion: '6000.3.2f1', + targetPlatform: 'StandaloneWindows64', + hostPlatform: 'win32', + hostOS: 'linux', + }); + + expect(image.toString()).toContain('windows-mono'); + expect(image.toString()).not.toContain('windows-il2cpp'); + }); + + it('falls back to hostPlatform when hostOS is not set (backward compat)', () => { + const image = new RunnerImageTag({ + engineVersion: '2099.1.1111', + targetPlatform: 'Test', + hostPlatform: 'win32', + }); + + expect(image.toString()).toContain('windows-'); + }); + + it('uses windows prefix when hostOS is windows', () => { + const image = new RunnerImageTag({ + engineVersion: '6000.3.2f1', + targetPlatform: 'StandaloneWindows64', + hostPlatform: 'win32', + hostOS: 'windows', + }); + + expect(image.toString()).toContain('windows-'); + expect(image.toString()).toContain('windows-il2cpp'); + }); + }); + + describe('toNodePlatform', () => { + it('maps "windows" to "win32"', () => { + expect(RunnerImageTag.toNodePlatform('windows', 'win32')).toBe('win32'); + }); + + it('maps "linux" to "linux"', () => { + expect(RunnerImageTag.toNodePlatform('linux', 'win32')).toBe('linux'); + }); + + it('maps "darwin" to "darwin"', () => { + expect(RunnerImageTag.toNodePlatform('darwin', 'darwin')).toBe('darwin'); + }); + + it('falls back to hostPlatform when hostOS is undefined', () => { + expect(RunnerImageTag.toNodePlatform(undefined, 'win32')).toBe('win32'); + }); + + it('falls back to hostPlatform when hostOS is unrecognized', () => { + expect(RunnerImageTag.toNodePlatform('freebsd', 'linux')).toBe('linux'); + }); + }); }); diff --git a/src/model/unity/runner/runner-image-tag.ts b/src/model/unity/runner/runner-image-tag.ts index c1bc031..28b1cb4 100644 --- a/src/model/unity/runner/runner-image-tag.ts +++ b/src/model/unity/runner/runner-image-tag.ts @@ -15,6 +15,7 @@ class RunnerImageTag { const { engineVersion = '2019.2.11f1', hostPlatform, + hostOS, targetPlatform, customImage, containerRegistryRepository = 'unityci/editor', @@ -25,6 +26,11 @@ class RunnerImageTag { throw new Error(`Invalid version "${engineVersion}".`); } + // When the container OS differs from the host OS (e.g. --container-os=linux + // on a Windows host running Docker Desktop in Linux-containers mode), image + // tags and build-module selection must match the *container* OS, not the host. + const containerPlatform = RunnerImageTag.toNodePlatform(hostOS, hostPlatform); + // Split on the last '/' so registries with a host+path prefix (e.g. // ghcr.io/example/editor) keep that whole prefix as the repository, // with only the final segment treated as the image name. @@ -38,9 +44,9 @@ class RunnerImageTag { this.name = name; this.engineVersion = engineVersion; this.targetPlatform = targetPlatform; - this.imagePlatformPrefix = RunnerImageTag.getImagePlatformPrefixes(hostPlatform); + this.imagePlatformPrefix = RunnerImageTag.getImagePlatformPrefixes(containerPlatform); this.builderPlatform = RunnerImageTag.getTargetPlatformToTargetPlatformSuffixMap( - hostPlatform, + containerPlatform, targetPlatform, engineVersion, ); @@ -48,6 +54,19 @@ class RunnerImageTag { this.imageRollingVersion = Number(containerRegistryImageVersion); } + /** + * Maps the resolved hostOS ('linux', 'windows', 'darwin') back to the + * Node-style platform string ('linux', 'win32', 'darwin') that + * getImagePlatformPrefixes and getTargetPlatformToTargetPlatformSuffixMap + * expect. Falls back to hostPlatform when hostOS is not set (backward + * compat with callers that only supply hostPlatform). + */ + static toNodePlatform(hostOS: string | undefined, hostPlatform: string | undefined): string { + if (hostOS === 'windows') return 'win32'; + if (hostOS === 'linux' || hostOS === 'darwin') return hostOS; + return hostPlatform ?? ''; + } + static get versionPattern() { // Matches both the legacy YYYY.M.PPl# scheme (e.g. "2021.3.45f1") and // Unity 6's "6000.M.PPl#" scheme (e.g. "6000.0.36f1") - previously only From ec12940f0ee295aa0b7b330f79605e467638c695 Mon Sep 17 00:00:00 2001 From: lucasvdiepen <49072755+lucasvdiepen@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:41:58 +0200 Subject: [PATCH 2/3] fix: use single-quoted volume mount format to avoid colon conflicts --- src/model/docker.test.ts | 4 ++-- src/model/docker.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/model/docker.test.ts b/src/model/docker.test.ts index 429db85..f8fb706 100644 --- a/src/model/docker.test.ts +++ b/src/model/docker.test.ts @@ -176,8 +176,8 @@ describe("Docker", () => { expect(command).toContain('--env UNITY_LICENSE="ci-stub-license"'); // The line-continuation strip (` \\\n` -> space) must not eat an escaped backslash at the end of a value. expect(command).toContain('--env CUSTOM_PARAMETERS="trailing backslash \\\\" --env'); - expect(command).toContain('--volume "/home/runner":"/root:z"'); - expect(command).toContain('--volume "/home/runner/work/cli/cli":"/github/workspace:z"'); + expect(command).toContain('--volume "/home/runner:/root:z"'); + expect(command).toContain('--volume "/home/runner/work/cli/cli:/github/workspace:z"'); expect(command).toContain("game-ci/unity-editor-stub:latest"); expect(command).toContain("/bin/bash /entrypoint.sh"); expect(command).not.toContain("\n"); diff --git a/src/model/docker.ts b/src/model/docker.ts index d9cc971..477dcef 100644 --- a/src/model/docker.ts +++ b/src/model/docker.ts @@ -280,8 +280,8 @@ class Docker { dockerMemoryLimit ? `--memory=${dockerMemoryLimit}` : "", resolveShmSize(dockerShmSize) ? `--shm-size=${resolveShmSize(dockerShmSize)}` : "", useHostNetwork ? "--net=host" : "", - `--volume "${home}":"/root:z"`, - `--volume "${currentWorkDir}":"${dockerWorkspacePath}:z"`, + `--volume "${home}:/root:z"`, + `--volume "${currentWorkDir}:${dockerWorkspacePath}:z"`, isUnityDefaultFlow ? `--volume "${cliDistPath}/default-build-script:/UnityBuilderAction:z"` : "", // Mounted for BOTH flows, unlike default-build-script: the settings // applier has to be available even when the user brings their own From fed5e280bdb0599d802f14f071496bd83b644ae7 Mon Sep 17 00:00:00 2001 From: lucasvdiepen <49072755+lucasvdiepen@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:33:57 +0200 Subject: [PATCH 3/3] fix: quote SSH Docker volume mounts to prevent splitting paths with spaces --- src/model/docker.test.ts | 23 +++++++++++++++++++++-- src/model/docker.ts | 6 +++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/model/docker.test.ts b/src/model/docker.test.ts index f8fb706..f15c1b7 100644 --- a/src/model/docker.test.ts +++ b/src/model/docker.test.ts @@ -376,7 +376,7 @@ describe("Docker", () => { engine: "unity", }); - expect(command).toContain("--volume /home/runner/.ssh/keys:/root/.ssh:ro"); + expect(command).toContain('--volume "/home/runner/.ssh/keys:/root/.ssh:ro"'); expect(command).not.toContain("known_hosts"); }); @@ -393,7 +393,26 @@ describe("Docker", () => { engine: "unity", }); - expect(command).toContain("--volume /home/runner/.ssh/keys:/root/.ssh:ro"); + expect(command).toContain('--volume "/home/runner/.ssh/keys:/root/.ssh:ro"'); + }); + + it("quotes Linux volume mounts completely so paths containing spaces remain a single argument on Windows/PowerShell", () => { + const command = (Docker as any).getLinuxCommand("game-ci/unity-editor-stub:latest", { + hostOS: "windows", + currentWorkDir: "C:/Program Files/GitHub/cli", + homeDir: "C:/Program Files/Users/runner", + cliDistPath: "C:/Program Files/GitHub/cli/dist", + sshAgent: "C:/Program Files/ssh/agent.sock", + sshPublicKeysDirectoryPath: "C:/Program Files/ssh/keys", + gitPrivateToken: "", + dockerWorkspacePath: "/github/workspace", + engine: "unity", + }); + + expect(command).toContain('--volume "C:/Program Files/Users/runner:/root:z"'); + expect(command).toContain('--volume "C:/Program Files/GitHub/cli:/github/workspace:z"'); + expect(command).toContain('--volume "C:/Program Files/ssh/agent.sock:/ssh-agent"'); + expect(command).toContain('--volume "C:/Program Files/ssh/keys:/root/.ssh:ro"'); }); it("applies docker resource limits and isolation mode on Windows", () => { diff --git a/src/model/docker.ts b/src/model/docker.ts index 477dcef..a34590f 100644 --- a/src/model/docker.ts +++ b/src/model/docker.ts @@ -301,9 +301,9 @@ class Docker { isUnityDefaultFlow && runTests ? `--volume "${cliDistPath}/test-standalone-scripts:/UnityTestRunnerAction:z"` : "", - sshAgent ? `--volume ${sshAgent}:/ssh-agent` : "", - sshAgent && !sshPublicKeysDirectoryPath ? "--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro" : "", - sshPublicKeysDirectoryPath ? `--volume ${sshPublicKeysDirectoryPath}:/root/.ssh:ro` : "", + sshAgent ? `--volume "${sshAgent}:/ssh-agent"` : "", + sshAgent && !sshPublicKeysDirectoryPath ? '--volume "/home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro"' : "", + sshPublicKeysDirectoryPath ? `--volume "${sshPublicKeysDirectoryPath}:/root/.ssh:ro"` : "", image, isUnityDefaultFlow ? "/bin/bash /entrypoint.sh" : wrappedCommands!, ]