Orchestrating Apple Ecosystem CI/CD via GitLab macOS Runners

The integration of macOS into a continuous integration and continuous deployment (CI/CD) pipeline represents a critical requirement for organizations developing software for the Apple ecosystem, including macOS, iOS, watchOS, and tvOS. Unlike Linux-based runners, which benefit from the lightweight and highly portable nature of Docker containers, macOS runners necessitate a different architectural approach due to the proprietary nature of Apple's operating systems and the specific hardware requirements for code signing and simulator execution. This technical landscape involves managing physical hardware, virtual machines, or hosted environments provided by GitLab, all while navigating the specific constraints of macOS service modes and user sessions. Understanding the nuances of GitLab Runner on macOS requires a deep dive into installation protocols, service management, virtualization frameworks, and the distinction between local self-managed runners and GitLab's managed hosted runners.

Architecture and Service Modes on macOS

The fundamental operational logic of GitLab Runner on a macOS host differs significantly from its implementation on Linux or Windows. On macOS, the runner is restricted to specific service modes to ensure compatibility with the operating system's security and session management protocols.

The only supported mode for GitLab Runner on macOS is the user-mode LaunchAgent. This is a critical distinction that impacts how the runner is initialized and how it interacts with the system.

A LaunchAgent operates within the context of a specific user session. This means the runner does not start at the system boot level but rather when the authenticated user logs into the macOS interface. Because it runs as a user-mode process, the runner executes with the permissions of the currently authenticated user rather than as the root user. This architectural choice is mandatory for several highly technical reasons:

  • Access to the User Keychain: macOS code signing, which is essential for distributing and testing Apple applications, relies on the user's keychain. A system-level process running as root would lack the necessary permissions to access these user-specific cryptographic secrets.
  • UI Session Interaction: To run the iOS Simulator or other graphical tools required for mobile testing, the runner must have access to an active UI session. LaunchAgents are tied to the user's graphical session, whereas LaunchDaemons are strictly background processes with no access to the window server or user interface.
  • Configuration Storage: Because it is a user-specific service, the configuration is not stored in global system directories but is instead localized to the user's home directory at ~/.gitlab-runner/config.toml.

In contrast, a system-level LaunchDaemon is designed to start at boot and run as the root user. While this is ideal for web servers or database engines, GitLab Runner explicitly does not support this mode on macOS because a LaunchDaemon has no access to the user session or the keychain, rendering it incapable of performing modern mobile DevOps tasks. To mitigate the limitation where the runner stops when a user logs out, administrators must enable automatic login on the macOS machine to ensure the user session—and thus the runner—is restored automatically following a system reboot.

Feature LaunchAgent (Supported) LaunchDaemon (Unsupported)
Execution Context Authenticated User Root User
Startup Trigger User Login System Boot
Keychain Access Full Access No Access
UI/Simulator Access Yes No
Configuration Path ~/.gitlab-runner/config.toml System-wide

Implementation Strategies: Hosted vs. Self-Managed

Organizations must choose between two primary deployment models: utilizing GitLab's hosted macOS runners or managing their own local infrastructure. This decision impacts the complexity of maintenance, the cost of operation, and the level of control over the build environment.

GitLab Hosted macOS Runners

GitLab provides hosted macOS runners as part of its Premium and Ultimate tiers. These are available via GitLab.com and represent an on-demand environment that is fully integrated into the GitLab CI/CD ecosystem. Currently in Beta status, these runners are designed to simplify the "Mobile DevOps" workflow by removing the burden of hardware maintenance.

When using hosted runners, users do not interact with Docker images as they would on Linux. Instead, macOS runners utilize specific Virtual Machine (VM) images. These images are pre-configured with specific versions of macOS and Xcode to ensure consistency across builds.

The selection of the environment is handled within the .gitlab-ci.yml file. If a developer does not explicitly specify an image, the runner defaults to macos-15-xcode-16.

The available hardware profiles for hosted runners are categorized by their resource allocation:

Runner Tag vCPUs Memory Storage
saas-macos-medium-m1 4 8 GB 50 GB
saas-macos-large-m2pro 6 16 GB 50 GB

For developers needing to build for Intel-based (x86-64) targets while utilizing Apple Silicon hardware, GitLab leverages Rosetta 2 to emulate the required architecture. The images provided are updated with each GitLab release to ensure that the preinstalled software remains current and secure.

The available VM images include:

  • macos-14-xcode-15 (General Availability)
  • macos-15-xcode-16 (General Availability)

Self-Managed Local Runners

For organizations requiring specialized hardware or custom software configurations, self-managed runners offer maximum flexibility. This involves installing the GitLab Runner software directly onto a macOS machine (either Apple Silicon or Intel x86-64).

The setup process for a local runner requires several technical prerequisites to ensure the shell and environment are prepared for CI/CD execution:

  • Shell Configuration: While modern macOS versions use Zsh as the default shell, the GitLab Runner shell executor requires Bash to ensure that CI/CD scripts, which often rely on Bash-specific syntax, execute without error. The system shell must be changed using chsh -s /bin/bash.
  • Dependency Management: A standard installation involves installing Homebrew, rbenv (for Ruby version management), and the GitLab Runner binary itself.
  • Xcode Installation: Xcode is mandatory for any task involving compilation or simulation of Apple-platform software.

The registration process involves using the command line to select shell as the executor. Once the runner is registered, a success message will appear, and the configuration can be managed via the GitLab web interface under Settings > CI/CD > Runners.

