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
| Term | What it is |
|---|---|
| Workflow | A YAML file in .github/workflows/ that defines automation |
| Event | What triggers the workflow (push, pull_request, schedule, etc.) |
| Job | A set of steps that run on the same runner |
| Step | A single command or Action within a job |
| Action | A reusable unit of work (from Marketplace or your own repo) |
| Runner | The 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:
| Condition | When 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
| Pattern | Use case |
|---|---|
on: push + pull_request | Run tests on every change |
if: github.ref == 'refs/heads/main' | Deploy only from main |
needs: [test] | Require tests to pass before deploy |
strategy.matrix | Test across multiple versions in parallel |
environment: production | Gate deployments with required approvals |
workflow_dispatch | Manual trigger with optional inputs |
cache: 'npm' | Speed up builds with dependency caching |
upload-artifact | Pass build output between jobs |
Summary
- Workflows live in
.github/workflows/— YAML files that trigger on events - Jobs run in parallel by default — use
needsto create dependencies - Use Secrets for API keys, tokens, and passwords — never hardcode them
- Cache dependencies to cut build times from minutes to seconds
- Browse the Marketplace — most common tasks already have a maintained Action
Related posts: