GitHub Actions CI/CD: Complete Tutorial for 2026

Build production-grade CI/CD pipelines with GitHub Actions. From your first workflow file to matrix builds, secrets management, dependency caching, automated deployments, custom actions, and monorepo strategies.

In This Guide
  1. Workflow Basics: Syntax and Structure
  2. Triggers: When Workflows Run
  3. Jobs, Steps, and Actions
  4. Your First CI Pipeline
  5. Matrix Builds
  6. Secrets and Environment Variables
  7. Dependency Caching
  8. Artifacts and Build Outputs
  9. Deployment Workflows
  10. Custom Actions
  11. Monorepo Strategies
  12. Related Developer Tools
  13. Frequently Asked Questions

GitHub Actions has become the default CI/CD platform for projects hosted on GitHub. It is integrated directly into the repository, runs workflows for free on public repositories, and has a massive marketplace of community-maintained actions. Whether you are setting up your first test pipeline or orchestrating complex multi-environment deployments, everything starts with a YAML file.

This tutorial takes you from zero to production-ready CI/CD. Every example is a complete, working workflow you can copy into your repository.

Validate your workflow YAML. The YAML Editor catches syntax errors, indentation problems, and structural issues before you commit and wait for GitHub to report an error.

Workflow Basics: Syntax and Structure

Workflow files live in .github/workflows/ at the root of your repository. Each .yml file defines one workflow. A workflow contains triggers, jobs, and steps.

# .github/workflows/ci.yml
name: CI                          # Workflow name (shows in GitHub UI)

on: [push, pull_request]          # Triggers

jobs:                              # One or more jobs
  test:                            # Job ID
    runs-on: ubuntu-latest         # Runner OS
    steps:                         # Sequence of steps
      - uses: actions/checkout@v4  # Step using an action
      - run: echo "Hello CI"      # Step running a command

The core hierarchy is: Workflow (file) > Jobs (run in parallel by default) > Steps (run sequentially within a job). Jobs can depend on other jobs using the needs keyword, creating a pipeline of sequential stages.

Triggers: When Workflows Run

The on key defines what events trigger the workflow. You can combine multiple triggers and filter by branches, paths, and tags.

on:
  # Run on push to main or release branches
  push:
    branches: [main, 'release/**']
    paths-ignore:
      - '**.md'
      - 'docs/**'

  # Run on all pull requests targeting main
  pull_request:
    branches: [main]

  # Run on a schedule (UTC)
  schedule:
    - cron: '0 6 * * 1'           # Every Monday at 6 AM UTC

  # Run manually from the GitHub UI
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy target'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]

  # Run when a release is published
  release:
    types: [published]

The paths and paths-ignore filters are critical for performance. If your workflow only tests backend code, use paths: ['src/**', 'package.json'] so documentation changes do not trigger a build.

Build cron expressions for your scheduled workflows with the Cron Expression Generator. It shows a human-readable description and upcoming run times.

Jobs, Steps, and Actions

Jobs Run in Parallel

By default, all jobs in a workflow run simultaneously on separate runners. 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 only after lint AND test succeed
  deploy:
    needs: [lint, test]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm run deploy

Steps: Commands and Actions

Steps either run shell commands with run or use pre-built actions with uses. Actions are reusable units of automation published to the GitHub Marketplace.

steps:
  # Use a published action (org/repo@version)
  - uses: actions/checkout@v4

  # Use an action with inputs
  - uses: actions/setup-node@v4
    with:
      node-version: 22
      cache: 'npm'

  # Run a shell command
  - run: npm ci

  # Run multiple commands
  - run: |
      npm run build
      npm run test:coverage

  # Set environment variables for a step
  - run: echo "Deploying to $TARGET"
    env:
      TARGET: production

  # Name your steps for readability
  - name: Run integration tests
    run: npm run test:integration
    env:
      DATABASE_URL: ${{ secrets.TEST_DB_URL }}

Your First CI Pipeline

Here is a complete CI workflow for a Node.js project that lints, tests, and builds on every push and pull request.

# .github/workflows/ci.yml
name: CI

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

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

jobs:
  ci:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npx tsc --noEmit

      - name: Run tests
        run: npm test -- --coverage

      - name: Build
        run: npm run build

      - name: Upload coverage
        if: github.event_name == 'push'
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

