DevToolBoxGRATIS
Blogg

GitHub Actions Komplett Guide: CI/CD Workflows

14 minby DevToolBox

GitHub Actions Complete CI/CD Guide

GitHub Actions is GitHub's built-in CI/CD and automation platform that lets you automate software workflows directly in your repository. Since its 2019 launch, it has become the most widely-used CI/CD system for open-source projects and is increasingly dominant in private codebases. This complete guide covers everything from basic workflow syntax to advanced patterns like matrix builds, reusable workflows, and deployment pipelines.

For cron-based scheduling patterns in GitHub Actions, see our Cron Expression Examples guide.

Core Concepts

GitHub Actions is built around four key concepts:

  • Workflow: A YAML file in .github/workflows/ that defines an automated process. A repo can have multiple workflows.
  • Event: A trigger that starts a workflow — push, pull_request, schedule, workflow_dispatch, etc.
  • Job: A set of steps that run on the same runner. Jobs can run in parallel or sequentially using needs.
  • Step: An individual task — either a shell command or an Action (reusable unit from the Marketplace).
# .github/workflows/example.yml
name: CI Pipeline           # Display name
on:                         # Event triggers
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:                     # Job ID
    runs-on: ubuntu-latest  # Runner environment
    steps:
      - uses: actions/checkout@v4           # Action from Marketplace
      - uses: actions/setup-node@v4         # Setup Node.js
        with:
          node-version: '20'
      - run: npm ci                         # Shell command
      - run: npm test

Workflow Triggers (on:)

GitHub Actions supports many event types. Here are the most commonly used:

on:
  # Push to specific branches or tags
  push:
    branches: [main, develop]
    tags: ['v*']
    paths-ignore: ['**.md', 'docs/**']

  # Pull request events
  pull_request:
    types: [opened, synchronize, reopened]
    branches: [main]

  # Manual trigger with inputs
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]

  # Scheduled (cron)
  schedule:
    - cron: '0 2 * * *'  # Every day at 2 AM UTC

  # When another workflow completes
  workflow_run:
    workflows: ['CI']
    types: [completed]
    branches: [main]

  # Repository dispatch (external trigger via API)
  repository_dispatch:
    types: [deploy-command]

Complete Node.js CI Workflow

A production-ready CI workflow for a Node.js project with testing, linting, and type checking:

name: Node.js CI

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

env:
  NODE_VERSION: '20'

jobs:
  quality:
    name: Code Quality
    runs-on: ubuntu-latest

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'              # Cache node_modules

      - name: Install dependencies
        run: npm ci                 # Clean install (respects package-lock.json)

      - name: Run linter
        run: npm run lint

      - name: Type check
        run: npm run type-check

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

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: quality            # Only run if quality passes

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

Matrix Builds: Testing Across Environments

Matrix builds let you run the same job with different configurations in parallel — ideal for cross-platform testing:

jobs:
  test-matrix:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false          # Don't cancel all jobs if one fails
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: ['18', '20', '22']
        exclude:
          - os: macos-latest
            node: '18'          # Skip this combination

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}

      - run: npm ci
      - run: npm test

  # Matrix with include for custom variables
  deploy-matrix:
    strategy:
      matrix:
        include:
          - environment: staging
            url: https://staging.example.com
            branch: develop
          - environment: production
            url: https://example.com
            branch: main
    steps:
      - name: Deploy to ${{ matrix.environment }}
        run: echo "Deploying to ${{ matrix.url }}"

Secrets and Environment Variables

GitHub Actions provides a secure way to store and use sensitive data:

# Reference secrets configured in Settings > Secrets
steps:
  - name: Deploy
    env:
      API_KEY: ${{ secrets.API_KEY }}
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
    run: |
      echo "Deploying with API key..."
      ./deploy.sh

# Environment-specific secrets
jobs:
  deploy:
    environment: production   # References environment secrets
    steps:
      - run: echo ${{ secrets.PROD_API_KEY }}

# GitHub-provided automatic variables
steps:
  - run: |
      echo "Repo: ${{ github.repository }}"
      echo "SHA: ${{ github.sha }}"
      echo "Branch: ${{ github.ref_name }}"
      echo "Actor: ${{ github.actor }}"
      echo "Event: ${{ github.event_name }}"

# Set environment variables dynamically
  - name: Set deployment URL
    run: echo "DEPLOY_URL=https://${{ github.sha }}.preview.example.com" >> $GITHUB_ENV

  - name: Use the variable
    run: echo "Deployed to $DEPLOY_URL"

Caching Dependencies

Proper caching dramatically speeds up workflows. Use the built-in cache from setup actions or the actions/cache action directly:

# Automatic caching via setup-node (recommended)
- uses: actions/setup-node@v4
  with:
    cache: 'npm'     # or 'yarn', 'pnpm'

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

