Before release, the familiar scene: someone in Slack asks "who can click TestFlight", another SSHs in to manuallyxcodebuild, a third says "works on my machine". Cloud Mac solves hardware—but if checks stay manual,"works on my machine" luck remains. After wiring OpenClaw to Zutcloud dedicated nodes, the goal is clear: useorchestrator + fixed nodes to put trigger, build, signing checks, and receipt into one auditable chain.
From manual release to chain release
Before automation, a mid-size iOS team spent 40+ minutes per release just "confirming environment":
| Manual step | Hidden cost | After automation |
|---|---|---|
| Confirm Xcode / cert versions | Verbal sync, easy to miss | Image version in pipeline manifest |
| Local Archive trial build | Everyone's env differs | Dedicated node with fixed DerivedData path |
| Upload TestFlight | Whoever is free does it | Sign stage auto-triggers altool / Transporter |
| Notify QA | Build numbers mismatch | Receipt webhook with commit + artifact hash |
Five-stage pre-release chain
We split pre-release checks into five independently retriable stages. Each ends with structured JSON to the orchestrator—not just stderr:
- Trigger— PR merge, tag push, or scheduled nightly; record
trigger_idand branch - Freeze— verify node image, Xcode version,
openclaw.yamlregion and cache root in openclaw.yaml - Build—
xcodebuild/fastlane; produce .xcarchive and unit test report - Gate— static analysis thresholds, coverage, package size delta; block downstream on fail
- Receipt— artifact hash, node ID, duration to webhook / read-only channel
Orchestrator config example
Simplified sketch: register a Zutcloud dedicated node as OpenClaw runner and inject metadata on trigger (field names depend on your CI):
name: pre-release-mac on: push: tags: ['v*'] jobs: build-on-cloud-mac: runs-on: [self-hosted, macOS, zutcloud-ap-northeast] steps: - run: openclaw daemon health --json - run: | export OPENCLAW_NODE_ID="vn-apne1-m4-01" export ARTIFACT_ROOT="/Volumes/artifacts/ap-northeast-1" ./scripts/ci-build.sh - run: openclaw receipt publish \ --trigger-id "${{ github.run_id }}" \ --sha "${{ github.sha }}" \ --artifact-hash "$(shasum -a 256 dist/*.ipa)"
Three details:health check first—avoid a 30-minute compile when daemon is down;export env vars explicitly in shell blocks—do not rely on SSH login profile;receipt as its own step—even if TestFlight upload fails, you keep proof the build succeeded.
Dedicated nodes: why not shared runners
Shared Mac CI looks cheap but hurts pre-release checks: uncontrollable cache dirs, hard cert isolation, unpredictable queues. Dedicated nodes let OpenClaw bind DerivedData, Pods cache, and signing keychain to one physical machine—chain determinismbeats random machine every job.
| Dimension | Shared runner | Zutcloud dedicated + OpenClaw |
|---|---|---|
| Cache | Cleared after job | Volume snapshots bound to Xcode version |
| Certificates | Multi-tenant risk | Single-tenant keychain, offline signer possible |
| Audit | Scattered logs | trigger → node → artifact end to end |
| Queue | Peak waits can be hours | Dedicated compute, schedulable nightly builds |
Receipts & auditability
Auditable does not mean a giant log tarball—each release should answer five questions:
- Who triggered? (PR author / tag / cron)
- Which node and region built? (
node_id+region) - Which Xcode and cache snapshot? (image version + cache root)
- What artifact hash? (reproducible IPA / dSYM)
- Which stage failed? (freeze / build / gate / upload)
We push receipts to a read-only Slack channel (or equivalent) as fixed single-line JSON summary + detail link. QA no longer asks "which build"—build number, commit, and hash align in the message.
{
"trigger_id": "gh-1849201",
"sha": "a1b2c3d",
"node_id": "vn-apne1-m4-01",
"region": "ap-northeast-1",
"xcode": "16.2",
"stages": {
"freeze": "ok",
"build": "ok",
"gate": "ok",
"receipt": "ok"
},
"artifact_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
Gradual rollout advice
You do not need to kill all local Archives week one. Safer path:
Week 2: enable gate stage; test failures block merge.
Week 3: tag releases run the full five-stage chain; local Archive becomes an exception path.
Use with theregion & disk sizing guide: pair node metadata in week one so week-three release cutover does not rework "chain works but artifact path wrong".
When things fail
Automation value is half success, halffailures you can locate. We keep a simple playbook:
- freeze failed— image drift or openclaw.yaml mismatch → freeze node, rollback image tag
- build failed— code or dependency issue → engineering fixes code, not infra
- gate failed— coverage or size regression → block release, need explicit waiver
- receipt failed— build succeeded but notify/upload failed → rerun receipt stage, not full rebuild
Split stages and "retry" drops from an hour full compile to minutes on receipt or gate—that is orchestrator time saved.
Run the full chain on cloud Mac
OpenClaw daemon, health checks, and pipeline scripts run 7×24 unattended on Zutcloud Mac mini M4. ~4W idle suits always-on CI; dedicated compute maps DerivedData and cert policy 1:1 to orchestrator config.
When pre-release checks move from manual clicks to trigger-to-receipt, team debate shifts from "whose env is wrong" to "which gate rule to tune"—the determinism cloud Mac should deliver.
Ready to build your first pre-release chain?Start with one APAC or US West dedicated node + nightly cron——View Mac cloud plansand run OpenClaw on fixed hardware.