Skip to content
C.W.K.
Stream
Lesson 06 of 06 · published

Shipping — a Cable Is a Build Check, Not a Delivery

~12 min · testflight, delivery, gates, reporting

Level 0Curious
0 XP0/80 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

The fact that gets skipped

Every other client in this household ships by copying a file. The web UI is a build directory served by the engine; the Mac apps are signed bundles copied into place. iOS is the one platform where the last step is not yours — Apple sits between the build and the device, and that changes what "done" means.

The family rule is one line: TestFlight is delivery. A cable install is a build check. A cable build expires in a week, needs the cable and an unlocked device, and reaches exactly one phone. It is how you find out whether the thing runs. It is not how anyone receives it.

This is written down because it was learned the expensive way, in a sibling app, twelve days after the household already had a working upload pipeline. A session finished the work, side-installed the build on Dad's locked phone, and logged the real delivery path as a future task. Dad had to point at it.

Principle: When the last step of shipping belongs to someone else, the habit of counting your own last step as the end is nearly irresistible. The correction is not more care — it is naming the steps separately so a report cannot blur them.

Four facts, and only the last one is delivery

The contract names them so a status update cannot collapse them into one:

The archive was exported and uploaded — your script says so. Apple finished processing — the store console says so, and it can take a while. The internal group has the build — a separate assignment, a separate screen. It is installed on the device — the device says so, or the tester row does.

Four separate truths. A report that says "shipped" when only the first is true is not lying on purpose; it is compressing a four-step process the way people compress one-step ones. So the rule is to say which one is true.

Gates that refuse, instead of mail that arrives later

Most of the upload script is refusals, and every refusal has the same origin story: Apple would otherwise accept the upload and send an email about it hours later.

The order is the design: everything cheap refuses before anything expensive runs, and all of it before the build number moves. First an App Store Connect key — or, without one, a refusal to upload while the screen is locked, because a locked screen kills the keychain session the archive needs. Then a dedicated simulator, so a test run never lands on a shared one. Then the app's inputs committed, so the build number names what is actually built; the shared kit's sources clean, because the archive compiles them too; the packaged renderer present; the icon the right size and opaque. Only then the suite, and only after it passes does the build number move.

After the archive lands, HEAD is checked again — a sibling session may have committed while the build ran — and then the archive itself is inspected: the app's icon name declared, the embedded watch app's too, the watch's compiled asset catalog present, the complication's extension actually there.

That last group is the interesting one. They are assertions about the output, not the input — the build succeeded and the artifact is still wrong. A green compile is not evidence about a bundle's contents.

The evidence for fact one was ungated: the first version of the export step piped the build tool through a filter and ended in a swallow-the-error clause. A shell pipeline reports the status of its last command, so strict mode had nothing to fire on — and the script printed its upload OK line whether or not anything had been uploaded. That line is exactly what the contract names as the evidence for the first of the four facts. It was fixed the same day: the tool's own status is read directly, and the log is separately checked for the failure banner, because some tools exit cleanly on a logical failure. The check you trust must be the check that knows.

The project file is generated, and the tree must be clean

Two smaller choices that matter more than they look.

The Xcode project is generated from a plain text source and then committed. Hand-editing the project in the IDE is how a team identifier or a signing setting ends up in a file nobody reads in review, and the next machine builds something subtly different. The text file is the source; the project is an artifact that happens to be tracked.

And the clean-tree gate is not hygiene — it is about what an archive actually compiles. It compiles the working tree, not the last commit. A dirty file, including one a sibling session left in a shared checkout, silently becomes part of the build you are about to ship and can never be reproduced from the commit you stamped it with.

What is already paid for

Nothing here is invented per app. One developer team carries the whole family, named in each project's text source rather than clicked into an IDE. A new app needs one store record and nothing else.

And since 2026-09-14 the pipeline itself is one program. Until that day the family carried eight hand-grown copies, and they differed by more lines than the smallest of them contained. The differences were not design choices. They were guards one app had learned and the others had not caught up to: two archives swallowed the build tool's status behind a filtered pipe, one had no clean-tree check and no HEAD re-check, one never moved its build number, only two refused a locked screen. Each was a guard learned once and copied short. Now every guard runs for every app in one order. The program is vendored into each repo and held byte-identical by a drift test, and each app keeps only its own facts beside it — names, paths, a few hooks — so a release never depends on a sibling checkout being present on the machine that builds it.

That is the same instinct the vessels track has been teaching about brains and the frontend track about sibling apps, pointed at a build pipeline: the canonical thing lives in one place, and every consumer carries a copy that is proven identical rather than a reimplementation that is hoped to be.

Code

Say which fact is true·text
1  archive exported + uploaded     <- your script's UPLOAD_OK line
2  Apple finished processing       <- the store console
3  internal group has the build    <- a separate assignment screen
4  INSTALLED on the device         <- the device, or the tester row

Only 4 is delivery.
"Shipped" after 1 is a four-step process being reported
like a one-step one.
The gates, in the order they run·text
BEFORE ANYTHING EXPENSIVE (and before the build number moves)
  auth        App Store Connect key — or refuse to upload on a locked screen
  simulator   a dedicated id, never a shared one
  inputs      the app's inputs committed — the build number names what is built
  kit         the shared kit's sources clean — the archive compiles them too
  renderer    the packaged web cell exists
  icon        1024 px, opaque

THEN
  suite       must pass
  build no.   bumped in the project source
  archive     the build tool's own status and a kept log — never a filtered pipe
  HEAD        did not move while the build ran
  contents    app + watch icon names, watch asset catalog, complication extension
  export      the tool's status AND the failure banner — two independent checks

Each refusal is an email Apple would otherwise send hours later.

Exercise

Take your own release process and split its last step into the separate facts it actually contains. Then look at how you reported the most recent release and ask which of those facts you had evidence for at the moment you said it was done.
Hint
The tell is a report written at the moment YOUR work ended rather than at the moment the artifact reached someone.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.