git clone refuses a non-empty target, so a COMFY_DIR pre-seeded with the cached checkpoint (or converted .mlmodelc) would break setup. Replace clone with git init + remote add + fetch + 'checkout -f', which populates the ComfyUI tree without touching untracked files. Setup order is now free.
6.3 KiB
Self-hosted M2 runner — setup
Tier 2 (ANE + integration + bench) runs on a self-hosted GitHub Actions runner registered against the maintainer's M-series Mac. Hosted macOS runners on GitHub do not expose the Apple Neural Engine, so the ANE half of the matrix has to live on real hardware.
One-time runner setup
-
Install dependencies on the Mac. Python 3.12.x (matching the
requires-python = ">=3.12,<3.13"pin),uv, andgit. The workflow clones and manages the ComfyUI checkout itself (see step 3), so you do not pre-install ComfyUI.brew install python@3.12 uv git -
Register the runner. From the repo Settings → Actions → Runners → New self-hosted runner, follow the macOS-ARM instructions. Add the labels exactly:
self-hosted,macOS,ARM64,coreml(the workflowruns-onclause requires all four).mkdir ~/actions-runner && cd ~/actions-runner curl -O -L https://github.com/actions/runner/releases/download/v2.317.0/actions-runner-osx-arm64-2.317.0.tar.gz tar xzf actions-runner-osx-arm64-2.317.0.tar.gz ./config.sh --url https://github.com/<owner>/<repo> \ --token <REGISTRATION_TOKEN> \ --labels self-hosted,macOS,ARM64,coreml \ --name "$(hostname)-m2" ./svc.sh install && ./svc.sh start # run as a launchd service -
Persist
COMFY_DIRfor the runner. Point it at a dedicated, runner-owned ComfyUI directory — not your personal dev checkout. The workflow doesgit reset --hardon it and rewrites itscustom_nodes/ComfyUI-CoreMLSuitesymlink, so it must be disposable. Add it to the runner's.env:echo 'COMFY_DIR=/Users/<you>/actions-runner/comfyui' >> ~/actions-runner/.envYou don't have to clone ComfyUI yourself: the
Set up ComfyUI checkoutstep clones it on first run, checks out the right ref (latest or the pinned SHA — see ComfyUI version under test), installs ComfyUI's deps, and symlinks$COMFY_DIR/custom_nodes/ComfyUI-CoreMLSuite→$GITHUB_WORKSPACEso the server always loads the checked-out PR. IfCOMFY_DIRis a real node directory rather than a symlink, the step fails loudly instead of deleting it. -
Provide the SD1.5 checkpoint. Drop
v1-5-pruned-emaonly.safetensors(~4 GB) into$COMFY_DIR/models/checkpoints/. This is the one heavy artifact that stays as a runner-local cache; everything derived from it (the.mlmodelcUNet variants) is converted automatically by theConvert UNet variants if missingstep and cached across runs. Override the filename withCKPT_NAMEin the runner.envif needed.Order is free: do this before or after the first run. The setup step initialises the ComfyUI repo in place (
git init, notgit clone, which would refuse a non-empty directory) and usescheckout -f, which never removes untracked files — so a checkpoint you dropped in first is preserved.
Triggers
The Tier 2 workflow (.github/workflows/tier2.yml) runs:
- On PR label
run-m2— maintainers add the label to opt a PR into the ANE lane (the runner is not free; default off). - Nightly at 04:00 UTC via
schedule:. - Manually via the workflow_dispatch button.
ComfyUI version under test
Tier 2 runs a hybrid strategy, keyed on the trigger, so the suite tracks a moving host without making PRs flaky to overnight upstream drift:
| Trigger | ComfyUI ref | ComfyUI deps |
|---|---|---|
| schedule (nightly) | latest origin/master |
its own requirements.txt, capped by constraints/comfy-ceiling.txt |
PR label run-m2 / dispatch |
pinned requires-comfyui SHA |
the frozen comfy uv group |
- Nightly = canary. It pulls the latest ComfyUI and installs ComfyUI's
own dependency set. The frozen
comfyuv group cannot track a moving host by hand (a latest checkout needs e.g.comfyui-frontend-package==1.44.19,comfy_aimdo,alembic,blake3that an older pin never listed), so latest mode defers to upstream'srequirements.txt. Upstream API or dependency breakage surfaces here, in CI, instead of in a user's install. - PR / dispatch = reproducible gate. It checks out the pinned
requires-comfyuiSHA and uses the frozencomfygroup — the known-good combination — so a PR fails for its own reasons, not because ComfyUI moved.
The resolved ComfyUI SHA and mode are written to the job step summary (and
COMFY_SHA in the env), so any failure names the exact commit it hit.
The toolchain ceiling is deliberate. constraints/comfy-ceiling.txt caps
torch<2.8 / numpy<2 / coremltools 9 while installing latest ComfyUI's
requirements. If upstream ever hard-requires something past those bounds the
nightly install fails on purpose — that is the signal that coremltools /
ml-stable-diffusion need a deliberate Phase-5-style bump, not a silent float
that would break the ANE path (see docs/deps.md).
pyproject.toml's requires-comfyui is both the PR-gate ref and the
published-compatibility declaration for the Comfy registry. Bump it once a
newer ComfyUI is validated (the nightly canary is what tells you it's safe).
Artifacts
- Bench JSON/MD are uploaded as
bench-results. - M2 golden image regressions surface as test failures in
tests/m2/test_golden_image.py; diff PNG is written next to the golden undertests/m2/_latest_generated.png(gitignored).
When the runner is down
If the maintainer's Mac is offline, the workflow queues until the runner comes back. Cancel a stuck run from the Actions UI; the gate is not blocking by default (Tier 0 + Tier 1 carry PR status). Tier 2 is "good to merge once it goes green," not "blocked until then."
Replacing the integration e2e
The legacy tests/integration/test_basic_conversion_1_5.py checked
CoreML output against an MPS reference image at PSNR > 25 dB. That
reference path is broken on macOS 26.x with torch 2.0.1 (see Phase 1
Gate report). Phase 4 moves the same coverage to
tests/m2/test_golden_image.py, which:
- runs the Core ML pipeline only (no MPS reference),
- asserts SHA256 against
tests/m2/goldens/sd15_seed42.sha256, - falls back to PSNR ≥ 40 dB if the hash drifts.
This removes the human-eyeball dependency: a regression is now a numerical fail, not a "looks different to me."