GitHub Actions CI/CD Guide | Workflows, Secrets, Matrix & Deployment

GitHub Actions CI/CD Guide | Workflows, Secrets, Matrix & Deployment

이 글의 핵심

GitHub Actions turns your repository into a CI/CD platform. This guide covers workflow syntax, caching, matrix builds, Docker publishing, environment secrets, and real deployment pipelines — all with copy-paste examples.

Workflow Basics

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

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

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm          # Built-in npm cache

      - run: npm ci
      - run: npm test
      - run: npm run build

Triggers

on:
  # Push to specific branches
  push:
    branches: [main]
    paths:                    # Only run if these paths changed
      - 'src/**'
      - 'package.json'
    paths-ignore:
      - '**.md'

  # PRs targeting main
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

  # Scheduled (cron)
  schedule:
    - cron: '0 2 * * 1'     # Every Monday at 2am UTC

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

  # Trigger from another workflow
  workflow_call:
    inputs:
      version:
        type: string
        required: true

Jobs and Dependencies

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

  build:
    runs-on: ubuntu-latest
    needs: test              # Wait for test to pass
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build

  deploy:
    runs-on: ubuntu-latest
    needs: [test, build]     # Wait for both
    if: github.ref == 'refs/heads/main'   # Only on main branch
    steps:
      - run: echo "Deploying..."

Caching Dependencies

# Node.js — cache node_modules
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: npm              # Caches ~/.npm

- run: npm ci

# Python
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: pip

- run: pip install -r requirements.txt

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

Matrix Builds

jobs:
  test:
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false       # Don't cancel other jobs if one fails
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [18, 20, 22]
        exclude:
          - os: windows-latest
            node: 18

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci && npm test

Secrets and Environment Variables

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # Use environment-level secrets

    env:
      NODE_ENV: production    # Workflow-level env var

    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}          # From repo secrets
          DB_URL: ${{ secrets.PROD_DB_URL }}       # From environment secrets
          GITHUB_SHA: ${{ github.sha }}            # Built-in context
        run: |
          echo "Deploying commit $GITHUB_SHA"
          ./deploy.sh
# Environment protection rules (in GitHub UI):
# Settings → Environments → production
#   Required reviewers: [team members]
#   Wait timer: 5 minutes
#   Deployment branches: main only

Docker: Build and Push

name: Docker

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

jobs:
  docker:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - 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: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: myorg/myapp
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,prefix=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         # GitHub Actions cache
          cache-to: type=gha,mode=max

Deploy to AWS

jobs:
  deploy-ecs:
    runs-on: ubuntu-latest
    needs: docker

    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push to ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/myapp:$IMAGE_TAG .
          docker push $ECR_REGISTRY/myapp:$IMAGE_TAG

      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: task-definition.json
          service: my-service
          cluster: my-cluster
          wait-for-service-stability: true

Deploy to Vercel / Railway

# Vercel
- 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'     # Remove for preview deployments

# Or use Vercel CLI directly
- run: npx vercel --token ${{ secrets.VERCEL_TOKEN }} --prod

Reusable Workflows

# .github/workflows/reusable-test.yml
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '22'
    secrets:
      NPM_TOKEN:
        required: false

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci && npm test
# .github/workflows/ci.yml — call the reusable workflow
jobs:
  test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '22'
    secrets: inherit

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."

Practical: Full Node.js CI/CD

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

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

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: testpassword
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

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

      - run: npm ci

      - name: Run migrations
        env:
          DATABASE_URL: postgresql://postgres:testpassword@localhost:5432/testdb
        run: npm run db:migrate

      - name: Run tests
        env:
          DATABASE_URL: postgresql://postgres:testpassword@localhost:5432/testdb
          JWT_SECRET: test-secret
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        if: github.ref == 'refs/heads/main'

  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          echo "$DEPLOY_KEY" > /tmp/deploy_key
          chmod 600 /tmp/deploy_key
          ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no user@server \
            "cd /app && git pull && npm ci --production && pm2 restart app"

Useful Patterns

# Concurrency — cancel in-progress runs on same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

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

# Download in another job
- uses: actions/download-artifact@v4
  with:
    name: build-output
    path: dist/

# Post a PR comment
- uses: peter-evans/create-or-update-comment@v4
  with:
    issue-number: ${{ github.event.pull_request.number }}
    body: |
      Build succeeded! Preview: https://preview-${{ github.sha }}.example.com

# Slack notification on failure
- name: Notify Slack on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {"text": "❌ Deploy failed on ${{ github.ref }} — ${{ github.run_url }}"}
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Related posts: