Skip to content
Codeloom
CI/CD

GitHub Actions: Complete CI/CD Guide for Any Project

Set up GitHub Actions CI/CD from scratch — workflows, jobs, caching, secrets, matrix builds, deployment, and reusable patterns for any language or framework.

·6 min read · By Codeloom
Beginner 14 min read

What you'll learn

  • How GitHub Actions workflows, jobs, and steps work
  • Setting up CI pipelines for testing and linting
  • Deploying to production on push or tag
  • Caching dependencies for faster builds
  • Using secrets and environment variables safely

Prerequisites

  • A GitHub repository
  • Basic command-line familiarity

GitHub Actions runs your CI/CD pipelines directly inside GitHub. No external service, no extra account. You write YAML files that describe when to run, what to run, and where to run it. This guide covers everything from your first workflow to production deployment patterns.

How GitHub Actions Works

Every workflow lives in .github/workflows/ as a YAML file. A workflow contains one or more jobs, each job runs on a virtual machine (the runner), and each job contains steps that execute commands or use pre-built actions.

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm test

This workflow triggers on pushes and pull requests to main. It checks out the code, installs Node.js, installs dependencies, and runs tests.

Trigger Events

The on key controls when workflows run. The most common triggers:

on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'package.json'

  pull_request:
    branches: [main]

  schedule:
    - cron: '0 6 * * 1'

  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy target'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

workflow_dispatch lets you trigger workflows manually from the GitHub UI with custom inputs. schedule uses cron syntax for recurring runs.

Jobs and Dependencies

Jobs run in parallel by default. Use needs to create dependencies:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

  deploy:
    needs: [lint, test]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - run: npm run deploy

lint and test run in parallel. deploy waits for both to pass and only runs on the main branch.

Caching Dependencies

Without caching, every run downloads and installs all dependencies from scratch. The actions/cache action stores files between runs:

- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: 'npm'

The setup actions for Node, Python, Go, and others have built-in caching. For custom caching:

- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

The key includes a hash of the lock file so the cache invalidates when dependencies change.

Secrets and Environment Variables

Store sensitive values in repository or organization secrets (Settings → Secrets and variables → Actions):

steps:
  - name: Deploy
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    run: aws s3 sync ./dist s3://my-bucket

Secrets are masked in logs automatically. For non-sensitive configuration, use vars:

env:
  API_URL: ${{ vars.API_URL }}

Matrix Builds

Test across multiple versions, operating systems, or configurations:

jobs:
  test:
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, macos-latest, windows-latest]
      fail-fast: false
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

This creates 9 jobs (3 Node versions × 3 operating systems). fail-fast: false lets all combinations finish even if one fails.

Artifacts

Share files between jobs or download build outputs:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: ls -la dist/

Environments and Approvals

GitHub Environments add protection rules like required reviewers and wait timers:

jobs:
  deploy-staging:
    environment: staging
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to staging"

  deploy-production:
    needs: deploy-staging
    environment: production
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to production"

Configure the production environment in repository settings to require manual approval before the job runs.

Docker Builds

Build and push Docker images as part of your pipeline:

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

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

The cache-from and cache-to options use GitHub’s cache backend to speed up Docker builds.

Reusable Workflows

Extract common patterns into reusable workflows that other repositories can call:

# .github/workflows/reusable-deploy.yml
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      deploy-key:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.deploy-key }}

Call it from another workflow:

jobs:
  deploy:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
    secrets:
      deploy-key: ${{ secrets.DEPLOY_KEY }}

Conditional Steps

Control which steps run based on conditions:

steps:
  - name: Run only on main
    if: github.ref == 'refs/heads/main'
    run: echo "On main branch"

  - name: Run only on PR
    if: github.event_name == 'pull_request'
    run: echo "This is a PR"

  - name: Run on failure
    if: failure()
    run: echo "Something failed"

  - name: Always run
    if: always()
    run: echo "Cleanup"

Concurrency Control

Prevent multiple deployments from running simultaneously:

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true

This cancels any in-progress run for the same branch when a new push arrives. Essential for deployment workflows where only the latest commit matters.

A Complete Production Workflow

name: CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: echo "Deploy dist/ to production"

Wrapping Up

GitHub Actions gives you CI/CD without leaving GitHub. Start with a simple test workflow, add caching to speed it up, use matrix builds for coverage, and layer in deployment jobs with environment protection. The YAML is declarative, the marketplace has thousands of pre-built actions, and the free tier is generous enough for most open-source and small-team projects.