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

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

이 글의 핵심

GitHub Actions brings CI/CD directly into your GitHub repository — no external servers, no complex setup. This guide walks you through workflows, testing, deployment, caching, and Secrets with real examples.

What is GitHub Actions?

GitHub Actions is GitHub’s built-in CI/CD and automation platform. You describe what should happen in a YAML file, and GitHub runs it automatically when events occur — on push, pull request, schedule, or manual trigger.

Developer pushes code
  → GitHub detects the push
  → Workflow triggers automatically
  → Jobs run in parallel on GitHub's cloud
  → Tests pass → auto-deploy to production

Key advantages over alternatives:

  • Zero infrastructure — GitHub manages the runners
  • Free for public repos — unlimited minutes
  • Marketplace — thousands of ready-made Actions
  • Native GitHub integration — status checks, PR comments, deployments

Core Concepts

TermWhat it is
WorkflowA YAML file in .github/workflows/ that defines automation
EventWhat triggers the workflow (push, pull_request, schedule, etc.)
JobA set of steps that run on the same runner
StepA single command or Action within a job
ActionA reusable unit of work (from Marketplace or your own repo)
RunnerThe VM that executes jobs (ubuntu-latest, macos-latest, windows-latest)

Your First Workflow

Create .github/workflows/hello.yml:

name: Hello World

on:
  push:
    branches: [main]

jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - name: Say hello
        run: echo "Hello, GitHub Actions!"

      - name: Show environment
        run: |
          echo "Branch: ${{ github.ref_name }}"
          echo "Commit: ${{ github.sha }}"
          echo "Actor: ${{ github.actor }}"

Push this file and watch it run under the Actions tab in your repository.


CI Workflow — Test on Every Push

This workflow runs tests on Node.js 18 and 20 in parallel using a matrix strategy:

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

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

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20]

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          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

The matrix creates two parallel jobs — one for Node 18, one for Node 20. Both must pass for the workflow to succeed.


Deployment Workflows

Deploy to Vercel

# .github/workflows/deploy-vercel.yml
name: Deploy to Vercel

on:
  push:
    branches: [main]

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

      - name: Install Vercel CLI
        run: npm install -g vercel

      - name: Deploy to Vercel
        run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

Build and Push Docker Image

# .github/workflows/docker.yml
name: Build and Push Docker Image

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

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

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

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: myuser/myapp
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}

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

Secrets Management

Store sensitive values in GitHub → Settings → Secrets and variables → Actions. Never hardcode them in YAML.

steps:
  - name: Deploy
    run: ./deploy.sh
    env:
      API_KEY: ${{ secrets.API_KEY }}
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Environment secrets — scoped to specific deployment environments (staging, production):

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment: production   # Uses secrets from "production" environment
    steps:
      - name: Deploy
        run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.API_KEY }}  # From the "production" environment

Conditional Execution

Use if to control when jobs or steps run:

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

  notify-failure:
    runs-on: ubuntu-latest
    needs: [test, deploy]
    # Run this job only if any previous job failed
    if: failure()
    steps:
      - name: Send Slack notification
        run: ./notify-failure.sh
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

Common if conditions:

ConditionWhen it runs
github.ref == 'refs/heads/main'Only on main branch
github.event_name == 'pull_request'Only on PRs
success()Previous steps succeeded (default)
failure()Any previous step failed
always()Regardless of previous steps
contains(github.event.pull_request.labels.*.name, 'deploy')PR has ‘deploy’ label

Caching Dependencies

Cache node_modules or pip packages to dramatically speed up workflows:

steps:
  - uses: actions/checkout@v4

  - uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: 'npm'    # Built-in npm cache — restores node_modules automatically

  - run: npm ci

For custom paths:

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

The key includes a hash of package-lock.json — the cache is invalidated automatically when dependencies change.


Useful Marketplace Actions

steps:
  # Checkout your code
  - uses: actions/checkout@v4

  # Set up language runtimes
  - uses: actions/setup-node@v4
    with:
      node-version: 20

  - uses: actions/setup-python@v5
    with:
      python-version: '3.12'

  # Upload build artifacts (accessible in the Actions UI)
  - uses: actions/upload-artifact@v4
    with:
      name: build-output
      path: dist/
      retention-days: 7

  # Download artifacts in a later job
  - uses: actions/download-artifact@v4
    with:
      name: build-output

  # Send a Slack notification
  - uses: slackapi/slack-github-action@v1
    with:
      payload: '{"text": "Deployment successful! :rocket:"}'
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Monorepo CI — Run Jobs Only for Changed Packages

When only apps/web changes, skip the api tests and vice versa:

# .github/workflows/monorepo-ci.yml
name: Monorepo CI

on: [push, pull_request]

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

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

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

Scheduled Workflows (Cron)

Run workflows on a schedule — useful for nightly builds, database backups, or report generation:

on:
  schedule:
    - cron: '0 2 * * *'   # Every day at 2:00 AM UTC
  workflow_dispatch:        # Also allow manual trigger
Cron format:
┌─ minute (0-59)
│ ┌─ hour (0-23)
│ │ ┌─ day of month (1-31)
│ │ │ ┌─ month (1-12)
│ │ │ │ ┌─ day of week (0-6, 0=Sunday)
│ │ │ │ │
0 2 * * *

Reusable Workflows

Define a workflow once and call it from multiple repositories:

# .github/workflows/reusable-deploy.yml
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      deploy-token:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: ./deploy.sh ${{ inputs.environment }}
        env:
          TOKEN: ${{ secrets.deploy-token }}

Call it from another workflow:

jobs:
  deploy-staging:
    uses: myorg/shared-workflows/.github/workflows/reusable-deploy.yml@main
    with:
      environment: staging
    secrets:
      deploy-token: ${{ secrets.STAGING_TOKEN }}

Essential Workflow Patterns

PatternUse case
on: push + pull_requestRun tests on every change
if: github.ref == 'refs/heads/main'Deploy only from main
needs: [test]Require tests to pass before deploy
strategy.matrixTest across multiple versions in parallel
environment: productionGate deployments with required approvals
workflow_dispatchManual trigger with optional inputs
cache: 'npm'Speed up builds with dependency caching
upload-artifactPass build output between jobs

Summary

  1. Workflows live in .github/workflows/ — YAML files that trigger on events
  2. Jobs run in parallel by default — use needs to create dependencies
  3. Use Secrets for API keys, tokens, and passwords — never hardcode them
  4. Cache dependencies to cut build times from minutes to seconds
  5. Browse the Marketplace — most common tasks already have a maintained Action

Related posts: