After a cloud Mac has been running build jobs continuously for several weeks, the most common environment problem is not that tools are missing, but that different people installed them at different times. One job depends on jq, another script assumes swiftlint is available, and an ad hoc troubleshooting session leaves several formulas behind. When a new machine takes over, cloning the repository alone cannot recreate those system-level dependencies. Brewfile is well suited to declaring which tools are required, but it is not an exact version lockfile. A reliable workflow manages declarations, version evidence, and cleanup procedures separately.
Audit the machine before exporting everything
Start by confirming the Homebrew path and current architecture so that scripts do not continue using a different legacy path.
command -v brew
brew --prefix
uname -m
brew doctor
Then inspect top-level formulas, all installed versions, and background services separately:
brew leaves
brew list --formula --versions
brew services list
brew leaves is a better starting point for a declaration than the complete package list because it primarily shows top-level tools that were installed explicitly. The full version list should be archived as build evidence rather than copied directly into Brewfile.
Do not run upgrades or cleanup while build jobs are still active. Stop dispatching jobs first, then confirm that no processes are using compilers, databases, or supporting services.
If the machine has been in service for some time, export a candidate file first and review it line by line:
mkdir -p ci/homebrew
brew bundle dump --force --file=ci/homebrew/Brewfile.candidate
git diff -- ci/homebrew/Brewfile.candidate
The candidate file may contain personal utilities, temporary debugging software, or graphical applications unrelated to the project. Only tools genuinely required for builds and troubleshooting should be included in the final manifest.
Treat Brewfile as a requirements declaration
A basic manifest for continuous iOS builds can remain very short:
brew "git"
brew "jq"
brew "swiftlint"
brew "xcbeautify"
Commit the file as ci/homebrew/Brewfile, and specify its path explicitly during installation:
brew bundle check --file=ci/homebrew/Brewfile
brew bundle install --file=ci/homebrew/Brewfile --no-upgrade
check is suitable for the start of a job. It only verifies that the declaration is satisfied; it should not trigger a full automatic upgrade during every build. install --no-upgrade installs missing tools while reducing the chance that a routine build will unexpectedly modify the existing environment.
Split Brewfiles by responsibility
If the same VMKeep cloud Mac must handle multiple types of work, split the manifest into a base layer and project-specific layers. For example, the base layer might contain only Git, JSON processing, and logging tools, while project layers add linting or release utilities. Keep the installation order fixed: base first, project second.
Do not use one ever-growing global Brewfile for every repository. It makes the impact of any cleanup difficult to assess and can lead new projects to mistake historical tools for their own required dependencies.
Record versions and runtime context separately
Brewfile does not normally lock standard formulas to exact versions. Seeing the same manifest on two machines does not prove that they will produce the same results. After every environment change, save a snapshot of the versions actually installed:
{
date -u
sw_vers
xcodebuild -version
brew --version
brew list --formula --versions
} > ci/homebrew/toolchain.snapshot.txt
The version snapshot can be stored as a pipeline artifact or committed to operations records after a reviewed environment upgrade. When differences appear, compare Xcode, macOS, Homebrew, and direct dependency versions first, then inspect the project's lockfiles. Do not begin by deleting every cache.
Define version boundaries explicitly
brew pin only prevents local upgrades on the current machine. It cannot guarantee that another new machine will still be able to obtain the same historical version. It can provide short-term protection, but it is not a substitute for a fixed machine image, a project-specific version manager, or verified binary artifacts.
For tools that directly affect build outputs, scripts should check an acceptable version range at startup and fail clearly when the requirement is not met. Tools used only to improve log formatting can allow a broader range so that noncritical differences do not become build failures.
Clean up safely with previews and idle windows
Before performing the actual cleanup, preview the items that are not included in Brewfile:
brew bundle cleanup --file=ci/homebrew/Brewfile
Use this step only to review the output. After confirming that none of the listed formulas are required by other projects, LaunchAgents, or background services, run:
brew bundle cleanup --file=ci/homebrew/Brewfile --force
brew autoremove
brew cleanup
Be especially careful on shared machines: a formula missing from the current repository's Brewfile may still be used by another job. A safer boundary is one dedicated physical node per long-running workload category, or at minimum separate execution accounts, working directories, and manifests.
After cleanup, run brew bundle check again, followed by a minimal acceptance sequence: print tool versions, parse the project configuration, and complete one build without uploading artifacts. The environment change is complete only after all three steps pass.
Make environment changes reviewable events
A stable workflow should not allow build scripts to run brew install casually. When adding a tool, the commit should include the Brewfile change, an explanation of its purpose, a version snapshot, and rollback instructions. Upgrades should be performed during a separate maintenance window: validate representative projects first, then resume job dispatching.
Reduce routine checks to four items:
- Does
brew bundle checkpass? - Are the Xcode and key formula versions as expected?
- Does Brewfile contain any unreviewed changes?
- Are any undeclared background services still running?
The value of Brewfile is not that it automatically fills a machine with every tool. Its value is that it turns system dependencies from established facts on a machine into declarations in a repository that can be discussed, compared, and rolled back. Combined with version snapshots and careful cleanup, it turns cloud Mac environment problems from ad hoc guesswork into evidence-backed configuration differences.
Frequently asked questions
Does Brewfile lock exact Homebrew package versions?
No. Brewfile declares which tools are required, while standard formulae continue to follow repository updates. Use version snapshots, a fixed machine image, or project-specific version managers when exact versions matter.
Should I run brew bundle cleanup --force directly on a shared cloud Mac?
No. Preview the removal list without --force, verify that no other job depends on those tools, and perform the cleanup during an idle period. Separate execution accounts are preferable on shared machines.
Continue validating this workflow on a cloud Mac
Choose the right memory, storage, node, and rental period from three Apple Silicon configurations.