Back to glossary

Continuous Deployment (CD)

Continuous deployment (CD) ships every change that passes automated tests to production with no human approval, including new dependency versions.

Also known as Continuous Release

Continuous deployment (CD), sometimes called continuous release, is the practice of automatically releasing every change that passes a pipeline's automated tests to production, with no human approval step. It extends continuous integration and continuous delivery. Because a dependency update is also a change, continuous deployment can carry a newly published package version from the registry to production unattended.

That last property is the one most teams never examine. During the axios compromise on March 31, 2026, malicious axios 1.14.1 was on npm for about 173 minutes. In that window we counted 196 public pull requests that adopted it, 193 of them opened by bots. Fifty-eight merged before npm removed the release, and 44 of those merged within five minutes of opening. In a repository that deploys every merge to main, a merge is a production release.

We think continuous deployment is the right way to ship software, and we do not think the answer is to put a person back in front of every deploy. The approval click that continuous deployment removed was never inspecting what a new dependency version contains. The gate belongs where third-party code enters the pipeline, before merge and at install, and it has to look at the code, not at test results or health metrics.

What is continuous deployment?

#

Continuous deployment is a release process in which every commit that passes the automated pipeline goes to production automatically. Nobody schedules a release, and nobody approves one. The pipeline is the release manager.

The term took hold in February 2009, when Timothy Fitz described how IMVU shipped. "On average we deploy new code fifty times a day," he wrote in Continuous Deployment at IMVU. IMVU's suite of around 15,000 test cases ran in nine minutes across 30 to 40 machines, and a push took six more. New code went live on a small subset of servers first, then to the whole cluster, with production metrics watched at each step. If the metrics moved the wrong way, the push script reverted on its own: "if there has been a statistically significant regression then the revision is automatically rolled back."

Every continuous deployment system since has the same three parts: a continuous integration (CI) stage that builds and tests each change, an automated path to production, and an automated way back. What makes the model safe is not the absence of a human. It is that each change is small, so a bad one is easy to find and cheap to revert.

Many teams that deploy continuously also separate deploying code from releasing a feature. A feature flag lets unfinished work ship dark, so the pipeline can push to production many times a day without exposing anything a user has not been given.

What is the difference between continuous delivery and continuous deployment?

#

Continuous delivery keeps every build ready to release and lets a person decide when to release it. Continuous deployment removes that decision, so every build that passes the pipeline is released. Jez Humble, co-author of Continuous Delivery, defined continuous deployment in 2010 as "the practice of releasing every good build to users." His summary of the relationship: "While continuous deployment implies continuous delivery the converse is not true," as he put it in Continuous Delivery vs Continuous Deployment.

The difference is one gate, and on most platforms it is one line of configuration. In GitHub Actions, a deploy job that names a protected environment waits for approval. GitHub allows up to six required reviewers per environment and a wait timer of up to 43,200 minutes, or 30 days, according to its deployments and environments reference. Environment secrets stay locked until a reviewer approves. Remove the reviewers and the same pipeline is continuous deployment. On GitHub's Free, Pro, and Team plans, required reviewers only work in public repositories, so private repositories on those plans do not get the built-in approval gate at all.

AspectContinuous integrationContinuous deliveryContinuous deployment
What triggers a production releaseNothing; CI stops at a tested buildA person, on demandEvery passing commit
Human gate before productionNot applicableYes, usually an approvalNone
What the pipeline must proveThe change builds and its tests passAny build could be released safelyEvery build will be released, so it must be safe
Typical GitHub Actions shapeWorkflow on push and pull_requestDeploy job with environment: and required reviewersDeploy job on push to the default branch, no reviewers
Main riskIntegration bugsA queue of unreleased changesAnything the tests cannot see ships automatically

Our verdict: choose continuous delivery when a release needs a business decision, such as coordinated launches, regulated change windows, or mobile app store submissions. Choose continuous deployment for services where you own the runtime and can roll back in minutes. Do not choose continuous delivery as a supply chain control. A reviewer approving a deploy sees a commit list, not the contents of the packages a one-line lockfile change pulled in.

How does a continuous deployment pipeline work?

#

