GitHub Actions — Complete CI/CD Guide 2026

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

GitHub Actions is the most widely adopted CI/CD platform in 2026 — tightly integrated with GitHub repositories, free for public repos, and offering a massive marketplace of reusable actions. Unlike Jenkins, there is no server to maintain. Unlike GitLab CI, there is no separate platform to learn if you are already on GitHub.

Understanding GitHub Actions deeply lets you automate not just builds and tests, but deployments, code reviews, dependency updates, security scans, and release management — all from YAML files committed directly to your repository.

Core Concepts

ConceptDescription
WorkflowA YAML file in .github/workflows/ that defines automation
EventA trigger that starts a workflow (push, pull_request, schedule)
JobA group of steps that run on the same runner
StepA single task — either a shell command or an action
ActionA reusable unit of automation from the Marketplace or your repo
RunnerThe virtual machine (GitHub-hosted or self-hosted) that executes jobs

Your First Workflow

Create .github/workflows/ci.yml:

name: CI
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  build-and-test:
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
 
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Run linter
        run: npm run lint
 
      - name: Run tests
        run: npm test
 
      - name: Build
        run: npm run build

Triggers and Events

on:
  # Trigger on push to specific branches
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'package*.json'
 
  # Trigger on pull requests
  pull_request:
    types: [opened, synchronize, reopened]
 
  # Scheduled trigger (cron syntax)
  schedule:
    - cron: '0 2 * * 1'  # Every Monday at 2am UTC
 
  # Manual trigger with inputs
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]
      deploy_version:
        description: 'Version tag to deploy'
        required: true
        type: string
 
  # Trigger from another workflow
  workflow_call:
    inputs:
      environment:
        required: true
        type: string

Matrix Builds for Cross-Platform Testing

Matrix builds run jobs in parallel across multiple configurations:

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

Caching Dependencies

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

For Docker layer caching:

  - name: Set up Docker Buildx
    uses: docker/setup-buildx-action@v3
 
  - name: Build and push
    uses: docker/build-push-action@v5
    with:
      push: true
      tags: ghcr.io/myorg/myapp:${{ github.sha }}
      cache-from: type=gha
      cache-to: type=gha,mode=max

Complete CI/CD Pipeline With Environments

name: Deploy Pipeline
 
on:
  push:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci && npm test
 
  build-image:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4
 
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha,prefix=
 
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
 
  deploy-staging:
    needs: build-image
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        run: |
          helm upgrade --install myapp ./charts/myapp \
            --set image.tag=${{ github.sha }} \
            --namespace staging \
            --atomic
        env:
          KUBECONFIG: ${{ secrets.STAGING_KUBECONFIG }}
 
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.example.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: |
          helm upgrade --install myapp ./charts/myapp \
            --set image.tag=${{ github.sha }} \
            --namespace production \
            --atomic
        env:
          KUBECONFIG: ${{ secrets.PRODUCTION_KUBECONFIG }}

Reusable Workflows

Extract common logic into reusable workflows:

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
 
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      kubeconfig:
        required: true
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: |
          helm upgrade --install myapp ./charts/myapp \
            --set image.tag=${{ inputs.image-tag }} \
            --namespace ${{ inputs.environment }} \
            --atomic
        env:
          KUBECONFIG: ${{ secrets.kubeconfig }}

Call it from any workflow:

jobs:
  deploy-prod:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
      image-tag: ${{ github.sha }}
    secrets:
      kubeconfig: ${{ secrets.PRODUCTION_KUBECONFIG }}

Common Mistakes

  • Checking in secrets — never hardcode credentials in workflow files; use secrets.* context values.
  • Not pinning action versions — use actions/checkout@v4 not actions/checkout@main; unpinned actions are a supply chain risk.
  • Skipping needs dependencies — without needs, jobs run in parallel by default; use it to enforce sequential execution.
  • Not caching dependencies — every run reinstalling node_modules wastes 1-3 minutes; use actions/cache or the built-in cache in setup-node.
  • Overusing workflow_dispatch for production deploys — use environment protection rules with required reviewers instead.

Best Practices

  • Use environment protection rules — configure required reviewers, deployment branches, and wait timers in GitHub repository Settings for production environments.
  • Pin actions to a commit SHA — for maximum security, pin third-party actions to their commit SHA: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683.
  • Use job summaries — add $GITHUB_STEP_SUMMARY output to create rich markdown reports visible in the Actions UI.
  • Self-host runners for private infra — deploy self-hosted runners in your VPC for jobs that need direct access to internal services without public network exposure.
  • Use concurrency to cancel stale runs — add concurrency: group: ${{ github.ref }} to cancel in-progress runs when a new push occurs.

Key Takeaways

  • GitHub Actions workflows live in .github/workflows/ as YAML files and are triggered by repository events like push, pull_request, or schedule.
  • Jobs run in parallel by default; use needs: [job-name] to enforce sequential execution and pass outputs between jobs.
  • Matrix builds run a job across multiple OS/version combinations in parallel — ideal for cross-platform testing.
  • The environment keyword enables deployment protection rules with required reviewers, branch filters, and deployment URLs.
  • Reusable workflows (workflow_call) let you centralize CI/CD logic across multiple repositories without copy-pasting YAML.
  • Always pin third-party actions to a specific version tag or commit SHA to prevent supply chain attacks.
  • Use actions/cache or built-in caching in setup actions to cache dependencies and reduce workflow run time by 50-80%.
  • The GITHUB_TOKEN secret is automatically provided in every workflow and can authenticate to GHCR, the GitHub API, and other GitHub services without additional configuration.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading