GitLab’s official documentation describes three Runner scopes—instance, group, and project—while Shell executor jobs still have limited isolation and may access other project data on the same host (Runner scope documentation and Shell executor security guidance). That leads to a clear deployment decision: use a controlled shared Mac build pool for trusted compilation and testing, but route production signing, untrusted merge requests, and different trust domains to dedicated Mac Runners.

This week’s action: keep the shared build pool and dedicated release pool separate from the first pipeline. Do not treat tags, directory names, or Protected Runner settings as a host-level security boundary.

This guide is for:

  • Platform engineers providing GitLab macOS build capacity to multiple repositories.
  • Security and release owners protecting certificates, private keys, and internal dependencies.
  • IT and procurement leaders deciding between shared, dedicated, and elastic remote Mac capacity.

The deployment timeline starts with two trust zones

The first failure often looks harmless: the tag route is correct, but the second project can still read files left by the first. A shared Mac Runner can send jobs to the intended node while the underlying Shell executor continues to use the same macOS host, local account, filesystem, processes, and user-level configuration.

Use a two-layer design:

  • Shared build pool: trusted projects run compilation, unit tests, UI tests, dependency resolution, and non-production packaging.
  • Dedicated release pool: protected release jobs run archive, signing, notarization-related preparation, and store upload tasks.

Shared hardware does not mean shared identity. A project can share a Mac’s compute capacity without sharing its source directory, Keychain items, signing identity, or internal network permissions.

Before registering any GitLab Runner, classify each project against four trust conditions:

  1. Code source: Is the repository controlled by the same organization and reviewed under the same policy?
  2. Repository permissions: Can its jobs access protected branches, deployment variables, or release credentials?
  3. Internal network access: Does the build need private APIs, package registries, databases, or production-adjacent services?
  4. Signing assets: Does the pipeline need certificates, private keys, Provisioning Profiles, or App Store Connect credentials?

Choose the shared pool only when all four conditions fit the same trust zone. If the project needs a separate service account or network boundary, start with an independent Runner identity. If it handles production signing, untrusted code, or a materially different trust domain, use a dedicated Mac Runner.

Can one GitLab Runner serve several projects at the same time?
Yes, a GitLab Runner can be registered at a scope that serves multiple projects, and a controlled group Runner is usually the practical starting point for related repositories. That capability is a routing and administration feature, not proof that jobs are isolated from one another. The Shell executor warning remains decisive: only trusted workloads should share the host (GitLab’s Shell executor guidance).

Hour one: set scope, labels, and protected routes

GitLab provides three useful Runner scopes:

  • Project Runner: limited to one project. Use it when the repository has a unique trust level, signing requirement, or network need.
  • Group Runner: available to selected projects in a group. Use it for a governed shared build pool with an explicit allowlist.
  • Instance Runner: potentially available across the GitLab instance. Treat this as the broadest administrative scope, not the default choice for sensitive enterprise Mac capacity.

For multi-project Mac builds, begin with a group Runner unless the project requires stronger separation. Do not register a release node as an unrestricted instance Runner simply because several teams need it.

The first route policy should have three properties:

  • Jobs must request an explicit Mac label, such as macos-shared or macos-release.
  • The release Runner accepts only protected branches or protected tags.
  • The Runner is configured to run only tagged jobs where your GitLab policy requires that control.

GitLab’s Runner configuration documentation explains tag matching, protected Runners, and the relationship between project configuration and job routing (official Runner configuration guidance). Use that document as the control reference, then verify the behavior with an actual pipeline rather than relying on the interface.

A minimal routing test should include:

build_shared:
  tags:
    - macos-shared
  script:
    - whoami
    - pwd
    - xcodebuild -version

release_protected:
  tags:
    - macos-release
  rules:
    - if: '$CI_COMMIT_TAG'
  script:
    - echo "Release route requires a protected tag"

The commands are intentionally small. At this stage, you are testing identity, location, label matching, and protection rules—not building the application.

Record five pieces of evidence:

  1. The Runner scope and permitted project list.
  2. The exact labels assigned to each node.
  3. A successful shared-build route.
  4. A rejected route when a job requests an unavailable or protected label.
  5. A release job that cannot run from an ordinary branch.

A Runner shown as online is not enough. You need proof that the correct project reaches the correct trust zone.

First pipeline: separate accounts, workspaces, and rebuildable data

