The same commit may leave a developer’s machine unchanged yet produce dozens of changed lines under Generated/ in cloud Mac CI. Worse, the project may still compile, allowing stale constants, missing resource keys, or outdated mocks to make their way into the build artifacts. Fixing this class of problem takes more than adding another generation command. The generator version, input set, output directory, and acceptance criteria must all become part of the build contract.
Identify the Source of the Drift First
The output of tools such as SwiftGen and Sourcery depends on four factors: the executable version, configuration and templates, input files, and the runtime environment. Pinning only the configuration file is not enough. The tool found in a developer’s PATH may be newer than the one used in CI, file traversal order may vary with directory state, and locale settings may affect sorting or date formats.
Start by capturing the same minimal diagnostics locally and in CI:
set -euo pipefail
sw_vers
xcodebuild -version
swiftgen --version
sourcery --version
printf 'LANG=%s
' "${LANG:-unset}"
printf 'PWD=%s
' "$PWD"
git status --short
The goal is not to collect as much information as possible, but to establish what the generation process actually consumed. If a template writes the current time, an absolute path, or a random identifier, remove those nondeterministic inputs first. Otherwise, every run will produce differences that are valid but meaningless.
“Can be generated again” does not mean “can generate the same result.” The gate verifies that identical inputs produce byte-for-byte identical artifacts.
Establish a Reproducible Generation Baseline
The generation directory should be owned entirely by the tools; do not mix manually maintained Swift files into it. Delete the old directory before every run. This exposes types that were removed from the inputs but left behind by incremental generation. Configuration files, templates, and input directories should all use deterministic paths relative to the repository root rather than depend on the caller’s current working directory.
A practical division of responsibilities looks like this:
| Item | How to pin it | CI acceptance check |
|---|---|---|
| Generator | Pin and print the version | Matches the repository declaration |
| Configuration and templates | Keep under version control | No temporary workspace changes |
| Inputs | Specify directories and extensions | Sorted input set remains stable |
| Outputs | Use a dedicated directory | Clear before generation |
| Environment | Fix locale settings | Do not write timestamps or absolute paths |
Teams may choose different installation methods, but there must be exactly one source of truth for version declarations. Do not put one version in a script, another in the developer documentation, and then let the cloud Mac use a third version from its PATH.
Run the Diff Gate Before Compilation
Run code generation before compilation to get faster failure feedback and prevent compiler errors from obscuring the actual cause. The following script assumes that all generated files are stored in Generated/ and committed to the repository:
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"
export LANG=C
export LC_ALL=C
rm -rf Generated
mkdir -p Generated build
swiftgen config run --config swiftgen.yml
sourcery --config sourcery.yml
find Generated -type f -print |
LC_ALL=C sort |
while IFS= read -r file; do
shasum -a 256 "$file"
done > build/generated.sha256
git diff --exit-code -- Generated
git status --short --untracked-files=all Generated
git diff only reports changes to tracked files, so untracked files must be checked as well. In the actual gate, explicitly fail whenever git status produces output, and preserve the diff patch and generated.sha256 as build artifacts. The hash manifest does not replace the diff check, but it does answer the question, “Which exact set of files did this CI run generate?”
Distinguish Between Three Failure Types
If only formatting changes, inspect template line endings, tool versions, and formatter execution order. If files were added or removed, inspect the input set and cleanup logic. If values changed, inspect resources, API descriptions, or template parameters. Classify the failure and fix its source rather than overwriting the artifacts in CI and continuing with compilation.
Avoid Concurrency Pitfalls in Xcode Build Phases
When multiple targets share one generation directory, placing the generation command in each target’s Run Script Phase may cause concurrent writes to the same files. Sometimes this merely runs the command more than once; in other cases, it produces truncated or temporarily missing files. A safer approach is to make generation an independent CI prerequisite and start all xcodebuild tasks only after it finishes.
If generation must run in an Xcode build phase, assign it a single owner and declare complete input and output file lists. The generation script itself must not modify project files or clear the generation directory while another process is reading it. When parallel builds require separate outputs, create temporary directories based on task identifiers and publish the verified result to the fixed location with a single atomic replacement.
Another common mistake is to run a formatter immediately after generation while the local pre-commit hook uses the opposite order. Generation, formatting, and diff checking must always run in the same sequence through the same entry-point script.
Pre-Merge and Incident Checklist
When committing changes to a generator or its inputs, code review should verify at least the following:
- The generator version declaration was updated consistently.
- Configuration, template, and input changes are included in the same commit.
- Removing an input also removes the corresponding generated file.
- The artifacts do not contain timestamps, user names, or absolute paths.
- After two consecutive local runs, the second run produces no differences.
- CI checks both tracked and untracked files before compilation.
- Parallel tasks write to isolated directories.
- Tool versions, diff patches, and the hash manifest are preserved on failure.
During investigation, do not begin by applying an automatic fix that overwrites the evidence. First preserve git status, tool versions, and the diff, then rerun the generation entry point from a clean checkout. If a clean checkout still produces inconsistent results, the problem lies in the environment or unpinned inputs. If only reused workspaces fail, focus on stale files, caches, and concurrent writes. The final standard is simple: for the same commit, any cloud Mac job should be able to run generation twice in succession with no observable difference on the second run.
Frequently asked questions
Should generated Swift files be committed to the repository?
Commit them when local development or code review must work without running the generator. CI should still regenerate the files and fail when the committed output is stale.
Why does cloud Mac CI generate different output from a local machine?
Common causes include generator versions, input ordering, locale, line endings, and the working directory. Make each dependency explicit and validate it before compilation.
Run Your Next Task on a Dedicated Physical Node
Choose a node and billing cycle, then start deploying with the fixed M4, 16GB RAM, and 256GB SSD configuration. Actual availability is based on the console's real-time status.