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, andtrust grantis forbidden. compileandtestsonly 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-diffaborts if the project root and the Git root differ.
Step 1: Prepare the runner image
The preparation is the same as for GitHub Actions:
-
Install Xcode (
xcodebuild >= 26.0.0, < 28.0.0) and the required simulator runtimes. -
Install CodeGuard with
Scripts/install.sh(repository URL: placeholder<REPOSITORY-URL>). -
Put a root-owned copy of SwiftLint in
/usr/local/bin. -
Install the organization policy with
trusted_rootsandexecution_environmentat/Library/Application Support/CodeGuard/policy.yml. -
Place the attestation file with exactly this content, owned by
rootwith mode0644:version: 1runner_class: ephemeralephemeral: truefilesystem_isolated: truenetwork_isolated: true -
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.
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.ymland, if needed,.swift-formatand.swiftlint.ymlto the root directory. .gitlab-ci.yml,.codeguard.yml,Package.swift,Package.resolved,*.xcconfig,project.pbxprojand 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-diffends 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 theruleswithmerge_request_event.set +eandexit $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:projectthen 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.
gitlab-sarifCodeGuard 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.