GitHub Actions has become the go-to CI/CD platform for modern development teams, offering powerful automation directly integrated with your code repositories. This comprehensive guide covers everything from basic workflows to advanced patterns used in production.
Why GitHub Actions?
GitHub Actions provides several advantages:
- Native Integration: Built directly into GitHub
- Free Tier: 2,000 minutes/month for private repos, unlimited for public
- Marketplace: Thousands of pre-built actions
- Matrix Builds: Test across multiple environments simultaneously
- Self-Hosted Runners: Run on your own infrastructure
- Secrets Management: Secure credential storage
- Reusable Workflows: DRY principle for CI/CD
Core Concepts
Workflows
YAML files in .github/workflows/
that define automation
Events
Triggers that start workflows (push, pull_request, schedule, etc.)
Jobs
Groups of steps that run on the same runner
Steps
Individual tasks that run commands or actions
Actions
Reusable units of code (from Marketplace or custom)
Runners
Servers that execute workflows (GitHub-hosted or self-hosted)
Basic Workflow Structure
# .github/workflows/ci.yml
name: CI Pipeline
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: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build application
run: npm run build
Advanced Triggers
on:
# Multiple events
push:
branches:
- main
- 'release/**'
paths:
- 'src/**'
- 'package.json'
paths-ignore:
- 'docs/**'
- '**.md'
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
# Schedule (cron)
schedule:
- cron: '0 2 * * *' # Daily at 2 AM UTC
# Manual trigger
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
type: choice
options:
- development
- staging
- production
version:
description: 'Version to deploy'
required: true
type: string
# Release published
release:
types: [published]
# Issue/PR events
issues:
types: [opened, labeled]
# Workflow call (reusable)
workflow_call:
inputs:
config-path:
required: true
type: string
Matrix Strategy
Test across multiple versions and platforms:
name: Matrix Build
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
include:
- os: ubuntu-latest
node-version: 18
experimental: false
- os: ubuntu-latest
node-version: 21
experimental: true
exclude:
- os: windows-latest
node-version: 16
fail-fast: false
max-parallel: 4
continue-on-error: ${{ matrix.experimental || false }}
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: Upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '18'
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
Docker Build and Push
name: Docker Build and Push
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 code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to 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=semver,pattern={{major}}
type=sha,prefix={{branch}}-
- name: Build and push
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=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
Kubernetes Deployment
name: Deploy to Kubernetes
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: $
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
env:
ECR_REGISTRY: $
IMAGE_TAG: $
run: |
docker build -t $ECR_REGISTRY/myapp:$IMAGE_TAG .
docker push $ECR_REGISTRY/myapp:$IMAGE_TAG
echo "image=$ECR_REGISTRY/myapp:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Configure kubectl
run: |
aws eks update-kubeconfig --name my-cluster --region us-east-1
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/myapp myapp=$/myapp:$
kubectl rollout status deployment/myapp
Reusable Workflows
Callable Workflow
# .github/workflows/reusable-build.yml
name: Reusable Build Workflow
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '18'
environment:
required: true
type: string
secrets:
deploy-token:
required: true
outputs:
build-id:
description: "Build identifier"
value: $
jobs:
build:
runs-on: ubuntu-latest
outputs:
build-id: $
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: $
- name: Install and build
run: |
npm ci
npm run build
- name: Generate build ID
id: build
run: echo "id=build-$" >> $GITHUB_OUTPUT
Calling the Workflow
# .github/workflows/deploy-staging.yml
name: Deploy to Staging
on:
push:
branches: [develop]
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '18'
environment: 'staging'
secrets:
deploy-token: $
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy
run: |
echo "Deploying build: $"
Composite Actions
Create custom reusable actions:
# .github/actions/setup-app/action.yml
name: 'Setup Application'
description: 'Install dependencies and setup caching'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '18'
cache-key:
description: 'Cache key prefix'
required: false
default: 'npm'
outputs:
cache-hit:
description: 'Whether cache was hit'
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: ${{ inputs.cache-key }}-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ inputs.cache-key }}-${{ runner.os }}-
- name: Install dependencies
shell: bash
run: npm ci
Use the composite action:
# .github/workflows/test.yml
name: Test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup application
uses: ./.github/actions/setup-app
with:
node-version: '18'
cache-key: 'test'
- name: Run tests
run: npm test
Environment Deployments
name: Deploy to Environments
on:
push:
branches: [main]
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to Staging
run: |
echo "Deploying to staging..."
# Deployment commands
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to Production
run: |
echo "Deploying to production..."
# Deployment commands
Caching Strategies
name: Build with Caching
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Cache Node modules
- name: Cache Node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Cache build output
- name: Cache build
uses: actions/cache@v3
with:
path: dist
key: ${{ runner.os }}-build-${{ github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm' # Built-in caching
- name: Install and build
run: |
npm ci
npm run build
Artifacts
name: Build and Share Artifacts
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build application
run: |
npm ci
npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-$
path: dist/
retention-days: 7
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: test-results/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: build-$
path: dist/
- name: Deploy
run: |
echo "Deploying artifacts..."
Security Scanning
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 0' # Weekly
jobs:
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
env:
SNYK_TOKEN: $
with:
args: --severity-threshold=high
code-scan:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: javascript, python
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
container-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:$ .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:$
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
Self-Hosted Runners
name: Self-Hosted Runner
on: [push]
jobs:
build:
runs-on: [self-hosted, linux, x64, gpu]
steps:
- uses: actions/checkout@v4
- name: Build with GPU
run: |
echo "Running on self-hosted runner with GPU"
# GPU-intensive tasks
Setup Self-Hosted Runner
# On your server
mkdir actions-runner && cd actions-runner
# Download latest runner
curl -o actions-runner-linux-x64-2.311.0.tar.gz \
-L https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
# Extract
tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz
# Configure
./config.sh --url https://github.com/yourorg/yourrepo --token YOUR_TOKEN
# Install and start service
sudo ./svc.sh install
sudo ./svc.sh start
# Check status
sudo ./svc.sh status
Secrets Management
name: Deploy with Secrets
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Organization secret
- name: Use organization secret
run: echo $
# Repository secret
- name: Use repository secret
run: echo $
# Environment secret
- name: Deploy to production
environment: production
run: |
echo $
# Azure Key Vault
- name: Get secrets from Azure Key Vault
uses: Azure/get-keyvault-secrets@v1
with:
keyvault: "myKeyVault"
secrets: 'mySecret'
id: azureSecrets
- name: Use Azure secret
run: echo $
Conditional Execution
name: Conditional Workflow
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run on main branch only
if: github.ref == 'refs/heads/main'
run: echo "Main branch"
- name: Run on PR only
if: github.event_name == 'pull_request'
run: echo "Pull request"
- name: Run on specific actor
if: github.actor == 'dependabot[bot]'
run: echo "Dependabot update"
- name: Run based on file changes
id: changes
uses: dorny/paths-filter@v2
with:
filters: |
src:
- 'src/**'
tests:
- 'tests/**'
- name: Build if source changed
if: steps.changes.outputs.src == 'true'
run: npm run build
- name: Test if tests changed
if: steps.changes.outputs.tests == 'true'
run: npm test
Status Badges
Add to your README.md:


Best Practices
✅ Use specific action versions (uses: actions/checkout@v4
)
✅ Cache dependencies to speed up workflows
✅ Use matrix builds for multi-platform testing
✅ Implement security scanning in CI pipeline
✅ Use reusable workflows for DRY principles
✅ Set appropriate timeouts to prevent hanging jobs
✅ Use environments for deployment approvals
✅ Leverage composite actions for repeated steps
✅ Monitor workflow costs and optimize runtime
✅ Use concurrency groups to cancel outdated runs
Concurrency Control
name: Deploy
on: [push]
concurrency:
group: $-$
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
run: echo "Deploying..."
Troubleshooting
name: Debug Workflow
on: [push]
jobs:
debug:
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
run: echo '${{ toJSON(github) }}'
- name: Dump job context
run: echo '${{ toJSON(job) }}'
- name: Dump runner context
run: echo '${{ toJSON(runner) }}'
- name: Enable debug logging
run: echo "::debug::This is a debug message"
- name: Set step output
id: test
run: echo "result=success" >> $GITHUB_OUTPUT
- name: Use step output
run: echo "Result was ${{ steps.test.outputs.result }}"
Conclusion
GitHub Actions provides a powerful, flexible platform for CI/CD automation. By leveraging reusable workflows, matrix builds, and the extensive marketplace, you can create sophisticated pipelines that improve code quality, accelerate deployments, and enhance team productivity.
Resources
What’s your favorite GitHub Actions workflow? Share in the comments!