Repository navigation
ci: Run the Actor dev-loop e2e on the self-hosted macOS arm64 runner - #65
Merged
Merged
Conversation
Adds an `e2e-macos-arm64` job to `ci.yml` that runs `actor-dev-loop.test.ts` on the `[self-hosted, macOS, ARM64]` runner - one file to start with, to prove the executor works end to end (runtime image build, container start, push / build / call through apify-cli against Docker Desktop's forwarded socket). The job is skipped for pull requests from forks, since the runner is a persistent Apify machine and the repository is public. Because that machine is never recreated, the job cleans up before and after the test with the new `.github/scripts/e2e-host-cleanup.sh`: the runtime container and its volume, the Actor run / probe / browser-viewer containers and volumes the runtime labels, the images it builds, dangling layers, and the suite's temp dirs. The pre-test pass also fails early with the holder named if 3333 or 3000 is still taken by something that is not ours. `startRuntimeContainer` now force-removes a stale container of its own name before `docker run`, so a run that died before `afterAll` (locally or on the runner) no longer fails the next one with "name is already in use". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
The self-hosted macOS arm64 runner ships with no Docker, Podman or OrbStack, so the e2e job now installs its own engine, entirely in user space, with `.github/scripts/macos-docker-engine.sh install`: the static Docker CLI 29.8.1, buildx v0.37.1 (the Dockerfile needs BuildKit), Lima 2.2.0 and Colima v0.10.3, all pinned and checksum-verified, under one directory in the job's temp dir. Colima runs the daemon in a Virtualization.framework VM; the docker context it activates is what the CLI uses, and `DOCKER_HOST` stays unset so the suite keeps mounting `/var/run/docker.sock`, which inside the VM is the daemon's socket. The install smoke-tests exactly that mount before the suite runs. `teardown` (always) stops and deletes the VM, removes the binaries, Colima's state, the isolated `DOCKER_CONFIG` and Lima's image cache, leaving the Mac as the job found it. The runner user's own `~/.docker`, `~/.colima` and `~/.lima` are never touched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
The script's entry-point function was named `install`, which in bash
shadows /usr/bin/install - so the `install -m 0755` copies inside it called
the function again, re-downloading the Docker CLI on every round until the
shell ran out of stack ("Segmentation fault: 11" on the runner). The
functions are now `engine_install` / `engine_teardown`, and the copies say
`command install` so a future rename cannot bring this back.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
buildx's `checksums.txt` lists no darwin (or windows) binaries - those are code-signed after the list is written and land in `checksums-signed.txt` instead - so the macOS engine script looked up an empty checksum and failed with "expected , got c3cb...". Read the signed list for buildx, and make `verify` say plainly when an asset is missing from a checksum list rather than reporting an empty expected value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
…ckets Lima puts unix sockets in the instance directory, and macOS caps a socket path at 104 characters; under the runner's `_work/_temp/...` the path came to 112 and `colima start` died with "instance name `colima` too long". The engine now lives in `$HOME/.actor-runtime-e2e` by default, which teardown removes as before. Also export the engine's env and PATH to the job before starting the VM, so the workflow's log-dump and teardown steps find `docker` and `colima` even when the start fails. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
…odman The macOS leg now runs every e2e file - browser view and the live dev folder included - once against Docker and once against Podman, as a two-engine matrix with one job per engine running `test:e2e` (the files are serialized on the fixed host ports, and there is one runner anyway). `macos-docker-engine.sh` becomes `macos-container-engine.sh` with `install docker|podman`: - docker: Colima + Docker CLI + buildx, as before - podman: the podman remote client against Lima's own `podman-rootful` template (Fedora's Podman, its socket at `/run/podman/podman.sock` in the VM - the suite's default mount path for Podman, so no `DOCKER_HOST`; the client is pointed at the forwarded socket via `CONTAINER_HOST`) Both VMs share the runner user's `$HOME` writable at the same path, and the job's `TMPDIR` is pointed under it: the dev-folder files register temp folders the runtime probes and bind-mounts through the daemon, so they must exist inside the VM. Rosetta is used for the amd64 Playwright images when the host has it (installation is attempted once), with QEMU user-mode emulation registered in the VM as the fallback. The install smoke-tests the socket mount, a `$HOME` bind mount and amd64 execution before the suite runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
…an hosts The macOS Podman leg runs Podman on Fedora (Lima's template, like Podman Desktop's machine) with SELinux enforcing, and there the runtime container cannot see the engine's socket bind-mounted into it - the documented `podman run` from README's "Running with Podman" fails the same way on any such host. `startRuntimeContainer` now adds `--security-opt label=disable` when Podman reports SELinux enabled (Docker hosts and the Ubuntu-based Podman legs stay unchanged), the install script's smoke test does the same and prints the socket's label and enforcing mode for diagnosis, and the README documents the flag for users. Also uses Lima v2's `template:` locator spelling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
macOS's bash 3.2 treats an empty array expansion (`"${secopt[@]}"`) as an
unbound variable under `set -u`, which failed the Docker leg's smoke test
right after the VM came up. The security options are now a plain string,
empty on Docker, and the engine-info line picks its format per CLI instead
of trying Podman's first.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
The dev-folder file's "anonymous node_modules volumes do not accumulate" case ran a hardcoded `docker volume ls`. On the Podman legs that counted the runner's unrelated Docker daemon's volumes (a vacuous pass), and on the macOS Podman leg, which has no `docker` binary, it failed with ENOENT. The count now goes through `helpers/docker.ts`'s `CONTAINER_CLI`, like every other engine call in the suite. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
… limit Each macOS job starts from an empty engine and pulls every base image again, and Docker Hub's anonymous pull limit is per IP - a day of runs on the one Mac hit "toomanyrequests" in five of seven e2e files' setup. The install script now logs both CLIs into Docker Hub when the workflow passes the Apify service account's credentials (the repository secrets `release.yml` already publishes with), so pulls count against the account. The credentials stay in the isolated `DOCKER_CONFIG` / `REGISTRY_AUTH_FILE` under the engine dir, which teardown removes; without credentials the script warns and pulls anonymously as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
A job that dies without reaching its teardown step (the runner losing contact with GitHub mid-suite leaves every later step unrun) leaves its Lima VM running and its host agent holding the forwarded ports. The install now runs the teardown first when the engine directory exists, stopping and deleting whatever VM is there with the binaries it left behind, instead of removing the directory from under a live VM. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
Conflicts resolved: - `.github/workflows/ci.yml`: the arm64 leg's comment now names both `build-image.yml` (master) and the macOS leg (this branch). - `README.md`: master's restructured Podman section, with this branch's SELinux note (`--security-opt label=disable`) as a bullet in it. - `test/e2e/helpers/docker.ts`: master's `startRuntimeContainer` mirrors apify-cli's `apify runtime start` command (`runtimeRunArgs`, pinned by `test/unit/e2e-runtime-command.test.ts`). This branch's stale-container removal stays in front of it, and its SELinux flag becomes an explicit suite-side `extraRunArgs` option placed before the image - `detectEngine` now reports `selinux` from `podman info` - so the pinned CLI-verbatim command is unchanged and the unit test pins the placement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
Pijukatel
added a commit
to apify/apify-cli
that referenced
this pull request
Oct 13, 2026
…esktops (#1495) Two cases where `apify runtime start` produced a runtime that reported the engine as unavailable. Both surfaced on actor-runtime's macOS CI leg ([apify/actor-runtime#65](apify/actor-runtime#65)). **SELinux on Podman hosts** - Under SELinux enforcing (Fedora, RHEL, and the Fedora-based VM of `podman machine`, so Podman Desktop on macOS and Windows), the runtime container may not connect to the engine's API socket bind-mounted into it: the socket carries the engine's label, not a container's. - `podmanInfo` now also reads `Host.Security.SELinuxEnabled`; `buildRuntimeRunArgs` adds `--security-opt label=disable` via the new `runtimeSecurityOptions`, next to `runtimeNetworkMode`. - Only the runtime container runs unconfined. The Actor containers it starts stay confined (verified by actor-runtime's dev-folder e2e under enforcing SELinux). **Docker socket on macOS and Windows** - The daemon lives in a VM there and serves `/var/run/docker.sock` inside it on every desktop engine. A `DOCKER_HOST` unix socket is a host path (Colima's docs suggest exporting one for tools that ignore contexts), which a bind mount cannot carry into the VM. - `resolveEngineSocketPath` now consults `DOCKER_HOST` only on Linux. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…art command apify-cli's `buildRuntimeRunArgs` now adds `--security-opt label=disable` itself on Podman hosts with SELinux enforcing (apify/apify-cli#1495), placed after the network mode. Move the suite's own `extraRunArgs` escape hatch into the mirrored `runtimeRunArgs` at that same position, so the pinned command is once again the CLI's verbatim, and pin the new shape in the unit test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
…ranch The runtime and the CLI are developed together, but CI tested the runtime against the CLI published on npm, so a change on either side that broke the other only surfaced once released. A new `apify-cli` job checks out the CLI's `actor-runtime` branch and packs it the way `npm publish` would; every e2e job (hosted matrix, Linux arm64, macOS arm64) installs that package with `.github/scripts/install-apify-cli-package.sh` and points the suite at it through `ACTOR_RUNTIME_E2E_APIFY_CLI`. Locally the suite still defaults to the published CLI. With that CLI always available, the monorepo e2e file joins the hosted matrix instead of skipping itself everywhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
…less and rootful
The runner now has Podman installed from podman.io's package, the install a developer has. The
macOS Podman leg therefore no longer brings Lima's Podman template and a remote client of its own;
it creates the installed podman's own machine (`podman machine init`, named `actor-runtime-e2e`)
per job and removes it at the end - the same Fedora CoreOS image, Virtualization.framework VM,
virtiofs home share and Rosetta a developer's machine is. Two legs: rootless, the default a
developer gets, and rootful, so both modes `apify runtime start` handles are covered.
The suite mounts the in-VM socket into the runtime container; for Podman the script now resolves
it the way the CLI does (`podman info --format '{{.Host.RemoteSocket.Path}}'`, rootless:
`/run/user/<uid>/podman/podman.sock`) and exports it as `DOCKER_HOST`. `TMPDIR` is pinned to the
engine dir before any podman call, since Podman keeps the machine's host-side sockets under
`$TMPDIR/podman` and resolves that per invocation. Docker stays as it was: the open-source stack
brought along per job.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
…tead of the samples' 128 MB The samples' own `defaultMemoryMbytes` (128 MB per page) buys a thirty-second of a core under the platform's memory-to-CPU rule, which the runtime enforces faithfully - and a run that small spent 40 s starting Node and 33 s fetching one page on the macOS runner, 85 s per run, eight runs in the dev-folder file alone. The suite now asks for `RUN_MEMORY_MBYTES` on every run it starts (`apify call --memory`, or `memory` in the start params of API-started runs): 1 GB by default, `ACTOR_RUNTIME_E2E_RUN_MEMORY_MBYTES` to raise it, 8 GB on the macOS jobs. The browser-bound files keep their 4 GB floor. The macOS VM gets all but two cores and all but 8 GiB of the machine, rather than half, so those runs have the CPU to use. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
…f-hosted runner The hosted legs already run every e2e file on every engine. The macOS leg is for what differs on a Mac - the engine in a VM, folders shared over virtiofs, amd64 images under Rosetta, `*.localhost` names and published ports crossing the VM boundary - so it now runs the smallest set of files that drives every hard feature through all of that: `all-modes` (one run with debug mode, the live dev folder and browser view on, a local edit running without a rebuild, the display streamed live), `web-server` (the run's live view at its container URL) and `standby` (Standby Actors, one reached from another Actor's container). Everything else is engine-independent and stays on the hosted legs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
The runner's Podman is 6.1.4, whose `machine init` has no `--rosetta` flag: Rosetta is the `[machine] rosetta` setting of containers.conf, on by default for applehv on Apple silicon. The machine is created without the flag; only when the host has no Rosetta is the setting switched off through `CONTAINERS_CONF_OVERRIDE` for that one command, so the QEMU fallback can take over. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
… the podman machine On the rootless podman machine the Playwright sample's amd64 Chrome ran under `qemu-x86_64-static` and crashed (`QEMU internal SIGSEGV`), although Rosetta is installed and the machine's `rosetta-activation.service` is enabled: the image's own QEMU binfmt handlers were what served x86_64 binaries. The standby and web-server files passed on that machine. After the machine starts, the script now runs, as the VM's root: a report of the activation service, the Rosetta mount and the registered handlers; Rosetta's registration if its share is mounted but no handler exists; and removal of the x86_64 QEMU handler whenever a Rosetta one is there, so Rosetta is the only handler for amd64 binaries. The smoke test reports which emulator an amd64 container actually ran under, and warns when it is QEMU. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
…h Rosetta The job log of the Podman legs shows the machine on `libkrun`: Podman 6 made it the default provider on Macs, and it has no Rosetta - amd64 binaries run under the image's QEMU user-mode emulation, under which Chrome crashes, so the browser view cannot work there whatever the binfmt handlers say. The machine is now created with `--provider applehv` and Rosetta set explicitly in a containers.conf override, since Podman 5.6 turned the setting off by default for a while. Also: `podman machine ssh` joins its arguments into one command line for the VM's shell, so the in-VM diagnostics script was never run (its quoted command collapsed to a bare `echo`); it now travels in a pipeline that needs no quoting. The smoke test reports Rosetta only when the amd64 container's `/proc/self/maps` names it - QEMU synthesizes that file and never does. README: Playwright-style Actors on Podman on Apple Silicon need the applehv provider. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
…start` The in-VM diagnostics of the applehv machine show the activation service running and failing to mount the Rosetta share: there was none. Podman reads `[machine] rosetta` again when it starts the machine and overwrites what `init` recorded, and the override file was unset between the two, so the VM started with Podman 6's default, off. The override now stays in force through `start`. README: on Podman 6, running browser Actors on Apple Silicon takes `rosetta = true` in containers.conf as well as the applehv provider. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
The new `test.md` bullets described the CI machinery - run memory numbers and the environment variable behind them, how the CLI package is built and installed, which VM stack each macOS job brings and cleans up. Requirements describe what is covered and where the runtime differs from the platform; the how lives in the workflow and scripts. Same trim for the README's Podman note and the Agent Skill's arm64 section, which no longer names the emulator. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oHHo44jxAVWrVrBvf2DBk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an
e2e-macos-arm64job toci.ymlthat runsactor-dev-loop.test.tson the[self-hosted, macOS, ARM64]runner - one file to start with, to prove the executor works end to end (runtime image build, container start, push / build / call through apify-cli against Docker Desktop's forwarded socket). The job is skipped for pull requests from forks, since the runner is a persistent Apify machine and the repository is public.Because that machine is never recreated, the job cleans up before and after the test with the new
.github/scripts/e2e-host-cleanup.sh: the runtime container and its volume, the Actor run / probe / browser-viewer containers and volumes the runtime labels, the images it builds, dangling layers, and the suite's temp dirs. The pre-test pass also fails early with the holder named if 3333 or 3000 is still taken by something that is not ours.startRuntimeContainernow force-removes a stale container of its own name beforedocker run, so a run that died beforeafterAll(locally or on the runner) no longer fails the next one with "name is already in use".Closes: #58