What Is GitHub Actions?
GitHub Actions is a CI/CD (Continuous Integration/Deployment) platform built into GitHub. When you push code, tests, builds, and deployments run automatically. Think of it as a factory assembly line: raw materials (code) come in, and quality inspection (tests), assembly (build), and shipping (deployment) happen automatically.
Core Concepts
| Concept | Description | Analogy |
|---|
| Workflow | The entire automation process | Assembly line |
| Event | Trigger that starts the workflow | Start button |
| Job | Unit of work running on the same runner | Workstation |
| Step | Individual command within a job | Work step |
| Runner | Server that executes the workflow | Worker |
| Action | Reusable work unit | Assembly part |
Writing a Basic Workflow
# .github/workflows/ci.yml
name: CI Pipeline
# Define trigger events
on:
push:
branches: [main, develop] # On push to main or develop
pull_request:
branches: [main] # On PR creation/update targeting main
# Environment variables (available throughout the workflow)
env:
NODE_VERSION: "22"
jobs:
# Job 1: Code quality checks
lint:
name: Lint & Type Check
runs-on: ubuntu-latest # Run on latest Ubuntu runner
steps:
# Check out source code
- uses: actions/checkout@v4
# Set up Node.js + caching
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm" # Cache node_modules (speeds up builds)
# Install dependencies
- run: npm ci # ci installs exact versions from lock file
# Run linter
- run: npm run lint
name: ESLint Check
# Type checking
- run: npm run typecheck
name: TypeScript Type Check
# Job 2: Testing
test:
name: Unit Tests
runs-on: ubuntu-latest
needs: lint # Runs only after lint job succeeds
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- run: npm ci
- run: npm test -- --coverage
name: Run Tests (with coverage)
# Upload test coverage results
- uses: actions/upload-artifact@v4
if: always() # Upload even if tests fail
with:
name: coverage-report
path: coverage/
# Job 3: Build
build:
name: Build
runs-on: ubuntu-latest
needs: test # Runs after tests succeed
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: Production Build
# Save build artifacts
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7 # Retain for 7 days
Matrix Builds: Test Across Multiple Environments
# Test simultaneously across multiple Node.js versions and OSes
jobs:
test-matrix:
name: Test (${{ matrix.os }} / Node ${{ matrix.node-version }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
exclude:
# Exclude macOS + Node 18 combination (unnecessary)
- os: macos-latest
node-version: 18
include:
# Add extra settings for a specific combination
- os: ubuntu-latest
node-version: 22
coverage: true # Collect coverage only in this combination
fail-fast: false # Continue running others even if one fails
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm test
# Run only in the combination with coverage setting
- run: npm test -- --coverage
if: matrix.coverage == true
Secrets and Environment Variables
# Secrets are set in GitHub Settings → Secrets and Variables → Actions
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Enables per-environment secrets
steps:
- uses: actions/checkout@v4
# Using secrets — automatically masked in logs
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }} # Secret
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
APP_ENV: production # Regular variable
run: |
echo "Deploy environment: $APP_ENV"
# $API_KEY and $DEPLOY_TOKEN are masked as *** in logs
./deploy.sh
Automated Deployment Workflow
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main] # Deploy only on push to main
# Prevent concurrent deployments
concurrency:
group: production-deploy
cancel-in-progress: false # Don't cancel in-progress deployments
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com # Deployment URL (shown in GitHub)
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm run build
# Docker image build & push
- name: Docker Build and Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
# Deploy to server via SSH
- name: Server Deployment
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /app
docker compose pull
docker compose up -d --remove-orphans
echo "Deployment complete: $(date)"
# Slack notification on success
- name: Deployment Success Notification
if: success()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Deployment success: ${{ github.repository }} (${{ github.sha }})"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
# Slack notification on failure
- name: Deployment Failure Notification
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Deployment failed: ${{ github.repository }} — investigation needed!"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
PR Automation
# .github/workflows/pr-check.yml
name: PR Checks
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
pr-check:
runs-on: ubuntu-latest
# Grant permissions to PR (for commenting, etc.)
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm run build
# Bundle size comparison comment
- name: Bundle Size Report
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Posts bundle size changes as a PR comment
# Auto-label
- uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
Speeding Up Builds with Caching
# Caching strategy — reduce dependency install time
steps:
# npm cache (handled automatically by setup-node)
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
# Custom cache — build cache, etc.
- name: Turbo Cache
uses: actions/cache@v4
with:
path: .turbo # Cache path
key: turbo-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
turbo-${{ runner.os }}-
| Cache Target | Impact | Configuration |
|---|
| npm/yarn/pnpm | 80% reduction in dependency install | setup-node cache option |
| Docker layers | 50% reduction in image build | docker/build-push-action cache-from |
| Build artifacts | Incremental builds | actions/cache for .next/, dist/ |
Practical Tips
- Use
npm ci instead of npm install: In CI environments, npm ci installs exact versions from the lock file and is faster.
- Control concurrent runs with
concurrency: When pushing multiple times to the same branch, previous runs are automatically cancelled to save resources.
- Use
if: always() and if: failure(): These enable follow-up actions like uploading reports or sending notifications even when tests fail.
- Reduce duplication with Reusable Workflows: Share common workflows across repositories with
uses: org/workflows/.github/workflows/ci.yml@main.
- Leverage the GitHub Actions Marketplace: Pre-built Actions exist for most tasks. Search before writing custom scripts.
- Monitor costs: Public repositories are free, but private repositories are billed after 2,000 free minutes per month. Clean up unnecessary workflows.
Was this article helpful?
Thanks for your feedback!