Deployment is an entirely different kettle of fish from integration, and it's better managed as a standalone workflow. Its role is to apply a release to a target environment. It can be triggered manually, on approval, or straight through as a request from a CI workflow - and in a regulated organisation it will usually be the middle one, permanently.
That's fine. What matters is what the workflow does once triggered, and what it does not ask a human to do.
CD orchestrates the system, not the application
The most common mistake is scoping CD to the application. Deploy the JAR, deploy the image, done.
But an application on its own doesn't constitute a release. What actually has to be correct, together, at the same time, is the whole set: applications, services, secrets, environmental configuration, compute, storage, and the environment itself. Deploying one of them and assuming the others already match is how environments drift, and drifted environments are why "it worked in UAT" is such a familiar sentence.
Applied together as one immutable, auditable artefact, that set is the System Version: everything that was applied, what it was applied to, and full traceback to how each piece was produced. That's the unit an approver should be approving and the unit an auditor should be able to query. I've written about that unit and the operating model around it in The platform is the product.
Promotion, not rebuild
Part one called this the invariant everything rests on, so here's the mechanical version of it.
The artefact is built once and its digest never changes. What varies between environments is configuration, and configuration is supplied to the artefact rather than baked into it. Endpoints, credentials, scaling, feature defaults - all injected at deploy time from a source that is itself versioned and evidenced.
Two things follow, and both matter to an auditor:
- What you tested is what you shipped. Not an equivalent build. The same bytes, provably
- Configuration is a change too. If a person can edit production configuration outside the pipeline, your change control has a hole in it exactly the size of the most dangerous edits anyone makes
Practical test, worth running this week: take what's running in production, take its digest, and walk it back to a commit, a test result, a scan, and an approval. If there's a rebuild anywhere in that chain, you can't - and neither can anybody who asks you to.
Every environment is a rehearsal
Here's the reasoning behind a rule that sounds fussy and isn't: the deployment mechanism must be identical in every environment. Same workflow, same steps, same automation, same configuration mechanism. What differs is only which values get injected and who is allowed to approve it.
The moment production has its own procedure - an extra manual step, a different script, somebody who "knows the trick for prod" - every deployment you did to test stopped being practice for the one that counts. You've been rehearsing a different play.
This is also the honest answer to "we don't trust the automation for production". You don't trust it because you've never actually run it. Run the same thing thirty times a week in the lower environments and the production run is the boring thirty-first.
The dialog is not a deployment
Watch a "modern" deployment in the regulated shops I have worked in and it goes like this. An operator opens the workflow, presses run, and a dialog asks which environment, which stack, which version - sometimes which region. They pick from the dropdowns and press the button. The workflow file is in git. The choices are not.
That is ClickOps in a YAML costume. Anyone who ran a DOS machine will recognise it. Direct Access put a numbered menu on the screen and ran a batch file when you picked one, and in 1990 that was the height of sophistication. A dispatch dialog is the same menu with better fonts and RBAC. The point of a pipeline was never a better menu. It was that nobody stands at one. The deployment is defined at the moment somebody clicks, by whatever they happened to select, and the only record of it is a run log. I have watched teams pick the wrong option and deploy the wrong stack to the wrong environment - not through carelessness, but because the workflow offered the choice, and a workflow that offers a choice will eventually be given the wrong one. Every runtime input is a decision the pipeline has handed back to a human, at the one moment you least wanted a human deciding anything.
The dropdown's options are canned, and that is where the second harm comes from. If the list of things that can be deployed lives inside the workflow file, the workflow file becomes the control - so the organisation locks it down. A central team owns the folder, a handful of people can merge to it, and every new environment, stack or region is a ticket to them. That is the Golden Pipeline trap arriving by another road. The team is not being restricted for safety. It is being restricted because the safety was put in the wrong file.
The fix is to move every one of those decisions out of the dialog and into an artefact: a deployment contract. One versioned file that states, explicitly and completely, what will be applied - the System Version, the configuration values, the network exposure, the compute and storage, the target environment, the rollback target. It is reviewed like code and approved like a release, and the workflow takes exactly one input: which contract to apply. Nothing the contract does not name can be deployed. If it is not in the contract, it does not happen.
Four things fall out of that. The approver reads the contract instead of trusting the operator to pick well. The diff between this contract and the last one is the change record, produced for free. The wrong-environment mistake becomes impossible rather than unlikely, because the environment is a property of the contract, not a choice in a dropdown. And the workflow file stops being the control, so it stops needing a lock: it becomes generic plumbing that applies whatever contract it is handed, and the review and the ownership attach to the contract instead.
If your deployment workflow has a dropdown, that is the seam a person can still fall through. Close it.
What earns the word "releasable"
Part one leans hard on the phrase provably releasable. This is the part that does the proving, and it's a good deal more than a green build.
By the time a System Version is offered to the gate, the pipeline has:
- Scanned the images - the built containers, not just the source that produced them. What is actually going to run
- Collected the release notes - assembled from the commits and tickets in the version, as a build output. Emitted by the pipeline rather than written by a person on the Thursday
- Deployed to DEV and run the full end-to-end suite against stubs
- Deployed to SIT and run the same suite against every real dependency
When all of that passes, you have a system you can deploy. Anything short of it and "releasable" is an opinion.
Running the same suite twice is the point
Those last two steps look redundant. They're the opposite: they ask two different questions, and the difference between the answers is the most valuable signal the pipeline produces.
DEV, against stubs, asks: is my system correct against the contract? The stubs are the contract made executable. Nothing else is in the way - no dependency outage, no upstream team halfway through their own deploy, no shared test data somebody changed an hour ago. A failure here is yours.
SIT, against the real dependencies, asks: does the world still match what I was told? Same suite, same assertions, real systems on the other end.
So when it passes in DEV and fails in SIT, you have not found a bug in your code. You've found a dependency that has drifted from its contract - and you've found it in a pipeline on a Tuesday rather than in production on a Saturday. That signal only exists because both runs are the same suite. Contracts are what earn it: without an agreed, executable contract the stub run is guesswork, and the two results have nothing meaningful to say to each other.
Small enough to see the whole shape? A reference implementation I keep public runs exactly this split in CI - an in-memory broker on every push, then the same image against a real RabbitMQ. The second job earns its place because of a bug the first cannot see: queue declarations the stub accepts and the real broker refuses.
Most pipelines throw the signal away. Smoke tests in one environment, a regression pack in another, something bespoke in UAT. The environments then can't be compared at all, "it worked in DEV" becomes unfalsifiable, and every failure needs a person to work out which layer it came from - which is exactly the manual diagnostic work that ends up on somebody's Saturday.
One detail that makes it hold: what gets deployed at each step is the System Version, not your service on its own. Testing against a fixed set of dependency versions is what lets the result mean anything later, because it's the same set that will be promoted.
What a good gate looks like
If the gate is staying - and it is - it's worth being precise about what a good one looks like:
- The approval is recorded against an artefact version and its evidence, not against a ticket number that refers to an intention
- Everything before the gate is automated. Everything after the gate is automated. The gate is the only human moment in the chain
- The approval is a decision, not an instruction for a human to go and perform steps
- The approver is reading evidence that no one spent two days assembling. If a person built that pack by hand, the pipeline should have built it instead
- "Rejected" costs nothing, because nothing was done in advance of the decision
- A change window is a scheduling input. The approved deployment runs itself, unattended, at 02:00, with the team asleep
That last one is worth sitting with. Most organisations treat a change window as a work window - a slot in which people perform a deployment. It isn't. It's a constraint on when the automation is permitted to fire. The distinction is the difference between a Saturday night in the office and a calendar entry.
Change freezes are usually a red flag
I'm skeptical of change freezes, and I'd encourage you to be too.
Strip away the policy language and a freeze is an organisation stating that it cannot release safely on demand, and substituting a calendar for the capability. If your changes are small, evidenced, tested in an environment that behaves like production, and reversible on a pre-approved path, the case for stopping in December gets thin fast. What exactly is riskier about that change on the 20th than the same change on the 3rd?
The answer is usually "the people who'd fix it are on leave" - which is a statement about how much manual intervention a release still requires. Fix that and the freeze loses its reason to exist.
There is one case I'd defend: legacy. Systems without meaningful test coverage, without a rollback path, where the deployment is genuinely hand-carried and the knowledge lives in two people's heads. Freezing that over a peak trading period is a rational hedge. It's also a statement about the system rather than a delivery policy, and it's worth recording it that way - as a risk with an owner and a remediation plan, not as a rule that creeps outward to cover the modern services that don't need it.
Where a freeze exists regardless, hold this line: a freeze constrains release. It does not constrain integration.
A freeze period says you may not change production. It does not say you may not merge, build, test, or produce evidenced releasable versions. The pipeline should run all the way through, right up to the gate, and stop there - with a queue of approved-and-waiting versions on the other side of it.
Teams that stop merging instead spend the freeze accumulating work on branches, then deploy the largest and riskiest release of their year the moment it lifts. The freeze existed to reduce risk. Used that way it manufactures risk, on a schedule everybody can see coming.
If your organisation freezes for six weeks over a year end, that's six weeks in which integration has to get more disciplined, not less.
Rollback is part of the release
A release you cannot reverse isn't a release, it's a commitment.
Decide roll-forward or roll-back before you need it, and exercise it in the lower environments so it's boring by the time it matters. An untested rollback path is a plan, not a capability.
In a regulated environment there's a wrinkle: the rollback is itself a change, and it needs an approval path. Pre-approve it alongside the release, as part of the same decision - "approving version 4.7 also approves reverting to 4.6" is a sentence your change process can absorb easily if you ask it in advance. The alternative is discovering during an incident that reversing the change requires a board that meets on Thursdays.
Rolling back code is easy. Data is where it hurts - which is exactly why the expand-and-contract discipline from part two earns its keep. If every schema change is additive first and destructive later, the window in which you can go backwards stays open. If it isn't, your rollback plan is fiction, and it's the kind of fiction that gets discovered at 3am.
Automating the governance seam
Part one makes the case that proving compliance should cost a team nothing. Here's what that looks like mechanically at release time.
The approver opens one thing. Not a folder containing forty PDFs, not a link to a dashboard requiring a login they don't have. A single release record: what changed, which commit, which tests ran and their results, which scans ran and what they found, what's being deployed and where, and what the rollback path is. Evidence assembled for reading, not filed for storage. Those are different jobs and most organisations only do the second one.
The pipeline delivers it, using its own identity. Not a person's account, not a shared service account somebody knows the password to. The upload is attributable to the pipeline, which means the chain from commit to evidence to approval has no unattributable human step in it. That's a control improvement, and worth saying out loud to the people who'll ask why you're touching their process.
It fails closed. If the evidence doesn't land, the release doesn't proceed. Evidence delivery is part of the release, not a notification about it. Get this wrong and you've built a seam that stops working in month four and reports success anyway, discovered when somebody asks for the March pack and finds February's.
The format becomes versioned - and that's the reciprocal ask. This is the part teams forget to collect. Once the pack is generated by a pipeline, requests to change it become change requests against that pipeline. "Can you also include the performance test summary" stops being an ad-hoc favour asked of a person a week before release, and becomes a small, scheduled, reviewable change to a shared interface. You've converted an unbounded request queue into a contract. That's worth more over time than the two days you saved.
Then measure it. An automated seam has a visible cost - runtime, failure rate, the change requests against it. Once it's visible, you can have the conversation about whether the destination still earns its place, holding data rather than an opinion. Argue for removing it on day one and you're a developer complaining about SharePoint. Argue for it in year two with numbers and you're an architect retiring an interface.
Retention outlives your pipeline
One practical detail that regulated teams find out too late.
All workflows share the same audit properties: they're immutable, they hold a history of every step performed, and they can have evidence attached. But workflow runs expire. Your CI platform's default retention is measured in weeks or months. Your obligation to produce evidence for a production release is frequently measured in years.
So promoting a release to production needs archival as an explicit step - the evidence copied somewhere with a retention policy that matches the obligation, not left sitting in a pipeline run that will be garbage-collected long before anybody asks about it. Cheap to do at release time. Impossible to do afterwards.
The number this owns
Time in gate: from an approved-and-evidenced artefact sitting ready, to that artefact running in production.
Unlike lead time to releasable, this one isn't entirely yours. It's a property of how your organisation governs change - the approval routing, the board cadence, the window, the notice period. Measure it anyway, and measure it separately.
Reported as a single blended lead time, engineering slowness hides behind "the CAB" and governance slowness hides behind "the engineers", and neither conversation can happen. Split them, and both become possible. You'll usually find time in gate is the larger number - which is a far better argument for changing the process than any amount of advocacy, because it's arithmetic rather than opinion.
And when you do make that argument, make it the right way round. You're not asking to remove the gate. You're asking why a decision takes eleven days when the evidence needed to make it was ready on day one.
Back to part one for the argument these two halves serve.
References & further reading
- Continuous Delivery - Humble & Farley (2010)
- The DevOps Handbook - Kim, Humble, Debois & Willis (2016)
- Investments Unlimited - Yates, Willis, Wilkinson, Byard et al. (2022)
- Accelerate - Forsgren, Humble & Kim (2018)
- Refactoring Databases: Evolutionary Database Design - Ambler & Sadalage (2006)