What is CI/CD?
CI/CD stands for Continuous Integration and Continuous Delivery (or Deployment). It is a software engineering practice where developers merge their code changes into a shared repository frequently, and each merge triggers an automated build-and-test pipeline. The goal is to catch bugs early, reduce integration hell, and ship reliable software faster.
Continuous Integration (CI) means that every time a developer pushes code, the system automatically builds the project and runs a suite of tests. If the build breaks or a test fails, the team knows immediately.
Continuous Delivery (CD) extends CI by automatically deploying the tested code to a staging or production environment after the CI pipeline passes, ensuring that the software is always in a deployable state.
Why GitHub Actions?
GitHub Actions is GitHub’s built-in CI/CD platform. It is deeply integrated with GitHub repositories, free for public repositories, and offers a vast ecosystem of pre-built actions from the community. Key advantages include:
- Tight GitHub integration — triggers on push, PR, issue comments, releases, and more.
- Matrix builds — test across multiple OS versions, language versions, and architectures in parallel.
- Hosted runners — Ubuntu, Windows, and macOS runners are provided free for public repos.
- Marketplace — thousands of community actions for deployments, notifications, code quality, and security scanning.
- Self-hosted runners — run workflows on your own infrastructure for private projects.
Workflow Structure
A GitHub Actions workflow is defined in a YAML file stored at .github/workflows/. Every workflow has three top-level components:
Events (Triggers)
What causes the workflow to run:
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * 1" # Every Monday at 6 AM
workflow_dispatch: # Manual trigger
Jobs
Jobs run in parallel by default on separate runners. Each job contains a series of steps:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: pytest
Steps
Steps are individual commands or actions. They run sequentially within a job and share the same filesystem.
Real-World Example: Python Project
Here is a complete workflow that lints, tests, and deploys a Python application:
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff pytest
- name: Lint with Ruff
run: ruff check .
- name: Test with pytest
run: pytest
deploy:
needs: lint-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to production
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
echo "Deploying to production server..."
# ssh, rsync, or use a deployment action
This workflow runs a matrix build against Python 3.11 and 3.12, lints with Ruff, runs tests, and only deploys if all tests pass on the main branch.
Secrets Management
Never hardcode credentials in your workflow files. GitHub provides encrypted secrets under Settings → Secrets and variables → Actions:
jobs:
deploy:
steps:
- name: Deploy
env:
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
API_TOKEN: ${{ secrets.API_TOKEN }}
run: deploy-script.sh
Secrets are masked in logs and never passed to forks.
Matrix Builds
Matrix strategies let you test across combinations of OS, language version, and environment variables with a single job definition:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.10", "3.11", "3.12"]
exclude:
- os: windows-latest
python-version: "3.10"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pytest
The exclude key removes specific combinations that are known to fail or are unnecessary, keeping the matrix manageable.
Best Practices
- Keep workflows fast — use caching for dependencies with
actions/cache. - Fail fast — set
fail-fast: truein matrix builds to cancel all jobs when one fails. - Pin action versions — use
@v4tags (not@main) to avoid unexpected breaking changes. - Use status badges — add a badge to your README so contributors see the build status at a glance.
- Separate concerns — one workflow per concern (test, lint, deploy, security scan).
- Self-hosted runners for large repos — if your team pushes frequently, self-hosted runners eliminate queue wait times.
Conclusion
GitHub Actions makes CI/CD accessible to every developer. With a single YAML file, you can lint, test, build, and deploy your application across multiple platforms. The marketplace ecosystem, matrix builds, and secret management make it production-ready from day one. Start with a simple lint-and-test workflow, then layer on deployment, security scanning, and notifications as your project grows.
