From ce8922dcee01ab35e01ec8863d0039069132671f Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Thu, 9 Jul 2026 17:25:46 +0800 Subject: [PATCH] Restore macOS persistent install integration gate --- .../+macos-persistent-install-ci.yaml | 3 ++ .github/workflows/integration.yml | 11 ++----- README.md | 2 +- docs/BACKLOG.md | 9 +----- docs/CROSS_PLATFORM_INSTALL_LOCATIONS.md | 13 +++++---- docs/FUTURE_DIRECTIONS.md | 2 +- docs/INSTALL_SCOPE_RUNTIME_SUPPORT.md | 20 ++++++++----- docs/MAINTAINING.md | 11 ++++--- internal/dockerdeploy/doctor_test.go | 29 ++++++++++--------- internal/dockerdeploy/install_test.go | 10 +++++-- internal/dockerdeploy/platform.go | 10 +++---- internal/dockerdeploy/platform_test.go | 18 +++++------- website/docs/blueprint-structure.md | 2 +- website/docs/support-matrix.md | 2 +- 14 files changed, 72 insertions(+), 70 deletions(-) create mode 100644 .changes/unreleased/+macos-persistent-install-ci.yaml diff --git a/.changes/unreleased/+macos-persistent-install-ci.yaml b/.changes/unreleased/+macos-persistent-install-ci.yaml new file mode 100644 index 0000000..da344b1 --- /dev/null +++ b/.changes/unreleased/+macos-persistent-install-ci.yaml @@ -0,0 +1,3 @@ +kind: Changed +body: Exercise Docker-managed persistent installs in Linux and macOS integration, including hosted macOS through a Docker-compatible runtime. +time: 2026-07-09T16:40:19+08:00 diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 6e411ab..a019363 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -32,18 +32,13 @@ jobs: include: - name: Linux amd64 runner: ubuntu-latest - persistent_install: false + persistent_install: true - name: Linux arm64 runner: ubuntu-24.04-arm - persistent_install: false + persistent_install: true - name: macOS amd64 runner: ${{ inputs.macos_runner || 'macos-15-intel' }} - # GitHub-hosted macOS runners use Colima, which provides a Linux - # Docker engine. Reploy's Docker-managed permanent install support - # on macOS is intentionally Docker Desktop-only, so this hosted - # gate validates the macOS staging/runtime path without claiming - # Docker Desktop install coverage. - persistent_install: false + persistent_install: true steps: - name: Check out repository uses: actions/checkout@v4 diff --git a/README.md b/README.md index 0d1b554..77408eb 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ supported runtime. | Host OS | Docker | Staging | User install | System install | | --- | --- | --- | --- | --- | | Linux | Docker Engine | ✅ | ✅ | ✅ | -| macOS | Docker Desktop | ✅ | ✅ | — | +| macOS | Docker-compatible | ✅ | ✅ | — | | Windows | Docker Desktop | ✅ | ✅ | — | ## Install diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 273e8c6..4e247bc 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -31,14 +31,7 @@ This file is the day-to-day queue for design and implementation gaps. ## Now -- [ ] `P0` Revert hosted macOS persistent-install CI policy change. - Revert the CI/docs change that downgraded hosted macOS integration to - runtime-only after the runner exposed a non-Docker-Desktop backend. - Acceptance checks: persistent-install coverage is restored or the workflow - fails clearly at the unsupported runner boundary; the remaining runtime - smoke fix is kept only if it still reflects the intended smoke contract; - and any decision to change macOS install coverage is made explicitly by - the maintainer, not hidden inside a fix. +No active backlog items. ## Pre-release diff --git a/docs/CROSS_PLATFORM_INSTALL_LOCATIONS.md b/docs/CROSS_PLATFORM_INSTALL_LOCATIONS.md index 7b59778..7342274 100644 --- a/docs/CROSS_PLATFORM_INSTALL_LOCATIONS.md +++ b/docs/CROSS_PLATFORM_INSTALL_LOCATIONS.md @@ -73,10 +73,11 @@ Explicit scopes must be validated against backend capabilities: candidate mechanisms include `systemd --user`; crontab `@reboot` is only startup glue and is not equivalent to a service manager for status, restart, logs, dependency ordering, or health supervision. -- Mac `user` uses the Docker Desktop-backed install path. Docker restart - policies can restart app containers after Docker Desktop starts, but Reploy - still depends on Docker Desktop itself being configured to start at login or - started by the user. +- Mac `user` uses the Docker-managed Compose install path. Docker restart + policies can restart app containers after the Docker-compatible runtime + starts, but Reploy still depends on that runtime being configured to start at + login or started by the user. Hosted CI currently validates this path with + Colima; Docker Desktop remains the primary user-facing runtime. - Windows `user` uses the Docker Desktop-backed install path. Docker restart policies can restart app containers after Docker Desktop starts, but Reploy still depends on Docker Desktop itself being configured to start at login or @@ -110,10 +111,10 @@ scope. Initial target defaults: | Host/backend | Scope | Default install root | -| --- | --- | +| --- | --- | --- | | Linux systemd install | `system` | `/opt/{{ app.id }}` | | Linux Docker-managed install | `user` | `{{ user.data }}/Reploy/installs/{{ app.id }}` | -| Mac Docker Desktop | `user` | `{{ user.data }}/Reploy/installs/{{ app.id }}` | +| Mac Docker-managed install | `user` | `{{ user.data }}/Reploy/installs/{{ app.id }}` | | Windows Docker Desktop | `user` | `{{ user.local_data }}/Reploy/installs/{{ app.id }}` | The concrete resolved examples are: diff --git a/docs/FUTURE_DIRECTIONS.md b/docs/FUTURE_DIRECTIONS.md index aae63a4..ab77a75 100644 --- a/docs/FUTURE_DIRECTIONS.md +++ b/docs/FUTURE_DIRECTIONS.md @@ -103,7 +103,7 @@ and uninstall would all exercise installed-app behavior without requiring Linux root/systemd system services for the common user-scope case. This should be evaluated as a backend option, not assumed as a replacement for -the current Docker/systemd and Docker Desktop paths. On macOS and Windows, +the current Docker/systemd and Docker-managed paths. On macOS and Windows, Podman containers still run on the host machine inside a Linux VM, so the security and lifecycle promises are VM-backed userland promises rather than native OS service promises. On Linux, rootless Podman depends on host diff --git a/docs/INSTALL_SCOPE_RUNTIME_SUPPORT.md b/docs/INSTALL_SCOPE_RUNTIME_SUPPORT.md index b4908be..733ddd5 100644 --- a/docs/INSTALL_SCOPE_RUNTIME_SUPPORT.md +++ b/docs/INSTALL_SCOPE_RUNTIME_SUPPORT.md @@ -41,6 +41,7 @@ unit, Compose project lifecycle, or host runtime authority belong to the | Linux | Podman rootful with systemd/Quadlet | Partial | Yes | Partial | Good system substrate. Can set container user IDs, but the runtime is still rootful unless the unit is installed in a rootless user path. | | Linux | Podman rootless with user systemd/Quadlet | Yes | No | Yes | Best candidate for a dedicated non-root app-user backend. Admin can create `arbiter`, place Quadlet files under the user's rootless unit path, and enable user-session persistence. | | macOS | Docker Desktop | Yes | No | No | Docker Desktop is a user-session/VM-backed runtime. It can need privileged setup for helper configuration, but app containers are not native macOS services or per-app macOS users. | +| macOS | Colima/Lima Docker-compatible VM | Yes | No | Not as a host guarantee | Hosted CI validates this as a current-user Docker-managed install substrate. Reboot resistance depends on the VM/runtime startup policy outside Reploy. | | macOS | Podman Machine | Yes | No | Not as a host guarantee | Containers run on the Mac inside a Podman-managed Linux VM. Per-app users inside the VM are possible, but they are not native macOS service-user isolation. | | Windows | Docker Desktop per-user install | Yes | No | No | Per-user Docker Desktop can install/update without admin and uses WSL 2 for Linux containers. It is not a per-app Windows service-user model. | | Windows | Docker Desktop all-users install | Partial | No | No | All-users install requires admin and can use WSL 2 or Hyper-V. It has broader host privileges but still does not provide a clean per-app user install scope for Linux containers. | @@ -92,14 +93,15 @@ Linux containers. Docker Desktop is a user-facing app backed by a Linux-container VM. It can be installed and operated for a user, and Docker documents privileged helper setup options, but this does not create a native macOS service-user boundary for each -installed app. It is best modeled as `current-user` Docker-managed persistence, -with reboot resistance depending on Docker Desktop starting at login. +installed app. It is best modeled as one `current-user` Docker-managed +persistence runtime, with reboot resistance depending on Docker Desktop +starting at login. -Podman Machine is similar from an install-scope perspective. It can provide a -local user-owned Linux VM on the Mac. Containers run on the Mac inside that VM, -not directly as macOS launchd services or macOS service users. A VM-internal -app user may still be useful, but it is not the same host guarantee as a Linux -dedicated app user. +Colima, Podman Machine, and similar Docker-compatible VM runtimes are similar +from an install-scope perspective. They can provide a local user-owned Linux VM +on the Mac. Containers run on the Mac inside that VM, not directly as macOS +launchd services or macOS service users. A VM-internal app user may still be +useful, but it is not the same host guarantee as a Linux dedicated app user. ## Windows @@ -132,8 +134,10 @@ service-user isolation. - Linux `dedicated app user` scope is most promising with rootless Podman plus user systemd/Quadlet. It requires privileged setup, then unprivileged runtime under the app account. -- macOS and Windows Docker Desktop should be treated as `current-user` +- macOS Docker-compatible VM runtimes should be treated as `current-user` Docker-managed installs, not native system installs. +- Windows Docker Desktop should be treated as a `current-user` Docker-managed + install, not a native system install. - macOS and Windows Podman Machine may improve runtime uniformity, but their app-user isolation is VM-backed, not native host service-user isolation. diff --git a/docs/MAINTAINING.md b/docs/MAINTAINING.md index 58410b4..e4a3fc6 100644 --- a/docs/MAINTAINING.md +++ b/docs/MAINTAINING.md @@ -58,12 +58,11 @@ nox -s cli-integration This integration test is intentionally outside the default CI session. It runs before publishing and can be triggered manually from the Integration workflow. -On Linux, it covers the real-Docker staging runtime path. On GitHub-hosted -macOS runners, it covers the staging runtime path through Colima; macOS -Docker-managed permanent install remains Docker Desktop-only and needs -separate host evidence. On Windows, when run through `tools/e2e/smoke_windows.ps1`, -it covers the Docker-managed persistent install path with generated control -scripts. +On Linux, it covers the real-Docker staging runtime and Docker-managed +persistent-install path. On GitHub-hosted macOS runners, it covers the same +runtime and persistent-install path through Colima. On Windows, when run +through `tools/e2e/smoke_windows.ps1`, it covers the Docker-managed persistent +install path with generated control scripts. For Windows release follow-up coverage, use the focused PowerShell wrapper: diff --git a/internal/dockerdeploy/doctor_test.go b/internal/dockerdeploy/doctor_test.go index 0eaa929..e8c92a8 100644 --- a/internal/dockerdeploy/doctor_test.go +++ b/internal/dockerdeploy/doctor_test.go @@ -156,7 +156,7 @@ func TestDoctorPreinstallPassesForInitializedDeployment(t *testing.T) { } } -func TestDoctorPreinstallOnDarwinRequiresDockerDesktopRuntime(t *testing.T) { +func TestDoctorPreinstallOnDarwinUserScopeAcceptsDockerRuntime(t *testing.T) { disableDoctorColor(t) restorePlatform := stubHostPlatform(t, hostPlatform{GOOS: "darwin"}) defer restorePlatform() @@ -168,7 +168,7 @@ func TestDoctorPreinstallOnDarwinRequiresDockerDesktopRuntime(t *testing.T) { if timeout != 2*time.Second { t.Fatalf("docker timeout = %s, want 2s", timeout) } - return dockerRuntimeInfo{Runtime: dockerRuntimeDockerDesktop, OperatingSystem: "Docker Desktop", ServerVersion: "29.5.3"}, nil + return dockerRuntimeInfo{Runtime: dockerRuntimeLinuxEngine, OperatingSystem: "Colima", ServerVersion: "29.5.3"}, nil } packDir := makeTestPack(t) @@ -182,12 +182,12 @@ func TestDoctorPreinstallOnDarwinRequiresDockerDesktopRuntime(t *testing.T) { } var stdout strings.Builder - code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Stdout: &stdout, DockerPreflightTimeout: 2 * time.Second}) + code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Scope: InstallScopeUser, Stdout: &stdout, DockerPreflightTimeout: 2 * time.Second}) if code != 0 { t.Fatalf("doctor exit = %d\n%s", code, stdout.String()) } for _, want := range []string{ - "ok: Docker Desktop runtime detected: Docker Desktop", + "ok: Docker runtime detected: Colima", "ok: preinstall checks passed", } { if !strings.Contains(stdout.String(), want) { @@ -225,16 +225,16 @@ func TestDoctorPreinstallOnDarwinFailsWhenDockerRuntimeUnavailable(t *testing.T) } var stdout strings.Builder - code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Stdout: &stdout}) + code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Scope: InstallScopeUser, Stdout: &stdout}) if code != 1 { t.Fatalf("doctor exit = %d\n%s", code, stdout.String()) } - if !strings.Contains(stdout.String(), "fail: Docker Desktop runtime is required for Docker-managed permanent install: docker is not running") { - t.Fatalf("stdout missing Docker Desktop failure:\n%s", stdout.String()) + if !strings.Contains(stdout.String(), "fail: Docker runtime is required for Docker-managed permanent install: docker is not running") { + t.Fatalf("stdout missing Docker runtime failure:\n%s", stdout.String()) } } -func TestDoctorPreinstallOnDarwinFailsWhenRuntimeIsNotDockerDesktop(t *testing.T) { +func TestDoctorPreinstallOnDarwinUserScopeAcceptsNonDockerDesktopRuntime(t *testing.T) { disableDoctorColor(t) restorePlatform := stubHostPlatform(t, hostPlatform{GOOS: "darwin"}) defer restorePlatform() @@ -257,15 +257,18 @@ func TestDoctorPreinstallOnDarwinFailsWhenRuntimeIsNotDockerDesktop(t *testing.T } var stdout strings.Builder - code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Stdout: &stdout}) - if code != 1 { + code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Scope: InstallScopeUser, Stdout: &stdout}) + if code != 0 { t.Fatalf("doctor exit = %d\n%s", code, stdout.String()) } - if !strings.Contains(stdout.String(), "fail: Docker Desktop runtime is required for Docker-managed permanent install; detected: Ubuntu 24.04") { - t.Fatalf("stdout missing Docker Desktop runtime failure:\n%s", stdout.String()) + if !strings.Contains(stdout.String(), "ok: Docker runtime detected: Ubuntu 24.04") { + t.Fatalf("stdout missing Docker runtime success:\n%s", stdout.String()) + } + if !strings.Contains(stdout.String(), "ok: preinstall checks passed") { + t.Fatalf("stdout missing preinstall success:\n%s", stdout.String()) } if strings.Contains(stdout.String(), "warn:") { - t.Fatalf("wrong runtime should fail without advisory warnings:\n%s", stdout.String()) + t.Fatalf("non-Docker-Desktop runtime should pass without advisory warnings:\n%s", stdout.String()) } } diff --git a/internal/dockerdeploy/install_test.go b/internal/dockerdeploy/install_test.go index 399347b..5495eba 100644 --- a/internal/dockerdeploy/install_test.go +++ b/internal/dockerdeploy/install_test.go @@ -74,7 +74,7 @@ func TestInstallDryRunPrintsPlan(t *testing.T) { } } -func TestInstallDryRunOnDarwinPrintsDockerDesktopPlan(t *testing.T) { +func TestInstallDryRunOnDarwinPrintsDockerManagedPlan(t *testing.T) { disableDoctorColor(t) restorePlatform := stubHostPlatform(t, hostPlatform{GOOS: "darwin"}) defer restorePlatform() @@ -83,7 +83,7 @@ func TestInstallDryRunOnDarwinPrintsDockerDesktopPlan(t *testing.T) { detectDockerRuntimeForDoctor = previousDetector }) detectDockerRuntimeForDoctor = func(context.Context, CommandSpec, time.Duration) (dockerRuntimeInfo, error) { - return dockerRuntimeInfo{Runtime: dockerRuntimeDockerDesktop, OperatingSystem: "Docker Desktop"}, nil + return dockerRuntimeInfo{Runtime: dockerRuntimeLinuxEngine, OperatingSystem: "Colima"}, nil } packDir := makeTestPackWithManifest(t, strings.Replace(testPackManifest(), ` owner: user: "1000" @@ -635,9 +635,15 @@ func TestInstallScopeValidationRejectsUnsupportedCombinations(t *testing.T) { if got := (hostPlatform{GOOS: "linux"}).installBackendForScope(InstallScopeUser); got != installBackendDockerManaged { t.Fatalf("linux user backend = %s, want %s", got, installBackendDockerManaged) } + if got := (hostPlatform{GOOS: "darwin"}).installBackendForScope(InstallScopeUser); got != installBackendDockerManaged { + t.Fatalf("macos user backend = %s, want %s", got, installBackendDockerManaged) + } if err := validateInstallScopeForBackend(InstallScopeUser, installBackendDockerManaged, hostPlatform{GOOS: "linux"}); err != nil { t.Fatalf("linux user docker-managed error = %v", err) } + if err := validateInstallScopeForBackend(InstallScopeUser, installBackendDockerManaged, hostPlatform{GOOS: "darwin"}); err != nil { + t.Fatalf("macos user docker-managed error = %v", err) + } if err := validateInstallScopeForBackend(InstallScopeSystem, installBackendDockerDesktop, hostPlatform{GOOS: "darwin"}); err == nil || !strings.Contains(err.Error(), "--scope system is not supported on macos with Docker Desktop") { t.Fatalf("macos system error = %v", err) } diff --git a/internal/dockerdeploy/platform.go b/internal/dockerdeploy/platform.go index d5247d2..0547210 100644 --- a/internal/dockerdeploy/platform.go +++ b/internal/dockerdeploy/platform.go @@ -98,7 +98,7 @@ func (platform hostPlatform) installBackend() installBackend { } func (platform hostPlatform) installBackendForScope(scope InstallScope) installBackend { - if platform.GOOS == "linux" && scope == InstallScopeUser { + if (platform.GOOS == "linux" || platform.GOOS == "darwin") && scope == InstallScopeUser { return installBackendDockerManaged } return platform.installBackend() @@ -118,7 +118,7 @@ func (platform hostPlatform) commandSupport(command platformCommand) platformCom case "linux": return linuxCommandSupport(command) case "darwin": - return dockerDesktopCommandSupport(command) + return dockerManagedPOSIXCommandSupport(command) case "windows": return windowsCommandSupport(command) default: @@ -157,7 +157,7 @@ func linuxCommandSupport(command platformCommand) platformCommandSupport { } } -func dockerDesktopCommandSupport(command platformCommand) platformCommandSupport { +func dockerManagedPOSIXCommandSupport(command platformCommand) platformCommandSupport { switch command { case platformCommandHelp, platformCommandIndex, @@ -177,13 +177,13 @@ func dockerDesktopCommandSupport(command platformCommand) platformCommandSupport platformCommandTest, platformCommandDoctorPreinstall, platformCommandInstall: - return supportedCommandSupport(true) + return supportedCommandSupport(false) case platformCommandUninstallServiceName, platformCommandUninstallList: return unsupportedCommandSupport("Linux/systemd service discovery is not part of Docker-managed install") case platformCommandInstalledPowerShell, platformCommandWindowsService: - return unsupportedCommandSupport("not part of the macOS Docker Desktop support path") + return unsupportedCommandSupport("not part of the macOS Docker-managed support path") default: return unsupportedCommandSupport("unknown command surface") } diff --git a/internal/dockerdeploy/platform_test.go b/internal/dockerdeploy/platform_test.go index 90d0297..5a90273 100644 --- a/internal/dockerdeploy/platform_test.go +++ b/internal/dockerdeploy/platform_test.go @@ -354,18 +354,16 @@ func TestPlatformCommandSupportSelectedNonWindowsContracts(t *testing.T) { wantReasonSubstring: "Linux support path", }, { - name: "macOS install requires Docker Desktop", - platform: hostPlatform{GOOS: "darwin"}, - command: platformCommandInstall, - wantStatus: platformSupportSupported, - wantDockerDesktop: true, + name: "macOS install uses Docker-managed backend", + platform: hostPlatform{GOOS: "darwin"}, + command: platformCommandInstall, + wantStatus: platformSupportSupported, }, { - name: "macOS runtime commands require Docker Desktop", - platform: hostPlatform{GOOS: "darwin"}, - command: platformCommandStagingRuntime, - wantStatus: platformSupportSupported, - wantDockerDesktop: true, + name: "macOS runtime commands use Docker-managed backend", + platform: hostPlatform{GOOS: "darwin"}, + command: platformCommandStagingRuntime, + wantStatus: platformSupportSupported, }, { name: "macOS list uninstall is unsupported", diff --git a/website/docs/blueprint-structure.md b/website/docs/blueprint-structure.md index 1006811..7d3bcb7 100644 --- a/website/docs/blueprint-structure.md +++ b/website/docs/blueprint-structure.md @@ -133,7 +133,7 @@ blueprint omits target defaults, Reploy chooses a built-in host default: | Field | Value | | --- | --- | -| Host/backend | Mac Docker Desktop | +| Host/backend | Mac Docker-managed runtime | | Built-in default | `{{ user.data }}/Reploy/installs/{{ app.id }}` | | For `app.id: example-app` | `$HOME/Library/Application Support/Reploy/installs/example-app` | diff --git a/website/docs/support-matrix.md b/website/docs/support-matrix.md index fd91341..52f258b 100644 --- a/website/docs/support-matrix.md +++ b/website/docs/support-matrix.md @@ -9,5 +9,5 @@ Reploy currently supports Docker-backed staging and installs. | Host OS | Release targets | Docker runtime | Staging | User install | System install | | --- | --- | --- | --- | --- | --- | | Linux | `linux-amd64`, `linux-arm64` | Docker Engine | ✅ | ✅ Docker-managed | ✅ systemd | -| macOS | `darwin-amd64`, `darwin-arm64` | Docker Desktop | ✅ | ✅ Docker-managed | — | +| macOS | `darwin-amd64`, `darwin-arm64` | Docker-compatible | ✅ | ✅ Docker-managed | — | | Windows | `windows-amd64`, `windows-arm64` | Docker Desktop | ✅ | ✅ Docker-managed | — |