reflutter
GitHub

The release model

Every Ship skill assumes one shared mental model. This page is that model — no commands, just the ideas the commands implement. (In the plugin it ships as the release-model skill, the required reading the other skills load first.)

Two layers, one human on top

A release is a deterministic pipeline plus judgment:

  • The deterministic layer — fastlane lanes wired to fastlane-plugin-flutter_deploy. Mechanical, versioned, identical every run. If a step’s correct action is the same every run, it belongs in the gem — never re-implemented in a skill or a script.
  • The judgment layer — the skills: the flow, the gates, and turning a raw failure into an exact next action. Anything that requires reading something ambiguous — an error string, a diff, CI state — lives here.
  • You — above both. Every irreversible or outward-facing act sits behind your explicit word. See Release Interlocks.

The candidate is frozen source

Staged apps ship through two stages, in order: staging first, then production. These are separate store listings, with different bundle ids and different keystores, so a staging binary can never be uploaded to the production listing.

That is why the release candidate is the source at the RC tag, not a binary. Each flavor’s binary is a rendering of that one frozen source:

staging flavor.aab / .ipa — build Nthe round that creates the candidate
rc/X.Y.Zthe candidate is frozen source, not a binary
production flavor.aab / .ipa — build Npromoted: same tag, same build, never re-prepared

Build number carries the train: staging build N reaches production as build N. A different number would mean a different build shipped than was tested.

  • Staging is prepared: bump, cut rc/X.Y.Z, build.
  • Production is promoted, never re-prepared: check out the same tag, rebuild the production flavor, reuse the version and build number.

The binary your testers signed off on is the one that ships.

Copy is approved once, then immutable

Store copy follows the same frozen logic as source. Play notes are drafted and approved once, at upload time. From there they flow verbatim to:

  • TestFlight copy.
  • Promoted-stage note files (same copy, same approval, no new gate).
  • The public site record.
  • The App Store Connect paste-ins.

Downstream steps derive; they do not improve. Consistency beats polish. In practice, every downstream “improvement” to approved copy ends up reverted back to the approved text. Divergence is proposed with a reason, or not at all.

The gates

Burn approval
You · gate
Prepare staging
Lane
Copy review
You · gate
Upload approval
You · gate
Upload + processed
Lane
Verify on device
You · gate
Promote + upload prod
Lane · your gates
Public promotion begins
You · console
Integrity gate → Finalize
destructive
GH release · site record
You · approvals
ASC paste + submit
You · last act

lane / skill work human gate — never automated across the one destructive step

Two facts about the gates that are easy to miss:

  • “Verified, promote” is both signal and consent. After you test the staging build on a device, your word carries the verification and the go-ahead in one sentence — and it is always asked for, never inferred.
  • The console acts split in two. Public promotion in the store consoles begins before finalize; the App Store Connect paste-in and review submission is the last act of the whole train, after the public record exists.

Why finalize is the dangerous step

Finalize does two things that cannot be walked back:

  • It deletes the RC branch with -D, bypassing git’s unmerged-work protection by design.
  • It tags the release with a bare git tag, which lands on whatever main points at when it runs.
main  ──●──────━━━━━━━━━━━━━━━━━━━──────●──
             │      bump X.Y.Z+N   FROZEN   finalize tags HERE└─ the bare tag X.Y.Z must land on this commitrc/X.Y.Z     └──○──────○  build base — COMMIT NOTHING HERE
                      deleted by finalize with -D: git's unmerged-work
                      protection is bypassed BY DESIGN

Two rules make it safe, and one skill enforces them (verifying-candidate-integrity):

  1. Commit nothing to an RC branch, ever. A fix discovered mid-release is either release content (goes to main, then re-cut as a new RC round) or tooling (goes to main, unrelated to this train). There is no third category.
  2. main is frozen from bump to finalize. Before finalize goes live, main..rc/X.Y.Z must be empty and the head of main must still be the bump commit. Any failure is a hard stop, not a warning.

Who leads what

You lead The skills lead
Invocations Mechanics
On-device verification Gate sequencing
Copy taste Failure interpretation
Console actions Drafting
Merge and publish decisions Cross-repo bookkeeping

A release is never started, advanced past a gate, or finalized on a skill’s initiative.