Two pipelines, four stages, nothing to operate
There is no queue to watch, no service to keep alive, and no second system to learn. A push starts a build; the branch decides which Worker it lands on. Everything below is the same page in one picture.
The whole flow
Both pipelines run the identical build. The only difference is which branch the push came from, and therefore which Worker receives the result. Read this grid once and you have the entire system.
Lint and the whole test suite. Fast, and the same command you run locally.
just check
The Worker and its static assets are staged into a deployable tree.
just worker-build
The branch decides. main is production; anything else goes to staging, if it is the oldest open pull request.
staging_branch.py
One wrangler deploy, against a config rendered for that environment.
wrangler deploy
main
gittrove.com
Merging a pull request is the production deploy. The merge commit becomes the push, the trigger fires, and the live site moves to that commit. There is no approval step in between, and nothing waits for staging to have passed.
The branch reaches staging only if it is the oldest open pull request. Every other branch runs the same build and then deploys nothing — a green build that did no work, with the log naming which pull request holds the slot.
The one rule people forget
There is one staging environment and every branch wants it. Rather than let them overwrite each other, exactly one holds it: the lowest pull request number. That is the change waiting longest, which is also the change whose staging environment is worth the most.
Environments
Staging and production are separate Workers with separate D1 databases. Nothing staging does can reach a real customer, which is what makes it safe to point at a pull request.
| Production | Staging | |
|---|---|---|
| Worker | gittrove |
gittrove-staging |
| Reached at | gittrove.com, through a zone route | its own workers.dev URL only, never the domain |
| Deployed by | a push to main |
the oldest open pull request |
| Database | its own D1 database | its own D1 database |
| Payments | Polar, live organisation | Polar sandbox — a checkout there cannot charge anyone |
| Build identity |
both stamp GITTROVE_BUILD_SHA, so any deployment can
name the commit it came from
|
|
What isn't there
Most of the confusion around a pipeline comes from assuming machinery that was removed. None of the following exists, and nothing here needs it.
The build runs on Cloudflare's builders. There is no local container to match, and no image to keep in step.
No service of ours sits in the path. Nothing needs restarting, and there is no queue to inspect.
Per-branch preview Workers are gone. They produced no stable URL to look at and gated nothing.
Deploying code never applies a migration. Schema changes are a separate, deliberate command against one environment.
A merge deploys. If a change should not reach production, the place to stop it is the pull request.
Node exists only because the Pyodide sync drives the interpreter through it. Nothing here runs an npm script.
When it breaks
Four stages means four places to look. Start at the top and stop at the first thing that is not what you expected.
This is the expected outcome for every branch that is not the oldest open pull request. Open the build log: it names the pull request that holds staging. If your branch is the oldest and still skipped, the gate reported a reason — read it rather than re-pushing.
Checks and build are stages 01 and 02, and they are the same commands you can run locally. Run just check and just worker-build and you will see the failure before the builder does.
Every deployment stamps the commit it came from. Read GITTROVE_BUILD_SHA off the running site and compare it with git rev-parse origin/main. If they differ, the latest push has not finished deploying yet, or its build failed — the Worker still serves the last good version, which is deliberate.
Migrations never run on their own. Apply them deliberately, against one environment at a time, and remember that code and schema move separately: a merge ships the code immediately while the schema waits for you.
just worker-dev serves the Worker against a local database and applies the committed migrations first, so a fresh checkout serves a working application instead of failing on a missing table.