Advanced Virtualization and the Tart Executor

For highly advanced users seeking to maximize throughput on a single host, the macOS virtualization framework can be utilized to run multiple macOS virtual machines in parallel. This is achieved through specialized tools that bridge the gap between the GitLab Runner and the macOS virtualization layer.

A notable implementation of this is the gitlab-tart-executor, which utilizes the tart command-line tool to provision and manage VMs. While a previous iteration of this project existed, it has been superseded by a more official implementation from Cirrus Labs, known as gitlab-tart-executor.

This method allows for significant parallelization. By using the macOS virtualization framework, an administrator can run two macOS virtual machines simultaneously on a single piece of hardware.

To implement this advanced configuration, the following technical steps are required:

  • Dependency Installation: The host machine requires gitlab-runner, daemonize, and the tart CLI, typically installed via Homebrew:
    brew install gitlab-runner daemonize cirruslabs/cli/tart
  • SSH Configuration: The host system must possess an SSH private key to manage the virtualized instances. If one does not exist, it must be generated:
    ssh-keygen -t ed25519
  • Configuration Customization: The gitlab-runner-example-config.toml must be modified to adjust the paths for prepare_exec, run_exec, and cleanup_exec to match the local environment.
  • Image Selection: The CI/CD pipeline controls the VM image via the image: tag. The default image for this configuration is ghcr.io/cirruslabs/macos-monterey-xcode:14, but users can pull other images from the Cirrus Labs package registry.

Troubleshooting and Technical Error Resolution

Operating GitLab Runner on macOS introduces specific failure modes that are not present in other operating systems. Engineers must be prepared to diagnose issues related to permissions, shell environments, and process execution.

The "Killed: 9" Error on Apple Silicon

A common issue encountered on Apple Silicon hardware is the Error: killed: 9 message when executing commands such as gitlab-runner install, gitlab-runner start, or gitlab-runner register. This error is typically not a sign of a software bug but rather a failure in the system's ability to manage the file paths required by the LaunchAgent.

To resolve this error, an administrator must verify that the directories specified for StandardOutPath and StandardErrorPath within the ~/Library/LaunchAgents/gitlab-runner.plist file exist and possess the correct write permissions. If the LaunchAgent attempts to write logs to a non-existent or restricted directory, the macOS kernel will terminate the process immediately.

Shell Discrepancies

If CI/CD jobs fail with syntax errors that appear valid in a local terminal, the first point of investigation should be the system shell. Because the runner's shell executor relies on Bash, any discrepancy between the runner's execution environment and the developer's local Zsh environment can lead to job failure.

To verify and correct the shell, the following commands are essential:

  • Check the current shell:
    echo $SHELL
  • If the output is not /bin/bash, change it:
    chsh -s /bin/bash

After performing this change, the user must restart the terminal or reconnect via SSH to ensure the environment variables and shell settings are refreshed.

Pipeline Configuration and Workflow Integration

Once the runner is established, the integration into the GitLab CI/CD pipeline is managed through the .gitlab-ci.yml file. The configuration must be precisely tuned to leverage the macOS environment, particularly regarding Ruby dependencies and mobile-specific tools like Fastlane and CocoaPods.

A robust macOS CI/CD configuration typically follows a multi-stage approach:

  1. Environment Setup: Defining variables like LANG: "en_US.UTF-8" to ensure consistent string encoding.
  2. Dependency Installation: Using before_script to manage Ruby gems and CocoaPods.
  3. Build Stage: Executing build commands (e.g., fastlane build).
  4. Test Stage: Executing automated testing (e.g., fastlane test).

The use of tags is mandatory to ensure that jobs are correctly routed to the macOS runner. If a job is intended for a macOS environment, the .gitlab-ci.yml must include a tag that matches the tag used during the runner registration process (e.g., tags: - macos).

Example configuration for a mobile application pipeline:

```yaml
stages:
- build
- test

variables:
LANG: "en_US.UTF-8"

before_script:
- gem install bundler
- bundle install
- gem install cocoapods
- pod install

build:
stage: build
script:
- bundle exec fastlane build
tags:
- macos

test:
stage: test
script:
- bundle exec fastlane test
tags:
- macos
```

Analytical Conclusion

The deployment of GitLab Runner on macOS is a specialized engineering task that necessitates a departure from standard Linux-based DevOps methodologies. The requirement for user-mode LaunchAgents creates a unique set of operational constraints, particularly regarding session persistence and the necessity of automatic login to maintain high availability. While the complexity of managing local macOS hardware—including Xcode updates, shell configurations, and keychain access—is significant, the advent of hosted runners and advanced virtualization tools like the tart executor provides scalable pathways for different organizational needs.

Organizations must weigh the granular control and cost-efficiency of self-managed runners against the operational simplicity and managed lifecycle of GitLab's hosted Premium/Ultimate offerings. The decision is ultimately a trade-off between the overhead of maintaining a dedicated macOS build farm and the need for highly customized, high-performance CI/CD pipelines. As the Apple ecosystem continues to evolve, particularly with the transition to Apple Silicon, the ability to orchestrate these runners effectively becomes a cornerstone of modern mobile application development and delivery.

Sources

  1. GitLab Docs: Install GitLab Runner on macOS
  2. Microfluidics: Hosted Runners on macOS
  3. GitLab Docs: Hosted Runners on macOS
  4. GitLab Docs: macOS Setup Configuration
  5. GitHub: gitlab-runner-tart Project

Related Posts