The second milestone is a real job under the GitLab Runner service account. An administrator opening Terminal and successfully running xcodebuild does not validate the CI environment. The service account may have a different home directory, shell configuration, Keychain visibility, permissions, PATH, and access to installed tools.

The first pipeline should print and inspect, without exposing secrets:

  • The effective user.
  • The home directory.
  • The working directory.
  • The temporary directory.
  • The Xcode path and version.
  • The ownership of the checkout and cache directories.

Keep these data classes separate:

  • Project checkout: disposable source code for the current job.
  • DerivedData and build intermediates: rebuildable data that should not become a cross-project dependency.
  • Dependency cache: optional acceleration data that must be treated as untrusted input, not as a credential store.
  • Build artifacts: outputs that should be uploaded through the pipeline and then removed locally.
  • Long-lived credentials: certificates, private keys, profiles, and access tokens that require a separate control path.

GitLab’s cache documentation makes an important distinction: caches are intended to speed up later jobs, but they are not a substitute for secure artifact or secret handling (GitLab CI/CD caching documentation). A cache key should not quietly turn one project’s dependency state into another project’s trusted input.

A cleanup sequence should run after every shared job, including failed jobs. It should remove the checkout, temporary files, generated archives, simulator data created for the job, and any project-specific DerivedData path. Directory separation reduces accidental reuse, but it does not stop the same Shell user from reading another directory.

How should a macOS Shell executor isolate different projects?
Use separate project paths, explicit cleanup, controlled cache keys, and a real service-account test. Do not describe those measures as a sandbox. On a shared Shell executor, a job running as the same local user may still inspect other accessible files or processes. If that risk is unacceptable, move the project to a separate Runner account or Mac node (Shell executor security limits).

Your first acceptance test should deliberately try to read a marker file created by another test project. The expected result is either that the file does not exist after cleanup or that the project cannot access the location. Save the job log and filesystem cleanup record. A green build without a failed cross-project access test proves very little.

Second project: test contamination before expanding access

Do not register ten repositories after the first project passes. Add one second project from the same intended trust zone and test the boundary between the two.

Run the projects sequentially first. Then test controlled concurrency if the shared node is expected to process jobs in parallel. Look for five contamination paths:

  • Residual source files in the checkout directory.
  • Environment variables or user-level configuration left by the previous job.
  • Background processes that continue after the job exits.
  • Shared caches or identical DerivedData paths.
  • Simulator state, Homebrew changes, or toolchain modifications that affect the next job.

The key question is not whether the second project receives the correct tag. The question is whether it can observe or influence state left behind by the first project.

Use a simple evidence sequence:

  1. Project A creates a uniquely named marker in its workspace.
  2. The job records the expected cleanup result.
  3. Project B searches only its permitted paths for that marker.
  4. Project B checks for unexpected processes and files.
  5. The node is inspected after both jobs complete.
  6. The same test is repeated after a failed build.

GitLab’s YAML reference can help keep cleanup commands and failure behavior explicit rather than hidden in local shell profiles (GitLab CI/CD YAML reference). Keep the job definition readable. A complex script that silently depends on administrator-installed state is difficult to audit.

When should two projects stop sharing one Mac Runner?
Stop at this milestone if the projects belong to different business units, use different internal network permissions, accept untrusted merge requests, or require different credential policies. A separate Runner account may be enough for low-risk separation. A separate Mac node is the safer choice when the project can execute arbitrary scripts, access sensitive services, or perform production signing.

Do not solve a trust-domain conflict by adding more tags. Tags decide where a job is eligible to run. They do not erase local files, terminate processes, or create a macOS security boundary.

Production release: move Keychain access to a dedicated node

The release milestone changes the risk profile. A normal build may need source code and dependencies. A release job may need a private signing key, a certificate, a Provisioning Profile, and store credentials. Those assets should not be present on a Mac that also accepts ordinary project jobs.

Apple documents Keychain Services and access-control behavior, but Keychain controls do not automatically neutralize the risk of arbitrary scripts running under the same local account (Apple Keychain Services documentation). Access control lists and item accessibility settings are useful controls, not a replacement for trust-zone separation.

Use the dedicated release node with these boundaries:

  • The Runner is limited to the release project or a tightly controlled release group.
  • Only protected branches or tags can reach it.
  • Signing material is unavailable to the shared build pool.
  • Release credentials are injected only for the minimum job scope.
  • The release job does not accept arbitrary merge-request code.
  • Keychain access is tested after reboot and after credential rotation.
  • Failed jobs remove temporary archives and exported signing material.

