Skip to main content

Overview

CI/CD is the practice of automating the steps between a code change and its delivery to users. Continuous integration (CI) builds and tests every change automatically; continuous delivery (CD) automates the path from a merged change to a deployed one. This page explains what a CI/CD pipeline provides, how to keep CI fast, how to automate releases, how to notify pipeline results, and how to treat metrics such as deployment frequency. Examples use GitHub Actions, but the principles apply to any CI service.

What a CI/CD pipeline provides

With a pipeline in place, the routine steps around every change run without manual intervention:

Automated test gates

Tests run on every pull request, and merging is blocked until they pass. A broken change is caught in minutes, not after release.

Automated deploys

Each environment deploys on a defined trigger — for example, staging when a pull request targets a release branch, production when it merges.
These two capabilities are the baseline. The following sections cover the properties that determine how much value the pipeline delivers: speed, release automation, and feedback that reaches developers.

Keep CI fast

The duration of CI is a cost paid by every change. As a guideline, keep the pipeline that gates pull requests within about 5–10 minutes.

Cache dependencies

Installing dependencies from scratch on every run is pure overhead. Official setup actions can cache them keyed on the lockfile, so the cache is rebuilt only when dependencies actually change.
The same idea extends beyond package installs: any expensive, reproducible setup — a prepared database schema, a build artifact — is a caching candidate.

Skip runs that cannot affect the result

A change that does not affect runtime behavior does not need the full test suite. paths-ignore skips the workflow when only the listed files changed:
If the skipped workflow is a required status check, the check stays pending and blocks the merge. In that case, keep the workflow running and skip only the expensive jobs inside it, or adjust the required checks.

Parallelize tests

As the test suite grows, total execution time grows with it. Parallelization splits the suite across jobs that run concurrently. The matrix strategy runs the same job once per listed value; each job uses its value to execute only its own slice of the suite:
The wall-clock time of a parallel run is the time of its slowest shard. If the runner has no shard option, or its default split leaves the shards unbalanced, split the file list yourself and distribute it by measured execution time — not by file count — so the shards finish together. Increase the shard count as the suite grows.

Scale up runners

Once caching and parallelization are in place, larger runners with more CPU cores and memory shorten builds and increase in-job parallelism. Cost per unit of run time rises with runner size, but a shorter run consumes fewer minutes, so the billed amount can stay flat or even drop while waiting time falls.
Treat CI optimization as a measurement loop. Per-job timings from the CI logs show where the time actually goes; find the largest slice, change one thing, and verify the delta instead of optimizing on a hunch.

Automate releases

A manual release — writing the changelog and running deploy steps in the right order — is where mistakes and delays creep in. The whole sequence can be automated:
1

Keep a traceable history

Release notes are assembled from the history of merged pull requests and their commits. See Commit messages for how to write messages that read well later.
2

Generate the release notes automatically

Assemble the list of pull requests merged since the last release as the release notes, so nobody hand-edits a changelog.
3

Reduce a deploy to merging a release pull request

One workflow generates a release pull request. A separate workflow, triggered by the push to main when it merges, runs the deploy. A production release becomes a review and a merge instead of a runbook.
The relationship between the branches is as follows: For example, the following workflow implements this flow for a branch model where develop collects merged work and main mirrors production — a simplified form of git flow. Triggered manually, it cuts a dated release branch from develop, assembles the list of pull requests merged since the last release into the body, and opens a release pull request into main:
The pull request list is extracted from merge commits, so this workflow assumes pull requests are merged into develop with “Create a merge commit”. With squash merges, replace the extraction with a query such as gh pr list --state merged.
A separate workflow triggers on the push to main — the merge of the release pull request — and runs the production deploy:
concurrency prevents overlapping deploys when pushes land in quick succession. The environment designation applies GitHub’s protection rules and environment-scoped secrets to production. The same approach removes recurring friction elsewhere in the workflow: assigning the author to their own pull request, applying labels based on branch name or changed paths, and extracting checked items from the pull request template as a QA hand-off at release time.

Notify pipeline results

A pipeline shortens the feedback loop only if its results reach the people who act on them. Two channels cover most cases:
  • Status checks on the pull request. The CI service reports each job’s result on the pull request itself. This is built in and covers feedback while a change is under review.
  • Chat notifications. Events that happen outside a pull request — a production deploy finishing, a scheduled workflow failing — need a push channel such as Slack.
For example, a step at the end of a deploy job can post the result to Slack whether the job succeeds or fails:
Notify successes as well as failures. If only failures produce a message, silence is ambiguous — it can mean the deploy succeeded, or that the notification path itself is broken. A success message confirms that the pipeline and the notification channel are both working.

Deployment frequency as a result, not a target

Deployment frequency is a widely used productivity metric, and a high value is the result of working practices: automated pipelines, small well-scoped changes, and a test suite that keeps the risk of each change low. Optimizing the number itself inverts the causality and degrades the outcome it is meant to measure:
  • Splitting unchanged work into more deploys adds operational overhead and raises the change failure rate without delivering more value.
  • Lowering deploy frequency to improve the failure rate batches more changes into each deploy, which makes failures more likely and their causes harder to isolate.
To raise deployment frequency sustainably, improve the inputs instead: keep CI fast, automate the release path as described above, and keep changes small and independently shippable (see Task breakdown and Testing).