CI/CD with GitHub Actions
Introduction
GitHub Actions automates software workflows directly in your repository. This guide covers CI/CD pipelines, automated testing, Docker builds, deployments, and best practices for modern development workflows.
1. GitHub Actions Basics
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
# Workflow syntax
on: # Trigger events
jobs: # Jobs to run
job-name:
runs-on: # Runner environment
steps: # Steps to execute
2. Multiple Jobs & Matrix Builds
# Matrix strategy - test multiple versions
name: Test Matrix
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [16, 18, 20]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test
# Jobs with dependencies
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
- uses: actions/upload-artifact@v3
with:
name: build-files
path: dist/
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
name: build-files
path: dist/
- run: npm test
deploy:
needs: [build, test]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- run: echo "Deploying..."
3. Docker Build & Push
# Build and push Docker image
name: Docker Build
on:
push:
branches: [main]
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=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
cache-to: type=gha,mode=max
# Multi-platform builds
- name: Build multi-platform
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
push: true
tags: user/app:latest
4. Environment & Secrets
# Using secrets
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
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: Deploy to production
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: |
echo "Deploying with secrets..."
./deploy.sh
# Environment protection rules
# Settings → Environments → New environment
# Add required reviewers, wait timer, deployment branches
# Environment-specific secrets
jobs:
deploy-staging:
environment: staging
steps:
- run: echo "${{ secrets.API_URL }}" # staging API_URL
deploy-prod:
environment: production
steps:
- run: echo "${{ secrets.API_URL }}" # production API_URL
5. Deploy to AWS, Azure, GCP
# Deploy to AWS ECS
name: Deploy to AWS ECS
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 Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push 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
wait-for-service-stability: true
# Deploy to Azure Web App
- name: Deploy to Azure
uses: azure/webapps-deploy@v2
with:
app-name: my-app
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
# Deploy to Google Cloud Run
- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@v1
with:
service: my-service
image: gcr.io/project/image:tag
credentials: ${{ secrets.GCP_SA_KEY }}
6. Kubernetes Deployment
# Deploy to Kubernetes
name: Deploy to K8s
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.28.0'
- name: Configure kubeconfig
env:
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
run: |
mkdir -p ~/.kube
echo "$KUBE_CONFIG" | base64 -d > ~/.kube/config
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/api api=myapp:${{ github.sha }}
kubectl rollout status deployment/api
- name: Verify deployment
run: |
kubectl get pods
kubectl get services
# Using Helm
- name: Deploy with Helm
run: |
helm upgrade --install my-app ./helm-chart \
--set image.tag=${{ github.sha }} \
--set environment=production \
--namespace production
7. Automated Testing
# Comprehensive testing pipeline
name: Test Suite
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm ci
- run: npm run lint
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm ci
- run: npm run test:unit
- name: Upload coverage
uses: codecov/codecov-action@v3
integration-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm ci
- name: Run integration tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
REDIS_URL: redis://localhost:6379
run: npm run test:integration
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: failure()
with:
name: playwright-screenshots
path: test-results/
8. Reusable Workflows
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
aws-access-key:
required: true
aws-secret-key:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.aws-access-key }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.aws-secret-key }}
run: |
echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
# Deployment logic here
# Use reusable workflow
name: Deploy Pipeline
on:
push:
branches: [main]
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
image-tag: ${{ github.sha }}
secrets:
aws-access-key: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
image-tag: ${{ github.sha }}
secrets:
aws-access-key: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
9. Advanced Features
# Conditional execution
jobs:
deploy:
if: github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
# Manual workflow dispatch
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
type: choice
options:
- staging
- production
version:
description: 'Version to deploy'
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: |
echo "Deploying version ${{ inputs.version }} to ${{ inputs.environment }}"
# Scheduled workflows (cron)
on:
schedule:
- cron: '0 2 * * *' # Every day at 2 AM
# Concurrency control
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Caching dependencies
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Composite actions
# .github/actions/setup-app/action.yml
name: 'Setup Application'
description: 'Install and configure app'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm ci
shell: bash
- run: npm run build
shell: bash
# Use composite action
- uses: ./.github/actions/setup-app
10. Monitoring & Notifications
# Slack notification
- name: Slack notification
if: always()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Deployment ${{ job.status }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Deployment Status:* ${{ job.status }}\n*Repository:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
# Discord notification
- name: Discord notification
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: "Deployment"
description: "Deployed to production"
# Create GitHub release
- name: Create Release
if: startsWith(github.ref, 'refs/tags/')
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
# Comment on PR
- name: Comment PR
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '✅ Tests passed! Ready to merge.'
})
11. Best Practices
✓ GitHub Actions Best Practices:
- ✓ Pin action versions to specific commits or tags
- ✓ Use secrets for sensitive data
- ✓ Cache dependencies to speed up workflows
- ✓ Use matrix builds for multiple environments
- ✓ Implement proper error handling
- ✓ Use reusable workflows for common tasks
- ✓ Set appropriate timeouts
- ✓ Use concurrency control to prevent conflicts
- ✓ Implement approval gates for production
- ✓ Monitor workflow execution times
- ✓ Use self-hosted runners for sensitive workloads
- ✓ Keep workflows DRY with composite actions
- ✓ Implement proper testing before deployment
- ✓ Use environment protection rules
- ✓ Document workflows with comments
Conclusion
GitHub Actions provides powerful automation for modern development workflows. Implement CI/CD pipelines with testing, Docker builds, and automated deployments. Always use secrets properly, cache dependencies, and implement appropriate approval gates for production.
💡 Pro Tip: Use the GitHub Actions VS Code extension for syntax highlighting and autocomplete. It helps catch errors early and provides inline documentation. Also enable workflow visualization in GitHub to understand job dependencies at a glance.