Basic Building Blocks and Components
Fundamentals
GitHub Actions is a CI/CD platform that automates your build, test, and deployment pipeline.
Workflows are defined in YAML files stored in .github/workflows/.
Core Components
🔄 Workflow
An automated process defined in YAML. Can contain one or more jobs.
⚡ Event
Triggers that start a workflow (push, pull_request, schedule, etc.)
💼 Job
A set of steps that execute on the same runner. Jobs run in parallel by default.
📝 Step
Individual task that runs commands or actions. Steps run sequentially.
🎬 Action
Reusable unit of code. Can be from marketplace or custom-built.
🖥️ Runner
Server that runs your workflows. Can be GitHub-hosted or self-hosted.
Basic Workflow Structure
name: CI Pipeline
on: [push, pull_request]
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'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Workflow File Location
my-repo/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── deploy.yml
│ └── release.yml
├── src/
└── package.json
Workflows and Events Deep Dive
EventsEvents are specific activities that trigger workflows. GitHub Actions supports dozens of event types for different scenarios.
Common Trigger Events
# Single event
on: push
# Multiple events
on: [push, pull_request, workflow_dispatch]
# Event with filters
on:
push:
branches:
- main
- 'releases/**'
paths:
- 'src/**'
- '!src/docs/**'
tags:
- v1.*
pull_request:
branches:
- main
types: [opened, synchronize, reopened]
# Scheduled workflows (cron)
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM UTC
- cron: '0 */6 * * *' # Every 6 hours
# Manual trigger
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'staging'
type: choice
options:
- staging
- production
debug_enabled:
description: 'Enable debug mode'
required: false
type: boolean
Event Types Reference
Code Events
push- Code pushed to repopull_request- PR opened/updatedpull_request_target- PR from forkcreate- Branch/tag createddelete- Branch/tag deleted
Issue & PR Events
issues- Issue activityissue_comment- Comment addedpull_request_review- PR reviewedpull_request_review_comment
Release Events
release- Release publishedworkflow_run- After workflowrepository_dispatch- External
Other Events
schedule- Cron-basedworkflow_dispatch- Manualworkflow_call- Reusable
Activity Types
on:
pull_request:
types:
- opened # PR first opened
- synchronize # New commits pushed
- reopened # Closed PR reopened
- closed # PR closed/merged
- labeled # Label added
- unlabeled # Label removed
- assigned # Assignee added
issues:
types: [opened, edited, deleted, closed, reopened]
release:
types: [published, created, edited, deleted]
pull_request_target — it runs
in the context of the base repository and has access to secrets, even for PRs from forks.
Only use it when necessary and validate inputs carefully.
Job Artifacts and Outputs
Data FlowArtifacts allow you to persist data after a job completes and share data between jobs. Outputs let you pass data between jobs in the same workflow.
Uploading Artifacts
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: |
dist/
build/
retention-days: 7
- name: Upload test results
if: always() # Upload even if tests fail
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results/
if-no-files-found: warn
Downloading Artifacts
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build
run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: ./dist
- name: Deploy
run: ./deploy.sh
Job Outputs
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
build_number: ${{ steps.build.outputs.number }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: get_version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build
id: build
run: |
npm run build
BUILD_NUM=$(date +%Y%m%d%H%M%S)
echo "number=$BUILD_NUM" >> $GITHUB_OUTPUT
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy version
run: |
echo "Deploying version ${{ needs.build.outputs.version }}"
echo "Build number: ${{ needs.build.outputs.build_number }}"
Matrix Outputs
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
outputs:
# Outputs from matrix jobs need special handling
test-results: ${{ steps.test.outputs.results }}
steps:
- name: Run tests
id: test
run: npm test
retention-days). They count against your storage quota. Clean up old artifacts
regularly or use shorter retention periods for temporary data.
Using Environment Variables and Secrets
ConfigurationEnvironment variables and secrets allow you to configure workflows without hardcoding values. Secrets are encrypted and should be used for sensitive data.
Environment Variables
# Workflow-level env vars
env:
NODE_VERSION: '20'
APP_NAME: 'my-app'
jobs:
build:
runs-on: ubuntu-latest
# Job-level env vars
env:
BUILD_ENV: production
DEBUG: false
steps:
- uses: actions/checkout@v4
# Step-level env vars
- name: Build
env:
API_URL: https://api.example.com
run: |
echo "Building $APP_NAME"
echo "Node version: $NODE_VERSION"
echo "Environment: $BUILD_ENV"
npm run build
# Using env vars in expressions
- name: Deploy
if: env.BUILD_ENV == 'production'
run: npm run deploy
Default Environment Variables
steps:
- name: Print GitHub context
run: |
echo "Repository: $GITHUB_REPOSITORY"
echo "Branch: $GITHUB_REF_NAME"
echo "Commit SHA: $GITHUB_SHA"
echo "Actor: $GITHUB_ACTOR"
echo "Workflow: $GITHUB_WORKFLOW"
echo "Run ID: $GITHUB_RUN_ID"
echo "Run Number: $GITHUB_RUN_NUMBER"
echo "Event: $GITHUB_EVENT_NAME"
Using Secrets
jobs:
deploy:
runs-on: ubuntu-latest
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: ${{ secrets.AWS_REGION }}
run: |
aws s3 sync ./dist s3://my-bucket
- name: Notify Slack
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
payload: |
{
"text": "Deployment completed!"
}
GitHub Context Variables
steps:
- name: Use GitHub context
run: |
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Repository: ${{ github.repository }}"
echo "Repository Owner: ${{ github.repository_owner }}"
echo "Head Ref: ${{ github.head_ref }}"
echo "Base Ref: ${{ github.base_ref }}"
- name: PR information
if: github.event_name == 'pull_request'
run: |
echo "PR Number: ${{ github.event.pull_request.number }}"
echo "PR Title: ${{ github.event.pull_request.title }}"
echo "PR Author: ${{ github.event.pull_request.user.login }}"
Setting Up Secrets
- Go to your repository on GitHub
- Click Settings → Secrets and variables → Actions
- Click New repository secret
- Enter name and value, then click Add secret
Controlling Workflow and Job Execution
Control FlowControl when and how jobs and steps run using conditions, dependencies, and concurrency controls.
Job Dependencies
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
test:
needs: build # Wait for build to complete
runs-on: ubuntu-latest
steps:
- run: npm test
deploy-staging:
needs: [build, test] # Wait for multiple jobs
runs-on: ubuntu-latest
steps:
- run: ./deploy-staging.sh
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- run: ./deploy-production.sh
Conditional Execution
jobs:
deploy:
runs-on: ubuntu-latest
# Job-level condition
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
# Step runs only on success (default)
- name: Build
run: npm run build
# Step runs even if previous steps failed
- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: logs
path: logs/
# Step runs only on failure
- name: Notify on failure
if: failure()
run: ./notify-failure.sh
# Step runs only on success
- name: Deploy
if: success()
run: ./deploy.sh
# Step runs only if job was cancelled
- name: Cleanup
if: cancelled()
run: ./cleanup.sh
# Complex conditions
- name: Deploy to production
if: |
github.ref == 'refs/heads/main' &&
github.event_name == 'push' &&
!contains(github.event.head_commit.message, '[skip ci]')
run: ./deploy-prod.sh
Concurrency Control
# Workflow-level concurrency
name: Deploy
on:
push:
branches: [main]
concurrency:
group: production-deploy
cancel-in-progress: false # Don't cancel running deployments
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
---
# Per-branch concurrency
name: CI
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # Cancel old runs for same branch
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
Timeouts and Continue on Error
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30 # Job timeout
steps:
- uses: actions/checkout@v4
- name: Run tests
timeout-minutes: 10 # Step timeout
run: npm test
- name: Optional linting
continue-on-error: true # Don't fail job if this fails
run: npm run lint
- name: Deploy
run: ./deploy.sh
Status Check Functions
Condition Functions
success()- All previous steps succeededfailure()- Any previous step failedcancelled()- Workflow was cancelledalways()- Always run
Expression Functions
contains()- String/array containsstartsWith()- String starts withendsWith()- String ends withformat()- Format stringjoin()- Join array
concurrency to prevent multiple deployments from
running simultaneously or to cancel outdated CI runs when new commits are pushed.
Jobs and Docker Containers
ContainersRun jobs inside Docker containers for consistent environments or use service containers for dependencies like databases.
Running Jobs in Containers
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:20-alpine
env:
NODE_ENV: test
options: --cpus 2 --memory 4g
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
Service Containers
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Run tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
run: npm test
Using Private Container Registry
jobs:
build:
runs-on: ubuntu-latest
container:
image: ghcr.io/myorg/myimage:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- run: echo "Running in private container"
Building and Pushing Docker Images
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
myorg/myapp:latest
myorg/myapp:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Building and Using Custom Actions
ActionsCreate reusable actions to share logic across workflows or with the community. Actions can be JavaScript, Docker, or composite (shell scripts).
Types of Actions
JavaScript Actions
Fast, cross-platform. Run directly on runner. Best for most use cases.
Docker Actions
Consistent environment. Linux only. Good for specific dependencies.
Composite Actions
Combine multiple steps. Simple, no code required. Great for reusing workflow patterns.
Composite Action Example
# .github/actions/setup-node-cache/action.yml
name: 'Setup Node with Cache'
description: 'Setup Node.js and cache dependencies'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
working-directory:
description: 'Working directory'
required: false
default: '.'
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: ${{ inputs.working-directory }}/node_modules
key: ${{ runner.os }}-node-${{ hashFiles(format('{0}/package-lock.json', inputs.working-directory)) }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
working-directory: ${{ inputs.working-directory }}
run: npm ci
Using Custom Composite Action
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node with caching
uses: ./.github/actions/setup-node-cache
with:
node-version: '20'
working-directory: './frontend'
- run: npm run build
JavaScript Action Structure
# action.yml
name: 'Hello World'
description: 'Greet someone'
inputs:
who-to-greet:
description: 'Who to greet'
required: true
default: 'World'
outputs:
time:
description: 'The time we greeted you'
runs:
using: 'node20'
main: 'dist/index.js'
// index.js
const core = require('@actions/core');
const github = require('@actions/github');
try {
const nameToGreet = core.getInput('who-to-greet');
console.log(`Hello ${nameToGreet}!`);
const time = (new Date()).toTimeString();
core.setOutput('time', time);
// Get the JSON webhook payload
const payload = JSON.stringify(github.context.payload, undefined, 2);
console.log(`The event payload: ${payload}`);
} catch (error) {
core.setFailed(error.message);
}
Docker Action Example
# action.yml
name: 'Container Action'
description: 'Run in Docker container'
inputs:
myInput:
description: 'Input parameter'
required: true
runs:
using: 'docker'
image: 'Dockerfile'
args:
- ${{ inputs.myInput }}
# Dockerfile
FROM alpine:3.18
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Security and Permissions
SecuritySecure your workflows with proper permissions, secret management, and security best practices.
GITHUB_TOKEN Permissions
# Workflow-level permissions (applies to all jobs)
permissions:
contents: read
pull-requests: write
issues: write
jobs:
build:
runs-on: ubuntu-latest
# Job-level permissions (overrides workflow-level)
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- run: npm run build
---
# Minimal permissions (recommended default)
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
id-token: write # For OIDC
steps:
- run: ./deploy.sh
Available Permission Scopes
Common Permissions
contents- Repository contentspull-requests- PRsissues- Issuespackages- GitHub Packagesdeployments- Deployments
Advanced Permissions
id-token- OIDC tokenactions- Workflow runschecks- Check runsstatuses- Commit statusessecurity-events- Code scanning
Environment Protection Rules
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- run: ./deploy.sh production
Configure environment protection rules in repository settings:
- Required reviewers - Require approval before deployment
- Wait timer - Delay deployment by specified time
- Deployment branches - Restrict which branches can deploy
- Environment secrets - Secrets specific to environment
OpenID Connect (OIDC) for Cloud Authentication
jobs:
deploy-aws:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- name: Deploy to AWS
run: |
aws s3 sync ./dist s3://my-bucket
aws cloudfront create-invalidation --distribution-id ABCD1234
---
deploy-azure:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Azure
run: az webapp deploy --name myapp --resource-group mygroup
Security Best Practices
- ✅ Use minimal permissions (
permissions: contents: readby default) - ✅ Pin actions to full commit SHA, not tags (
actions/checkout@8e5e7e5a...) - ✅ Never log secrets or use them in URLs
- ✅ Use OIDC instead of long-lived credentials when possible
- ✅ Review third-party actions before using them
- ✅ Use environment protection rules for production deployments
- ✅ Enable branch protection rules
- ✅ Use
pull_requestinstead ofpull_request_targetwhen possible - ✅ Validate and sanitize inputs in custom actions
- ✅ Use Dependabot to keep actions updated
Script Injection Prevention
# ❌ VULNERABLE - Don't do this
- name: Print PR title
run: echo "PR title is ${{ github.event.pull_request.title }}"
# ✅ SAFE - Use environment variables
- name: Print PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "PR title is $PR_TITLE"
# ✅ SAFE - Use intermediate step output
- name: Get PR title
id: pr
run: echo "title=${{ github.event.pull_request.title }}" >> $GITHUB_OUTPUT
- name: Print PR title
run: echo "PR title is ${{ steps.pr.outputs.title }}"
Matrix Builds and Strategy
ScalingMatrix strategies let you run the same job across multiple configurations in parallel, perfect for testing across different OS, language versions, or configurations.
Basic Matrix
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 21]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
This creates 9 jobs (3 OS × 3 Node versions) that run in parallel.
Matrix with Include/Exclude
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
# Exclude specific combinations
exclude:
- os: macos-latest
node-version: 18
# Add specific combinations with extra properties
include:
- os: ubuntu-latest
node-version: 21
experimental: true
- os: windows-latest
node-version: 20
npm-cache: 'C:\npm-cache'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Matrix with Custom Variables
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- target: linux-x64
os: ubuntu-latest
artifact: myapp-linux-x64
- target: linux-arm64
os: ubuntu-latest
artifact: myapp-linux-arm64
- target: darwin-x64
os: macos-latest
artifact: myapp-darwin-x64
- target: win-x64
os: windows-latest
artifact: myapp-win-x64.exe
steps:
- uses: actions/checkout@v4
- name: Build for ${{ matrix.target }}
run: npm run build -- --target=${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: dist/${{ matrix.artifact }}
Fail-Fast and Continue on Error
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
# Don't cancel all jobs if one fails
fail-fast: false
# Limit concurrent jobs
max-parallel: 3
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 21]
include:
- os: ubuntu-latest
node-version: 22
experimental: true
# Allow experimental builds to fail
continue-on-error: ${{ matrix.experimental == true }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Dynamic Matrix from JSON
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- name: Generate matrix
id: set-matrix
run: |
MATRIX=$(jq -c . < .github/test-matrix.json)
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
test:
needs: generate-matrix
runs-on: ${{ matrix.os }}
strategy:
matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
steps:
- run: echo "Testing on ${{ matrix.os }} with ${{ matrix.version }}"
fail-fast: false when you want to see all test
results even if some fail. Use max-parallel to limit concurrent jobs and avoid
overwhelming external services or hitting rate limits.
Caching Dependencies
PerformanceCaching speeds up workflows by reusing dependencies and build outputs between runs. GitHub provides 10GB of cache storage per repository.
Basic Caching with actions/cache
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
Built-in Caching with Setup Actions
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Node.js with built-in caching
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # or 'yarn', 'pnpm'
- run: npm ci
- run: npm run build
---
python-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Python with built-in caching
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- run: pip install -r requirements.txt
- run: python -m pytest
---
java-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Java with built-in caching
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven' # or 'gradle'
- run: mvn clean install
Multiple Cache Paths
- name: Cache dependencies and build
uses: actions/cache@v3
with:
path: |
~/.npm
~/.cache
node_modules
.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
${{ runner.os }}-nextjs-
Cache Management
- name: Cache with save/restore
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
- name: Install if cache miss
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
# Explicitly save cache (useful for conditional caching)
- name: Save cache
if: success()
uses: actions/cache/save@v3
with:
path: node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
# Restore cache only (don't save)
- name: Restore cache
uses: actions/cache/restore@v3
with:
path: node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
Docker Layer Caching
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build with cache
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Language-Specific Cache Paths
Node.js / npm
~/.npm(Linux/macOS)%AppData%/npm-cache(Windows)node_modules
Python / pip
~/.cache/pip(Linux/macOS)~\AppData\Local\pip\Cache(Windows)
Ruby / Bundler
vendor/bundle~/.bundle
Go
~/go/pkg/mod~/.cache/go-build
- Use
hashFiles()to create cache keys based on dependency files - Provide
restore-keysfor partial cache matches - Cache is immutable - changing key creates new cache
- Caches are scoped to branch and can be accessed by child branches
- Unused caches are evicted after 7 days
- Total cache size limit is 10GB per repository
Reusable Workflows
DRYReusable workflows let you call entire workflows from other workflows, reducing duplication and centralizing common patterns.
Creating a Reusable Workflow
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy Workflow
on:
workflow_call:
inputs:
environment:
description: 'Environment to deploy to'
required: true
type: string
node-version:
description: 'Node.js version'
required: false
type: string
default: '20'
dry-run:
description: 'Perform dry run'
required: false
type: boolean
default: false
secrets:
deploy-token:
description: 'Deployment token'
required: true
api-key:
description: 'API key'
required: false
outputs:
deployment-url:
description: 'URL of deployment'
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm run build
- name: Deploy
id: deploy
env:
DEPLOY_TOKEN: ${{ secrets.deploy-token }}
API_KEY: ${{ secrets.api-key }}
DRY_RUN: ${{ inputs.dry-run }}
run: |
if [ "$DRY_RUN" = "true" ]; then
echo "Dry run mode - skipping actual deployment"
URL="https://dry-run.example.com"
else
./deploy.sh ${{ inputs.environment }}
URL="https://${{ inputs.environment }}.example.com"
fi
echo "url=$URL" >> $GITHUB_OUTPUT
Calling a Reusable Workflow
# .github/workflows/deploy-staging.yml
name: Deploy to Staging
on:
push:
branches: [develop]
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
node-version: '20'
dry-run: false
secrets:
deploy-token: ${{ secrets.STAGING_DEPLOY_TOKEN }}
api-key: ${{ secrets.STAGING_API_KEY }}
notify:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- name: Notify team
run: |
echo "Deployed to ${{ needs.deploy-staging.outputs.deployment-url }}"
---
# .github/workflows/deploy-production.yml
name: Deploy to Production
on:
release:
types: [published]
jobs:
deploy-prod:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
node-version: '20'
secrets:
deploy-token: ${{ secrets.PROD_DEPLOY_TOKEN }}
api-key: ${{ secrets.PROD_API_KEY }}
Calling Workflows from Other Repositories
jobs:
call-workflow:
uses: octo-org/shared-workflows/.github/workflows/deploy.yml@main
with:
environment: production
secrets:
token: ${{ secrets.DEPLOY_TOKEN }}
# Pin to specific version for stability
call-workflow-pinned:
uses: octo-org/shared-workflows/.github/workflows/deploy.yml@v1.2.3
with:
environment: staging
secrets: inherit # Pass all secrets
Matrix Strategy with Reusable Workflows
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: echo 'matrix=["staging", "production"]' >> $GITHUB_OUTPUT
deploy:
needs: generate-matrix
strategy:
matrix:
environment: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: ${{ matrix.environment }}
secrets: inherit
- Can nest reusable workflows up to 4 levels deep
- Can call maximum of 20 reusable workflows from a single workflow file
- Environment variables set in
envcontext are not passed to called workflows - The
GITHUB_TOKENpermissions are passed to the called workflow
Monitoring and Debugging
DebuggingEffective debugging and monitoring help you quickly identify and fix workflow issues.
Enable Debug Logging
Set repository secrets to enable detailed logging:
ACTIONS_STEP_DEBUG=true- Enable step debug loggingACTIONS_RUNNER_DEBUG=true- Enable runner debug logging
Debugging Techniques
jobs:
debug:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Print all environment variables
- name: Dump env
run: env | sort
# Print GitHub context
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: echo "$GITHUB_CONTEXT"
# Print job context
- name: Dump job context
env:
JOB_CONTEXT: ${{ toJSON(job) }}
run: echo "$JOB_CONTEXT"
# Print steps context
- name: Dump steps context
env:
STEPS_CONTEXT: ${{ toJSON(steps) }}
run: echo "$STEPS_CONTEXT"
# Print runner context
- name: Dump runner context
env:
RUNNER_CONTEXT: ${{ toJSON(runner) }}
run: echo "$RUNNER_CONTEXT"
# Print strategy context
- name: Dump strategy context
env:
STRATEGY_CONTEXT: ${{ toJSON(strategy) }}
run: echo "$STRATEGY_CONTEXT"
# Print matrix context
- name: Dump matrix context
env:
MATRIX_CONTEXT: ${{ toJSON(matrix) }}
run: echo "$MATRIX_CONTEXT"
Conditional Debugging
on:
workflow_dispatch:
inputs:
debug_enabled:
description: 'Enable debug mode'
type: boolean
default: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Debug information
if: ${{ inputs.debug_enabled }}
run: |
echo "Debug mode enabled"
echo "Working directory: $(pwd)"
echo "Files:"
ls -la
echo "Git status:"
git status
- name: Build
run: npm run build
SSH Debugging with tmate
jobs:
debug:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup tmate session
if: ${{ failure() }} # Only on failure
uses: mxschmitt/action-tmate@v3
timeout-minutes: 30
with:
limit-access-to-actor: true # Only workflow initiator can access
Workflow Status Badges



Notifications
jobs:
notify:
runs-on: ubuntu-latest
if: always()
needs: [build, test, deploy]
steps:
- name: Notify Slack on failure
if: ${{ contains(needs.*.result, 'failure') }}
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
payload: |
{
"text": "❌ Workflow failed: ${{ github.workflow }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Workflow:* ${{ github.workflow }}\n*Status:* Failed\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}\n*Author:* ${{ github.actor }}"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Workflow"
},
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
]
}
]
}
- name: Notify on success
if: ${{ !contains(needs.*.result, 'failure') }}
run: echo "✅ All jobs succeeded!"
limit-access-to-actor: true and set reasonable timeouts.
GitHub-hosted vs Self-hosted Runners
InfrastructureChoose between GitHub-hosted runners (managed by GitHub) or self-hosted runners (managed by you) based on your needs.
GitHub-hosted Runners
✅ Advantages
- Zero maintenance
- Automatic updates
- Clean environment every run
- Multiple OS options
- Free for public repos
❌ Limitations
- Limited customization
- No persistent storage
- Fixed hardware specs
- Costs for private repos
- No access to internal resources
Available GitHub-hosted Runners
jobs:
# Ubuntu (latest = 22.04)
ubuntu-job:
runs-on: ubuntu-latest # or ubuntu-22.04, ubuntu-20.04
# Windows
windows-job:
runs-on: windows-latest # or windows-2022, windows-2019
# macOS
macos-job:
runs-on: macos-latest # or macos-13, macos-12, macos-11
# macOS with Apple Silicon (M1)
macos-arm-job:
runs-on: macos-14 # M1 chip
Runner Specifications
Linux & Windows
- 2-core CPU
- 7 GB RAM
- 14 GB SSD
macOS
- 3-core CPU
- 14 GB RAM
- 14 GB SSD
Self-hosted Runners
✅ Advantages
- Custom hardware
- Access internal resources
- Persistent caching
- Pre-installed tools
- Cost control
❌ Considerations
- Maintenance required
- Security responsibility
- Manual updates
- Infrastructure costs
- Cleanup needed
Setting Up Self-hosted Runner
- Go to repository Settings → Actions → Runners
- Click New self-hosted runner
- Select OS and architecture
- Follow the provided installation commands
- Configure and start the runner
# Download
mkdir actions-runner && cd actions-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
tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz
# Configure
./config.sh --url https://github.com/owner/repo --token YOUR_TOKEN
# Run
./run.sh
# Or install as service
sudo ./svc.sh install
sudo ./svc.sh start
Using Self-hosted Runners
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- run: npm run build
# Use specific labels
build-gpu:
runs-on: [self-hosted, linux, x64, gpu]
steps:
- run: python train_model.py
# Combine with matrix
test:
runs-on: ${{ matrix.runner }}
strategy:
matrix:
runner: [ubuntu-latest, [self-hosted, linux]]
steps:
- run: npm test
Runner Groups (Enterprise)
jobs:
deploy:
runs-on:
group: production-runners
labels: [self-hosted, linux, x64]
steps:
- run: ./deploy.sh
Deployment Strategies
CDImplement various deployment strategies for different scenarios and risk tolerances.
Basic Deployment Pipeline
name: Deploy Pipeline
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
deploy-staging:
needs: [build, test]
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/download-artifact@v4
with:
name: dist
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/download-artifact@v4
with:
name: dist
- run: ./deploy.sh production
Blue-Green Deployment
jobs:
deploy-blue-green:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to green environment
run: |
./deploy.sh green
echo "GREEN_URL=https://green.example.com" >> $GITHUB_ENV
- name: Run smoke tests on green
run: |
npm run smoke-test -- --url=$GREEN_URL
- name: Switch traffic to green
run: |
./switch-traffic.sh green
- name: Monitor for 5 minutes
run: |
sleep 300
./check-metrics.sh
- name: Rollback on failure
if: failure()
run: |
./switch-traffic.sh blue
./cleanup.sh green
Canary Deployment
jobs:
canary-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy canary (10% traffic)
run: ./deploy-canary.sh --percentage=10
- name: Monitor canary for 10 minutes
run: |
sleep 600
ERROR_RATE=$(./get-error-rate.sh canary)
if [ "$ERROR_RATE" -gt "1" ]; then
echo "Error rate too high, rolling back"
exit 1
fi
- name: Increase to 50%
run: ./deploy-canary.sh --percentage=50
- name: Monitor for 10 minutes
run: sleep 600 && ./check-metrics.sh
- name: Full rollout
run: ./deploy-canary.sh --percentage=100
- name: Rollback on failure
if: failure()
run: ./rollback-canary.sh
Rolling Deployment
jobs:
rolling-deploy:
runs-on: ubuntu-latest
strategy:
max-parallel: 1 # Deploy one at a time
matrix:
instance: [instance-1, instance-2, instance-3, instance-4]
steps:
- uses: actions/checkout@v4
- name: Deploy to ${{ matrix.instance }}
run: |
./deploy-to-instance.sh ${{ matrix.instance }}
- name: Health check
run: |
./health-check.sh ${{ matrix.instance }}
- name: Wait before next
run: sleep 60
Feature Flag Deployment
jobs:
deploy-with-flags:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy with feature flags disabled
env:
FEATURE_NEW_UI: false
FEATURE_BETA_API: false
run: ./deploy.sh
- name: Enable for internal users
run: |
./set-feature-flag.sh NEW_UI --users=internal
- name: Monitor metrics
run: ./monitor.sh --duration=1h
- name: Gradual rollout
run: |
./set-feature-flag.sh NEW_UI --percentage=10
sleep 600
./set-feature-flag.sh NEW_UI --percentage=50
sleep 600
./set-feature-flag.sh NEW_UI --percentage=100
Multi-Region Deployment
jobs:
deploy-regions:
runs-on: ubuntu-latest
strategy:
max-parallel: 2
matrix:
region: [us-east-1, us-west-2, eu-west-1, ap-southeast-1]
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE }}
aws-region: ${{ matrix.region }}
- name: Deploy to ${{ matrix.region }}
run: |
./deploy-region.sh ${{ matrix.region }}
- name: Verify deployment
run: |
./verify-region.sh ${{ matrix.region }}
- Always include health checks and smoke tests
- Implement automated rollback on failure
- Use environment protection rules for production
- Monitor key metrics during deployment
- Keep deployment artifacts for quick rollback
- Use deployment slots or blue-green for zero-downtime
Integration with External Services
IntegrationsConnect GitHub Actions with cloud providers, notification services, and other tools.
AWS Integration
jobs:
deploy-aws:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync ./dist s3://my-bucket --delete
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id E1234567890ABC \
--paths "/*"
- name: Deploy Lambda
run: |
aws lambda update-function-code \
--function-name my-function \
--zip-file fileb://function.zip
- name: Update ECS service
run: |
aws ecs update-service \
--cluster my-cluster \
--service my-service \
--force-new-deployment
Azure Integration
jobs:
deploy-azure:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: my-app
package: ./dist
- name: Deploy to Azure Functions
uses: Azure/functions-action@v1
with:
app-name: my-function-app
package: ./function-app
- name: Deploy to AKS
run: |
az aks get-credentials --resource-group myRG --name myAKS
kubectl apply -f k8s/
kubectl rollout status deployment/my-app
Google Cloud Integration
jobs:
deploy-gcp:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Deploy to Cloud Run
run: |
gcloud run deploy my-service \
--image gcr.io/my-project/my-image:${{ github.sha }} \
--region us-central1 \
--platform managed
- name: Deploy to App Engine
run: gcloud app deploy app.yaml --quiet
- name: Deploy to GKE
run: |
gcloud container clusters get-credentials my-cluster --region us-central1
kubectl apply -f k8s/
kubectl rollout status deployment/my-app
Slack Notifications
jobs:
notify-slack:
runs-on: ubuntu-latest
steps:
- name: Notify Slack
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
payload: |
{
"text": "Deployment Status",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🚀 Deployment Complete"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Repository:*\n${{ github.repository }}"
},
{
"type": "mrkdwn",
"text": "*Branch:*\n${{ github.ref_name }}"
},
{
"type": "mrkdwn",
"text": "*Commit:*\n<${{ github.event.head_commit.url }}|${{ github.sha }}>"
},
{
"type": "mrkdwn",
"text": "*Author:*\n${{ github.actor }}"
}
]
}
]
}
Discord Notifications
- name: Notify Discord
run: |
curl -H "Content-Type: application/json" \
-d '{
"username": "GitHub Actions",
"avatar_url": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
"embeds": [{
"title": "Deployment Complete",
"description": "Successfully deployed to production",
"color": 3066993,
"fields": [
{
"name": "Repository",
"value": "${{ github.repository }}",
"inline": true
},
{
"name": "Branch",
"value": "${{ github.ref_name }}",
"inline": true
},
{
"name": "Commit",
"value": "[${{ github.sha }}](${{ github.event.head_commit.url }})"
}
]
}]
}' \
${{ secrets.DISCORD_WEBHOOK_URL }}
Terraform Integration
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
- name: Terraform Init
run: terraform init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Plan
run: terraform plan -out=tfplan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve tfplan
Database Migrations
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run database migrations
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
npm run migrate
# Or with Flyway
- name: Flyway migrate
uses: joshuaavalon/flyway-action@v3
with:
url: ${{ secrets.DATABASE_URL }}
user: ${{ secrets.DB_USER }}
password: ${{ secrets.DB_PASSWORD }}
locations: filesystem:./migrations
# Or with Liquibase
- name: Liquibase update
uses: liquibase/liquibase-github-action@v7
with:
operation: 'update'
classpath: 'changelog'
changeLogFile: 'changelog.xml'
username: ${{ secrets.DB_USER }}
password: ${{ secrets.DB_PASSWORD }}
url: ${{ secrets.DATABASE_URL }}
- AWS: aws-actions/configure-aws-credentials
- Azure: azure/login, azure/webapps-deploy
- GCP: google-github-actions/auth
- Docker: docker/build-push-action
- Kubernetes: azure/k8s-deploy
- Terraform: hashicorp/setup-terraform
- Notifications: slackapi/slack-github-action