The concurrency block cancels in-progress runs when new commits are pushed to the same branch. This avoids wasting runner minutes on outdated code.

Set Timeout Limits

Always set timeout-minutes on jobs. The default is 360 minutes (6 hours). A stuck process or infinite loop will consume your entire monthly runner allocation if left unchecked.

Matrix Builds

Matrix builds test your code across multiple configurations in parallel. Essential for libraries and tools that need to work on different platforms and runtime versions.

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
        include:
          - os: ubuntu-latest
            node: 22
            coverage: true

    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
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

fail-fast: false lets all matrix jobs complete even if one fails. This is useful for seeing the full picture of which platforms pass and which fail, rather than stopping at the first failure.

Secrets and Environment Variables

Never hardcode sensitive values. Use GitHub's encrypted secrets and environment variables.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production        # Links to GitHub Environment settings

    env:
      NODE_ENV: production         # Available to all steps

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to AWS
        run: |
          aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }}
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CF_DIST_ID }} \
            --paths "/*"
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: us-east-1

OIDC: No Long-Lived Secrets

For AWS, GCP, and Azure, use OIDC (OpenID Connect) to get short-lived credentials without storing access keys as secrets.

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-deploy
          aws-region: us-east-1

      # No AWS keys needed - OIDC token is exchanged automatically
      - run: aws s3 sync dist/ s3://my-bucket

The Env File Editor helps you organize which variables belong in secrets versus regular environment configuration.

Dependency Caching

Caching dependencies is the single biggest speedup for most workflows. Instead of downloading packages from npm, pip, or Docker Hub on every run, cache them between workflow runs.

# Option 1: Built-in cache in setup actions (recommended)
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: 'npm'           # Automatically caches ~/.npm

# Option 2: Explicit cache action (more control)
- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-
# Python pip caching
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'

# Docker layer caching
- uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: my-app:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

The cache key uses hashFiles('package-lock.json') so the cache invalidates automatically when dependencies change. The restore-keys fallback means a partial cache miss still uses the most recent cache rather than downloading everything from scratch.

Artifacts and Build Outputs

Artifacts let you pass files between jobs and download build outputs from the GitHub UI.

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

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 7

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      - name: Deploy
        run: ./deploy.sh dist/

Deployment Workflows

Deploy to Vercel

name: Deploy to Vercel

on:
  push:
    branches: [main]

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

      - name: 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 Docker to Container Registry

name: Build and Push Docker

on:
  push:
    tags: ['v*']

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        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=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Generate your Docker Compose and Dockerfile configurations with the Docker Compose Generator before adding them to your CI pipeline.

Deploy with Environment Protection

name: Deploy to Production

on:
  workflow_dispatch:

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

  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: ./deploy.sh staging

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.com
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: ./deploy.sh production

Configure the production environment in GitHub Settings to require manual approval. Reviewers must approve the deployment before the production job starts.

Custom Actions

Create reusable actions for logic that multiple workflows share. Composite actions are the simplest type, defined in a single YAML file.

# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Install dependencies and build the project'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '22'

runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'

    - name: Install dependencies
      shell: bash
      run: npm ci

    - name: Build
      shell: bash
      run: npm run build
# Usage in a workflow
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup-project
        with:
          node-version: '22'
      - run: npm test

Create your .gitignore file to exclude build artifacts, node_modules, and environment files from your repository before setting up your CI pipeline.

Monorepo Strategies

In a monorepo with multiple packages or services, you need workflows that only build what changed.

