Delivery is a system, not a YAML file
Continuous integration means regularly integrating small changes and checking that the combined code works. Continuous delivery means keeping software in a releasable state through an automated pipeline. Continuous deployment goes further by releasing qualifying changes to production automatically.
A team can practice continuous delivery while requiring a deliberate production release. The point is to shorten feedback and reduce uncertainty, not to make every repository change reach users as quickly as possible.
A useful pipeline answers three questions: what exactly are we shipping, why do we believe it works, and how will we recover if it does not?
Begin with a reliable integration loop
Developers submit small changes with a clear purpose. Automated checks run early enough that feedback is useful, and reviewers examine behavior, maintainability, and risks. Keeping changes small makes failures easier to isolate and rollback easier to reason about.
Change → Review → Static checks → Tests → Build artifact
→ Deploy to staging → Verify → Release → Observe
Not every project needs every possible test. Choose checks around plausible failures. Type checks and linting find certain mistakes quickly; unit tests examine isolated behavior; integration tests check real boundaries such as database queries or authentication; a few end-to-end tests protect important user workflows.
A thousand brittle tests can be less useful than a smaller trustworthy suite. Track flaky failures and fix their cause rather than repeatedly rerunning until a green result appears.
Build once, promote the same artifact
Create one versioned artifact after checks pass: a container image, package, or compiled bundle. Promote that exact artifact between environments. Rebuilding for production can introduce different dependencies, compiler behavior, or build-time configuration, making the staging result less meaningful.
Record the source commit, artifact digest, dependency information, and build provenance where practical. Pin dependency versions through lockfiles and use reproducible installation commands. A tag is convenient, but a digest identifies immutable container image content more precisely.
Keep environment-specific runtime configuration separate from the artifact when the framework permits it. Some frontend frameworks embed configuration during compilation, so understand that boundary instead of assuming every value can change at runtime.
Treat the pipeline as privileged infrastructure
A pipeline may be able to publish packages, deploy services, or modify production databases. Protect its permissions accordingly. Prefer short-lived federated credentials over long-lived cloud keys, separate permissions by stage, and limit which branches or workflows can deploy.
Untrusted pull requests should not receive production secrets. Be careful with workflows that execute contributor-controlled code in a privileged context. Review third-party actions and dependencies, pin them appropriately, and protect release settings.
Secret scanning and dependency scanning provide useful signals, but a scan is not proof of safety. Define a process for evaluating findings and updating dependencies without turning every low-risk issue into an indefinite release blocker.
Deploy with a controlled blast radius
A rolling deployment replaces instances gradually. A blue-green deployment prepares another environment and switches traffic. A canary exposes a limited portion of traffic to the new version before broader rollout.
Each method needs reliable health signals. “The process started” is not enough. Check request errors, latency, resource pressure, and meaningful business operations. Compare the new release with an appropriate baseline and account for low traffic, where short observation periods can be misleading.
Feature flags can separate deployment from feature exposure. They also need ownership and cleanup: abandoned flags create combinations that become hard to test. A flag is not a substitute for access control.
Database changes deserve special care
Application rollback is easy to imagine when the only change is a binary. Database schema and data changes may not be reversible. Avoid deploying a destructive migration that immediately breaks the previous application version.
An expand-and-contract sequence is often safer:
- Add a backward-compatible field or table.
- Deploy code that can use it while tolerating the old shape.
- Backfill existing data with observable, resumable work.
- Switch reads or writes after verification.
- Remove old structures only when no supported version needs them.
Large migrations can lock tables or overwhelm replicas. Test against realistic data volumes, understand the database's behavior, and choose batch sizes and online migration techniques deliberately.
Recovery must be part of the release
Define what triggers rollback, who can perform it, and whether previous versions remain compatible with current data. A rollback restores application code; it may not undo sent emails, external transactions, or corrupted records.
Practice restoring backups and handling failed migrations. Keep deployment logs and link a release to its artifact and source change. Observability should let the team answer “what changed?” during an incident without guessing.
Measure delivery outcomes: change lead time, deployment frequency, failed-change rate, and recovery time can help reveal bottlenecks. Treat these as learning tools, not targets that reward splitting changes artificially or hiding incidents.
A strong pipeline makes routine releases predictable and exposes unusual risks early. Its value comes from trustworthy feedback, reproducible delivery, and tested recovery—not the number of stages in its configuration.