Ask a team whether a feature is done and they will say yes. Ask when it can be in front of users and the answer grows qualifiers. There is the migration to write. Someone has to update the environment variables. The installer has not been rebuilt since March. The manual is out of date. Nobody has run it against a database with real data in it.
This gap, between the moment the work is called done and the moment it could actually reach a user, is where software projects quietly lose weeks. It is invisible while you are in it, because none of the tasks in it are hard. They are small, dull, and unaccounted for, and they are discovered rather than planned. By the time the team is doing them they are also under pressure, so they do them by hand, skip the automation that would have made them repeatable, and arrive at the next release having learned nothing.
The goal that closes the gap is a strange one to state, and it is the destination of this whole series.
You should be able to deploy everything except the last few hours of work, at any moment, without preparation.
Two kinds of ready
That sounds like it demands finished features. It does not, and the distinction we drew when talking about branches as inventory is the one that makes it achievable.
Being functionally ready means the feature is complete and worth showing to someone. Halfway through a week's work, it is not, and no discipline can make it so.
Being technologically ready means the trunk builds, the tests pass, the migrations apply, the deploy pipeline works, and shipping this commit would produce a running system. That has nothing to do with whether the quota feature is finished. It is a property of the machinery, and the machinery can be kept in that condition permanently.
You will not actually release in the middle of an iteration, because half the stories are half built. The point is that nothing technical stands in the way. Releasing becomes a business decision rather than an engineering event. And the way you get there is not by preparing carefully before each release. It is by making every integration resemble a release so closely that the real one is unremarkable.
Grow the release machinery with the product
The consequence is that release infrastructure is not a phase. It is not something a team builds once the product works. It is product code, built alongside the feature code from the first story, tested, refactored, and reviewed like anything else.
The first story on a new project should ship. Not to users, necessarily, but through the whole pipeline: built, tested, packaged, deployed somewhere real. When that exists on day one, it stays working, because it is exercised dozens of times a day and breaks loudly when it stops. When it is deferred, it is built in a hurry by someone who is already late, and it never becomes trustworthy, because the only way to trust a deployment is to have done it a thousand times.
This is the same principle as a test suite. A suite written after the code tests what the code does. A pipeline built after the product deploys whatever the product happens to be.
Deploying the commit you tested
Here is a piece of our own machinery that took a while to get right, and which encodes more of this argument than its size suggests.
on:
workflow_run:
workflows: ["DORA Metrics"]
types: [completed]
branches: [main]
jobs:
deploy-server:
if: github.event.workflow_run.conclusion == 'success'
Two things are happening. The deploy is chained to the test run rather than to the push, so nothing reaches production that has not been proven green. And then the deploy triggers on the specific commit that was tested:
hoststack deploy trigger svc_... --commit ${{ github.event.workflow_run.head_sha }}
Not the tip of main. The head of the workflow run that just passed.
The difference matters on exactly the day you would least like it to. Suppose two commits land within a few minutes. The first goes green and its deploy begins. In the seconds between, the second commit lands on main. A pipeline that deploys "the latest main" now ships a commit whose tests have not finished, on the authority of a green tick that belonged to a different commit. It will work almost every time, and the once it does not, the deploy log will show a green build next to a broken production, and you will not believe your own eyes.
Deploy the artefact you tested. It is the same instinct as never repairing the build by hand on the integration machine: do not let the thing you verified and the thing you ship drift apart, even by a few seconds.
Release infrastructure is a design problem
The other half of our deploy configuration is not a workflow at all. It is a decision, written down where the next person will find it.
Automatic deployment is switched off for the server on our host. That sounds like a regression until you know why. Auto deploy is also what produces preview environments for pull requests, and on our platform a preview inherits the production environment, including the link to the production database. A branch containing a migration would therefore run that migration against production the moment its preview booted, before anyone had reviewed a line of it.
So the server has exactly one deploy path, the workflow above, and it fires only for commits on main whose tests are green.
The web service keeps its automatic deployment, because it is a static site that reaches no database and its previews are harmless. That decision has a consequence which took us an embarrassingly long time to say out loud.
Our test workflow takes about three minutes. Automatic deployment ships the frontend the moment a commit lands on main. So on every single push, for roughly three minutes, the frontend serving real users is a commit whose tests have not finished running. If it is broken, the tests will say so, and they will say so to a site that is already broken.
Read honestly, then, the pipeline guarded half the system. The server waited for green. The frontend never did, and the diagram on the wall did not distinguish between them.
Continuous integration cannot gate what has already gone out. So the checks that protect the frontend were moved into the one place that still stands between a commit and a running site: the build stage of its Dockerfile. A failing type check there produces no image, and no image means no deploy.
Read that again, because it inverts the usual instinct. When the pipeline could not be made to refuse, the refusal was pushed down into the artefact itself. The gate did not become a policy, or a note in a document, or a rule someone has to remember during review. It became a property of the thing being built, which is the same move as turning a convention into a plugin, applied to a deployment.
The point is not the specific configuration. It is that this reasoning exists, is written in a comment above the code that enforces it, and was arrived at deliberately rather than after an incident. That is what it means to treat release infrastructure as part of the product: the tricky bits get designed, the design gets recorded, and it is reviewed like anything else.
The last mile is still not free
Being technologically ready to deploy is not the same as being correct once deployed, and it would be dishonest to end a series on a note of triumph about pipelines.
We have written about the fix that was merged while production kept showing the old numbers, because derived data is a cache and deploying new code does not refresh it. We have written about operations that must be safe to run twice, because a deploy that retries, or a migration that runs during a rolling restart, will happen more than once whatever the diagram says. The pipeline gets your code to the machine. It does not make your code true.
What it does is remove the class of problems that come from not having done this recently. And that, in the end, is the thread running through all seven of these posts.
The thread
We began with a conflict in a file nobody wrote, produced by two branches that had been apart for a single day. Along the way: your local trunk is a photograph of the real one; the rebase and merge argument is loud because the branches are old; a branch is inventory and depreciates; a server that runs your tests is not the practice of integrating; a green trunk is valuable because it shrinks the search; a check that cannot fail is theatre.
Every one of those is the same idea in different clothing. Distance from the trunk is the cost, and every practice here is a way of paying it early, in small amounts, when it is cheap. The conflict, the stale cache, the broken build, the frightening release, the derived data that lied: all of them are what deferred integration looks like at the moment it finally presents its bill.
Integrate every few hours. Never leave it broken. Keep the machinery that ships it working, always, as part of the work rather than after it.
Then the release, when it comes, is a nonevent. Which is the highest praise available to a deploy.
The practice of continuous integration described in this series is set out in the continuous integration chapter of The Art of Agile Development by James Shore and Shane Warden.

