GitHub Actions
A powerful CI/CD platform integrated directly into GitHub repositories for automating build, test, and deployment workflows.
GitHub Actions Cheatsheet
A powerful CI/CD platform integrated directly into GitHub repositories for automating build, test, and deployment workflows.
Overview
GitHub Actions enables automation of software workflows directly from your GitHub repository. It uses YAML-based configuration files to define workflows that respond to events, run jobs on runners, and execute steps that can include marketplace actions or custom scripts.
graph TB
subgraph "GitHub Actions Architecture"
E[Event Trigger] --> W[Workflow]
W --> J1[Job 1]
W --> J2[Job 2]
W --> J3[Job 3]
J1 --> S1[Step 1]
J1 --> S2[Step 2]
J1 --> S3[Step 3]
S1 --> A1[Action]
S2 --> A2[Run Command]
S3 --> A3[Action]
J2 --> S4[Steps...]
J3 --> S5[Steps...]
end
subgraph "Runners"
R1[GitHub-hosted]
R2[Self-hosted]
end
J1 -.-> R1
J2 -.-> R1
J3 -.-> R2
Workflow Syntax and Structure
Key Concepts
- Workflow: Automated process defined in YAML, stored in
.github/workflows/ - Event: Trigger that starts a workflow (push, pull_request, schedule, etc.)
- Job: Set of steps that execute on the same runner
- Step: Individual task within a job (action or shell command)
- Action: Reusable unit of code from marketplace or custom repository
- Runner: Server that executes workflow jobs
Basic Workflow Structure
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Workflow File Location
repository/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── cd.yml
│ └── scheduled-tasks.yml
└── src/
Common Triggers
Key Concepts
- Push events: Triggered when code is pushed to specified branches
- Pull request events: Triggered on PR actions (opened, synchronise, closed)
- Schedule: Cron-based triggers for periodic execution
- Manual triggers:
workflow_dispatchfor on-demand execution - Repository dispatch: API-triggered workflows
- Workflow call: Reusable workflows triggered by other workflows
Common Patterns
on:
# Push to specific branches
push:
branches:
- main
- 'releases/**'
paths:
- 'src/**'
- '!src/**/*.md'
tags:
- 'v*'
# Pull request events
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- 'docs/**'
# Scheduled (cron syntax - UTC)
schedule:
- cron: '0 2 * * 1-5' # Weekdays at 2 AM UTC
- cron: '0 0 * * 0' # Sundays at midnight
# Manual trigger with inputs
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
debug_enabled:
description: 'Enable debug logging'
required: false
type: boolean
default: false
# Trigger from another repository
repository_dispatch:
types: [deploy-trigger]
# Release events
release:
types: [published, created]
# Issue and PR comments
issue_comment:
types: [created]
Examples
Branch and Tag Filtering
on:
push:
branches:
- main
- 'feature/**'
- '!feature/experimental-*'
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
Conditional Workflow Based on Changed Files
on:
push:
paths:
- 'backend/**'
- 'shared/**'
jobs:
build-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# Build only backend when relevant files change
Job and Step Definitions
Key Concepts
- Job dependencies: Define execution order with
needs - Conditional execution: Use
ifexpressions to control execution - Outputs: Pass data between jobs
- Concurrency: Control parallel execution and cancellation
- Timeout: Set maximum execution time
- Continue on error: Allow workflow to continue despite failures
Job Configuration
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
timeout-minutes: 30
# Job-level environment variables
env:
CI: true
# Conditional execution
if: github.event_name == 'push' || github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v6
- run: npm test
build:
name: Build Application
runs-on: ubuntu-latest
needs: test # Wait for test job
outputs:
version: ${{ steps.version.outputs.value }}
steps:
- uses: actions/checkout@v6
- id: version
run: echo "value=$(cat VERSION)" >> $GITHUB_OUTPUT
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [test, build] # Wait for multiple jobs
# Use output from previous job
env:
APP_VERSION: ${{ needs.build.outputs.version }}
steps:
- run: echo "Deploying version $APP_VERSION"
Job Dependencies Diagram
graph LR
subgraph "Job Dependencies"
A[lint] --> D[deploy]
B[test] --> D
C[build] --> D
B --> C
end
Step Configuration
steps:
# Using an action
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.PAT }}
# Running a command
- name: Install dependencies
run: npm ci
working-directory: ./frontend
shell: bash
# Multi-line command
- name: Build and test
run: |
npm run build
npm run test:coverage
npm run lint
# Conditional step
- name: Deploy to production
if: github.ref == 'refs/heads/main' && success()
run: ./deploy.sh
# Continue on error
- name: Run optional checks
run: npm run optional-lint
continue-on-error: true
# Set environment variables for subsequent steps
- name: Set build info
run: |
echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
# Use outputs
- name: Get version
id: get-version
run: echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUT
- name: Use version
run: echo "Version is ${{ steps.get-version.outputs.version }}"
Concurrency Control
# Cancel in-progress runs for same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Or per job
jobs:
deploy:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false # Don't cancel deploys
Using Actions from the Marketplace
Key Concepts
- Official actions: Maintained by GitHub (actions/*)
- Verified creators: Trusted third-party actions
- Version pinning: Use specific versions for stability
- Fork for security: Fork critical actions to your organisation
Common Actions
steps:
# Checkout code
- uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
# Setup language runtimes
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'
- uses: actions/setup-go@v6
with:
go-version: '1.22'
# Docker actions
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
# GitHub script for API calls
- uses: actions/github-script@v8
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'Build completed!'
})
Using Actions from Different Sources
steps:
# Public action from marketplace
- uses: actions/checkout@v6
# Action from a different repository
- uses: owner/repo@v1
# Action from a specific branch
- uses: owner/repo@main
# Action from a specific commit SHA (most secure)
- uses: owner/repo@a1b2c3d4e5f6
# Local action in same repository
- uses: ./.github/actions/my-action
# Docker Hub action
- uses: docker://alpine:3.23
Creating a Local Composite Action
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Setup Node.js and install dependencies'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
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@v6
with:
node-version: ${{ inputs.node-version }}
- name: Cache dependencies
id: cache
uses: actions/cache@v5
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
shell: bash
Secrets and Environment Variables
Key Concepts
- Secrets: Encrypted values for sensitive data (tokens, passwords)
- Variables: Non-sensitive configuration values
- Environment-specific: Secrets/variables scoped to deployment environments
- Masking: Automatic masking of secret values in logs
Configuration Levels
# Repository-level environment variable
env:
GLOBAL_VAR: 'available-to-all-jobs'
jobs:
build:
runs-on: ubuntu-latest
# Job-level environment variable
env:
JOB_VAR: 'available-to-all-steps'
steps:
- name: Use variables
# Step-level environment variable
env:
STEP_VAR: 'only-this-step'
SECRET_TOKEN: ${{ secrets.API_TOKEN }}
run: |
echo "Global: $GLOBAL_VAR"
echo "Job: $JOB_VAR"
echo "Step: $STEP_VAR"
Using Secrets
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Login to registry
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: echo "$REGISTRY_TOKEN" | docker login -u user --password-stdin
- name: Deploy with token
run: ./deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
# GitHub token (automatically provided)
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release create v1.0.0
Deployment Environments
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: ./deploy.sh
env:
API_URL: ${{ vars.API_URL }} # Environment variable
API_KEY: ${{ secrets.API_KEY }} # Environment secret
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment:
name: production
url: https://myapp.com
steps:
- run: ./deploy.sh
env:
API_URL: ${{ vars.API_URL }}
API_KEY: ${{ secrets.API_KEY }}
Dynamic Secrets Masking
steps:
- name: Generate token
id: token
run: |
TOKEN=$(./generate-token.sh)
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> $GITHUB_OUTPUT
- name: Use token
run: curl -H "Authorization: Bearer ${{ steps.token.outputs.token }}" https://api.example.com
Matrix Builds
Key Concepts
- Matrix strategy: Run job with multiple configurations in parallel
- Include/exclude: Add or remove specific combinations
- Fail-fast: Control whether to cancel other jobs on failure
- Max-parallel: Limit concurrent jobs
Basic Matrix
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Advanced Matrix Configuration
jobs:
test:
strategy:
fail-fast: false # Continue other jobs if one fails
max-parallel: 4 # Limit concurrent jobs
matrix:
os: [ubuntu-latest, windows-latest]
python: ['3.10', '3.11', '3.12']
# Include additional configurations
include:
- os: ubuntu-latest
python: '3.12'
coverage: true
- os: macos-latest
python: '3.12'
# Exclude specific combinations
exclude:
- os: windows-latest
python: '3.10'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
- name: Run tests
run: pytest
- name: Upload coverage
if: matrix.coverage
run: ./upload-coverage.sh
Dynamic Matrix
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
# Generate matrix from files or API
echo 'matrix={"service":["api","web","worker"]}' >> $GITHUB_OUTPUT
build:
needs: prepare
strategy:
matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
runs-on: ubuntu-latest
steps:
- run: echo "Building ${{ matrix.service }}"
Caching Dependencies
Key Concepts
- Cache action: Store and restore files between workflow runs
- Cache key: Unique identifier for cache entries
- Restore keys: Fallback keys for partial matches
- Cache scope: Branch-level isolation with fallback to default branch
Common Caching Patterns
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# Node.js with npm
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm' # Built-in caching
# Manual cache for custom paths
- name: Cache node modules
uses: actions/cache@v5
with:
path: |
node_modules
~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Python with pip
- uses: actions/cache@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
# Go modules
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
# Gradle
- uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
Conditional Caching
steps:
- name: Cache dependencies
id: cache
uses: actions/cache@v5
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
Cache Limits and Best Practices
# Good: Specific, content-based keys
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# Good: Multiple restore keys for fallback
restore-keys: |
${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
${{ runner.os }}-node-
${{ runner.os }}-
# Avoid: Generic keys that rarely change
key: ${{ runner.os }}-deps # Bad - won't invalidate properly
Artifacts Management
Key Concepts
- Artifacts: Files preserved after workflow completion
- Retention: Configurable storage duration (default 90 days)
- Upload/download: Share files between jobs or for later retrieval
- Compression: Automatic compression for efficiency
Uploading Artifacts
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: build-output
path: |
dist/
!dist/**/*.map
retention-days: 7
- name: Upload test results
if: always() # Upload even on failure
uses: actions/upload-artifact@v7
with:
name: test-results
path: test-results/
if-no-files-found: warn # warn, error, or ignore
Downloading Artifacts
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-name: ${{ steps.artifact.outputs.name }}
steps:
- run: npm run build
- uses: actions/upload-artifact@v7
id: artifact
with:
name: build-${{ github.sha }}
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: build-${{ github.sha }}
path: dist/
- name: Deploy
run: ./deploy.sh dist/
Sharing Artifacts Between Jobs
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npm run build
- uses: actions/upload-artifact@v7
with:
name: app
path: dist/
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
name: app
- run: npm test
deploy:
needs: [build, test]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
name: app
- run: ./deploy.sh
Artifacts Flow Diagram
graph LR
subgraph "Artifacts Flow"
B[Build Job] -->|upload| A[(Artifacts Storage)]
A -->|download| T[Test Job]
A -->|download| D[Deploy Job]
end
GHCR (GitHub Container Registry)
Key Concepts
- GHCR: GitHub's container registry at ghcr.io
- Authentication: Use GITHUB_TOKEN or PAT
- Visibility: Public or private packages
- Permissions: Linked to repository or organisation
Building and Pushing to GHCR
name: Build and Push Container
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:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Multi-Architecture Builds
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ github.repository }}:latest
Using GHCR Images in Workflows
jobs:
test:
runs-on: ubuntu-latest
# Use container from GHCR
container:
image: ghcr.io/owner/custom-runner:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
services:
postgres:
image: ghcr.io/owner/postgres-test:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
env:
POSTGRES_PASSWORD: test
steps:
- uses: actions/checkout@v6
- run: npm test
Best Practices for CI/CD Pipelines
Security Best Practices
# Pin actions to full SHA for security
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
# Minimal permissions
permissions:
contents: read
jobs:
deploy:
permissions:
contents: read
packages: write
# Use environments for production deployments
jobs:
deploy:
environment:
name: production
url: https://myapp.com
# Requires manual approval if configured
Performance Optimisation
jobs:
build:
runs-on: ubuntu-latest
steps:
# Shallow clone for faster checkout
- uses: actions/checkout@v6
with:
fetch-depth: 1
# Use caching
- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
# Parallelise where possible
- name: Run tests
run: npm test -- --parallel
Reusable Workflows
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
DEPLOY_KEY:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v6
- run: ./deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
# Calling workflow
name: Deploy
on:
push:
branches: [main]
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
secrets:
DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
Workflow Organisation
# Use meaningful names
name: CI - Build, Test, and Lint
# Group related jobs
jobs:
lint:
name: Code Quality
# ...
test:
name: Unit Tests
# ...
integration:
name: Integration Tests
needs: [lint, test]
# ...
deploy:
name: Deploy to Production
needs: integration
if: github.ref == 'refs/heads/main'
# ...
Troubleshooting Common Issues
Key Concepts
- Workflow logs: Detailed execution logs in Actions tab
- Debug logging: Enhanced logging with secrets
- Status checks: Required checks for branch protection
- API limits: Rate limiting for GitHub API calls
Common Issues and Solutions
Workflow Not Triggering
# Check: Workflow file must be on default branch for some events
# Check: Paths filter might be excluding your changes
# Check: Branch protection might require status checks
# Debug: Use workflow_dispatch to test manually
on:
push:
branches: [main]
workflow_dispatch: # Add for manual testing
Permission Denied Errors
# Solution: Add explicit permissions
permissions:
contents: read
packages: write
issues: write
pull-requests: write
# For GITHUB_TOKEN in forks
# Note: Secrets aren't available in PRs from forks by default
Cache Not Restoring
# Check key matches exactly
- uses: actions/cache@v5
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# Ensure lockfile exists and is committed
# Debug: Check cache-hit output
id: cache
- run: echo "Cache hit: ${{ steps.cache.outputs.cache-hit }}"
Job Timeout
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 60 # Reduce from default 360 to fail fast
steps:
- name: Long running step
timeout-minutes: 30 # Step-level timeout
run: ./long-process.sh
Secrets Not Available
# Secrets aren't available in:
# - Forked PRs (security)
# - Reusable workflows (unless passed explicitly)
# - workflow_run triggered workflows
# Solution for reusable workflows:
on:
workflow_call:
secrets:
MY_SECRET:
required: true
Debugging Workflows
Enable Debug Logging
# Set repository secrets:
# ACTIONS_RUNNER_DEBUG = true
# ACTIONS_STEP_DEBUG = true
# Or use workflow_dispatch input
on:
workflow_dispatch:
inputs:
debug:
type: boolean
default: false
jobs:
build:
runs-on: ubuntu-latest
env:
ACTIONS_STEP_DEBUG: ${{ inputs.debug }}
Debugging Techniques
steps:
# Print context information
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
# Print all environment variables
- name: Print environment
run: env | sort
# Interactive debugging with tmate
- name: Setup tmate session
if: failure()
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
# Add continue-on-error for investigation
- name: Failing step
continue-on-error: true
run: ./might-fail.sh
- name: Check previous result
run: echo "Previous step outcome: ${{ steps.previous.outcome }}"
Workflow Run Logs
# Download logs via API
- name: Download logs
run: |
gh run view ${{ github.run_id }} --log
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Common Debug Scenarios
# Debug: Check if file exists
- run: |
if [ -f "package-lock.json" ]; then
echo "File exists"
ls -la package-lock.json
else
echo "File not found"
ls -la
fi
# Debug: Check working directory
- run: |
echo "Current directory: $(pwd)"
echo "Contents:"
ls -la
# Debug: Network connectivity
- run: |
curl -v https://registry.npmjs.org
# Debug: Check installed tools
- run: |
node --version
npm --version
which node
Using Self-Hosted Runners
Key Concepts
- Self-hosted runners: Your own machines running GitHub Actions jobs
- Labels: Identify runner capabilities and target specific runners
- Runner groups: Organise and control access to runners
- Security: Ensure proper isolation and access control
Registering a Self-Hosted Runner
# Download and configure runner
# (check Settings → Actions → Runners → New runner for the current version and command)
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.334.0.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.334.0/actions-runner-linux-x64-2.334.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.334.0.tar.gz
# Configure
./config.sh --url https://github.com/owner/repo --token TOKEN
# Run
./run.sh
# Or install as service
sudo ./svc.sh install
sudo ./svc.sh start
Using Self-Hosted Runners in Workflows
jobs:
build:
# Use self-hosted runner
runs-on: self-hosted
steps:
- uses: actions/checkout@v6
- run: ./build.sh
gpu-training:
# Use runner with specific labels
runs-on: [self-hosted, linux, gpu]
steps:
- uses: actions/checkout@v6
- run: python train.py
macos-build:
# Multiple labels for precise targeting
runs-on: [self-hosted, macOS, ARM64]
steps:
- uses: actions/checkout@v6
- run: xcodebuild
Runner Labels and Groups
jobs:
# Target specific runner group
deploy:
runs-on:
group: production-runners
labels: [self-hosted, linux]
steps:
- run: ./deploy.sh
Self-Hosted Runner Security
# Limit to specific repositories
# Configure in: Settings > Actions > Runner groups
# Use ephemeral runners for better isolation
# ./config.sh --ephemeral
# Run in container for isolation
jobs:
build:
runs-on: self-hosted
container:
image: node:20
steps:
- uses: actions/checkout@v6
- run: npm test
Auto-Scaling Runners
# Using Actions Runner Controller (ARC) for Kubernetes
# Deploy runner scale set that auto-scales based on demand
# Example workflow targeting ARC runners
jobs:
build:
runs-on: arc-runner-set
steps:
- uses: actions/checkout@v6
- run: ./build.sh
Quick Reference
| Component | Syntax | Description |
|---|---|---|
| Workflow file | .github/workflows/*.yml |
Location for workflow definitions |
| Trigger on push | on: push |
Run on code push |
| Trigger on PR | on: pull_request |
Run on pull request |
| Manual trigger | on: workflow_dispatch |
Allow manual execution |
| Job definition | jobs: <name>: |
Define a job |
| Runner | runs-on: ubuntu-latest |
Specify execution environment |
| Use action | uses: actions/checkout@v6 |
Use marketplace action |
| Run command | run: npm test |
Execute shell command |
| Environment variable | env: KEY: value |
Set environment variable |
| Secret | ${{ secrets.NAME }} |
Access secret value |
| Matrix | strategy: matrix: |
Run multiple configurations |
| Cache | actions/cache@v5 |
Cache dependencies |
| Artifact upload | actions/upload-artifact@v7 |
Store workflow output |
| Artifact download | actions/download-artifact@v8 |
Retrieve stored output |
| Job dependency | needs: [job1, job2] |
Wait for jobs to complete |
| Conditional | if: github.ref == 'refs/heads/main' |
Conditional execution |
| Output | echo "key=value" >> $GITHUB_OUTPUT |
Set step output |
| Set env var | echo "KEY=value" >> $GITHUB_ENV |
Set for subsequent steps |
| Permissions | permissions: contents: read |
Set GITHUB_TOKEN scope |
| Concurrency | concurrency: group: ${{ github.ref }} |
Control parallel runs |
| Timeout | timeout-minutes: 30 |
Set maximum run time |
Common Issues and Solutions
| Issue | Cause | Solution |
|---|---|---|
| Workflow not running | File not on default branch | Merge workflow file to main/master first |
Permission denied |
Insufficient GITHUB_TOKEN permissions | Add explicit permissions: block |
| Cache miss | Key doesn't match | Check hashFiles() path and file existence |
| Secret is empty | Wrong secret name or scope | Verify secret exists in correct scope |
Resource not accessible |
Missing permissions | Add required permission (issues, packages, etc.) |
| Job stuck in queue | No available runners | Check runner status and labels |
| Matrix job failing | Invalid combination | Use exclude: to remove problematic combos |
| Artifact not found | Wrong name or expired | Check artifact name and retention period |
| Docker login fails | Invalid credentials | Verify GITHUB_TOKEN has packages: write |
| Workflow cancelled | Concurrency settings | Adjust cancel-in-progress setting |
| Step timeout | Default timeout exceeded | Add explicit timeout-minutes |
| Fork PR secrets empty | Security restriction | Expected behaviour; use pull_request_target carefully |
| Self-hosted runner offline | Runner service stopped | Restart runner service |
| Rate limit exceeded | Too many API calls | Add delays or reduce API usage |
Bad credentials |
Expired or invalid token | Regenerate PAT or check GITHUB_TOKEN scope |
Related Topics
The following topics would complement this GitHub Actions cheatsheet:
-
Docker and Container Basics - Understanding containerisation is essential for building and deploying container images with GitHub Actions
-
YAML Syntax and Best Practices - Deep knowledge of YAML helps write cleaner, more maintainable workflow files
-
Git Branching Strategies - Understanding branching models (GitFlow, trunk-based) helps design effective CI/CD triggers
-
Shell Scripting (Bash) - Most workflow steps involve shell commands; strong scripting skills improve automation
-
Kubernetes Deployments - Many CI/CD pipelines deploy to Kubernetes; understanding K8s concepts enables better deployment workflows
-
Security Best Practices for CI/CD - Covers secrets management, supply chain security, and secure deployment patterns