Product

Five parts, and what each one refuses to do

Every tool’s feature list looks the same. The shape of a product is decided by what it declines — so the refusals are here with equal billing.

The pipeline

A DAG of typed steps, declared in release.yaml and validated against a registry of 42 of them before anything is scheduled.

  • Matrix expansion, dependency edges and job-level concurrency groups
  • Line-accurate diagnostics with stable codes, so an error is searchable
  • An escape hatch for shell, deliberately unergonomic and blocked where it would be dangerous

Refuses: No expression language in credential requirements. A job’s credential set must be computable by reading the file, not by evaluating it.

The fleet

Your Macs, enrolled over mTLS, matched to jobs by what they actually carry rather than by what they are called.

  • Capability matching on Xcode version, SDK, architecture, simulator and emulator
  • Speed weighting, so a slower machine is scheduled as a slower machine
  • Drain and quarantine, and a page that says which one machine holds most of your capacity

Refuses: No pretending two Macs are interchangeable. An Intel machine and an Apple-silicon one differ in which Xcode they can run and which emulator images will boot, and scheduling that ignores it produces a queue that mysteriously stops.

Credentials

Encrypted at rest, scoped per job to exactly the purposes the steps declare, and never returned by any API.

  • Purpose-scoped issuance — a job gets an App Store Connect key only if a step in it needs one
  • Log scrubbing before output leaves the machine
  • A blast-radius view: this credential, and every app it can sign

Refuses: No route that shows you a secret again, for anybody, including an owner. A minted secret is returned exactly once.

Distribution

TestFlight, the App Store, Play, Firebase, S3 and OTA — each a named target that release.yaml refers to by name and never by account.

  • Staged rollout, halt and resume, with honest copy about iOS having no true rollback
  • Audiences mapped across provider groups, with drift detection
  • N distinct approvers, keyed on who — never a counter

Refuses: Approval is checked before any bytes move. A system that uploads and then waits is guarding a door that is already open.

Portability

Move a project to a different Apple team or Play account as a configuration change, with a preflight that tells you what will break first.

  • No store-account identifier may appear in release.yaml — the validator refuses it
  • A preflight that checks certificates, app records and bundle ids against the destination
  • A recorded move, so the next person can see what happened and when

Refuses: No silent re-pointing. A move invalidates what the old account owned and regenerates provisioning, because half a move is worse than none.

Five frameworks, one pipeline

Flutter, React Native, Expo, native iOS and native Android — detected from the repository rather than declared, and warned about rather than silently switched if the two disagree. Each gets its own bootstrap, build and test steps; all of them get the same signing, distribution, approval and audit path, because that half of a release has nothing to do with which framework wrote the app.

The step library lists all 42 steps with what each declares it needs — generated from the same file the validator, the scheduler and the secret scoper read, so it cannot be out of date.