name: Monorepo CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      api: ${{ steps.filter.outputs.api }}
      web: ${{ steps.filter.outputs.web }}
      shared: ${{ steps.filter.outputs.shared }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            api:
              - 'packages/api/**'
              - 'packages/shared/**'
            web:
              - 'packages/web/**'
              - 'packages/shared/**'
            shared:
              - 'packages/shared/**'

  test-api:
    needs: detect-changes
    if: needs.detect-changes.outputs.api == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test --workspace=packages/api

  test-web:
    needs: detect-changes
    if: needs.detect-changes.outputs.web == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test --workspace=packages/web

The dorny/paths-filter action detects which directories changed and sets outputs that downstream jobs use to decide whether to run. If only the API package changed, the web test job is skipped entirely.

Monorepo Tip: Shared Dependencies

Include shared package paths in the filters for all dependent packages. When packages/shared/ changes, both API and web test jobs should run since both depend on it.

Related Developer Tools

Free browser-based tools for your CI/CD workflow.


Frequently Asked Questions

GitHub Actions is a CI/CD platform built into GitHub that automates software workflows directly from your repository. You define workflows in YAML files inside the .github/workflows directory. Each workflow contains one or more jobs that run on virtual machines called runners. Jobs contain steps that either run shell commands or use pre-built actions from the GitHub Marketplace. Workflows are triggered by events like pushing code, opening a pull request, creating a release, or on a cron schedule. GitHub provides free runners for public repositories and 2000 minutes per month for private repositories on the free plan.

Create a file at .github/workflows/ci.yml in your repository. Define the trigger (typically on push and pull_request to the main branch), specify a job that runs on ubuntu-latest, and add steps to check out the code, set up your runtime like Node.js or Python, install dependencies, run linting, and run tests. Push the file to your repository and GitHub will automatically run the workflow on the next matching event. The workflow status appears as a check on pull requests and commits. A basic Node.js CI pipeline takes about 10 lines of YAML and typically runs in under two minutes.

Matrix builds run the same job across multiple configurations in parallel. You define a strategy.matrix in your job with variables like operating system and language version. GitHub Actions creates a separate job for each combination. For example, a matrix of os: [ubuntu-latest, windows-latest] and node: [18, 20, 22] creates six parallel jobs testing every OS and Node version combination. You can exclude specific combinations with the exclude key or add one-off configurations with the include key. Matrix builds are essential for libraries that need to verify compatibility across multiple platforms and runtime versions.

Store sensitive values like API keys, tokens, and passwords as encrypted secrets in your repository or organization settings under Settings then Secrets and variables then Actions. Reference them in workflows using the syntax secrets.SECRET_NAME in the env block or directly in step inputs. GitHub automatically masks secret values in logs so they are never printed in plain text. For additional security, use environment-specific secrets that require approval before deployment, use OIDC tokens instead of long-lived credentials for cloud providers like AWS and GCP, and limit secret access to specific branches using environment protection rules. Never hardcode secrets in workflow files or pass them as command line arguments where they might appear in process listings.

The most effective optimizations are dependency caching, parallel jobs, and targeted triggers. Cache npm, pip, or other package manager dependencies using actions/cache or the built-in cache option in setup actions like actions/setup-node. This avoids re-downloading packages on every run and can save two to five minutes per workflow. Split independent tasks like linting, unit tests, and integration tests into separate parallel jobs. Use path filters in your triggers so workflows only run when relevant files change. Use concurrency groups to cancel redundant runs when new commits are pushed to the same branch. For Docker-based workflows, cache Docker layers. These optimizations together can reduce a 15-minute workflow to under three minutes.

Yes. GitHub Actions supports deployment to any platform including AWS, Google Cloud, Azure, Vercel, Netlify, DigitalOcean, and custom servers. You can set up continuous deployment that automatically deploys on push to the main branch, or use GitHub Environments with protection rules that require manual approval before production deployments. A typical deployment workflow runs tests first, builds the application, then deploys using platform-specific actions or CLI tools. For production safety, use environment protection rules that require one or more reviewers to approve the deployment, restrict which branches can deploy to production, and add a wait timer between approval and deployment. This gives you automation with human oversight for critical environments.

NT

Christian Bucher

We build free developer tools including YAML editors, Docker generators, environment managers, and 269 more. All browser-based, no signup required.

269 Developer Tools, One Place

Browse 269 indexed tool pages with no QTool account required, and inspect the source on GitHub.

Open Source — Free Forever Try Free Tools

Related Tools

CSS Box Shadow Generator · Emoji Picker & Search · Free Git Diff Viewer

Related Tools

Free JSON Validator · Free API Mock Server · Code Screenshot Generator - Beautiful Code Images

Related Articles

Built by Miguel

Need a custom tool or website?

From . Delivered in 24-48h. You own the code.

View Services →