GitHub Actions CI/CD: Complete 2026 Guide


GitHub Actions CI/CD: Complete 2026 Guide

GitHub Actions has become the standard for CI/CD pipelines in modern software development. This comprehensive guide covers everything from basic workflows to advanced deployment patterns.

Why GitHub Actions?

  • Native integration - Built into GitHub
  • Generous free tier - 2,000 minutes/month for public repos
  • Marketplace - Thousands of pre-built actions
  • Matrix builds - Test across multiple environments
  • Self-hosted runners - Use your own infrastructure
  • Secrets management - Secure credential storage

Workflow Basics

Workflow Structure

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

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  workflow_dispatch: # Manual trigger

env:
  NODE_VERSION: '20'
  
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: $
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests
        run: npm test

Triggers

on:
  # Push events
  push:
    branches:
      - main
      - 'feature/**'
    paths:
      - 'src/**'
      - '!src/**/*.md'
    tags:
      - 'v*'

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

  # Scheduled events
  schedule:
    - cron: '0 2 * * *' # Daily at 2 AM UTC

  # Manual trigger with inputs
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production
      debug:
        description: 'Enable debug mode'
        type: boolean
        default: false

  # External webhook
  repository_dispatch:
    types: [deploy]

Job Configuration

Matrix Builds

jobs:
  test:
    runs-on: $
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [18, 20, 22]
        exclude:
          - os: windows-latest
            node-version: 18
        include:
          - os: ubuntu-latest
            node-version: 20
            coverage: true

    steps:
      - uses: actions/checkout@v4
      
      - name: Use Node.js $
        uses: actions/setup-node@v4
        with:
          node-version: $
      
      - run: npm ci
      - run: npm test
      
      - name: Upload coverage
        if: matrix.coverage
        uses: codecov/codecov-action@v4

Job Dependencies

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: $
    steps:
      - uses: actions/checkout@v4
      
      - id: version
        run: echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUT
      
      - run: npm ci && npm run build
      
      - uses: actions/upload-artifact@v4
        with:
          name: build
          path: dist/

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build
          path: dist/
      
      - run: npm test

  deploy:
    needs: [build, test]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - run: echo "Deploying version $"

Concurrency

concurrency:
  group: $-$
  cancel-in-progress: true

jobs:
  deploy:
    concurrency:
      group: production-deploy
      cancel-in-progress: false # Don't cancel ongoing deploys

Complete CI/CD Pipeline

Full-Stack Application

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

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

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: $

jobs:
  # Lint and type check
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check

  # Unit tests
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      
      redis:
        image: redis:7
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - run: npm ci
      
      - name: Run tests
        run: npm test -- --coverage
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/test
          REDIS_URL: redis://localhost:6379
      
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: $

  # E2E tests
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - run: npm ci
      - run: npx playwright install --with-deps
      
      - name: Run E2E tests
        run: npm run test:e2e
      
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

  # Build Docker image
  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      image-tag: $
      image-digest: $

    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: $
          username: $
          password: $
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: $/$
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=semver,pattern=
      
      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: $
          tags: $
          labels: $
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

  # Deploy to staging
  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to staging
        run: |
          echo "Deploying $"
          # Add your deployment commands here

  # Deploy to production (manual approval)
  deploy-production:
    needs: [build, deploy-staging]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to production
        run: |
          echo "Deploying $"
          # Add your deployment commands here

Reusable Workflows

Creating Reusable Workflow

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      DEPLOY_KEY:
        required: true
    outputs:
      url:
        description: "Deployment URL"
        value: $

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

Using Reusable Workflow

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      image-tag: $
    secrets:
      DEPLOY_KEY: $

Composite Actions

Creating Composite Action

# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Setup Node.js and install dependencies'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '20'
  install-playwright:
    description: 'Install Playwright browsers'
    required: false
    default: 'false'

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: $
        cache: 'npm'
    
    - name: Install dependencies
      shell: bash
      run: npm ci
    
    - name: Install Playwright
      if: inputs.install-playwright == 'true'
      shell: bash
      run: npx playwright install --with-deps

Using Composite Action

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: ./.github/actions/setup-project
        with:
          node-version: '20'
          install-playwright: 'true'
      
      - run: npm test

Secrets and Environments

Repository Secrets

steps:
  - name: Deploy
    env:
      API_KEY: $
      DATABASE_URL: $
    run: ./deploy.sh

Environment Secrets

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production # Uses production environment secrets
    steps:
      - run: echo "API_KEY=$"

OIDC Authentication (Keyless)

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
      
      - name: Deploy to AWS
        run: aws s3 sync ./dist s3://my-bucket

Advanced Patterns

Dynamic Matrix

jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      matrix: $
    steps:
      - uses: actions/checkout@v4
      
      - id: set-matrix
        run: |
          SERVICES=$(ls services/ | jq -R -s -c 'split("\n") | map(select(. != ""))')
          echo "matrix={\"service\":$SERVICES}" >> $GITHUB_OUTPUT

  build:
    needs: prepare
    runs-on: ubuntu-latest
    strategy:
      matrix: $
    steps:
      - run: echo "Building $"

Path-based Triggering

on:
  push:
    paths:
      - 'packages/api/**'
      - 'packages/shared/**'

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

  build-api:
    needs: detect-changes
    if: needs.detect-changes.outputs.api == 'true'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building API"

  build-frontend:
    needs: detect-changes
    if: needs.detect-changes.outputs.frontend == 'true'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building Frontend"

Caching

steps:
  # NPM cache
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'

  # Custom cache
  - uses: actions/cache@v4
    with:
      path: |
        ~/.cache/pip
        ~/.cache/pre-commit
      key: $-pip-$
      restore-keys: |
        $-pip-

  # Docker layer cache
  - uses: docker/build-push-action@v5
    with:
      cache-from: type=gha
      cache-to: type=gha,mode=max

Notifications

jobs:
  notify:
    needs: [build, deploy]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Slack notification
        uses: 8398a7/action-slack@v3
        with:
          status: $
          fields: repo,message,commit,author,action,eventName,ref,workflow
        env:
          SLACK_WEBHOOK_URL: $
        if: always()

Security Best Practices

Pinning Actions

steps:
  # Bad - uses mutable tag
  - uses: actions/checkout@v4
  
  # Good - uses immutable SHA
  - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Permissions

permissions:
  contents: read # Minimum required
  
jobs:
  deploy:
    permissions:
      contents: read
      packages: write
      id-token: write

Security Scanning

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          ignore-unfixed: true
          severity: 'CRITICAL,HIGH'
      
      - name: Run CodeQL
        uses: github/codeql-action/analyze@v3

Conclusion

GitHub Actions provides a powerful, flexible CI/CD platform that integrates seamlessly with your GitHub workflow. By following these patterns, you can build robust automation pipelines for any project.

Key takeaways:

  • Use matrix builds for cross-platform testing
  • Leverage reusable workflows and composite actions
  • Implement proper caching strategies
  • Use OIDC for secure cloud authentication
  • Follow security best practices

Resources


이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)