AB
The final part of our GitHub Actions guide covers practical applications, real-world examples, integrations with other tools, and troubleshooting techniques.
Welcome to the third and final part of our GitHub Actions guide! In Part 1 we introduced the fundamentals of GitHub Actions, and in Part 2 we explored building advanced workflows. Now, let’s dive into real-world applications, practical examples, and techniques for troubleshooting common issues.
Let’s explore how GitHub Actions enables continuous integration and continuous deployment for various application types.
Here’s a practical workflow for a Node.js application that:
name: Node.js CI Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Run tests with coverage
run: npm test -- --coverage
- name: Upload coverage report
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: coverage/
Key Features:
npm ci
: Uses clean install, which is faster and more reliable for CI environmentsFor static websites or documentation, GitHub Pages provides free hosting directly from your repository. Here’s how to automate deployment:
name: Deploy to GitHub Pages
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm ci
- name: Build static site
run: npm run build
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: build
branch: gh-pages
Key Features:
For applications that rely on Docker, you can build and push images to a registry:
name: Docker CI/CD Pipeline
on:
push:
branches:
- main
tags:
- "v*"
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: username/my-app
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=username/my-app:buildcache
cache-to: type=registry,ref=username/my-app:buildcache,mode=max
Key Features:
GitHub Actions excels at automating routine tasks that would otherwise require manual intervention.
This workflow demonstrates how to schedule regular database backups:
name: Scheduled Database Backup
on:
schedule:
- cron: "0 2 * * *" # Run at 2 AM UTC every day
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up PostgreSQL client
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client
- name: Create backup
run: |
PGPASSWORD=${{ secrets.DB_PASSWORD }} pg_dump \
-h ${{ secrets.DB_HOST }} \
-U ${{ secrets.DB_USER }} \
-d ${{ secrets.DB_NAME }} \
-F c > backup_$(date +%Y-%m-%d).dump
- name: Upload backup to S3
uses: aws-actions/configure-aws-credentials@v2
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: Copy file to S3
run: |
aws s3 cp backup_$(date +%Y-%m-%d).dump s3://my-backup-bucket/
Key Features:
Stay on top of dependencies with automated pull requests for package updates:
name: Dependency Updates
on:
schedule:
- cron: "0 9 * * 1" # Every Monday at 9 AM UTC
jobs:
update-dependencies:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Update dependencies
run: npx npm-check-updates -u
- name: Install updated dependencies
run: npm install
- name: Run tests
run: npm test
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
title: "chore: update dependencies"
commit-message: "chore: update dependencies"
branch: "dependency-updates"
delete-branch: true
Key Features:
Automate maintenance tasks like stale issue/PR management:
name: Repository Maintenance
on:
schedule:
- cron: "30 1 * * *" # 1:30 AM UTC every day
jobs:
stale-management:
runs-on: ubuntu-latest
steps:
- name: Stale issue and PR management
uses: actions/stale@v8
with:
stale-issue-message: "This issue has been automatically marked as stale due to inactivity. It will be closed in 7 days if no further activity occurs."
stale-pr-message: "This PR has been automatically marked as stale due to inactivity. It will be closed in 14 days if no further activity occurs."
stale-issue-label: "stale"
stale-pr-label: "stale"
days-before-stale: 60
days-before-close: 7
days-before-pr-close: 14
exempt-issue-labels: "pinned,security,enhancement"
exempt-pr-labels: "pinned,dependencies"
Key Features:
Let’s explore more complex GitHub Actions scenarios for advanced projects.
This example demonstrates deploying a microservice to multiple environments (development, staging, production) with different configurations:
name: Microservice Deployment
on:
push:
branches:
- develop
- staging
- main
jobs:
determine-environment:
runs-on: ubuntu-latest
outputs:
env-name: ${{ steps.set-env.outputs.env-name }}
steps:
- name: Set environment name
id: set-env
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "env-name=production" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" = "refs/heads/staging" ]; then
echo "env-name=staging" >> $GITHUB_OUTPUT
else
echo "env-name=development" >> $GITHUB_OUTPUT
fi
build-and-deploy:
needs: determine-environment
runs-on: ubuntu-latest
environment: ${{ needs.determine-environment.outputs.env-name }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build and push Docker image
env:
ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ENV_NAME: ${{ needs.determine-environment.outputs.env-name }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$ENV_NAME-${{ github.sha }} .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$ENV_NAME-${{ github.sha }}
- name: Update ECS service
env:
ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ENV_NAME: ${{ needs.determine-environment.outputs.env-name }}
CLUSTER_NAME: ${{ secrets.CLUSTER_NAME }}
SERVICE_NAME: ${{ secrets.SERVICE_NAME }}-${{ needs.determine-environment.outputs.env-name }}
run: |
aws ecs update-service \
--cluster $CLUSTER_NAME \
--service $SERVICE_NAME \
--force-new-deployment
Key Features:
For monorepo projects with multiple components, you can use path filtering to trigger specific workflows:
name: Monorepo CI
on:
push:
branches:
- main
paths:
- "frontend/**"
- "backend/**"
- "common/**"
pull_request:
branches:
- main
paths:
- "frontend/**"
- "backend/**"
- "common/**"
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
common: ${{ steps.filter.outputs.common }}
steps:
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'frontend/**'
- 'common/**'
backend:
- 'backend/**'
- 'common/**'
common:
- 'common/**'
build-frontend:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.frontend == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install and build
working-directory: ./frontend
run: |
npm install
npm run build
- name: Run tests
working-directory: ./frontend
run: npm test
build-backend:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.backend == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.21"
- name: Build and test
working-directory: ./backend
run: |
go build ./...
go test ./...
Key Features:
if
conditions to skip unnecessary jobsThis workflow safely manages database migrations, ensuring they’re applied correctly and can be rolled back if needed:
name: Database Migrations
on:
push:
branches:
- main
paths:
- "migrations/**"
jobs:
validate-migrations:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.21"
- name: Install migrate tool
run: go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
- name: Test migrations (up)
run: |
migrate -path ./migrations -database "postgres://postgres:postgres@localhost:5432/test?sslmode=disable" up
- name: Test migrations (down)
run: |
migrate -path ./migrations -database "postgres://postgres:postgres@localhost:5432/test?sslmode=disable" down -all
deploy-migrations:
needs: validate-migrations
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.21"
- name: Install migrate tool
run: go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
- name: Apply migrations
run: |
migrate -path ./migrations -database "${{ secrets.DATABASE_URL }}" up
Key Features:
GitHub Actions works well with various external tools and services. Let’s explore some common integrations.
Integrate static code analysis tools to maintain code quality:
name: Code Quality
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
sonarcloud:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
command: test
Key Features:
Set up notifications for important workflow events:
name: Build with Notifications
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: |
echo "Building project..."
# Your build steps here
- name: Slack Notification on Success
if: success()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: builds
SLACK_COLOR: good
SLACK_MESSAGE: ":rocket: Build succeeded! ${{ github.repository }}@${{ github.ref }}"
SLACK_TITLE: Build Success
- name: Slack Notification on Failure
if: failure()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: builds
SLACK_COLOR: danger
SLACK_MESSAGE: ":x: Build failed! ${{ github.repository }}@${{ github.ref }}"
SLACK_TITLE: Build Failure
Key Features:
Deploy your application to cloud providers like AWS, Azure, or Google Cloud:
name: Deploy to AWS
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
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: Build application
run: |
npm install
npm run build
- name: Deploy to S3
run: |
aws s3 sync ./build s3://my-website-bucket/ --delete
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"
Key Features:
Even the best-designed workflows can encounter issues. Let’s explore common problems and their solutions.
When a workflow fails, follow these steps to diagnose and fix the issue:
The GitHub Actions logs provide detailed information about each step’s execution. Look for:
For more verbose logs, add this to your workflow:
env:
ACTIONS_RUNNER_DEBUG: true
ACTIONS_STEP_DEBUG: true
step.debug
actionAdd debugging steps to print relevant information:
- name: Debug information
run: |
echo "GitHub context: ${{ toJSON(github) }}"
echo "Job context: ${{ toJSON(job) }}"
echo "Steps context: ${{ toJSON(steps) }}"
echo "Runner context: ${{ toJSON(runner) }}"
echo "Strategy context: ${{ toJSON(strategy) }}"
echo "Matrix context: ${{ toJSON(matrix) }}"
Here are some frequently encountered issues and how to resolve them:
Issue:
Error: Permission denied (publickey).
fatal: Could not read from remote repository.
Solution:
Ensure the GITHUB_TOKEN
has sufficient permissions or use SSH keys with appropriate access:
- name: Checkout repository
uses: actions/checkout@v3
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
Issue:
Error: Input required and not supplied: token
Solution: Check that all required secrets are properly configured in the repository settings and correctly referenced in the workflow:
- name: Use API
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
run: |
if [ -z "$API_TOKEN" ]; then
echo "API_TOKEN is not set. Please check repository secrets."
exit 1
fi
# Use API_TOKEN here
Issue: Build runs out of memory or disk space.
Solution: Optimize your build process or use a larger runner:
jobs:
build:
runs-on: ubuntu-latest
# For larger projects, consider self-hosted runners
# runs-on: self-hosted
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
Improve workflow performance and reliability with these techniques:
Choose the right runner for each job:
jobs:
build:
runs-on: ubuntu-latest
deploy:
runs-on: ubuntu-latest
# or for performance-intensive tasks:
# runs-on: ubuntu-latest-4-core
Cache dependencies and build artifacts to speed up workflow runs:
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
Break down complex workflows to improve maintainability and execution time:
jobs:
build:
# Build job
test:
needs: build
# Test job
deploy:
needs: test
# Deploy job
Let’s conclude with some best practices for GitHub Actions workflows.
Protect your workflows and sensitive data with these security practices:
GITHUB_TOKEN
Restrict token permissions to only what’s needed:
permissions:
contents: read
issues: write
pull-requests: write
Set up environment protection rules for sensitive environments:
Then use these environments in your workflow:
jobs:
deploy:
environment: production
# ...
Before using actions from the marketplace:
- name: Checkout
uses: actions/checkout@a12a3c4d5e6f # Use a specific commit hash
Make your workflows faster and more efficient:
Parallelize tests but avoid unnecessary matrix combinations:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [16, 18]
fail-fast: true # Stop all jobs if one fails
Use conditional execution to skip unnecessary workflows:
jobs:
build:
if: |
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]')
runs-on: ubuntu-latest
# ...
For Docker-based workflows, use multi-stage builds and caching:
# Dockerfile
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
Then in your workflow:
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: myorg/myapp:latest
cache-from: type=registry,ref=myorg/myapp:buildcache
cache-to: type=registry,ref=myorg/myapp:buildcache,mode=max
Help your team understand and maintain workflows:
Add comments to explain complex parts of your workflow:
name: Build and Deploy
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
# Install dependencies and build the project
- name: Checkout code
uses: actions/checkout@v3
# This step sets up the correct Node.js version and enables caching
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: "npm"
# Clean install ensures consistent builds in CI
- name: Install dependencies
run: npm ci
# Build static assets
- name: Build project
run: npm run build
Create reusable workflow templates in .github/workflow-templates/
to maintain consistency across repositories.
Add a README that explains:
Throughout this three-part GitHub Actions guide, we’ve explored everything from the basics to advanced use cases. GitHub Actions provides a powerful platform for automating virtually any aspect of your development workflow, from testing and deployment to maintenance and security checks.
By implementing the patterns, best practices, and troubleshooting techniques discussed in this guide, you’ll be well-equipped to build efficient, reliable, and secure CI/CD pipelines for your projects.
Remember that GitHub Actions is constantly evolving with new features and improvements. Stay updated with the GitHub Actions documentation and the GitHub Changelog to leverage the latest capabilities.
This concludes our GitHub Actions series. Return to Part 1: Introduction and Setup or Part 2: Building Advanced Workflows if you’d like to review previous topics.