219. A deployment names the provider that hosts it, and every provider file is derived from it
Kontext
On 2026-10-12 the owner disconnected Fly.io: "deploy only binaries + GitHub Pages". The
deployment contract followed. Its provider block and the fly.toml rendered from it left the
tree (9af3fa65a40, pull request #1032), rule majordomus.deployment-contract v1 began to refuse
a provider key, and milestone fly-deployment and issue I0909 put "a provider abstraction
covering hosts other than the one this repository deploys to" and "buildpacks" out of scope.
On 2026-10-20 and 21 the owner reversed that, in Czech, in session 0c: deploy Prismatic
Majordomus to a new Gigalixir app "in full", then "optimise that deployment, it must be the
Majordomus way: derived, inferred, documented, iterative, generic, dynamic, always up to date,
verified, validated, enforced — over the Gigalixir deployment", and "Rustler only if Elixir is
really needed; it must be a derived surface, enforced, always up to date, not another parallel
universe". The account's free tier holds one application; the owner deleted the old one and
the application prismatic-majordomus was created on stack gigalixir-24.
The first deployment was assembled by hand, outside the tree: the files the deployment object's
build.inputs names, read at master 573df967bb5, the statically linked x86_64-unknown-linux-musl
executable of release v1.0.0-alpha.1, a Procfile, and the inline buildpack pinned to one commit.
It answered /api/v1/live at 22:59Z on 2026-10-20 and serves every surface the image serves.
Three things it showed:
- Elixir is not needed. Gigalixir runs any Heroku-compatible buildpack and a Procfile. A Rustler wrapper would be a second application whose only job is to start the first.
- A hand-assembled slug is the parallel universe the owner refused. Its file list, its start command and its port are facts the deployment object already states.
- The deployment cannot say what it serves.
/api/v1/liveanswered 9a5a9b77520, the release binary's commit, while the tree it served was 573df967bb5, soserved.observehad nothing true to compare.
Rozhodnutí
- A deployment may name the provider that hosts it.
deployment/v1gains an optionalproviderblock:name(today onlygigalixir), the provider's application name, its stack and the public URL the deployment answers on. A deployment with no provider is what it was: the image, published to GHCR, run by nobody. A provider name the contract does not know is refused, like any other key it does not have. - Every file a provider reads is derived.
majordomus generate deploymentrenders the provider's files from the object, underdeploy/<provider>/, each with the provenance header and undergenerate --check. For Gigalixir they are the Procfile (the start command, the interface and the port the provider injects) and the buildpack list and hooks (the inline buildpack, pinned by commit, that installs the prebuilt executable and compiles nothing). - What is pushed is assembled, never the repository. The tree a provider receives is
built from three things only: the object's
build.inputsat one commit, the provider files of that commit, and the executable built at that same commit. The repository itself, its history and the checkout-local half never reach the provider. - One commit, answered. The executable is built at the commit the tree was read from, so
/api/v1/liveanswers the commit the deployment serves andserved.observecan judge it. The provider's own build-time variable for the source commit is mapped to the one the crate's build script reads, so a build the provider runs answers its commit too. - Always current, verified. After master's
cisucceeds, the deployment is published from master and observed:served.observereads the URL the object names and judges the answer against master. The object's status isactiveonly while an observation says it serves master; until then it isdeclared. The credential that authorises the push is a secret of the workflow, never a field of the object, a generated file or a log line. - Remote callers stay what ADR 0126 made them. A hosted server reads the repository to any caller and changes nothing for one beyond loopback except through signed input. What a public caller may read of the mesh, and whether the hosted server is a rendezvous at all, is ADR 0220's decision, not this one's.
Důsledky
- Rule
majordomus.deployment-contractmoves to version 2: a deployment names no provider unless it names one the contract knows, and every provider file is generated. Case 84's refusal of anyproviderkey becomes a refusal of an unknown provider, and case 86 checks the Procfile and the buildpack files like the image definition. - Milestone
fly-deploymentand I0909 no longer exclude buildpacks or a second host; the container image stays the repository's own, and a provider's files are one more projection of the same object. - The Gigalixir application is a deployment of this repository in the records, not a fact of a dashboard: its name, URL and stack are diffed in review like the port and the routes.
- The owner's statement of 2026-10-12 still holds for Fly.io; it no longer holds for every host.
Zamítnuté alternativy
- A Rustler or Elixir wrapper. Gigalixir does not need one, and it would add a second runtime, a second build and a second thing to keep current.
- Compiling on the provider with the Rust buildpack. The crate's release compile is about ten minutes on a developer machine (measured 2026-10-12) and would run on every push inside a 0.4-size builder; the executable is already built and checksummed by the release path.
- Keeping the hand-assembled slug. It is the parallel universe: a second statement of the inputs, the command and the port that nothing checks.
- Pushing the repository. It would ship the history and the checkout-local half, and a
provider's
git pushof the whole tree is one wrong remote away (the CLI adds agigalixirremote to whichever repository the shell is in).