fly-deployment — A deployment is a projection of the repository model, and a reachable URL is its evidence
One canonical deployment object under .ai/repo/deployments/ describes the deployment; a typed Rust model parses and validates it; the container image, the Fly configuration, the smoke suite, the doctor checks, the documentation, the cockpit view and the CI jobs are all projections of that object and of the capability registry, and every one of them is checked for drift. The service runs on Fly.io on a single shared-CPU Machine that stops when idle, its health and readiness are verified from outside, the smoke suite is derived from the route registry rather than written twice, and the deployed URL answers.
PLANNED 0 of 32 issues done
- ready
- 1
- blocked
- 31
- active
- 0
- verify
- 0
- done
- 0
- cancelled
- 0
Problem
Majordomus has no deployment at all: no container, no hosting provider, no way for anyone who is not standing in front of this checkout to see the layer it serves. The obvious repair is the wrong one. A Dockerfile and a fly.toml written by hand would be a second description of facts the repository already holds — the port the server binds, the routes it registers, the assets the site build produces, the binary the workspace names — and every one of those pairs would drift the first time one half changed. The repository already refuses that shape everywhere else: capabilities are declared once and projected into CLI, HTTP, MCP, OpenAPI, docs and the site, and generate --check fails when a projection is stale. A deployment must arrive the same way or not at all.
Outcome
One canonical deployment object under .ai/repo/deployments/ describes the deployment; a typed Rust model parses and validates it; the container image, the Fly configuration, the smoke suite, the doctor checks, the documentation, the cockpit view and the CI jobs are all projections of that object and of the capability registry, and every one of them is checked for drift. The service runs on Fly.io on a single shared-CPU Machine that stops when idle, its health and readiness are verified from outside, the smoke suite is derived from the route registry rather than written twice, and the deployed URL answers.
Current state
No Dockerfile, no fly.toml, no deployment kind, no deploy capability module and no container test exist anywhere in the repository. The HTTP server binds loopback by default and warns when it does not. system.health exists as a capability; /healthz and /readyz do not. The site is built by one pipeline already and deployed to GitHub Pages by scripts/site-deploy.
Desired state
majordomus deploy validates the canonical object, verifies that every generated projection matches it, builds the pinned multi-stage Alpine image, deploys one Machine to the chosen region, waits for Fly's health checks, asserts the Machine inventory against the declared cost profile, runs the route-derived smoke suite against the public URL and writes a machine-readable report. Changing the region, the memory, the port or the Machine count means changing one file and regenerating.
Scope
- .ai/repo/deployments
- .ai/repo/adrs
- .ai/repo/rules/project
- .ai/repo/policy.yaml
- apps/majordomus-cli/src/deploy
- apps/majordomus-cli/src/capability/builtin
- apps/majordomus-cli/src/http
- deploy
- fly.toml
- share/kinds.yaml
- share/allow
- share/schemas
- docs
- site
- test/cases
- .github/workflows
Out of scope
- Any dependency, in code, script, documentation, CI or default, on ~/dev/prismatic-platform
- A provider abstraction covering hosts other than the one this repository deploys to
- Kubernetes, Nomad, buildpacks, Nixpacks or a second container implementation
- A persistent volume, a database or any stateful service, unless a measured requirement forces one
- Mutating deployment operations exposed to unauthenticated HTTP or to the browser
- Profile-guided optimisation
Acceptance criteria
- One canonical deployment object is the only place the port, the region, the resources, the Machine count, the health routes, the build inputs and the budgets are stated
- Every provider artifact — the Dockerfile, its ignore file and fly.toml — is generated, marked as generated with its source and its regeneration command, and fails generate --check when edited by hand
- The deployment reads are declared once in the capability registry and appear in the CLI, the HTTP API, MCP, OpenAPI and the generated documentation without a second registration
- The smoke suite is derived from the route registry; no second list of endpoints exists anywhere
- The production image runs as a non-root user, contains no build toolchain, no repository history, no secret and no absolute developer path
- The deployed service answers on its public HTTPS URL, its health and readiness checks pass from outside, and the Machine inventory matches the declared cost profile
- Image size, binary size, build context size, cold start, resident memory and endpoint latency are measured, recorded as baselines and gated against unexplained regression
- No Fly credential is written to the repository, to a Docker layer, to a generated file, to the site or to a log, and CI deploys with an app-scoped token rather than an account-wide one
- Every rule this milestone states is enforced by a check that CI executes
Validation
- bash test/run.sh
- bin/majordomus doctor
- cargo test --workspace --all-features
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- majordomus generate --check
- majordomus deploy doctor
- fly config validate
Evidence required
- canonical_object
- typed_model
- generated_projections
- registry_derived_smoke
- container_minimal
- deployed_url
- budgets_measured
- secrets_isolated
- rules_enforced
Risks
- Generation can hide the Dockerfile from the people who must read it. The generated file has to stay idiomatic and reviewable; the source describes intent, the output describes mechanics, and a contributor who edits the output is told which file to change instead.
- The route topology and the served surfaces are being rewritten on other branches under ADR 0013. A smoke suite pinned to today's paths would be stale on arrival, so it must read the route registry rather than a path list.
- 256 MB is a hypothesis. It is held only as long as a measured run under that limit passes; the evidence, not the preference, decides the final number.
- A deployment touches money. The first deploy must not leave two Machines running, and the Machine inventory is asserted rather than assumed.
- The bootstrap credential comes from outside this repository. It may be used to obtain an authenticated session and nowhere else; anything else would make Majordomus depend on Prismatic Platform, which is forbidden.
Issues
Each one is an execution contract with its own acceptance criteria, validation command and required evidence. The wave column is the layer of the dependency graph it sits in.
| issue | status | wave | depends on | title |
|---|---|---|---|---|
| I0901 | READY | 0 | — | The deployment description is a kind of the layer, not a provider file |
| I0902 | BLOCKED | 1 | I0901 | The deployment object becomes typed Rust that refuses nonsense |
| I0903 | BLOCKED | 2 | I0902 | Deployment reads are declared once and projected everywhere |
| I0904 | BLOCKED | 2 | I0902 | The listen address is one fact with two projections |
| I0905 | BLOCKED | 2 | I0902 | Liveness and readiness are registered capabilities, not router hacks |
| I0906 | BLOCKED | 3 | I0904 | Termination is graceful and proved against the real container |
| I0907 | BLOCKED | 2 | I0902 | The running process says what it is, from an allowlist |
| I0908 | BLOCKED | 2 | I0902 | A hosted process has no repository, and says so instead of failing |
| I0909 | BLOCKED | 3 | I0902, I0904, I0905 | The production image is a generated projection of the build specification |
| I0910 | BLOCKED | 4 | I0909 | The release profile is chosen by measurement, not by folklore |
| I0911 | BLOCKED | 4 | I0909 | A source change does not rebuild the world, and the context stays small |
| I0912 | BLOCKED | 4 | I0909 | The image serves the site the canonical pipeline produced |
| I0913 | BLOCKED | 3 | I0902, I0903 | The provider configuration is generated, and the cheap profile is declared |
| I0914 | BLOCKED | 4 | I0903, I0909, I0913 | One deployment core, one command, and a dry run that mutates nothing |
| I0915 | BLOCKED | 5 | I0914 | Deployment joins the diagnostics that already exist |
| I0916 | BLOCKED | 5 | I0905, I0914 | The smoke suite is derived from the route registry, never written twice |
| I0917 | BLOCKED | 5 | I0914 | A bad deployment can be undone deterministically |
| I0918 | BLOCKED | 2 | I0902 | The bootstrap credential is used once and belongs to nothing |
| I0919 | BLOCKED | 4 | I0909 | Nothing secret and nothing local leaves in the image |
| I0920 | BLOCKED | 4 | I0909 | The image's dependencies are known and its advisories are policy |
| I0921 | BLOCKED | 5 | I0910, I0911 | Size is measured first and budgeted second |
| I0922 | BLOCKED | 6 | I0930 | The memory hypothesis is tested, not asserted |
| I0923 | BLOCKED | 6 | I0930 | Cold start and endpoint cost are benchmark targets like everything else |
| I0924 | BLOCKED | 1 | I0901 | The decision is recorded where architecture decisions are recorded |
| I0925 | BLOCKED | 6 | I0915 | Every deployment rule is a check that runs |
| I0926 | BLOCKED | 4 | I0903, I0913, I0924 | The deployment documentation is generated and reachable |
| I0927 | BLOCKED | 3 | I0903 | The cockpit shows the deployment from the same data |
| I0928 | BLOCKED | 6 | I0919, I0921, I0930 | A pull request cannot merge a deployment that would not work |
| I0929 | BLOCKED | 8 | I0918, I0931 | Production deploys from CI with a token scoped to one application |
| I0930 | BLOCKED | 5 | I0906, I0909, I0912 | The real image runs under the real limits before anything is deployed |
| I0931 | BLOCKED | 7 | I0913, I0914, I0916, I0917, I0918, I0922, I0930 | The application exists, one machine runs it, and the URL answers |
| I0932 | BLOCKED | 9 | I0920, I0923, I0925, I0926, I0927, I0928, I0929, I0931 | The milestone is accepted on its evidence |
The graph
flowchart LR
I0901["I0901<br/>The deployment description is a kind of the layer, not a provider file"]:::ready
I0902["I0902<br/>The deployment object becomes typed Rust that refuses nonsense"]:::blocked
I0903["I0903<br/>Deployment reads are declared once and projected everywhere"]:::blocked
I0904["I0904<br/>The listen address is one fact with two projections"]:::blocked
I0905["I0905<br/>Liveness and readiness are registered capabilities, not router hacks"]:::blocked
I0906["I0906<br/>Termination is graceful and proved against the real container"]:::blocked
I0907["I0907<br/>The running process says what it is, from an allowlist"]:::blocked
I0908["I0908<br/>A hosted process has no repository, and says so instead of failing"]:::blocked
I0909["I0909<br/>The production image is a generated projection of the build specification"]:::blocked
I0910["I0910<br/>The release profile is chosen by measurement, not by folklore"]:::blocked
I0911["I0911<br/>A source change does not rebuild the world, and the context stays small"]:::blocked
I0912["I0912<br/>The image serves the site the canonical pipeline produced"]:::blocked
I0913["I0913<br/>The provider configuration is generated, and the cheap profile is declared"]:::blocked
I0914["I0914<br/>One deployment core, one command, and a dry run that mutates nothing"]:::blocked
I0915["I0915<br/>Deployment joins the diagnostics that already exist"]:::blocked
I0916["I0916<br/>The smoke suite is derived from the route registry, never written twice"]:::blocked
I0917["I0917<br/>A bad deployment can be undone deterministically"]:::blocked
I0918["I0918<br/>The bootstrap credential is used once and belongs to nothing"]:::blocked
I0919["I0919<br/>Nothing secret and nothing local leaves in the image"]:::blocked
I0920["I0920<br/>The image's dependencies are known and its advisories are policy"]:::blocked
I0921["I0921<br/>Size is measured first and budgeted second"]:::blocked
I0922["I0922<br/>The memory hypothesis is tested, not asserted"]:::blocked
I0923["I0923<br/>Cold start and endpoint cost are benchmark targets like everything else"]:::blocked
I0924["I0924<br/>The decision is recorded where architecture decisions are recorded"]:::blocked
I0925["I0925<br/>Every deployment rule is a check that runs"]:::blocked
I0926["I0926<br/>The deployment documentation is generated and reachable"]:::blocked
I0927["I0927<br/>The cockpit shows the deployment from the same data"]:::blocked
I0928["I0928<br/>A pull request cannot merge a deployment that would not work"]:::blocked
I0929["I0929<br/>Production deploys from CI with a token scoped to one application"]:::blocked
I0930["I0930<br/>The real image runs under the real limits before anything is deployed"]:::blocked
I0931["I0931<br/>The application exists, one machine runs it, and the URL answers"]:::blocked
I0932["I0932<br/>The milestone is accepted on its evidence"]:::blocked
I0901 --> I0902
I0901 --> I0924
I0902 --> I0903
I0902 --> I0904
I0902 --> I0905
I0902 --> I0907
I0902 --> I0908
I0902 --> I0909
I0902 --> I0913
I0902 --> I0918
I0903 --> I0913
I0903 --> I0914
I0903 --> I0926
I0903 --> I0927
I0904 --> I0906
I0904 --> I0909
I0905 --> I0909
I0905 --> I0916
I0906 --> I0930
I0909 --> I0910
I0909 --> I0911
I0909 --> I0912
I0909 --> I0914
I0909 --> I0919
I0909 --> I0920
I0909 --> I0930
I0910 --> I0921
I0911 --> I0921
I0912 --> I0930
I0913 --> I0914
I0913 --> I0926
I0913 --> I0931
I0914 --> I0915
I0914 --> I0916
I0914 --> I0917
I0914 --> I0931
I0915 --> I0925
I0916 --> I0931
I0917 --> I0931
I0918 --> I0929
I0918 --> I0931
I0919 --> I0928
I0920 --> I0932
I0921 --> I0928
I0922 --> I0931
I0923 --> I0932
I0924 --> I0926
I0925 --> I0932
I0926 --> I0932
I0927 --> I0932
I0928 --> I0932
I0929 --> I0932
I0930 --> I0922
I0930 --> I0923
I0930 --> I0928
I0930 --> I0931
I0931 --> I0929
I0931 --> I0932
classDef done stroke:#16a34a,fill:#052e16,stroke-width:2px
classDef active stroke:#2563eb,fill:#eff6ff,stroke-width:2px
classDef verify stroke:#7c3aed,fill:#f5f3ff,stroke-width:2px
classDef ready stroke:#0891b2,fill:#ecfeff,stroke-width:2px
classDef blocked stroke:#b45309,fill:#fffbeb,stroke-width:2px
classDef cancelled stroke:#6b7280,fill:#f9fafb,stroke-width:2pxCanonical record: .ai/repo/project/milestones/fly-deployment.yaml. Read it back with majordomus plan show fly-deployment.