GitHub Actions Essentials
Quick reference guide for GitHub Actions automation.
What is GitHub Actions?
GitHub Actions is a CI/CD platform built into GitHub that:
Core Concepts
Workflow
├── Events (triggers)
├── Jobs (run in parallel)
│ ├── Runs-on (runner)
│ └── Steps (sequential tasks)
│ ├── Uses (action)
│ └── Run (command)
└── Artifacts (outputs)
Key Components
Basic Workflow
Simple Example
# .github/workflows/hello.yml
name: Hello World
on: [push]
jobs:
greet:
runs-on: ubuntu-latest
steps:
- name: Say hello
run: echo "Hello, World!"
Complete CI Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
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'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Build
run: npm run build
Events (Triggers)
Push Events
# Trigger on any push
on: push
# Trigger on specific branches
on:
push:
branches:
- main
- develop
- 'release/**'
# Trigger on specific paths
on:
push:
paths:
- 'src/**'
- 'package.json'
# Ignore specific paths
on:
push:
paths-ignore:
- 'docs/**'
- '**.md'
Pull Request Events
on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
# Run on PR to specific branches
on:
pull_request:
branches:
- main
- develop
Schedule (Cron)
on:
schedule:
# Run every day at 2 AM UTC
- cron: '0 2 * * *'
# Run every Monday at 9 AM
- cron: '0 9 * * 1'
Manual Trigger
on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
type: choice
options:
- development
- staging
- production
version:
description: 'Version to deploy'
required: false
default: 'latest'
Multiple Events
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 0' # Weekly
workflow_dispatch:
Jobs
Parallel Jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
Sequential Jobs (Dependencies)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: [build, test]
runs-on: ubuntu-latest
steps:
- run: npm run deploy
Conditional Jobs
jobs:
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to production"
test:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- run: npm test
Matrix Strategy
Multiple Versions
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Multiple OS and Versions
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Matrix with Include/Exclude
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [16, 18, 20]
include:
# Add extra combination
- os: macos-latest
node-version: 20
exclude:
# Remove combination
- os: windows-latest
node-version: 16
runs-on: ${{ matrix.os }}
steps:
- run: npm test
Environment Variables and Secrets
Environment Variables
jobs:
build:
runs-on: ubuntu-latest
env:
NODE_ENV: production
API_URL: https://api.example.com
steps:
- name: Build
run: npm run build
- name: Use environment variable
run: echo "API URL is $API_URL"
- name: Set step-level env
env:
CUSTOM_VAR: value
run: echo $CUSTOM_VAR
Using Secrets
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ./dist s3://my-bucket
GitHub Context
steps:
- name: Print context
run: |
echo "Repository: ${{ github.repository }}"
echo "Branch: ${{ github.ref }}"
echo "Commit SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Event: ${{ github.event_name }}"
Common Actions
Checkout Code
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history
submodules: true # Include submodules
Setup Languages
# Node.js
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# Python
- uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
# Java
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven'
# Go
- uses: actions/setup-go@v4
with:
go-version: '1.21'
cache: true
Caching
# NPM cache
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Custom cache
- uses: actions/cache@v3
with:
path: |
~/.cache
./node_modules
key: ${{ runner.os }}-cache-${{ hashFiles('**/*.lock') }}
Upload/Download Artifacts
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: dist/
retention-days: 7
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/
- run: npm run deploy
Language-Specific Workflows
Node.js
name: Node.js CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
Python
name: Python CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: flake8 .
- name: Test with pytest
run: pytest
Docker Build
name: Docker Build
on:
push:
branches: [main]
jobs:
build:
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.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: user/app:latest
cache-from: type=registry,ref=user/app:cache
cache-to: type=registry,ref=user/app:cache,mode=max
Deployment Workflows
Deploy to AWS S3
name: Deploy to S3
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build
run: |
npm ci
npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync ./dist s3://my-bucket --delete
Deploy to Vercel
name: Deploy to Vercel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Deploy to Kubernetes
name: Deploy to K8s
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- name: Deploy to K8s
run: |
kubectl apply -f k8s/
kubectl rollout status deployment/my-app
Testing and Coverage
Run Tests with Coverage
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Run tests with coverage
run: npm test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
fail_ci_if_error: true
Reusable Workflows
Define Reusable Workflow
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
deploy-token:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
TOKEN: ${{ secrets.deploy-token }}
run: ./deploy.sh
Use Reusable Workflow
# .github/workflows/production.yml
name: Production Deploy
on:
push:
branches: [main]
jobs:
deploy-prod:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
secrets:
deploy-token: ${{ secrets.PROD_DEPLOY_TOKEN }}
Composite Actions
Create Custom Action
# .github/actions/setup-app/action.yml
name: 'Setup App'
description: 'Setup Node.js and install dependencies'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
shell: bash
Use Custom Action
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-app
with:
node-version: '20'
- run: npm run build
Advanced Features
Conditional Steps
steps:
- name: Run on main only
if: github.ref == 'refs/heads/main'
run: echo "Main branch"
- name: Run on PR
if: github.event_name == 'pull_request'
run: echo "Pull request"
- name: Run if previous step succeeded
if: success()
run: echo "Previous step passed"
- name: Run if previous step failed
if: failure()
run: echo "Previous step failed"
Continue on Error
steps:
- name: Run tests
continue-on-error: true
run: npm test
- name: This runs even if tests fail
run: echo "Continuing..."
Timeout
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- run: npm test
timeout-minutes: 5
Concurrency Control
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
Security Best Practices
Pin Actions to SHA
steps:
# ❌ BAD: Using branch or tag
- uses: actions/checkout@v4
# ✅ GOOD: Using SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
Least Privilege Permissions
permissions:
contents: read
pull-requests: write
issues: write
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- run: npm test
Use GitHub Environment Secrets
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy
env:
SECRET: ${{ secrets.PRODUCTION_SECRET }}
run: ./deploy.sh
Debugging
Enable Debug Logging
# Set repository secrets:
ACTIONS_STEP_DEBUG=true
ACTIONS_RUNNER_DEBUG=true
Debug Step
steps:
- name: Debug info
run: |
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
env
Use tmate for SSH Debug
steps:
- name: Setup tmate session
if: failure()
uses: mxschmitt/action-tmate@v3
Tips
Common Patterns
Monorepo with Path Filters
on:
push:
paths:
- 'packages/api/**'
jobs:
test-api:
if: contains(github.event.head_commit.modified, 'packages/api')
runs-on: ubuntu-latest
steps:
- run: npm test -- packages/api
Release Automation
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false