Does iOS production signing require a dedicated GitLab Runner?
It should use one when the signing job handles production private keys, protected release credentials, or code from a trust domain that ordinary shared jobs do not share. A dedicated Runner does not eliminate every application or credential risk, but it reduces the number of jobs and users that can reach the signing environment. If your release process cannot tolerate a shared Shell user, a dedicated Mac node is the appropriate boundary.

The release acceptance test should contain six checks:

  1. A permitted protected release job can access only the required signing item.
  2. An ordinary project cannot schedule onto the release label.
  3. An unprotected branch is rejected.
  4. A failed signing job removes temporary exported material.
  5. Credential rotation invalidates the old access path.
  6. A node restart requires the intended Keychain and Runner controls to be restored.

Apple’s guidance on restricting Keychain item accessibility should be reviewed alongside your account and Runner design (Keychain item accessibility guidance). The documentation helps define item behavior; your pipeline logs must prove that the intended project can and cannot use it.

First week: choose shared, dedicated, or hybrid capacity

After the first week, make the capacity decision from evidence rather than the Runner status page. Collect:

  • Queue time by project and job type.
  • Concurrent job collisions.
  • Cleanup failures.
  • Failed-build residue.
  • Node restart recovery results.
  • Release-node occupancy.
  • Frequency of simulator and toolchain conflicts.
  • Number of projects that need protected signing access.

Do not invent a concurrency limit or build-time target without enterprise records or a documented test. GitLab’s configuration model can route jobs, but it does not tell you how many Xcode builds your particular Mac, project, simulator set, or dependency graph can sustain.

Use these decision conditions:

  • If all projects share the same code trust, repository permissions, network boundary, and credential policy, choose the controlled shared build pool.
  • If one project needs a different service account or internal network path, move it to an independent Runner identity and repeat the contamination tests.
  • If any project handles production signing or untrusted code, choose a dedicated Mac Runner.
  • If queue evidence shows ordinary builds blocking protected release work, add a dedicated release node before expanding the shared pool.
  • If demand is irregular and the team needs a temporary Mac capacity increase, test a hybrid model with stable shared capacity plus an elastic remote Mac.
  • If cleanup, restart recovery, or signing tests fail, stop rollout and fix the boundary before adding projects.

For teams evaluating remote capacity, start with a controlled remote Mac rental proof-of-concept rather than buying several nodes before the trust model is validated. Review VMSPIN pricing only after you define the number of shared-build and dedicated-release roles you actually need. The useful comparison is not simply monthly hardware cost; it is the cost of an isolated node, administration, replacement, credential recovery, and unused capacity.

The final acceptance path must cover the whole build loop

A multi-project Mac Runner deployment is not complete when GitLab shows the node as available. The final test must run the complete path:

  1. The job is accepted by the intended Runner scope.
  2. The repository is checked out under the expected service account.
  3. Dependencies are restored without importing another project’s secret state.
  4. Xcode builds the application with the intended toolchain.
  5. Tests complete without leaving uncontrolled simulator or process state.
  6. The artifact is uploaded through the approved path.
  7. Signing occurs only on the protected release node.
  8. Cleanup removes rebuildable and temporary data.
  9. A failure is recoverable without manual administrator intervention.
  10. The next project cannot reuse prohibited state.

Keep evidence with the platform record: Runner scope, labels, permitted projects, job logs, cleanup results, rejected-route tests, signing tests, and restart recovery notes. Recheck GitLab’s macOS Runner setup guidance before publication because executor support and security behavior can change (GitLab macOS Runner setup).

A single Mac that handles ordinary builds, untrusted branches, and production signing may appear efficient, but it concentrates credentials, residue, and operational failure in one host. Buying more identical machines does not correct a weak routing model. The safer sequence is to separate the trust zones first, then size the capacity.

If your current setup uses one Mac for every project, the decision is straightforward: keep trusted compilation in a shared pool, move signing and materially different trust domains to dedicated Mac Runners, and add elastic remote capacity only where queue evidence justifies it. VMSPIN is a reasonable way to trial those separate roles before committing to a larger purchase, especially when you need temporary Mac capacity, a controlled CI environment, or a second node for release validation. You can request the relevant VMSPIN Mac environment after the isolation tests define the required role.