CI/CD Pipelines: Automating Build, Test, and Deploy
Continuous Integration and Continuous Delivery (CI/CD) pipelines automate the journey from code commit to production deployment. This guide covers GitHub Actions and GitLab CI with practical examples for caching, secrets, artifacts, and matrix builds.
GitHub Actions
GitHub Actions workflows live in .github/workflows/. A workflow triggers on
events and runs jobs on runners.
Basic Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest --junitxml=results.xml
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: test-results
path: results.xml
Caching Dependencies
Speed up builds by caching package managers:
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
Secrets and Variables
Store sensitive values in repository or organization secrets:
- name: Deploy to server
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
SERVER_HOST: ${{ vars.PRODUCTION_HOST }}
run: |
echo "$DEPLOY_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
rsync -avz -e "ssh -i /tmp/deploy_key" ./dist/ user@$SERVER_HOST:/var/www/
Never print secrets in logs. GitHub automatically masks known secret values.
Matrix Builds
Test across multiple versions or operating systems:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.10", "3.11", "3.12"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements.txt && pytest
Deploy Job
Gate deployments behind successful tests:
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy
run: ./scripts/deploy.sh
GitLab CI
GitLab CI configuration lives in .gitlab-ci.yml at the repository root.
# .gitlab-ci.yml
stages:
- build
- test
- deploy
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
paths:
- .cache/pip
- venv/
build:
stage: build
image: python:3.12-slim
script:
- python -m venv venv
- source venv/bin/activate
- pip install -r requirements.txt
artifacts:
paths:
- venv/
expire_in: 1 hour
test:
stage: test
image: python:3.12-slim
script:
- source venv/bin/activate
- pytest --junitxml=report.xml
artifacts:
reports:
junit: report.xml
deploy_prod:
stage: deploy
image: alpine:latest
only:
- main
script:
- apk add --no-cache openssh rsync
- rsync -avz ./dist/ deployer@$PROD_HOST:/var/www/
environment:
name: production
url: https://example.com
Key GitLab CI Features
- Stages define the execution order. Jobs in the same stage run in parallel.
- Artifacts pass files between stages. Use
expire_into auto-clean. - Cache persists across pipelines for dependency directories.
- Variables are set in the CI/CD settings UI for secrets, or inline for non-sensitive values.
Pipeline Best Practices
- Fail fast -- put linting and unit tests in the earliest stage.
- Cache aggressively -- dependency installs are often the slowest step.
- Use artifacts to pass build outputs between stages instead of rebuilding.
- Pin action/image versions to SHA or tag to avoid surprises.
- Protect deploy branches -- require approvals before production deployments.
- Keep secrets out of code -- use the platform's secret store.
Return to the DevOps hub or continue to Git for Sysadmins and Docker Guide.