A continuous deployment pipeline is a sequence of automated stages that every commit passes through on its way to production, and each stage can stop it. The deployment pipeline is "the key pattern introduced in continuous delivery," according to continuousdelivery.com, and continuous deployment runs the same pipeline with the last gate removed.

The stages, in order:

  1. Commit stage. A push or merge to the default branch starts the pipeline. The build compiles the code, installs dependencies from the lockfile, and runs fast tests. This stage is CI.
  2. Build the artifact once. The pipeline packages the result, usually a container image, and records its content digest. "Only build packages once" is a core practice, and the reason is stated plainly on the same page: "We want to be sure the thing we're deploying is the same thing we've tested throughout the deployment pipeline."
  3. Acceptance tests. Slower integration and end-to-end tests run against that exact artifact.
  4. Progressive rollout. The artifact goes to a small slice of production first. This is a canary release.
  5. Automated verification. The pipeline compares the canary's health metrics against a threshold.
  6. Promote or roll back. If the metrics hold, the artifact goes to all of production. If they do not, the previous artifact is restored.

A minimal deploy-on-merge workflow in GitHub Actions looks like this. Tests run in a job with no write access, the build job pushes an image and records its digest, and the deploy job deploys that digest and nothing else:

YAML
name: deploy
on:
  push:
    branches: [main]
