
Cover image credit: Photo by Javier Allegue Barros on Unsplash
Should you run macOS jobs from Tekton?
A CustomRun connects Tekton to disposable macOS VMs, but choosing that integration means owning results, retries and cleanup after Tekton stops waiting, with runner integrations offering another route.
Series · Running macOS Builds from Tekton
Post 7 of 7
Series · Running macOS Builds from Tekton
Post 7 of 7
- Connecting a Mac build host to a private EKS cluster
- Running macOS builds from Tekton with a CustomRun
- Passing source and artifacts between Tekton and a macOS VM
- Tracking a macOS build until its VM is deleted
- Recovering macOS builds when the controller or agent stops.
- Starting macOS builds in seconds instead of half a minute
- Should you run macOS jobs from Tekton?
Table of Contents
Running the macOS build was only part of the integration. Tekton needed to know when it could stop waiting, and the Mac needed to know when its VM slot was safe to reuse. Those events did not always coincide: with the agent unavailable, the CustomRun timed out before the VM was deleted.
I used a CustomRun to let Tekton track the external job while an agent on the Mac ran it in a disposable VM. The four build cases also ran through a private EKS cluster in Part 0. On normal completion, the controller waits for confirmed VM deletion before finishing the CustomRun. An unreachable agent needs a separate timeout and host cleanup path. Whether this integration is worth operating depends on who will own that work.
The connection lets the Mac receive requests. The execution contract gives Tekton a result to track, and digest checks let the next stage verify the returned bytes. Recovery has a separate job: preserving the outcome and keeping a VM slot reserved until deletion is confirmed. Faster startup changes the wait before execution. It leaves those responsibilities in place.
Who drives execution decides the integration
The two routes compared here put macOS work behind different Kubernetes interfaces. A provider can represent the Mac as a node and map a Pod to a VM, or a controller can track an external job through a CustomRun. Compatibility depends on which component drives the process.
Agoda’s macOS-vz-kubelet takes the node route. Its README says the first container is the VM, further containers run on the host’s Docker, exec and attach reach the VM over SSH, and init containers are not supported.
Agoda describes running GitLab’s Kubernetes executor on it, with the VM as the build environment. GitLab’s documentation describes the runner driving execution through Kubernetes attach or exec. That path lets the CI runner drive commands from outside the Pod. This comparison follows the documentation and Agoda’s post. I did not run that integration.
A TaskRun expects Tekton’s entrypoint binary inside each step container. Steps coordinate through shared files, and their exit statuses determine success or failure. An init container copies the entrypoint in. Part 1 traced this contract and showed the published Linux binary exiting 126 in the macOS guest. A Pod-to-VM provider would need more than a matching node label: it would need to support that execution contract.
The diagram separates the TaskRun requirement from the external CustomRun path:
flowchart TD G["GitLab runner drives commands<br/>through attach or exec"] --> N["Pod-as-VM provider<br/>macOS-vz-kubelet"] T["Tekton TaskRun needs its entrypoint<br/>and shared files inside the Pod"] --> P["A provider must implement<br/>that Pod contract"] C["Tekton CustomRun delegates<br/>through an external task contract"] --> A["Controller creates MacBuild;<br/>Mac agent runs the VM"]
An executor that drives commands remotely avoids Tekton’s in-guest entrypoint requirement. That distinction explains the CustomRun choice here. It does not establish that every remote executor will work with a Pod-to-VM provider.
The lifecycle is the work
The runs exposed five responsibilities the integration must own:
- On normal completion, report the result after VM deletion. The controller maps the agent’s verdict but holds the CustomRun at
Unknownuntil the agent confirmsVMDeleted=True. Part 3 recorded the build outcome and confirmed deletion as separate journal transitions. Cancellation and an unavailable agent need their own completion rules. - Hold the slot until deletion is confirmed. If deletion fails, the attempt is quarantined and keeps its slot reserved while cleanup remains unresolved. With one slot, no other build can start. Part 4 exercised this with an injected delete failure. The next attempt started only after deletion succeeded.
- Bound Tekton’s wait and host cleanup separately. The controller can time out an unreachable CustomRun with
vmRetirement=unconfirmed. On an awake Mac, an independent watchdog removed overdue VMs after the host deadline. Without it, cleanup waited for agent recovery. The deadlines start at different events, and Part 4 gives their grace periods and measured deletion times. Ending the CustomRun does not delete the VM. - Recover from the host journal. The agent records transitions before updating cluster status, so a restarted agent can finish deletion or resubmit an outcome the cluster never received. Part 4 exercised redelivery by refusing terminal status writes. Journal-write failure has a different consequence: the error is logged and execution continues without recording that transition. Recovery can reconstruct only what was recorded. I would resolve that failure policy before relying on the journal for ongoing builds.
- Make repeated execution tolerable. In Part 4, killing the agent interrupted a build. Recovery deleted its VM and retried in a second one. The build started twice and finished once. Publishing or signing must tolerate repetition, and rollback of external side effects was not tested.
A missed lease renewal stops no process on the Mac. Part 4’s stale-write check rejects a status update from an attempt the agent no longer owns, but it cannot stop a stale agent already acting on the host. A live stale agent was not exercised. Deadlines and deletion limit how long the VM remains under the tested conditions. They cannot undo an external side effect already performed.
A provider can take over some of these responsibilities. Before choosing one, check its behavior when the build finishes but deletion fails, or when the process driving the build disappears. The integration needs an owner for both outcomes.
A faster start consumes capacity
Part 5 cut median time to the first build command through Tekton by roughly 21s with a pre-booted VM: 30.40s cold versus 9.27s warm. Each condition had 10 runs on an idle M3 Max with kind. The pool did not consistently make compilation faster.
A ready VM reserves capacity before the request. The other operating costs remain:
| Constraint | Operational consequence | Operating response | Evidence scope |
|---|---|---|---|
| A pre-booted VM | Holds one of two slots and adds about 10 GiB of combined VM-process RSS while waiting | Use it when faster starts between batches justify the occupied slot and memory | One ready VM on an idle Mac. Only 10 of 60 jobs in pool-enabled six-job batches were warm. |
| Two additional macOS VMs per Mac | Apple’s agreement, section 2B(iii), permits up to two additional macOS instances on an Apple-branded Mac you own or control that already runs macOS, for specified purposes including development and testing during development | Assess the operating arrangement under the license before adding hosts or offering CI services | A fleet of Macs was not tested. |
| Base disk: about 86 GB apparent size | Xcode occupies substantial disk space; pinning an image does not verify a locally modified base disk | Budget for the base and limit drift with a scheduled re-pull. That does not reduce its size. | The scheduled re-pull was not tested. |
| Components you must operate | The agent, watchdog, controller, CRD, registry and base image span host execution, cluster status and artifact retention | Assign ownership and complete scoped agent access and persistent, access-controlled artifact storage | Part 0 and Part 2 describe those remaining access and storage requirements. |
The paragraph following section 2B(iii) restricts service-bureau, time-sharing, terminal-sharing, relay and similar uses, except as expressly permitted in section 3. Section 3 separately permits developer-service leasing, including CI, under stated conditions. The two-instance allowance alone does not establish permission for a hosted CI service.
The Xcode checks in Part 3 compare the host catalog before cloning and the guest version after boot. Neither hashes the base disk.
In six-job batches, median completion was 182.5s with a ready VM against 205.5s without, and the ranges overlap. Each pool-enabled batch consumed its initial warm VM. With both slots busy and jobs queued, the pool could not refill, so the remaining five jobs were cold. Part 5 did not establish a general throughput gain.
Where the macOS boundary should sit
Keeping Tekton does not require splitting the build across Linux and macOS. Each Linux stage still needs to contribute useful work.
The Linux → macOS → Linux path in Part 2 lets Linux fetch the archive and check it against the macOS stage’s digest without mounting the guest’s disk. That checks agreement with the producer. It does not establish that the build is trustworthy.
If the Linux stages contribute nothing, I would keep checkout, build, test and packaging in one disposable macOS VM. That whole-workflow arrangement was not run here. Source still has to enter and artifacts leave, and the agent still owns cancellation, recovery and VM deletion. Keep a Linux stage when it brings an existing preparation task, tooling that lives on Linux, or an independent check of the published bytes. Shared storage does not justify a boundary on its own: a PVC bound to Linux tasks does not also mount that storage inside the external guest.
A separate choice is whether to use a VM at all. Bare metal avoids VM boot and the two-additional-VM condition, and a job can select among installed Xcode versions with DEVELOPER_DIR. It also leaves build state on the host. Cleanup has to cover the keychain and signing certificates, caches, DerivedData, simulator state, LaunchAgents and temporary files. Test execution has session requirements too: in Part 1, xcodebuild test ran through the auto-logged-in user’s GUI session.
Use a fresh VM when isolation from other builds matters. Bare metal with explicit cleanup may fit trusted internal builds, but trust does not remove residual state. Repeatability also needs pinned inputs and toolchain checks. A fresh VM alone does not establish it. The bare-metal option was not tested.
When another route is shorter
These routes put more of the job lifecycle under the CI runner or VM provider. The comparison follows their documentation, not runs of this workload.
| If this is your situation | Consider | What the documentation says |
|---|---|---|
| You have no Macs | A hosted macOS runner from GitHub, Buildkite or GitLab | GitHub: standard macOS jobs run on fresh VMs. Buildkite: each job runs in an isolated VM. GitLab’s hosted macOS runners are in Beta on Premium and Ultimate. |
| You have Macs but no Tekton on this workload | A CI agent on the Mac that starts a VM per job, with Tart’s GitLab Runner or Buildkite integration | Tart documents jobs in ephemeral VMs through GitLab’s custom executor and a Buildkite plugin. |
| You run GitLab on Kubernetes and own Mac hosts | macOS-vz-kubelet, which maps a Pod to a VM | Agoda describes GitLab’s Kubernetes executor with the VM as the build environment, and two VMs per host. |
| You run Tekton and would rather use managed Macs than operate your own | MacStadium Orka with orka-tekton | Linux Tekton tasks provision and delete Orka VMs, reaching them over SSH. The README requires a VPN, Tekton Pipelines v0.16.0 or later and Orka 1.4.1 or later; compatibility with current Orka is not stated. |
Check the test suite’s session requirements before choosing hosted runners. GitLab’s macOS runners are headless, and its documentation excludes workloads requiring UI interactions such as testmanagerd. That matters when considering the GUI-session path used in this series.
Where the CI runner owns the job, you can avoid this implementation’s controller and MacBuild CRD. The runner or provider still needs its own artifact and cleanup arrangements. The Orka integration keeps Tekton and requires access to Orka over the VPN.
What this covers and what it does not
The runs used one M3 Max and one small macOS app. kind hosted the lifecycle, recovery and timing cases. A private EKS cluster hosted the connection and four build cases. Parts 0 and 5 report repeated timing samples, while most fault cases ran once. There was no iOS, signing or large build. Multi-slot recovery, sleep, reboot and fleet behavior were not tested. The complete workflow in one VM, bare metal and the alternative integrations were not run.
Who owns the work after Tekton stops waiting?
If Tekton needs to own the pipeline and you control the Macs, a CustomRun is a workable way to include the macOS stage, provided build steps tolerate repetition. When that integration does not justify operating the Mac agent and controller, a runner integration is the shorter route. Before relying on this implementation for ongoing builds, I would resolve the journal-failure policy, validate scoped access and persistent artifact storage, and test recovery at the intended host capacity.
The series began with a Mac outside the cluster. The connection made it reachable, and the CustomRun made its build visible to Tekton. The remaining responsibility stays on the host: preserve the outcome, tolerate a repeated attempt, and confirm deletion before reusing the slot. Tekton can stop waiting before that work is finished. The host must keep accounting for the VM until it is gone.
References
- Custom Tasks and CustomRuns, Tekton Pipelines
- macOS-vz-kubelet, Agoda
- How We Integrated Native macOS Workloads with Kubernetes, Agoda Engineering
- Tart
- macOS 27 Software License Agreement, Apple
- orka-tekton, MacStadium
- Kubernetes executor, GitLab Runner
- GitHub-hosted runners, GitHub Actions
- macOS hosted agents, Buildkite
- Hosted runners on macOS, GitLab
- GitLab Runner integration, Tart
- Buildkite integration, Tart