CI/CD Pipelines Explained with GitHub Actions
You just pushed a commit and now you're manually running tests, building the app, and uploading files to a server. It works, but it's slow, error-prone, and doesn't scale. That's where CI/CD pipelines come in.
In this guide, we'll break down CI/CD concepts and show you how to build a real pipeline with GitHub Actions. By the end, you'll understand how to automate testing, building, and deployment for any project.
What is CI/CD?
CI (Continuous Integration) is the practice of automatically testing and merging code changes into a shared branch. Every push triggers a build and test run, catching integration issues early.
CD (Continuous Delivery/Deployment) extends CI by automatically preparing (and optionally deploying) your application to production. Continuous Delivery means the code is always ready to deploy; Continuous Deployment means it deploys automatically.
Together, CI/CD forms a pipeline: a series of automated steps that take your code from commit to production.
Why use GitHub Actions for CI/CD?
GitHub Actions is a CI/CD platform built into GitHub. It's free for public repositories and offers generous minutes for private ones. Key benefits:
- Integrated: No external service needed; workflows live in your repo.
- Event-driven: Trigger workflows on push, pull request, schedule, or manual dispatch.
- Extensible: Thousands of pre-built actions in the GitHub Marketplace.
- Matrix builds: Test across multiple OSes and language versions easily.
Core concepts of GitHub Actions
Before writing your first workflow, understand these terms:
| Term | Description |
|---|---|
| Workflow | An automated process defined in a YAML file under .github/workflows/. |
| Event | A trigger that starts a workflow (e.g., push, pull_request). |
| Job | A set of steps that run on the same runner. Jobs run in parallel by default. |
| Step | A single task within a job. Can run a command or use an action. |
| Action | A reusable unit of code (e.g., actions/checkout) that you can include in a step. |
| Runner | A virtual machine that executes jobs (Ubuntu, Windows, macOS). |
Building your first CI pipeline
Let's create a basic CI workflow for a Node.js project. It will run tests on every push and pull request to the main branch.
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
This workflow:
- Triggers on pushes and PRs to
main. - Runs a job called
teston the latest Ubuntu runner. - Uses a matrix to test against Node.js 18 and 20.
- Checks out the code, sets up Node, installs dependencies, and runs tests.
Push this file and watch the Actions tab. You'll see two parallel jobs (one per Node version). If any test fails, the workflow fails and GitHub notifies you.
Adding continuous deployment
CI is great, but CD is where automation shines. Let's extend the workflow to deploy to a server after tests pass on the main branch.
We'll add a deploy job that depends on test and runs only on pushes to main.
deploy:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm ci --production
pm2 restart myapp
Key points:
needs: testensures deployment only runs if tests pass.- The
ifcondition restricts deployment to pushes onmain. - Secrets (
SSH_HOST,SSH_USER,SSH_KEY) are stored in GitHub repository settings and injected securely.
This pattern works for many deployment targets: SSH, Docker registries, cloud platforms (AWS, Vercel, Netlify), or Kubernetes.
Best practices for CI/CD with GitHub Actions
- Keep workflows fast: Cache dependencies (e.g.,
actions/cache) to reduce build times. - Fail fast: Run quick checks (linting) before slower tests.
- Use environments: Configure protection rules for production deployments.
- Secure secrets: Never hardcode credentials; use GitHub Secrets.
- Monitor and alert: Set up notifications for failed workflows.
Debugging failed workflows
When a workflow fails, GitHub shows logs for each step. Common issues include:
- Missing dependencies: ensure
npm cior equivalent runs before tests. - Incorrect secrets: double-check names and values.
- Permission errors: verify runner permissions and SSH keys.
You can also enable debug logging by setting repository secrets ACTIONS_STEP_DEBUG to true.
FAQ
What's the difference between CI and CD?
CI automates testing and integration of code changes. CD automates the delivery or deployment of those changes to an environment. CI ensures code quality; CD ensures it reaches users quickly and reliably.
Is GitHub Actions free?
GitHub Actions is free for public repositories. For private repositories, you get a monthly allowance of free minutes (e.g., 2,000 minutes for Free accounts) and pay for additional usage. Self-hosted runners are also an option.
Can I use GitHub Actions for non-GitHub projects?
Yes, GitHub Actions can run any command-line tools. You can use it to build and deploy projects hosted elsewhere, as long as the code is on GitHub or you use actions to fetch it.
Ready to streamline your CI/CD? Start by automating your tests with GitHub Actions. For a quick way to analyze deployment logs, try our Nginx Log Analyzer to spot errors and performance issues after each deploy.