permissions: {}
jobs:
  test:
    runs-on: ubuntu-latest
    permissions: { contents: read }
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - run: npm ci && npm test
  build:
    needs: test
    runs-on: ubuntu-latest
    permissions: { contents: read, packages: write }
    outputs:
      digest: ${{ steps.push.outputs.digest }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - id: push
        env: { TOKEN: "${{ github.token }}" }
        run: |
          echo "$TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
          docker build -t "ghcr.io/acme/web:$GITHUB_SHA" .
          docker push "ghcr.io/acme/web:$GITHUB_SHA"
          ref=$(docker inspect --format '{{index .RepoDigests 0}}' "ghcr.io/acme/web:$GITHUB_SHA")
          echo "digest=${ref#*@}" >> "$GITHUB_OUTPUT"
  deploy:
    needs: build
    runs-on: ubuntu-latest
    permissions: { contents: read, id-token: write }
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - run: ./deploy.sh "ghcr.io/acme/web@${{ needs.build.outputs.digest }}"

There is no environment: key with reviewers, so every push to main ends in production. The deploy job holds production credentials, and the build job still runs dependency code next to a registry write token. CI/CD security covers how to split those jobs further; this entry follows what happens to a dependency on its way through.

A canary release sends a small share of traffic to the new version and watches it before going further. Argo Rollouts, a CNCF project, documents a typical configuration in its analysis guide: shift 20% of traffic, pause 10 minutes, then 40%, 60%, and 80% with the same pause, while a background analysis queries Prometheus every 5 minutes. The check that decides promotion is the share of requests that did not return a 5xx error:

YAML
# AnalysisTemplate (simplified from the Argo Rollouts docs)
metrics:
  - name: success-rate
    interval: 5m
    successCondition: result[0] >= 0.95
    failureLimit: 3
    provider:
      prometheus:
        query: |
          sum(irate(istio_requests_total{destination_service=~"{{args.service-name}}",response_code!~"5.*"}[5m]))
          /
          sum(irate(istio_requests_total{destination_service=~"{{args.service-name}}"}[5m]))

That query is the whole safety net in many continuous deployment setups. It answers one question: is the new version failing requests?

A dependency update moving through a continuous deployment pipeline in seven stages, showing which stages run the new dependency's code and what each automated check measures. Pipeline stage New code runs? 1Bot opens a pull requestlockfile now pins the new version2CI runs npm ci and the testscheck: tests pass3Auto-merge to maincheck: required statuses green4Build once, record digestimage@sha256:…5Canary takes 20% of traffic10 minutes per step6Automated analysischeck: ≥ 95% of requests not 5xx7Promote to 100% or roll backrollback restores old image continuous delivery adds a human approval here continuous deployment: no human step CI runner installs, imports Build if the build installs it Production runs at import in every new pod, with production secrets Automated checks: tests pass, statuses green, request success rate. None of them reads what the new dependency does.
Figure 1. Where a dependency update's code executes in a continuous deployment pipeline, and what the pipeline checks.

Figure 1 in text: a dependency update moves through seven stages. (1) An update bot opens a pull request that changes the lockfile. (2) CI runs npm ci and the test suite on that branch. (3) An auto-merge rule merges it to main when checks pass. (4) The pipeline builds the image once and records its digest. (5) The new image takes 20% of production traffic. (6) Analysis checks that at least 95% of requests succeed. (7) The pipeline promotes to 100% or rolls back. Continuous delivery would add a human approval between stages 4 and 5; continuous deployment has none. The new dependency's code first runs at stage 2, on a CI runner, through install scripts where they are enabled and through imports during tests. It runs again at stage 4 if the image build installs or imports it, and at stages 5 through 7 in production processes that hold production credentials. Every automated check in the pipeline measures test results or request health. None measures what the dependency's code does.

How does a malicious dependency reach production through continuous deployment?

#

A malicious dependency reaches production through continuous deployment by riding an automated dependency update through a pipeline that has no step where anyone reads the new code. Each step behaves exactly as designed.

Start with how versions move. A manifest declares a range such as "axios": "^1.14.0", and the lockfile pins the exact version that range resolved to. npm ci installs only what the lockfile records, so a deterministic build does not pick up a new release by itself. Something has to change the lockfile. In most continuous deployment repositories, that something is an automated dependency update bot. It notices the new release, regenerates the lockfile, and opens a pull request. CI runs. If the tests pass, an auto-merge rule merges. The merge is a push to main, and the push starts the deploy workflow. The hidden blast radius of the axios compromise named this path directly: lockfile protection "breaks down when new dependency resolution is introduced," including when teams are "automatically merging dependency update PRs."

The axios compromise is the clearest measurement we have of how fast that path runs. The npm registry still records the publish times of the malicious versions, even though the versions themselves are gone:

Bash
$ npm view axios time --json | jq '{"1.14.0": .["1.14.0"], "1.14.1": .["1.14.1"], "0.30.4": .["0.30.4"]}'
{
  "1.14.0": "2026-03-27T19:01:40.915Z",
  "1.14.1": "2026-03-31T00:21:58.168Z",
  "0.30.4": "2026-03-31T01:00:57.285Z"
}
$ npm view plain-crypto-js time --json | jq 'del(.created, .modified)'
{
  "4.2.0": "2026-03-30T05:57:32.867Z",
  "4.2.1": "2026-03-30T23:59:12.278Z",
  "0.0.1-security": "2026-03-31T03:25:11.636Z",
  "0.0.1-security.0": "2026-03-31T04:26:33.055Z"
}

The axios postmortem dates the removal: "March 31, 03:15 UTC: malicious versions removed from npm." That gives a window of 2 hours and 53 minutes for axios 1.14.1. On November 7, 2026, we searched GitHub for pull requests that adopted it, using the titles the two common update bots generate:

Bash
gh api -X GET search/issues \
  -f q='"update dependency axios to v1.14.1" is:pr created:2026-03-31T00:00:00Z..2026-03-31T03:59:59Z'
gh api -X GET search/issues \
  -f q='"bump axios from" "to 1.14.1" is:pr created:2026-03-31T00:00:00Z..2026-03-31T03:59:59Z'

We kept pull requests opened between 00:21:58 and 03:15 UTC and compared each merge time with the publish time:

MeasureResult
Pull requests adopting axios 1.14.1, opened while it was on npm196
Opened by bot accounts193 (Renovate 116, Dependabot 69, other bot accounts 8)
First pull request opened00:25:27 UTC, 3 min 29 s after publish
Merged before npm removed the release58
Merged within 60 seconds of opening28
Merged within 5 minutes of opening44
First merge00:25:53 UTC, 3 min 55 s after publish
Median time from publish to merge60 minutes (01:22 UTC)
Merged in the first hour after publish29

These are lower bounds. GitHub search covers public repositories only, and grouped updates, custom titles, and private repositories do not appear. We did not verify which of these repositories deploy on merge. The point is the timing. A pipeline that merged dependency updates unattended and deployed every merge would have run the payload on its CI and build machines, and shipped plain-crypto-js to production, roughly two hours before npm pulled it.

Timeline of the axios compromise on March 31, 2026: axios 1.14.1 was on npm from 00:21:58 to 03:15 UTC, and 58 public pull requests adopting it merged during that window. axios 1.14.1 on npm · 2 h 53 min each red line is one merged PR (58) 00:0001:0002:0003:00 1234567 Times in UTC, March 31, 2026 123:59:12 (Mar 30)plain-crypto-js 4.2.1 published200:05:41Socket flags the payload as malware300:21:58axios 1.14.1 published400:25:53First adoption PR merged501:22Half of the 58 merges done603:15npm removes axios 1.14.1703:25:11Security placeholder replaces payload
Figure 2. The axios 1.14.1 window, with every public adoption PR that merged before npm removed it.

Figure 2 in text, all times UTC on March 31, 2026, except the first. At 23:59:12 on March 30, plain-crypto-js 4.2.1, the package carrying the payload, was published. At 00:05:41, Socket's scanner flagged it as malware. At 00:21:58, axios 1.14.1 was published with plain-crypto-js as a new dependency. At 00:25:53, the first adoption pull request merged. By 01:22, half of the 58 merges had happened. At 03:15, npm removed the malicious axios versions. At 03:25:11, npm replaced plain-crypto-js with a security placeholder. Of the 58 merges, 12 happened in the first 30 minutes, 17 in the next 30, 6 between 60 and 90 minutes, 13 between 90 and 120, 4 between 120 and 150, and 6 in the last 23 minutes before removal.

One line in that timeline matters more than the rest. Socket's scanner flagged plain-crypto-js@4.2.1 at 00:05:41, six and a half minutes after it was published and 16 minutes before axios 1.14.1 existed, according to Socket's axios compromise report. Detection was not the bottleneck. Every one of the 58 merges happened after the payload had been flagged. None of those pipelines asked.

Canary analysis measures health, not intent

#

A canary release catches code that breaks things. Malware is written not to break things. The axios payload ran from a postinstall hook, contacted its command-and-control server, fetched a platform-specific second stage, and then deleted its own files and restored a clean package.json, according to the same report. Nothing in that sequence fails an HTTP request. A credential stealer that reads process.env at import and posts it out returns 200 on every request it serves.

Where the code runs decides which stage could have seen it:

  • Install time, on the CI runner. Install scripts run during npm ci unless they are disabled. npm v12, released in July 2026, skips dependency install scripts by default, and pnpm 10 did the same in January 2025. With install scripts enabled, as on npm 11 and earlier, the axios payload ran on the runner at stage 2, before any canary existed, with whatever credentials that job held.
  • Import time, in tests and in production. A payload that runs when the module loads executes during the test suite and again in every canary pod at startup. Turning off install scripts does nothing about it.
  • Runtime. A payload that waits for a condition, such as a production hostname or a specific environment variable, can pass every test and every canary step and act later.

The canary limits blast radius for bugs because a buggy pod hurts only the 20% of traffic it serves. That math does not apply to a payload that exfiltrates secrets. One pod reading one set of production credentials is a full compromise. In the Argo example above, analysis starts at the 40% step, so 20% of production runs the new code for 10 minutes before anything is checked. A rollback replaces the code. It does not revoke the credentials the code already sent out. Fitz's automatic rollback was built for "a statistically significant regression." A successful supply chain attack is designed to cause none.

Build once, deploy the digest

#

Dependency resolution depends on time, so a pipeline that resolves dependencies more than once can deploy a dependency tree that its tests never saw. Here is the same package.json resolved by npm 12.2.2 as of two different dates, with no lockfile:

Bash
$ cat package.json
{ "name": "web", "version": "1.0.0", "private": true, "dependencies": { "axios": "^1.14.0" } }
$ npm install --before=2026-03-30T23:00:00Z && node -p "require('axios/package.json').version"
1.14.0
$ npm install --before=2026-10-28T00:00:00Z && node -p "require('axios/package.json').version"
1.21.0

Nothing in the repository changed between those two runs. Only the registry changed. Three common patterns re-resolve against the registry at deploy time: a Dockerfile that runs npm install instead of npm ci, a pipeline that rebuilds the image separately for staging and production, and a deploy step that runs a tool through npx. A release published between the test run and the deploy run goes to production untested.

The fix is the practice continuous delivery has always prescribed. Commit the lockfile, install with npm ci, build the artifact once, and deploy it by content digest, never by a mutable tag such as :latest. The digest also gives you the evidence you will want later. After the axios removal, npm view axios@1.14.1 returns E404 No match found for version 1.14.1, so you cannot rebuild what you shipped that night to check it. An image digest in your registry and the lockfile in its commit are the only record that survives.

Package provenance and build attestations extend that record. They prove which pipeline and which commit produced an artifact. They do not prove the artifact is benign. A malicious dependency that passed through the pipeline carries a valid attestation, because the pipeline did build it.

Where to put the gate in a continuous deployment pipeline

#

The gate belongs at the two places where third-party code enters: when a dependency change is merged, and when a package is installed. Controls further downstream measure the wrong thing. Here is what each common control does against a malicious dependency release:

ControlWhere it actsWhat it stopsWhat it misses
Human deploy approval (continuous delivery)Between build and productionReleases nobody intended to shipDependency contents; reviewers see commits, not packages
Release-age cooldown in the package managerVersion resolutionMalicious releases removed before they age past the windowMalware that stays hidden longer than the window; it is a clock, not a detector
Content check as a required merge statusPull request mergeKnown malware and risky new behavior before auto-mergeCI on the same branch already installed the package
Install-time package firewallEvery install, including CIPackages already identified as malicious, before downloadPackages nobody has analyzed yet
Lockfile, npm ci, build once, deploy by digestBuild and deployUntested trees reaching productionA malicious version already in the lockfile
Canary analysis with automatic rollbackProductionBugs, crashes, error spikesCode that behaves normally while stealing data
No secrets in jobs that run new dependenciesCI job designCredential theft at install and test timeImport-time code in production, which has production credentials by definition

A dependency cooldown is the cheapest row on that table. pnpm 11 waits 1,440 minutes, or 24 hours, before resolving a new version by default. npm added an opt-in min-release-age setting, measured in days, in npm 11.10, and as of npm 12.2.2 it still defaults to off. A window of even one day, enforced where those lockfiles were written, would have kept axios 1.14.1 out of all 58 merges above, because npm removed it in under three hours. Set it in the package manager, not only in the update bot, so it also binds a developer's npm install and any CI job that resolves without a lockfile. npm ci installs the lockfile as written, so the window has to act when the lockfile changes.

A cooldown only buys time. Something has to look at the release while it waits. In Socket for GitHub, an alert whose action is set to Block fails the "Socket Security: Pull Request Alerts" check, according to Socket's alert actions documentation. Marking that check as required in branch protection means an auto-merge rule cannot merge a dependency pull request until the check passes. Bot pull requests get the same check as human ones. For the install step itself, prefixing it as sfw npm ci routes package downloads through Socket Firewall, a package firewall that blocks confirmed malware before it reaches the runner. The free tier blocks only confirmed malware and warns on packages that only AI analysis has flagged, per its documentation. Neither control catches a package nobody has analyzed yet, which is exactly the gap a cooldown exists to cover.

Our verdict: keep continuous deployment, and keep auto-merging dependency updates if your test suite earns it. Make three changes. Put a release-age window in the package manager. Make a content check a required status that auto-merge must wait for. Build once and deploy by digest. Do not rely on deploy approvals or canary metrics to catch a supply chain attack. Neither one looks at the code.

The pipeline proves sameness, not safety

#

A mature continuous deployment pipeline answers two questions very well. Is the artifact in production the one we tested? Is it healthy? Seventeen years of tooling, from IMVU's push script to digest-pinned rollouts with automated analysis, have made both answers fast and reliable. Neither answer says anything about what a new dependency does when it runs. In the axios window, 58 public repositories merged a release whose payload had already been flagged as malware, and whatever checks they ran let it through. Continuous deployment did not create that gap. It removed the approval step that had been hiding it.