Skip to main content

CI with GitLab

This guide sets up CodeGuard for merge requests, the default branch and nightly runs in GitLab CI/CD. It requires self-hosted GitLab runners on macOS that are ephemeral and isolated, so that CodeGuard may build and test on them.

How CodeGuard works in CI​

  • Every invocation gets --ci. This profile is non-interactive, local trust tickets do not apply, and trust grant is forbidden.
  • compile and tests only start if the runner carries an organization attestation (see Organization policy). Without it, only the static checks run and the run ends with exit 6.
  • The job status follows CodeGuard's exit code. The reports are stored as a job artifact.

Prerequisites​

  • A GitLab runner on macOS 15 or later whose jobs each run in a fresh, ephemeral environment that is filesystem- and network-isolated. Only then may the attestation file be placed there.
  • Admin access to the runner image to place files as root.
  • The project lives in the root directory of the repository. check-diff aborts if the project root and the Git root differ.

Step 1: Prepare the runner image​

The preparation is the same as for GitHub Actions:

  1. Install Xcode (xcodebuild >= 26.0.0, < 28.0.0) and the required simulator runtimes.

  2. Install CodeGuard with Scripts/install.sh (repository URL: placeholder <REPOSITORY-URL>).

  3. Put a root-owned copy of SwiftLint in /usr/local/bin.

  4. Install the organization policy with trusted_roots and execution_environment at /Library/Application Support/CodeGuard/policy.yml.

  5. Place the attestation file with exactly this content, owned by root with mode 0644:

    version: 1
    runner_class: ephemeral
    ephemeral: true
    filesystem_isolated: true
    network_isolated: true
  6. Check as the runner user: codeguard version, codeguard --ci config validate, codeguard --ci doctor.

All commands and files in detail: CI with GitHub Actions, step 1.

danger

The attestation guarantees that the runner is ephemeral and isolated. CodeGuard then runs project code from merge requests without human approval. Never place it on a persistent or networked runner.

Assign a tag to the runner, for example macos-codeguard (placeholder), so that only matching jobs run there.

Step 2: Prepare the repository​

  • Commit .codeguard.yml and, if needed, .swift-format and .swiftlint.yml to the root directory.
  • .gitlab-ci.yml, .codeguard.yml, Package.swift, Package.resolved, *.xcconfig, project.pbxproj and entitlements are protected files. A merge request that changes one of them ends with exit 6 (protected_file). Such changes deliberately need human review.

Step 3: Create the pipeline​

File .gitlab-ci.yml:

stages:
- codeguard

variables:
GIT_DEPTH: "0" # full clone so the base revision exists

.codeguard:
stage: codeguard
tags: [macos-codeguard] # placeholder: tag of your ephemeral macOS runners
timeout: 3h
artifacts:
when: always
expire_in: 30 days
paths:
- codeguard-reports/

codeguard:diff:
extends: .codeguard
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- mkdir -p codeguard-reports
- set +e
- codeguard --ci --format json --output codeguard-reports/codeguard.json check-diff --base "$CI_MERGE_REQUEST_DIFF_BASE_SHA"
- rc=$?
- echo "CodeGuard exit code $rc"
- exit $rc

codeguard:project:
extends: .codeguard
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE == "schedule"
script:
- mkdir -p codeguard-reports
- set +e
- codeguard --ci --format sarif --output codeguard-reports/codeguard.sarif check-project
- rc=$?
- echo "CodeGuard exit code $rc"
- exit $rc

Explanations:

  • GIT_DEPTH: "0": By default, GitLab makes a shallow clone. Without the base revision, check-diff ends with exit 2 (unknown reference) or exit 3 (commit object missing).
  • CI_MERGE_REQUEST_DIFF_BASE_SHA: Base of the merge request diff. This variable only exists in merge request pipelines, hence the rules with merge_request_event.
  • set +e and exit $rc: The script runs to the end and uses CodeGuard's exit code as the job result. This form requires the script lines to run in a shared shell, as is usual with the shell executor. Check this in your runner configuration.
  • artifacts: when: always: The report is stored even if there are findings.
  • Nightly run: Create a pipeline schedule in the project for this. codeguard:project then runs with $CI_PIPELINE_SOURCE == "schedule".
  • timeout: Builds for several platforms and simulator tests take time. CodeGuard has no overall time limit for the whole run.
Format gitlab-sarif

CodeGuard also supports --format gitlab-sarif. This variant contains only security-related findings with a file and position, at most 5000 results, and truncates messages to 1024 characters. Integration with GitLab security reports is not part of this guide; here the reports are only stored as an artifact.

Check the result​

With a valid attestation, the JSON report contains "build_authorization": "operator_attested" under run, and compile and tests appear under Completed. If the attestation is missing, the report says:

1. ERROR
Rule: codeguard-build-execution-denied
Build and test execution was not authorized: ci-untrusted execution requires an attested isolated runner: no declaration is installed at the organisation's execution-environment path.
Grant project trust, or run inside an attested isolated environment.

You can find the full report for this and the meaning of all exit codes in the pipeline context under CI with GitHub Actions; they apply to GitLab in the same way.

Simulators on the runner​

For tests, CodeGuard creates disposable simulators in the runner user's default device set and deletes them afterwards. On an ephemeral runner, the remaining logs disappear with the environment. If a job is aborted hard, a leftover device is only cleaned up by the next run on the same runner. See Trust and security.

See also​