Skip to content

A deployment names the provider that hosts it, and every provider file is derived from it

A deployment names the provider that hosts it, and every provider file is derived from it

majordomus://adr/adr-0219accepted deploymentderivationhosting

it references

  • put_in_force apps/majordomus-cli/src/deploy/mod.rs (file)
  • put_in_force apps/majordomus-cli/src/deploy/render.rs (file)
  • put_in_force apps/majordomus-cli/src/served.rs (file)
  • put_in_force share/schemas/majordomus/deployment/deployment.v1.schema.json (file)
  • put_in_force adr-0126
  • put_in_force adr-0204
  • put_in_force majordomus (deployment)
  • put_in_force I0909
  • put_in_force fly-deployment
  • put_in_force majordomus.deployment-contract@2
  • put_in_force test/cases/84_deployment_contract.sh (test)
  • put_in_force test/cases/86_deployment_projections.sh (test)

referenced by

219. A deployment names the provider that hosts it, and every provider file is derived from it

Context

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:

  1. 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.
  2. 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.
  3. The deployment cannot say what it serves. /api/v1/live answered 9a5a9b77520, the release binary's commit, while the tree it served was 573df967bb5, so served.observe had nothing true to compare.

Decision

  1. A deployment may name the provider that hosts it. deployment/v1 gains an optional provider block: name (today only gigalixir), 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.
  2. Every file a provider reads is derived. majordomus generate deployment renders the provider's files from the object, under deploy/<provider>/, each with the provenance header and under generate --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).
  3. What is pushed is assembled, never the repository. The tree a provider receives is built from three things only: the object's build.inputs at 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.
  4. One commit, answered. The executable is built at the commit the tree was read from, so /api/v1/live answers the commit the deployment serves and served.observe can 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.
  5. Always current, verified. After master's ci succeeds, the deployment is published from master and observed: served.observe reads the URL the object names and judges the answer against master. The object's status is active only while an observation says it serves master; until then it is declared. 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.
  6. 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.

Consequences

  • Rule majordomus.deployment-contract moves 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 any provider key becomes a refusal of an unknown provider, and case 86 checks the Procfile and the buildpack files like the image definition.
  • Milestone fly-deployment and 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.

Alternatives rejected

  • 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 push of the whole tree is one wrong remote away (the CLI adds a gigalixir remote to whichever repository the shell is in).

Where this comes from

canonical file
.ai/repo/adrs/0219-a-deployment-names-its-provider-and-every-provider-file-is-derived.md
identity
adr-0219
also served as
MCP resource majordomus://adr/adr-0219 · majordomus entity show adr/adr-0219 · GET /api/v1/entity?uri=majordomus://adr/adr-0219
in the Cockpit
/cockpit/objects/adr/adr-0219

This page is generated from the file above by scripts/generate-site-data; editing the page changes nothing, editing the file changes every surface. The relations above are edges of docs/generated/graph.json, which the executable writes from the same index.

Searches the titles and descriptions of every page this site publishes.