Skip to content

ci: Run the Actor dev-loop e2e on the self-hosted macOS arm64 runner - #65

Merged
Pijukatel merged 22 commits into
masterfrom
claude/fervent-sagan-3b11vx
Oct 13, 2026
Merged

Pijukatel merged 22 commits into
masterfrom
claude/fervent-sagan-3b11vx

Conversation

@Pijukatel

@Pijukatel Pijukatel commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

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".

Closes: #58

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
@Pijukatel Pijukatel added the t-tooling Issues with this label are in the ownership of the tooling team. label Sep 17, 2026
Pijukatel and others added 11 commits September 17, 2026 13:02
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>
claude added 10 commits October 13, 2026 09:21
…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
@Pijukatel
Pijukatel merged commit edf2bff into master Oct 13, 2026
41 checks passed
@Pijukatel
Pijukatel deleted the claude/fervent-sagan-3b11vx branch October 13, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-tooling Issues with this label are in the ownership of the tooling team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Set-up Mac (ARM64 based) Self-Hosted Github executors for e2e tests

3 participants