Durable Fly Deploy on a Steward
Show the whole prompt
Read https://intentius.io/chant/tutorials/fly-durable-deploy/.
In `examples/fly-durable-deploy`, run `npm install` and export `FLY_FLAPS_BASE_URL=http://localhost:4280`. Run `npx chant run fly-durable-deploy --no-record` twice against mudflaps.
Show me the output of both runs and the Machine state mudflaps reports after each.
Run `npm run build:fountain` and `npm run lint`, and show me the findings. Do not post the Op to a fountain steward with `--on fountain`.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/tutorials/fly-durable-deploy/.In `examples/fly-durable-deploy`, run `npm install` and export `FLY_FLAPS_BASE_URL=http://localhost:4280`. Run `npx chant run fly-durable-deploy --no-record` twice against mudflaps.Show me the output of both runs and the Machine state mudflaps reports after each.Run `npm run build:fountain` and `npm run lint`, and show me the findings. Do not post the Op to a fountain steward with `--on fountain`.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.The fly-durable-deploy
example runs a Fly Machines deploy on a schedule, unattended. The op is scheduled on a fountain steward:
one teammate, one persistent machine, one thread carrying every deploy this app
has had.
Where fly-deploy-rollback uses a Sprite
checkpoint as its recovery boundary, durability here comes from two properties
that have nothing to do with a workflow engine.
The machine persists. The steward’s sandbox is sandbox_mode: persistent,
so the checkout, the npm cache and the installed chant survive a turn ending. A
turn is a command on a computer that was already there, not a container booted
to run one deploy and thrown away.
The op converges. Build serializes src/infra.ts to the flaps plan; Deploy
applies that plan to the Machines API and waits each machine to started.
Applying it twice reaches the same App and the same Machine. So a turn that dies
halfway leaves the estate wherever it got to, and the next run walks the same
steps against that state and finishes. Nothing is resumed, because a convergent
op has the same shape from every starting point.
The op
Section titled “The op”ops/fly-durable-deploy.op.ts is two phases and a cron:
import { Op, phase, build } from "@intentius/chant/op";import { flyApplyStep } from "@intentius/chant-lexicon-fly";
export default Op({ name: "fly-durable-deploy", overview: "Deploy the Fly App + Machine, convergently, on a cadence", schedule: { cron: "*/30 * * * *" }, phases: [ phase("Build", [build(".", { script: "build:fly" })]), phase("Deploy", [flyApplyStep("dist/fly.json")]), ],});schedule is data on the Op, not a resource. The flaps endpoint comes from
FLY_FLAPS_BASE_URL, which points at local mudflaps offline and at real Fly when unset.
The steward
Section titled “The steward”ops/fountain.ts declares the machine that runs it. The Steward composite
returns the Agent, the Teammate, and one Schedule per op that carries a
cron (plus a Webhook when you declare one); the Environment and the Vault
it reads are declared in the same file and emitted alongside them:
export const { agent, teammate, schedules } = Steward({ name: "fly-steward", environment: toolchain, vault: flyCreds, ops: [flyDurableDeploy],});toolchain is the Environment the sandbox is provisioned from: the estate
repo mounted at /workspace/estate, a setup script that installs chant, and
networking_type: "limited" with an allowlist narrow enough to review
(api.machines.dev, github.com, registry.npmjs.org). flyCreds is the
Vault that holds FLY_API_TOKEN, which is set in fountain and never in the
repo. The Steward reads the op’s name and its cron off the declaration, so the
cadence is written once, next to the op it paces.
chant.config.ts declares the fountain profile the CLI reads:
fountain: { profiles: { demo: { endpoint: "https://fountain.inevitable.fyi", token: { env: "FOUNTAIN_TOKEN" }, team: "fly-steward" } }, defaultProfile: "demo",}Prerequisites
Section titled “Prerequisites”- Node.js and
npm. - Docker (for the mudflaps emulator).
- A fountain instance and a token, for the steward half. The local half needs neither.
Run it locally
Section titled “Run it locally”The op runs here, in-process, with no fountain at all. Same steps, one observation instead of a cadence:
docker run -d --rm -p 4280:4280 --name mudflaps ghcr.io/intentius/mudflaps:0.4.1
cd examples/fly-durable-deploynpm installexport FLY_FLAPS_BASE_URL=http://localhost:4280
chant run fly-durable-deploy[phase] Build ✓ chantBuild(script=build:fly) 3.1s[phase] Deploy ✓ flyApply(plan=dist/fly.json) 6.8sOp "fly-durable-deploy" completed in 9.9sThe App and Machine are now in mudflaps:
curl -s http://localhost:4280/v1/apps/fly-durable-demo/machines | jq '.[].state'# "started"Now run it a second time.
chant run fly-durable-deployThe second run is the point. It reports the same steps and reaches the same App and Machine rather than deploying a second one. That is the property the schedule leans on, and it is why nothing here has to be resumed.
Run it on the steward
Section titled “Run it on the steward”npm run build:fountain # dist/fountain.yamlexport FOUNTAIN_TOKEN=... # the variable chant.config.ts namesnpm run lint # FTN001, FTN010, FTN020..FTN023chant run fly-durable-deploy --on fountainchant run … --on fountain posts the command line chant run fly-durable-deploy
to the steward’s thread, tails the conversation’s event stream, and hands back a
record in the shape a local run would have written. Open the conversation in
fountain’s UI and every turn is one command somebody could have typed, so
scrolling the thread is scrolling this app’s deploy history.
If the steward is mid-turn the post is refused rather than queued:
fountain runtime: the steward "fly-steward" is running another op(https://fountain.inevitable.fyi/conversations/c_01J…). A teammate runs oneturn at a time; wait for it to finish and run this again.A teammate runs one turn at a time, which is what keeps two deploys off one checkout, and it is also why the schedule is in-thread: a fire that lands mid-turn is dropped, and the next one re-applies everything anyway.
Point the sandbox at your own estate with the build parameter:
chant build ops --lexicon fountain -o dist/fountain.yaml \ --param repoUrl=https://github.com/you/your-estateRun it against real Fly
Section titled “Run it against real Fly”Drop the emulator override and give it a token. The op does not change:
docker rm -f mudflapsunset FLY_FLAPS_BASE_URL # flyApply falls through to real Flyexport FLY_API_TOKEN=...
chant run fly-durable-deployOn the steward, the token lives in the Vault instead, and the op is unchanged.
Read next
Section titled “Read next”- The Steward — which fountain noun answers which chant need
- Running an Op on fountain — profiles,
status,log, and the busy case fountain-steward— the fuller worked example: a watch, a converge and a gated apply on one thread- Ops as teammates — where each half of an op’s durability lives