# Cache for Python pip
- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}

# Cache for Docker layers
- name: Cache Docker layers
  uses: actions/cache@v4
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-buildx-

Docker Build and Push

Build and push Docker images to Docker Hub or GitHub Container Registry:

name: Docker Build & Push

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

jobs:
  docker:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            myorg/myapp
            ghcr.io/myorg/myapp
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=sha

      - name: Set up QEMU (multi-platform)
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        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

Reusable Workflows

Reusable workflows eliminate duplication across multiple repositories. Define once, call from anywhere:

# .github/workflows/reusable-deploy.yml (in shared repo or same repo)
name: Reusable Deploy

on:
  workflow_call:           # Makes this workflow reusable
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      DEPLOY_KEY:
        required: true
    outputs:
      deploy-url:
        description: 'Deployed URL'
        value: ${{ jobs.deploy.outputs.url }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    outputs:
      url: ${{ steps.deploy.outputs.url }}
    steps:
      - name: Deploy
        id: deploy
        run: |
          echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
          echo "url=https://${{ inputs.environment }}.example.com" >> $GITHUB_OUTPUT

---

# Calling the reusable workflow
name: CI/CD Pipeline
on:
  push:
    branches: [main]

jobs:
  ci:
    uses: ./.github/workflows/ci.yml  # or owner/repo/.github/workflows/ci.yml@main

  deploy-staging:
    needs: ci
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      image-tag: ${{ github.sha }}
    secrets:
      DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}

Conditional Execution and Expressions

jobs:
  deploy:
    # Only run on main branch push (not PRs)
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
      # Conditional steps
      - name: Deploy to production
        if: github.ref == 'refs/heads/main'
        run: ./deploy-production.sh

      - name: Deploy to staging
        if: startsWith(github.ref, 'refs/heads/feature/')
        run: ./deploy-staging.sh

      # Always run even if previous steps fail
      - name: Notify on failure
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          payload: '{"text": "Deployment failed!"}'

      # Run on success or failure (not cancelled)
      - name: Cleanup
        if: always()
        run: ./cleanup.sh

      # Complex expressions
      - name: Tag release
        if: |
          github.event_name == 'push' &&
          startsWith(github.ref, 'refs/tags/v') &&
          !contains(github.ref, 'beta')
        run: ./tag-release.sh

GitHub Actions Security Best Practices

# Pin actions to specific commit SHA (not just tag)
# BAD: Tag can be moved to malicious commit
- uses: actions/checkout@v4

# GOOD: Pin to specific SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

# Minimal permissions (principle of least privilege)
permissions:
  contents: read
  packages: write
  # Don't request permissions you don't need

# Restrict secret access with environments
jobs:
  deploy:
    environment: production    # Require approval before accessing secrets

# Prevent command injection in run steps
# BAD: Interpolating untrusted input directly
- run: echo "${{ github.event.issue.title }}"

# GOOD: Set as env var and reference that
- env:
    TITLE: ${{ github.event.issue.title }}
  run: echo "$TITLE"

# Use CODEOWNERS to protect workflow files
# .github/CODEOWNERS
# /.github/workflows/ @security-team

Frequently Asked Questions

What are GitHub Actions runner minutes limits?

Free accounts get 2,000 minutes/month for private repos (public repos are free). Ubuntu runners use 1x multiplier, Windows 2x, macOS 10x. Self-hosted runners are free and have no minutes limit.

How do I debug a failing workflow?

Enable debug logging by setting secret ACTIONS_RUNNER_DEBUG to true. You can also use tmate action to get an SSH session into the runner for interactive debugging.

How do I pass data between jobs?

Use outputs to pass small values between jobs, and actions/upload-artifact + actions/download-artifact to pass files. Jobs can't directly share the filesystem since they run on different machines.

Can I run GitHub Actions locally?

Yes, use act (github.com/nektos/act). It runs GitHub Actions locally using Docker, which is useful for faster iteration without committing and pushing.

Use our Cron Expression Parser to validate your schedule triggers, and the JSON Formatter to debug webhook payloads.

𝕏 Twitterin LinkedIn
Var detta hjälpsamt?

Håll dig uppdaterad

Få veckovisa dev-tips och nya verktyg.

Ingen spam. Avsluta när som helst.

Try These Related Tools

{ }JSON Formatter📋YAML Formatter

Related Articles

GitHub Actions Advanced: Matrix Builds, Reusable Workflows, Custom Actions (2026)

Advanced GitHub Actions: matrix builds, reusable workflows, composite actions, caching, and security hardening.

GitHub Actions CI/CD: Komplett guide

Konfigurera CI/CD-pipelines med GitHub Actions.

CI/CD Pipeline Best Practices: GitHub Actions, Testning och Deploy

Bygg robusta CI/CD-pipelines med GitHub Actions — teststrategier och deploy-mönster.