CI/CD with GitHub Actions: Automate Builds, Tests & Deployments

CI/CD (Continuous Integration / Continuous Deployment) is the practice of automatically building, testing, and deploying code on every change — catching bugs faster, shipping features in hours instead of weeks, and removing the anxiety of manual deployments. GitHub Actions is the dominant CI/CD platform in 2026, with over 20,000 Actions in the marketplace and native integration with GitHub repositories, Docker Hub, major cloud providers, and virtually every developer tool. This guide takes you from a basic workflow to a multi-stage production pipeline with Docker, secrets, caching, and cloud deployment.

1. What is CI/CD and Why It Matters

Continuous Integration (CI): Every code push triggers an automated pipeline that builds the code, runs linting, and runs tests. The goal: detect integration errors immediately, before they compound. Teams that practice CI report fewer bugs in production and dramatically reduced integration hell (the nightmare of merging long-lived branches).

Continuous Deployment (CD): Every change that passes CI is automatically deployed to production (or staging). Eliminates manual deployment steps, reduces the batch size of changes (smaller deploys = less risk), and shortens the feedback loop from code to user.

Business impact: Google's DevOps Research and Assessment (DORA) report consistently shows that elite engineering teams deploy 973× more frequently with 6,570× faster change lead times than low-performing teams. CI/CD is the primary enabler of this performance gap.

2. GitHub Actions Anatomy

A GitHub Actions workflow is a YAML file stored in .github/workflows/ in your repository:

name: CI                          # Workflow name (shown in UI)

on:                               # Triggers (see section 3)
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:                              # Global environment variables
  NODE_VERSION: '20'

jobs:                             # One or more parallel jobs
  test:                           # Job ID
    name: Test                    # Job display name
    runs-on: ubuntu-24.04         # Runner OS

    steps:                        # Sequential steps within a job
      - name: Checkout code
        uses: actions/checkout@v4  # Use an Action from marketplace

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:                      # Action inputs
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci                # Shell command

      - name: Run tests
        run: npm test
        env:                       # Step-level environment variables
          CI: true

3. Triggers: When Workflows Run

TriggerYAMLUse Case
Push to branchon: push: branches: [main]Run CI on every merge to main
Pull Requeston: pull_request: branches: [main]Run tests before PR can be merged
Schedule (cron)on: schedule: - cron: '0 8 * * 1'Weekly scheduled tasks, nightly builds
Manual triggeron: workflow_dispatch:Deploy on-demand with optional inputs
Tag pushon: push: tags: ['v*']Release pipeline triggered by version tags
Another workflowon: workflow_call:Reusable workflow called by others
Issue/PR commenton: issue_comment: types: [created]Slash command bots (e.g., /deploy staging)

4. Runners: Where Workflows Run

Runners are the virtual machines that execute workflow jobs:

  • GitHub-hosted runners: ubuntu-24.04, ubuntu-22.04, windows-latest, macos-latest. Free tier: 2000 minutes/month for public repos; 500 minutes/month for private (then metered). Each job gets a fresh VM — clean, isolated environment.
  • Self-hosted runners: Your own servers (on-premise or cloud). No minute costs; can access internal networks; can use specialised hardware (GPUs, high-memory). Set up with: Settings → Actions → Runners → New self-hosted runner.
  • Larger runners: GitHub offers 4-core, 8-core, 16-core, and GPU runners for premium plan users. Useful for intensive test suites or model training pipelines.

5. Actions Marketplace

Actions are reusable workflow building blocks. Browse 20,000+ at github.com/marketplace. Essential actions:

ActionPurpose
actions/checkout@v4Clone your repository into the runner
actions/setup-node@v4Install Node.js (with npm/yarn cache)
actions/setup-python@v5Install Python (with pip cache)
actions/cache@v4Cache arbitrary directories between runs
actions/upload-artifact@v4Save files (build output, test reports) to share between jobs
docker/build-push-action@v5Build and push Docker images (with layer caching)
aws-actions/configure-aws-credentials@v4Authenticate to AWS (supports OIDC keyless auth)
google-github-actions/deploy-cloudrun@v2Deploy to Google Cloud Run
azure/webapps-deploy@v3Deploy to Azure App Service

