GitHub Actions Tutorial: Complete CI/CD Guide for Modern Development
Complete GitHub Actions tutorial from basics to advanced CI/CD pipelines. Learn workflows, testing, deployment automation, and best practices.
GitHub Actions Tutorial: Complete CI/CD Guide for Modern Development
GitHub Actions has revolutionized how developers build, test, and deploy applications. This comprehensive tutorial will take you from GitHub Actions basics to building sophisticated CI/CD pipelines that can handle any project scale.
What are GitHub Actions?
GitHub Actions is a powerful automation platform integrated directly into GitHub repositories. It enables you to create custom workflows that automatically build, test, package, release, and deploy your code.
Key Benefits
- Native Integration: Built into GitHub, no external CI/CD setup required
- Marketplace: 10,000+ pre-built actions from the community
- Matrix Builds: Test across multiple operating systems and versions
- Cost-Effective: 2,000 free minutes per month for public repositories
- Self-Hosted Runners: Run on your own infrastructure
- Event-Driven: Trigger on any GitHub event
Core Concepts
| Component | Description | Example |
|---|---|---|
| Workflow | Automated process defined in YAML | CI/CD pipeline |
| Job | Set of steps executed on same runner | Build, Test, Deploy |
| Step | Individual task within a job | Run tests, Build Docker image |
| Action | Reusable unit of code | Checkout code, Setup Node.js |
| Runner | Server executing workflows | Ubuntu, Windows, macOS |
Getting Started with GitHub Actions
Workflow File Structure
GitHub Actions workflows are defined in .github/workflows/ directory using YAML files.
# .github/workflows/basic.yml
name: Basic Workflow
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run a simple command
run: echo "Hello, GitHub Actions!"
Workflow Triggers
Push and Pull Request Triggers
on:
push:
branches: [main, develop]
paths: ['src/**', 'tests/**']
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
Scheduled Triggers
on:
schedule:
# Run every day at 2 AM UTC
- cron: '0 2 * * *'
# Run 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
Building Your First CI/CD Pipeline
Node.js Application Example
# .github/workflows/nodejs.yml
name: Node.js CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- name: Checkout repository
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 linting
run: npm run lint
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
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: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-files
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-files
path: dist/
- name: Deploy to production
run: |
echo "Deploying to production..."
# Add your deployment commands here
Python Application Pipeline
# .github/workflows/python.yml
name: Python CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest pytest-cov
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest --cov=src --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
Advanced GitHub Actions Patterns
Reusable Workflows
Create reusable workflows to avoid duplication across repositories.
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
DEPLOY_TOKEN:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Deploy application
run: |
echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
# Deployment logic here
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Using Reusable Workflow
# .github/workflows/main.yml
name: Main Pipeline
on:
push:
branches: [main]
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
image-tag: ${{ github.sha }}
secrets:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
Custom Actions
Create custom actions for reusable functionality.
# .github/actions/setup-app/action.yml
name: 'Setup Application'
description: 'Setup Node.js and install dependencies'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '18'
outputs:
cache-hit:
description: 'Whether dependencies were cached'
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Cache dependencies
id: cache
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
shell: bash
Matrix Builds for Cross-Platform Testing
name: Cross-Platform Testing
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
include:
- os: ubuntu-latest
node-version: 20
coverage: true
exclude:
- os: windows-latest
node-version: 16
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Generate coverage report
if: matrix.coverage
run: npm run test:coverage
Docker Integration
Building and Pushing Docker Images
# .github/workflows/docker.yml
name: Docker Build and Push
on:
push:
branches: [main]
tags: ['v*']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into registry ${{ env.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}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Multi-Stage Docker Builds
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Security Best Practices
Using Secrets Safely
name: Secure Deployment
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ vars.AWS_REGION }}
run: |
aws s3 sync ./dist s3://${{ vars.S3_BUCKET_NAME }}
OIDC Authentication
name: Deploy with OIDC
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: us-east-1
- name: Deploy to AWS
run: |
aws s3 sync ./dist s3://${{ vars.S3_BUCKET_NAME }}
Dependency Scanning
name: Security Scanning
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
Environment Management
Environment Protection Rules
name: Multi-Environment Deployment
on:
push:
branches: [main, develop]
jobs:
deploy-staging:
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: echo "Deploying to staging..."
deploy-production:
if: github.ref == 'refs/heads/main'
needs: [deploy-staging]
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.com
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: echo "Deploying to production..."
Dynamic Environment URLs
- name: Deploy Preview
id: deploy
run: |
# Deploy logic here
echo "url=https://pr-${{ github.event.number }}.preview.myapp.com" >> $GITHUB_OUTPUT
- name: Update deployment status
uses: actions/github-script@v7
with:
script: |
github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: context.payload.deployment.id,
state: 'success',
environment_url: '${{ steps.deploy.outputs.url }}'
});
Performance Optimization
Caching Strategies
Dependency Caching
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.npm
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
Build Caching
- name: Cache build output
uses: actions/cache@v3
with:
path: |
dist
.next
key: ${{ runner.os }}-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
Conditional Job Execution
jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
backend: ${{ steps.changes.outputs.backend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
frontend:
- 'frontend/**'
backend:
- 'backend/**'
test-frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- name: Test frontend
run: echo "Testing frontend..."
test-backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- name: Test backend
run: echo "Testing backend..."
Monitoring and Notifications
Slack Notifications
- name: Notify Slack on failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
channel: '#deployments'
message: |
Deployment failed for ${{ github.repository }}
Branch: ${{ github.ref }}
Commit: ${{ github.sha }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
GitHub Status Checks
- name: Update commit status
uses: actions/github-script@v7
with:
script: |
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'success',
target_url: 'https://example.com/build/123',
description: 'Build completed successfully',
context: 'continuous-integration/github-actions'
});
Advanced Deployment Patterns
Blue-Green Deployment
name: Blue-Green Deployment
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Determine target environment
id: target
run: |
CURRENT=$(curl -s https://api.myapp.com/health | jq -r '.environment')
if [ "$CURRENT" == "blue" ]; then
echo "target=green" >> $GITHUB_OUTPUT
else
echo "target=blue" >> $GITHUB_OUTPUT
fi
- name: Deploy to ${{ steps.target.outputs.target }}
run: |
echo "Deploying to ${{ steps.target.outputs.target }} environment"
# Deployment logic here
- name: Run health checks
run: |
curl -f https://${{ steps.target.outputs.target }}.myapp.com/health
- name: Switch traffic
run: |
# Update load balancer to point to new environment
echo "Switching traffic to ${{ steps.target.outputs.target }}"
Canary Deployment
name: Canary Deployment
on:
push:
branches: [main]
jobs:
canary:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy canary (10% traffic)
run: |
echo "Deploying canary with 10% traffic"
# Deploy with 10% traffic
- name: Monitor metrics
run: |
sleep 300 # Wait 5 minutes
# Check error rates, response times, etc.
- name: Promote to full deployment
run: |
echo "Promoting canary to 100% traffic"
# Promote if metrics are good
Troubleshooting Common Issues
Debug Actions
- name: Debug information
run: |
echo "GitHub context:"
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Repository: ${{ github.repository }}"
echo "Runner information:"
echo "OS: ${{ runner.os }}"
echo "Architecture: ${{ runner.arch }}"
echo "Environment variables:"
env | sort
Common Error Solutions
1. Permission Denied
# Add required permissions to job
permissions:
contents: read
packages: write
id-token: write
2. Artifact Upload/Download Issues
- name: Upload with proper path
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
dist/
!dist/**/*.map
retention-days: 30
3. Matrix Build Failures
strategy:
fail-fast: false # Continue other jobs if one fails
matrix:
include:
- os: ubuntu-latest
experimental: false
- os: windows-latest
experimental: true
continue-on-error: ${{ matrix.experimental }}
Best Practices Summary
Workflow Organization
- ✅ Use descriptive names for workflows and jobs
- ✅ Organize workflows by trigger type (CI, CD, scheduled)
- ✅ Use reusable workflows to avoid duplication
- ✅ Document complex workflows with comments
Performance
- ✅ Cache dependencies and build outputs
- ✅ Use conditional execution for changed files
- ✅ Minimize checkout depth when possible
- ✅ Use matrix builds efficiently
Security
- ✅ Use OIDC instead of long-lived credentials
- ✅ Store secrets in GitHub Secrets
- ✅ Limit permissions to minimum required
- ✅ Scan for vulnerabilities regularly
Maintenance
- ✅ Pin action versions using commit SHA
- ✅ Update actions regularly with Dependabot
- ✅ Monitor workflow run times and costs
- ✅ Clean up old artifacts automatically
Real-World Example: Full Stack Application
# .github/workflows/fullstack.yml
name: Full Stack CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '18'
PYTHON_VERSION: '3.11'
jobs:
test-frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./frontend
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:ci
- name: Build
run: npm run build
test-backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./backend
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests
run: |
pytest --cov=src --cov-report=xml
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
deploy:
needs: [test-frontend, test-backend]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "Deploying full stack application..."
# Add deployment commands here
Conclusion
GitHub Actions provides a powerful, integrated CI/CD platform that can handle any development workflow. By mastering these patterns and best practices, you'll be able to:
- Automate entire development lifecycle from code to production
- Build robust, secure pipelines with proper error handling
- Scale workflows across multiple environments and platforms
- Implement advanced deployment strategies like blue-green and canary
- Monitor and optimize pipeline performance and costs
Next Steps
- Start Simple: Begin with basic CI workflows
- Iterate and Improve: Add complexity as needed
- Monitor Performance: Track workflow execution times
- Security First: Implement scanning and secret management
- Scale Gradually: Move to advanced patterns as you grow
Need help implementing GitHub Actions for your team? Schedule a consultation to design custom CI/CD pipelines that accelerate your development workflow.
Related Resources
Share this article
Related Articles
Continue your learning journey with these related posts
GitHub Actions CI/CD: Complete DevOps Guide for Startups 2025 | Save 70% on CI Costs
Build enterprise-grade CI/CD pipelines with GitHub Actions. Complete startup guide with cost optimization strategies, automated testing workflows, and production deployment patterns that save 70% on CI costs.
Complete Docker Tutorial: A Beginner's Guide to Containerization in 2025
Complete step-by-step Docker tutorial covering everything from installation to production deployment. Perfect for beginners wanting to master containerization.
Complete Guide to Self-Hosting n8n on Google Cloud Platform with Auto-Updates
A complete walkthrough of deploying n8n workflow automation platform on Google Cloud Platform with zero monthly costs, automatic updates, and professional SSL setup.