CI/CD may be the only acronym in software that teams adopt backwards. When a team says it wants CI/CD, what it usually means is that it wants the deploy button to hurt less, which is the CD half. That is understandable, because painful releases wear people down. Nobody enjoys release weekends, deployment checklists, or sitting around waiting to find out whether the thing they just shipped is about to break production.
The trouble is that CD is rarely the first problem a team needs to solve. Before you can continuously deliver software, you have to continuously integrate it, and a lot of teams skip right past that part. They try to automate deployments while still carrying long-running branches, manual test gates, and code that gets merged before anyone can say with a straight face that it is ready for production. What they end up with is a faster way to move uncertainty around.
If you are not already running a full pipeline, I would start with two things: trunk-based development and automation. Both involve some tooling, but mostly they ask the team to change its mindset.
Trunk-based development changes the meaning of "done"
Trunk-based development sounds simple. Keep branches short-lived, merge frequently, and keep the main branch healthy. In practice it changes how a team thinks about unfinished work.
In a trunk-based workflow, code is developed in a production-ready manner. Features can merge well before users see them, but the code on the main branch has to be releasable at all times, and that part is non-negotiable.
There are only two acceptable states for a change. Either the feature is done and ready to release, or it is unfinished and safely wrapped behind a feature flag so users cannot reach it yet. There is no third state where the code gets merged because it is "mostly done" and everyone promises to clean it up before release. That third state is how teams turn the main branch into a staging area for unfinished thought, and once it takes hold, CI stops meaning anything useful.
The main branch should not be a junk drawer.
This is hard for teams used to long-running branches. Those branches feel safe because they keep unfinished work away from main, but they delay integration until the worst possible time, when everyone is under pressure and the differences between branches have had time to grow teeth. Avoiding integration only saves the risk up for later. Trunk-based development drags that risk into the open earlier, which is uncomfortable at first and is also exactly what you want, because integration turns into a normal daily activity instead of a dramatic event at the end of a release.
Feature flags separate deployment from release
If a team is committed to trunk-based development, feature flags become part of the basic toolkit, because they are how you separate code deployment from feature release.
Without flags, teams tend to hold off merging until the entire feature is ready, which means bigger pull requests, slower reviews, and more painful integration. With flags, a team can merge small slices of production-ready code without exposing unfinished behavior to users.
A flag hides incomplete product behavior while the code underneath stays safe to deploy, so it gives nobody permission to merge broken code. Code behind a flag should still compile, still pass tests, and still respect the shape of the system, and it should carry no extra operational risk just because the user cannot see it yet.
Automation is the second step
The second major step is automation. I have released software with manual testing, with automated testing, and in some cases with almost no testing at all. People sometimes call that last one "customer testing," usually with a nervous laugh, because everyone knows it means the customer finds the problem first.
Most teams know they need more automation. Where they get stuck is believing they need perfect automation before they can make progress. Demanding 100% coverage first sounds responsible, and in practice it paralyzes teams, because it turns automation into a massive cleanup project that nobody has time to finish. The work gets so large and abstract that teams either avoid it or build a brittle suite nobody trusts.
What you need is useful coverage in the places where it reduces risk, and the better starting rule is simple: all new work includes automation. Stop making the backlog worse while you wait for some future project to clean it up. Every new feature and bug fix should leave the system slightly better tested than it was before.
Unit tests should do most of the work
Most of the automated testing should be unit testing. Unit tests do not solve every problem, but they should make up the body of the suite because they are fast, focused, and cheap to run.
A good unit test checks local behavior. It should not need a database, a network call, or a running application stack, and its dependencies should usually be mocked. The test asks a narrow question: given this input, and assuming this dependency returns this result, does this unit behave correctly? Proving the whole system works is a job for other layers, and each unit test only has to show that one piece behaves correctly under specific conditions.
When unit tests become slow, fragile, or too dependent on the outside world, teams stop trusting them, and after that they are ceremony. They still run in the pipeline, but when one goes red, people rerun the build instead of reading the failure.
Use integration and contract tests where they pay for themselves
Some behavior cannot be proven with unit tests alone, and that is where integration tests and contract tests earn their place. Integration tests help when the risk sits between components. The application code and the database schema need to agree, a service call needs to serialize data in the expected shape, or two internal modules work fine alone and fail once they are wired together.
Contract tests help when systems depend on each other across boundaries. They answer a practical question: can this consumer and this provider still work together without everyone deploying everything at the same time? That is the kind of automation that supports CI.
Resist the urge to add every kind of test a testing pyramid diagram mentions. Put the right tests at the right risk points, and be suspicious of any test whose failure nobody would act on.
End-to-end tests should protect the main roads
End-to-end tests are where teams often get themselves in trouble. They are valuable and expensive, slower and more fragile than the rest of the suite, and they tend to fail for reasons that have nothing to do with the change being tested. Try to cover every setting and every edge case through end-to-end automation and you can end up with a test suite that has become its own production system, which is a bad trade.
For end-to-end tests I prefer simple coverage of the primary user flows. Cover the 80% case, the paths most users depend on, and the flows where a failure would create obvious customer pain. Push detailed logic down into unit tests, use integration tests where components meet, and let the end-to-end tests confirm that the main roads are open.
I would take a small end-to-end suite that people trust over a huge one everyone works around, because the job of that layer is to catch the failures that would hurt the most users the fastest.
Manual testing does not disappear immediately
None of this means manual testing goes away on day one. A lot of teams support systems that were never built with strong automated coverage, and pretending manual testing can vanish immediately is wishful thinking dressed up as leadership. The transition has to be honest.
Manual testing may be needed for a while around legacy areas, high-risk releases, and places where automation has not caught up yet. What has to change is its role as the permanent safety net for everything, with the team steadily moving risk out of people's heads and into repeatable checks.
Over time, manual testing should get more targeted. It should spend its effort on judgment, exploration, and the areas nobody fully understands yet, and less on catching a customer-impacting defect that could have been caught the same way every time. If the same manual test is being repeated every release, that is usually a candidate for automation.
CI is the discipline that makes CD safe
Once trunk-based development and automation are in place, CD becomes a much more achievable goal. The deploy pipeline is no longer being asked to perform magic, since all it has to do is take a known-good state of the main branch and move it through a controlled path to production. That is a much easier job than automating deployment while the team is still guessing whether the code is safe.
CI gives you the signal, and CD acts on the signal. If the signal is weak, CD just helps you ship uncertainty faster.
Continuous delivery depends on continuous integration that holds up every day. In practice that means a main branch that is always releasable and tests that are fast enough and useful enough to trust. It also means the team knows how unfinished work stays hidden from users without poisoning the release path, and that the pipeline encodes the team's agreement about quality instead of a collection of steps somebody copied from another project.
Start smaller than you want to
The practical path is simple to describe and hard to stick with. Move toward trunk-based development, with short branches and production-ready code at merge time, and put incomplete features behind flags. Add automation to all new work, starting with unit tests, bringing in integration and contract tests where the risk justifies them, and keeping end-to-end tests on the primary user flows.
Do not wait for the perfect test suite, the perfect repository structure, or a massive transformation program to sign off on common sense. The first milestone is a main branch the team trusts.
A full CI/CD pipeline is a working agreement about how software moves from idea to production without creating avoidable risk for customers, and the deployment toolchain is only the visible part of that agreement. You get there by making integration boring first, so that delivery has something solid to stand on, which also happens to be the order the acronym was written in.
Frequently asked questions
Why should CI come before CD?
- Because continuous delivery depends on continuous integration that works. CD takes a known-good state of the main branch and moves it through a controlled path to production. If the team is still guessing whether the code is safe, automating deployment ships that uncertainty faster. CI produces the signal, CD acts on the signal, and a weak signal makes a fast pipeline dangerous.
Do I need 100% test coverage before automation is worth it?
- No, and waiting for it usually backfires. Demanding perfect coverage first turns automation into a massive cleanup project nobody finishes, and teams either avoid it or build a brittle suite they do not trust. What you need is useful coverage where it reduces risk. The practical rule is that all new work should include automation, so every change leaves the system slightly better tested than before.
Are feature flags required for trunk-based development?
- In practice, yes. Feature flags are how you separate code deployment from feature release, which is what lets you merge small slices of production-ready code without exposing unfinished behavior to users. Without them, teams delay merging until a whole feature is done, which means bigger branches, slower reviews, and more painful integration. Flagged code still has to compile, pass tests, and avoid operational risk, so a flag gives nobody permission to merge broken code.
What kinds of tests should make up most of the suite?
- Most of it should be unit tests, because they are fast, focused, and cheap to run. They check local behavior with dependencies mocked, asking whether a unit behaves correctly under specific conditions rather than trying to prove the whole system works. Use integration and contract tests at the risk points between components and across service boundaries, and keep end-to-end tests for the primary user flows. Put the right tests at the right risk points instead of chasing a pyramid diagram.
What belongs in end-to-end tests?
- The main roads. Cover the primary user flows, the 80% case, and the paths where a failure would create obvious customer pain. End-to-end tests are slow and fragile and often fail for reasons unrelated to the change, so trying to prove every edge case through the browser builds a suite that becomes its own production system. Push detailed logic down into unit tests and use integration tests where components meet. A small suite people trust beats a huge one everyone works around.
When can manual testing go away?
- Not on day one, and pretending otherwise is wishful thinking dressed up as leadership. Many teams support systems that were never built with strong automated coverage, so manual testing stays useful for a while around legacy areas, high-risk releases, and complicated workflows. The goal is to move risk out of people's heads and into repeatable checks over time, so manual testing becomes more targeted and focuses on judgment and exploration. If the same manual test runs every release, automate it.