6. Matrix Builds for Multi-Version Testing

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false   # don't cancel other matrix jobs on failure
      matrix:
        os: [ubuntu-24.04, windows-latest]
        node: ['18', '20', '22']
        include:
          # Add extra variable for a specific combination
          - os: ubuntu-24.04
            node: '20'
            coverage: true
        exclude:
          - os: windows-latest
            node: '18'

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: 'npm'
      - run: npm ci
      - run: npm test
      - name: Upload coverage
        if: matrix.coverage == true
        run: npm run coverage

7. Caching Dependencies

Without caching, npm ci or pip install downloads and installs all dependencies on every run. With actions/setup-node@v4's built-in cache: 'npm', the ~/.npm cache is stored between runs. For custom caching:

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

# Docker layer caching (dramatically speeds up image builds)
- uses: docker/build-push-action@v5
  with:
    context: .
    cache-from: type=gha         # GitHub Actions Cache as Docker cache
    cache-to: type=gha,mode=max  # Save all layers
    push: true
    tags: myimage:latest

8. Build and Push Docker Images

name: Build and Push Docker Image

on:
  push:
    branches: [main]

jobs:
  docker:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4

      # Set up Docker Buildx (required for multi-platform builds)
      - uses: docker/setup-buildx-action@v3

      # Login to Docker Hub
      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}   # use access token, not password

      # Extract metadata for tags (automatically tags :latest, :1.2.3, :sha-abc123)
      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: myuser/myapp
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,prefix=sha-

      # Build and push (multi-platform: amd64 + arm64)
      - uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

9. Deploying to Cloud Platforms

Deploy to Vercel

- uses: amondnet/vercel-action@v25
  with:
    vercel-token: ${{ secrets.VERCEL_TOKEN }}
    vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
    vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
    vercel-args: '--prod'

Deploy to Fly.io

- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
  env:
    FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Deploy to AWS ECS

- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
    aws-region: us-east-1
- run: |
    aws ecs update-service \
      --cluster prod \
      --service my-app \
      --force-new-deployment

10. Secrets Management

  • Repository secrets: Settings → Secrets and variables → Actions. Available to all workflows.
  • Environment secrets: Scoped to a specific environment (staging, production). Can require manual approval via environment protection rules — nobody deploys to production without a reviewer.
  • Organisation secrets: Available across all repos in an organisation. Useful for shared credentials like a Docker Hub token.
  • OIDC (no long-lived secrets): Use aws-actions/configure-aws-credentials@v4 with an IAM role — GitHub issues a short-lived token. No AWS keys stored anywhere. Best practice for cloud deployments.
- run: echo "Deploy to ${{ secrets.DEPLOY_HOST }}"
# Output: Deploy to **** (secrets are masked in logs automatically)

11. Complete Production Workflow Example

12. Frequently Asked Questions

How do I make a workflow run only on specific file changes?

Use paths and paths-ignore filters: on: push: paths: ['src/**', 'package.json'] — the workflow only runs if a file matching the pattern changes. Use paths-ignore: ['docs/**', '*.md'] to skip docs-only changes.

How do I share data between jobs?

Jobs run on separate VMs — the filesystem is not shared. Use actions/upload-artifact in one job and actions/download-artifact in subsequent jobs. For simple values (like an image tag), use outputs on the job and access them via needs.job-name.outputs.output-name.

What is the best way to handle environment-specific deployments?

GitHub Environments are the recommended approach: create environments (staging, production) in repository Settings → Environments. Each environment can have: its own secrets, required reviewers (manual approval gate), deployment branch restrictions, and a wait timer. This gives full governance over what gets deployed where, by whom, without separate tooling.

13. Glossary

CI (Continuous Integration)
The practice of merging code changes frequently and verifying each integration with automated builds and tests.
CD (Continuous Deployment)
Automatically deploying every change that passes CI to production (or staging) without manual intervention.
Workflow
A YAML file in .github/workflows/ describing automated processes triggered by events in a GitHub repository.
Job
A set of steps that run on the same runner. Multiple jobs in a workflow run in parallel by default.
Step
A single task within a job — either a shell command (run) or a marketplace Action (uses).
Runner
The server (GitHub-hosted or self-hosted) that executes workflow jobs.
OIDC
OpenID Connect — enables keyless cloud authentication from GitHub Actions without storing long-lived credentials.
Environment
A GitHub feature scoping secrets and requiring approvals for deployments to specific targets (staging, production).

14. References & Further Reading

Create a .github/workflows/ci.yml in your project right now with just checkout, install, and test steps. Getting tests running automatically on every push is the highest-ROI engineering improvement for most teams. Add deployment automation after CI is solid.