# NoMac Builder (legacy) > The Builder is NoMac's earlier publishing product: source pushed to NoMac, built, > signed and uploaded to TestFlight on NoMac's build service. It is closed to new > subscriptions and kept only for existing subscribers. Everyone else, and every > new project: give your agent a Mac with Mac credit, as https://nomac.app/llms.txt > describes. Signing, TestFlight, App Store submission and notarization all work > from a Mac session there. ## What the Builder tools are The Builder's tools (push_project, check_project, build, status, get_failure, get_billing_status, get_refund_status) serve existing Builder subscribers only. The Builder plans have no checkout. They are not needed for, and do not include, cloud Mac sessions: Mac time is prepaid Mac credit. The App Store and TestFlight tools (review_lint, set_metadata, upload_screenshots, publish, get_review, manage_testflight, read_store, update_store, get_feedback) work for every account with Apple connected and are documented in https://nomac.app/llms.txt. ## For existing Builder subscribers Run `push_project` then `check_project` for source-readiness findings. Fix source findings; compilation, signing and TestFlight remain unverified. Use `get_billing_status` for activation, the renewal amount, the quota reset in UTC and the guarantee deadline. Finish Apple setup in /wizard: upload the .p8 file (Key ID is inferred from its name), enter Issuer ID, and verify. A build without an active plan returns build_started:false. After setup, resume once with a fresh request_key. If a request's outcome is unknown, reconcile it using its existing request_key before starting any new build. A subscriber can switch to Mac credit at https://nomac.app/usage: the subscription ends and its last payment becomes Mac credit. Only the account owner can do this; an agent cannot. A Builder subscription does not include Mac credit. Refunds are human-only: agents cannot request one. If the human asks, send them to https://nomac.app/usage (or support@nomac.app). Once they have requested it, `get_refund_status` tracks the refund and cancellation. ## Ship an app to TestFlight with the Builder 1. `push_project`: pack + upload the working tree (secrets never leave the machine); auto-detects the Xcode project; runs source-level review lint. 2. `build` with workflow=release: signed build on a cloud Mac, uploaded to TestFlight. Poll `status` (~3 minutes total). workflow=smoke is a fast unsigned compile check. 3. When state=ready, tell your human to open TestFlight on their phone; the build is there. They tap around; you read crashes/comments with `get_feedback`. Fix, push, build again. 4. If a build fails: `get_failure` returns {stage, code, kind, error_lines, hints}. kind=project: you fix the source. kind=account: relay the deep link to your human. A Builder build can also be submitted for review: `publish` accepts its build_id like any other build. ## Rules of the road - Builder builds cost quota (Starter 50 a month, Pro 300); smoke builds also count, so don't loop release builds to debug compile errors. Builds sent from a Mac with send_to_testflight cost only Mac time. - Do not retry-loop an identical unknown build failure; file it with `report_issue` and poll the same tool for a reply. ## Builder CLI and API reference Moved here from https://nomac.app/docs/cli and https://nomac.app/docs/api. Everything else on those pages (Mac sessions, secrets, signing, TestFlight, the App Store endpoints, account and keys) applies to every account. ### Builder commands These commands drive the Builder's build service. They do not start a Mac session. nomac push Pack the current directory (respecting .gitignore, secrets excluded) and upload a snapshot. Creates the project on the first push, detects the Xcode project, scheme and bundle ID, and runs source-level review lint. nomac build Start a signed release build from the latest snapshot. Prints the build ID and returns at once. nomac build --smoke Fast unsigned compile check: verify the project builds before spending a release build. It counts toward the monthly cap too. nomac build --testflight Release build that waits, polling every 10 seconds, until the build is on TestFlight, printing each stage. --wait is the same flag; nomac build --smoke --wait is a blocking compile check. Exits non-zero on failure. nomac status [build-id] Show a build as JSON (the last build started from this directory by default). With no build ID, a folder that has a Builder build and no Mac session shows that build; otherwise nomac status shows the Mac. nomac logs [build-id] Print per-step build logs, plus the failure excerpt if the build failed. nomac push and nomac build keep per-project state (project ID, last snapshot, last build) in .nomac.json next to your code. Commit it, so CI and teammates push to the same project instead of creating a new one. ### The Builder by hand One-time setup: sign in at https://nomac.app/sign-in and connect your Apple Developer account with an App Store Connect API key (the wizard at https://nomac.app/wizard walks you through it). Then: cd YourApp/ # the folder with the .xcodeproj npx @nomac/cli login # approve in the browser, once per machine npx @nomac/cli push # upload a snapshot and run review lint npx @nomac/cli build --testflight About 3 minutes later the build is in TestFlight on your phone. From there the loop is push, build, test on the device, repeat. When a build fails, nomac logs shows the failing step and excerpt. ### The Builder in CI/CD The CLI runs on plain Linux runners: no macOS runner, no certificates, no fastlane. Authenticate with NOMAC_API_KEY: 1. Create a key at https://nomac.app/keys (or run nomac login locally and reuse it). 2. Store it as a repository secret named NOMAC_API_KEY. 3. Commit .nomac.json (created by your first nomac push) so CI builds the same project. Ship every push to main to TestFlight: # .github/workflows/testflight.yml name: TestFlight on: push: branches: [main] jobs: testflight: runs-on: ubuntu-latest env: NOMAC_API_KEY: ${{ secrets.NOMAC_API_KEY }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npx @nomac/cli push - run: npx @nomac/cli build --testflight A cheap compile check on pull requests (smoke builds do not sign or upload): # .github/workflows/smoke.yml name: Smoke build on: pull_request jobs: smoke: runs-on: ubuntu-latest env: NOMAC_API_KEY: ${{ secrets.NOMAC_API_KEY }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npx @nomac/cli push - run: npx @nomac/cli build --smoke --wait Worth knowing before you wire this to every commit: - Every build attempt counts toward the monthly cap (Starter 50, Pro 300), failed ones included. Gate the TestFlight job on main and use smoke builds for pull requests. - Build numbers (CFBundleVersion) increase automatically per project; no version bump step is needed. Bump the marketing version in your Xcode project when you cut a release. - build --testflight exits non-zero on failure, so the job fails visibly; run npx @nomac/cli logs (or GET /builds/:id/failure) to see why. - POSTs accept an Idempotency-Key header. The CLI sends a new one for each build it starts, so rerunning a CI step starts another build. ### Builder API endpoints Base URL https://nomac.app/api/v1, with Authorization: Bearer nmk_… as on https://nomac.app/docs/api. For these endpoints, 402 means a plan or quota cap and 422 a precondition (no snapshot, no Apple connection, validation). POST /projects Body { name, connection_id? }. Creates a Builder project. If the account has exactly one Apple connection, it is linked automatically. POST /projects/:id/snapshots/uploads Returns { upload_url, upload_key, max_bytes } for a direct upload: PUT the gzipped tarball to upload_url (Content-Type: application/gzip), then confirm it with POST /projects/:id/snapshots and the JSON body { upload_key }. Up to 32 MiB. POST /projects/:id/snapshots This is nomac push. The body is { upload_key } after a direct upload, or the gzipped tarball itself (Content-Type: application/gzip, up to about 4 MB). Secret-looking files are deny-listed at intake. Detects the .xcodeproj, scheme, bundle ID and version, resolves the matching App Store Connect app record and runs source-level review lint. Returns { snapshot_id, commit, files, detected, asc_app_id, lint }, plus a warning if no App Store Connect app record exists for the bundle ID yet. POST /builds Body { project_id, workflow }, where workflow is release (signed and uploaded to TestFlight; the default) or smoke (fast unsigned compile check). Builds the latest snapshot; build numbers increase automatically. Every attempt counts toward the monthly cap (Starter 50, Pro 300); over the cap returns 402. GET /builds?project_id=… The latest 50 builds, newest first. GET /builds/:id Build status. state walks queued, mirrored, dispatched, building, uploading, processing, ready (or failed); a full release run is typically about 3 minutes. Poll this: it also moves the pipeline forward. GET /builds/:id/logs Per-step logs and, for failures, the relevant excerpt. GET /builds/:id/failure The structured failure an agent acts on: { stage, code, kind, error_lines, hints }. kind "project" means fix the source; kind "account" means a person must act (a deep link is included). POST /review-lint includes the source findings from the latest push, and POST /publish accepts a Builder build_id like any other build.