The most surprising thing about CI/CD is that it’s not about speed, but about certainty. It gives you the confidence to deploy frequently because each step rigorously validates your code.
Let’s watch it in action. Imagine a simple Node.js app.
Here’s our package.json:
{
"name": "my-node-app",
"version": "1.0.0",
"scripts": {
"start": "node index.js",
"test": "jest",
"lint": "eslint ."
},
"devDependencies": {
"eslint": "^8.0.0",
"jest": "^29.0.0"
}
}
And a basic index.js:
function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet("World"));
And a simple test.js for Jest:
const greet = require('./index');
test('greets the world', () => {
expect(greet('World')).toBe('Hello, World!');
});
Now, let’s wire this up into a pipeline. We’ll use GitHub Actions for simplicity. This .github/workflows/ci.yml file defines our pipeline:
name: CI Pipeline
on: [push, pull_request]
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Run tests
run: npm test
When you push code or open a pull request, GitHub Actions kicks off this workflow.
The Checkout code step pulls your repository’s latest commit onto the runner. Then, Set up Node.js installs a specific version of Node.js (here, 18) on the runner. Install dependencies uses npm ci, which is crucial: it installs dependencies exactly as specified in package-lock.json (or npm-shrinkwrap.json), ensuring consistent environments. Lint code runs eslint ., checking for code style violations. Finally, Run tests executes npm test, which in our case runs Jest to verify our application’s logic.
If any of these steps fail (e.g., linting errors, failing tests), the entire workflow stops, and the commit/pull request is marked as failed. This immediately tells you that something is wrong before it can get any further.
This is Continuous Integration (CI). It’s the automated process of merging code changes frequently into a central repository, followed by automated builds and tests. The goal is to detect integration issues early.
Continuous Delivery (CD) builds on this. Once the CI stage passes, you might have further stages:
- Build Artifact: Package your application (e.g., a Docker image).
- Deploy to Staging: Push the artifact to a testing environment.
- Automated E2E Tests: Run end-to-end tests against the staging environment.
- Manual Approval: A gatekeeper gives the go-ahead.
- Deploy to Production: Push the artifact to production.
Here’s a snippet for deploying a Docker image (assuming you have a Dockerfile):
# ... previous steps ...
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: your-dockerhub-username/my-node-app:latest
The secrets.DOCKERHUB_USERNAME and secrets.DOCKERHUB_TOKEN are sensitive credentials you’d store securely in GitHub’s repository settings. The docker/build-push-action then builds your Docker image using the Dockerfile in your repository’s root and pushes it to Docker Hub with the specified tag.
The true power of CI/CD lies in its ability to create a feedback loop. Every commit, every pull request, is a tiny experiment. The pipeline is the scientific method applied to software development: hypothesize (write code), test (run linters, tests), analyze (pipeline results), and iterate. This systematic validation prevents regressions and ensures that the codebase remains in a deployable state at all times.
What most people miss is that the "pipeline" isn’t just a sequence of commands; it’s a declaration of your team’s quality standards. Each stage represents a non-negotiable check that must pass for code to be considered "good enough." If you find yourself skipping or disabling stages because they’re too slow or flaky, you’re not doing CI/CD; you’re just running scripts. The rigor of the pipeline is the value.
Once you have a robust CI pipeline, the next logical step is to automate deployments to different environments.