Migration Model Fleet — What a Release Must Rehearse
A history entry that applies cleanly to the model's validation surface proves
nothing about the live databases behind it, because that surface is current
and a current database has nothing pending. Before a release carries an
entry, a throwaway copy of every live database the model owns is converged
through that model's real boot sequence. Each model in a project's
migration_model capability declares what that means for it in the project's
migration_fleet capability, {"models": {"<model>": <fleet>}}. The fleet is
release-evidence configuration, so it is its own document beside the
authoring declaration. There is one preflight and one release gate for every
project; the declaration is the only per-project part.
Declaration
kind |
Databases rehearsed | Boot sequence | Other keys |
|---|---|---|---|
engine_tenants |
The engine's tenant databases on the environment's admin cluster, rostered with the reason each candidate is or is not a member | The engine's boot-time schema and history convergence | none |
named_databases |
names: databases on the environment's admin cluster |
converge_argv, run from the project checkout with the copy's DSN bound to runner.config.connection_env_var; optional verify_argv runs the same way afterwards and must exit 0 |
schema_shape_sources: checkout-relative files whose content is the schema the boot converges |
none |
Nothing | Nothing | reason: why this model has no live databases a release can rehearse |
// The engine's own model, under "models": {"primary": ...}.
{"kind": "engine_tenants"}
// A service converging its own registry database at boot.
{
"kind": "named_databases",
"names": ["service_registry"],
"converge_argv": ["uv", "run", "--project", "services/svc", "python", "-m", "svc.schema", "init"],
"verify_argv": ["uv", "run", "--project", "services/svc", "python", "-m", "svc.schema", "status"],
"schema_shape_sources": ["services/svc/src/svc/schema.py"]
}
// One database on a host the admin cluster cannot copy.
{"kind": "none", "reason": "single SQLite database on the production host"}
A release gate refuses a model that declares no fleet, and a fleet for a model
the project's migration_model does not declare: an undeclared fleet and an
empty one are different facts. Create the document with
yoke projects capability-settings set --project P --cap-type migration_fleet --new --settings-json '{"models": {...}}'; change one model with
capability-settings merge --set models.<model>=<fleet JSON>. Writing the
capability takes project-admin authority (projects.capability_settings.set /
merge), because a named_databases fleet names commands the release
machinery runs on an operator's machine. Those commands receive a minimal
environment — locale, home, temp, certificate paths, the project's UV_*
settings, the copy's DSN, and its restore point — never the operator's
credentials, and their failure output has the copy's DSN and password removed.
The admin cluster is the environment's release.admin_connection setting on
the project's own environment. A named_databases fleet runs from the
project's checkout registered on this machine (yoke project register), so
the declared commands find the code they converge with. A non-Python schema
source is digested byte for byte; a Python one by its syntax tree, so comments
and docstrings do not invalidate coverage.
Fleet copy admission is automatic at the shared rehearsal kernel: an exclusive
OS file lock identifies the physical local cluster and disposable database name.
It refuses immediately with rehearsal_copy_busy, holder diagnostics and a retry
after the holder finishes; the refused runner neither copies nor cleans up.
Admission covers dump, restore, the entire pending-history batch, invariants or
diagnostic callbacks, connection close and cleanup. Transfer and declared-command
children retain admission if their parent dies, until the children stop. Stable
lock files are retained; metadata is diagnostic and never decides ownership.
Distinct clusters and copy names proceed independently. This protects callers of
the kernel, including yoke watch preflight, not manual/ad hoc database operations.
The persistent yoke migration rehearse ITEM validation database and its existing
migration-territory claim remain reusable and unchanged.
Preflight
yoke watch preflight -- --project P [--model M] [--checkout PATH] \
<environment> [db ...] \
--record-receipt --product-sha SHA --receipt-env <control-plane>
The preflight runs from the installed Yoke package in any working directory;
no Yoke source checkout or PYTHONPATH is needed. A declared project boot
command still uses that project’s checkout. Startup and subprocess failures
name a reason and a diagnostic capture; read it, fix the named cause and retry.
For a missing packaged module, reinstall with yoke update or report a
defective release and its capture to the control-plane operator.
--model defaults to the capability's default_model. --checkout names the
checkout a named_databases fleet converges from (default: the project's
checkout registered on this machine). The live databases are
only read: each is dumped, restored into the local embedded cluster with
parallel pg_restore jobs, converged, verified, and dropped. A passing run with --record-receipt writes the
covered history entry names and the schema-shape digest onto that project
environment's own settings document under release.fleet_rehearsal.<model>,
on the prod release-gate control plane. Coverage is the union of passing runs
and never ages out; one environment's receipt never satisfies another, and one
model's never covers a sibling model that shares an entry name. A receipt
claims the whole declared fleet, so naming databases is a diagnostic run and
--record-receipt refuses it.
For a read-only census of the restored source, reuse that same copy path:
yoke watch preflight -- --project P [--model M] <environment> [db ...] --instant-census-output DIR --copy-budget-gib N --minimum-free-gib N
This diagnostic mode does not apply history, converge schema or exercise release-driver writes. It inspects a read-only copy transaction, retains aggregate instant counts and archive/database sizes with phase timings before cleanup, and cannot record a release receipt. Counts include blanks, missing columns, unclassified candidate columns, invalid calendars and unknown offsets. Separate format-shape counts identify qualified calendar text (including PostgreSQL offset spellings), calendars without offsets, dates without times, and other values. These shapes do not certify calendar validity or timezone provenance; collection success does not authorize conversion. Original row bodies are not logged. The explicit disk budget also counts concurrent filesystem consumption, and the free-space floor remains required throughout each guarded transfer. Put retained output on the copy filesystem. Inspect all refusals before retrying or authoring a repair; this mode supplies no timezone or nullability decisions.
Release gate
After real boot convergence and history invariants on each engine tenant copy,
product sessions, frontier, board, launch get/list, message-attempt and item
get/list readers must consume the copied rows. Per-record session/item reads
and paged launch reads cover rows beyond list windows. A refused or crashing
reader fails preflight with its operation and recovery; it earns no fleet
receipt. Once the readers pass, the verifier runs the secret-receipt replay
proof directly on the same converged copy and transaction: it inherits no
rehearsal module context or skip, a copy that still holds one-time secret
receipts refuses before replay, and the replayed scrub must rewrite nothing
and leave the ledger and event receipts byte-identical. A replay failure fails
preflight with its named secret_receipt_replay_failed reason. Only then do
the release-surface checks run. The first failure wins and is reported with its
own operation and recovery. All these operations stay inside the existing copy
admission and cleanup. Named-database fleets retain their project-declared boot
and verification commands.
Before a deployment run dispatches a GitHub workflow, it rehearses every
project that workflow ships: the run's own project at its release commit, and
each project a stage binds through input_bindings at the commit the run
recorded for that binding (a Yoke release that carries a Platform pin ships
Platform's registry history this way). Each project, model and target
environment is checked once per attempt; a bound project is checked under its
environment of the same name, from its checkout registered on this machine,
fetching the bound commit when that checkout lacks it. A binding with no
recorded commit, two commits of one project, or a bound project with no
registered checkout or no readable environment refuses the dispatch with its
recovery. When preparation re-binds a stale source just before dispatch, the
new commit is checked again before the workflow goes out.
For each shipped project the gate reads every model's fleet. A none fleet is
skipped with its reason printed. An undeclared fleet refuses the dispatch with
the declaration recipe. Otherwise the gate reads that commit's history
entries (from the model's runner.config.modules_dir) and schema-shape digest,
and when the target environment's receipts do not cover both it rehearses
exactly that commit's code before dispatching. The installed Yoke package only
orchestrates the copy; the candidate commit supplies the history, schema and
boot it is judged by. The rehearsal runs while
the stage's CI gate waits on the same commit, so it adds no wait of its own
once CI is the longer one; dispatch still needs both the passing gate and the
covering receipt, and a failed CI gate is the refusal reported. A named_databases fleet
converges from a disposable checkout of that commit, and an engine_tenants
fleet rehearses only after the dispatching engine's history bytes and schema
shape are proven identical to the commit's (otherwise it refuses and names the
release-driver recovery). A Yoke self-release driver, stage and production
alike, executes the release commit itself, so that proof holds without any
local install of the release. It then re-reads coverage and refuses if the receipt
still falls short. The Yoke release workflow's pre-tag check,
require_fleet_migration_preflight --project P <environment>, reads the same
declarations and model-scoped receipts from a checkout of the release commit.
Item rehearsal is a different receipt
yoke migration rehearse PREFIX-N proves the item's entry against the
validation surface and writes its receipt to migration_audit on the control
plane that holds the item, where the implementing -> reviewing-implementation
gate reads it. Fleet coverage is release evidence and lives on environment
settings. Neither substitutes for the other.