CI/CD with GitHub Actions: Complete Guide
CI/CD with GitHub Actions: Complete Guide
GitHub Actions provides powerful automation capabilities for continuous integration and continuous deployment. This guide will help you master CI/CD workflows from basics to advanced patterns.
Understanding GitHub Actions
Key Concepts
- Workflow - Automated process defined in YAML
- Job - A set of steps executed on the same runner
- Step - Individual task within a job
- Action - Reusable unit of code
- Runner - Server that executes workflows
- Event - Trigger that starts a workflow
Workflow File Location
.github/
└── workflows/
├── ci.yml
├── deploy.yml
└── release.yml
Basic Workflow Structure
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
Triggers
Push and Pull Request
on:
push:
branches: [main]
paths:
- 'src/**'
- 'package.json'
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
Scheduled Workflows
on:
schedule:
# Every day at 2 AM UTC
- cron: '0 2 * * *'
# Every Monday at 9 AM UTC
- cron: '0 9 * * 1'
Manual Triggers
on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
version:
description: 'Version to deploy'
required: true
Reusable Workflows
# .github/workflows/called.yml
on:
workflow_call:
inputs:
node-version:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
# .github/workflows/caller.yml
jobs:
call-workflow:
uses: ./.github/workflows/called.yml
with:
node-version: '20'
Jobs and Dependencies
Sequential Jobs
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: npm run deploy
Parallel Jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- run: npm test
type-check:
runs-on: ubuntu-latest
steps:
- run: npm run type-check
deploy:
needs: [lint, test, type-check]
runs-on: ubuntu-latest
steps:
- run: npm run deploy
Matrix Strategy
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: macos-latest
node: 18
include:
- os: ubuntu-latest
node: 20
experimental: true
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
Environment Variables and Secrets
Workflow Environment Variables
env:
NODE_ENV: test
CI: true
jobs:
build:
runs-on: ubuntu-latest
env:
DATABASE_URL: postgres://localhost:5432/test
steps:
- run: echo $NODE_ENV
Secrets
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to production
env:
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }}
run: |
aws s3 sync ./build s3://my-bucket
Storing Secrets
- Go to repository Settings
- Navigate to Secrets and variables > Actions
- Click "New repository secret"
- Add name and value
Caching
NPM Cache
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
Custom Cache
steps:
- uses: actions/checkout@v4
- name: Cache node modules
uses: actions/cache@v4
with:
path: |
node_modules
~/.cache/puppeteer
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
Cache for Multiple Paths
- uses: actions/cache@v4
with:
path: |
~/.npm
~/.cache
node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
Artifacts
Uploading Artifacts
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 5
Downloading Artifacts
jobs:
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- run: npm run deploy
Conditional Execution
jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- run: npm run deploy
notify:
runs-on: ubuntu-latest
if: failure()
needs: [deploy]
steps:
- run: echo "Deployment failed!"
Using Context and Expressions
steps:
- name: Production only
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: npm run deploy:production
- name: Skip draft PRs
if: github.event.pull_request.draft == false
run: npm test
- name: Conditional with environment
if: env.DEPLOY_ENV == 'production'
run: echo "Production deployment"
Deployment Examples
Deploy to GitHub Pages
name: Deploy to GitHub Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './dist'
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
Deploy to AWS
name: Deploy to AWS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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 ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: my-app
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$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
Deploy to Vercel
name: Deploy to Vercel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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'
Notifications
Slack Notifications
jobs:
notify:
runs-on: ubuntu-latest
if: always()
steps:
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Best Practices
1. Use Pin Versions
# Good - Pinned version
- uses: actions/checkout@v4
# Better - Full SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
# Bad - Floating version
- uses: actions/checkout@main
2. Limit Permissions
jobs:
build:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
3. Use Concurrency
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
4. Secure Secrets
# Never echo secrets
- name: Safe secret usage
env:
SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
run: |
# Use secret in command
curl -H "Authorization: Bearer $SECRET_TOKEN" https://api.example.com
5. Use Reusable Workflows
# .github/workflows/shared-setup.yml
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
jobs:
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
Debugging Workflows
Enable Debug Logging
# Add as secret
ACTIONS_STEP_DEBUG: true
ACTIONS_RUNNER_DEBUG: true
SSH into Runner
- name: SSH Debug
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
Conclusion
GitHub Actions provides a powerful, flexible platform for CI/CD automation. By mastering these patterns, you can create robust, maintainable pipelines that improve your development workflow.
Key Takeaways
- Use matrix strategies for testing multiple configurations
- Cache dependencies to speed up workflows
- Store sensitive data in secrets
- Pin action versions for stability
- Use reusable workflows for DRY principles
Start automating your workflows today and focus on what matters: writing great code!