
Writing Github Actions
- 67 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
writing-github-actions is a skill that writes GitHub Actions CI/CD workflows with reusable workflows, composite actions, matrix builds, caching, and security best practices.
About
A skill that writes GitHub Actions workflows for CI/CD pipelines, automated testing, deployments, and repository automation. It covers workflow YAML syntax, reusable workflows, composite actions, matrix builds, caching, and security practices like OIDC. A developer uses it when creating CI/CD workflows for GitHub-hosted projects or automating repository tasks.
- Writes GitHub Actions workflows with correct YAML syntax, triggers, and jobs
- Covers reusable workflows, composite actions, matrix builds, and caching
- Includes secrets management, OIDC auth, and concurrency control
Writing Github Actions by the numbers
- 67 all-time installs (skills.sh)
- Ranked #628 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
writing-github-actions capabilities & compatibility
- Capabilities
- ci cd · devops
- Works with
- github
- Use cases
- ci cd · devops
What writing-github-actions says it does
Write GitHub Actions workflows with proper syntax, reusable workflows, composite actions, matrix builds, caching, and security best practices.
Use when creating CI/CD workflows for GitHub-hosted projects or automating GitHub repository tasks.
npx skills add https://github.com/ancoleman/ai-design-components --skill writing-github-actionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 67 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Write GitHub Actions workflows for CI/CD, testing, deployments, and repository automation.
Who is it for?
Creating CI/CD workflows and automating tasks for GitHub-hosted repositories
Skip if: Non-GitHub CI systems or infrastructure-as-code provisioning
When should I use this skill?
You are writing or optimizing a GitHub Actions workflow YAML file
By the numbers
- covers reusable workflows, composite actions, and matrix builds
- reusable-workflow vs composite-action comparison table
- 3 common trigger categories documented
Files
Writing GitHub Actions
Create GitHub Actions workflows for CI/CD pipelines, automated testing, deployments, and repository automation using YAML-based configuration with native GitHub integration.
Purpose
GitHub Actions is the native CI/CD platform for GitHub repositories. This skill covers workflow syntax, triggers, job orchestration, reusable patterns, optimization techniques, and security practices specific to GitHub Actions.
Core Focus:
- Workflow YAML syntax and structure
- Reusable workflows and composite actions
- Matrix builds and parallel execution
- Caching and optimization strategies
- Secrets management and OIDC authentication
- Concurrency control and artifact management
Not Covered:
- CI/CD pipeline design strategy → See
building-ci-pipelines - GitOps deployment patterns → See
gitops-workflows - Infrastructure as code → See
infrastructure-as-code - Testing frameworks → See
testing-strategies
When to Use This Skill
Trigger this skill when:
- Creating CI/CD workflows for GitHub repositories
- Automating tests, builds, and deployments via GitHub Actions
- Setting up reusable workflows across multiple repositories
- Optimizing workflow performance with caching and parallelization
- Implementing security best practices for GitHub Actions
- Troubleshooting GitHub Actions YAML syntax or behavior
Workflow Fundamentals
Basic Workflow Structure
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm testKey Components:
name: Workflow display nameon: Trigger events (push, pull_request, schedule, workflow_dispatch)jobs: Job definitions (run in parallel by default)runs-on: Runner type (ubuntu-latest, windows-latest, macos-latest)steps: Sequential operations (uses actions or run commands)
Common Triggers
# Code events
on:
push:
branches: [main, develop]
paths: ['src/**']
pull_request:
types: [opened, synchronize, reopened]
# Manual trigger
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [dev, staging, production]
# Scheduled
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM UTCFor complete trigger reference, see references/triggers-events.md.
Decision Frameworks
Reusable Workflow vs Composite Action
Use Reusable Workflow when:
- Standardizing entire CI/CD jobs across repositories
- Need complete job replacement with inputs/outputs
- Want secrets to inherit by default
- Orchestrating multiple steps with job-level configuration
Use Composite Action when:
- Packaging 5-20 step sequences for reuse
- Need step-level abstraction within jobs
- Want to distribute via marketplace or private repos
- Require local file access without artifacts
| Feature | Reusable Workflow | Composite Action |
|---|---|---|
| Scope | Complete job | Step sequence |
| Trigger | workflow_call | uses: in step |
| Secrets | Inherit by default | Must pass explicitly |
| File Sharing | Requires artifacts | Same runner/workspace |
For detailed patterns, see references/reusable-workflows.md and references/composite-actions.md.
Caching Strategy
Use Built-in Setup Action Caching (Recommended):
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # or 'yarn', 'pnpm'Available for: Node.js, Python (pip), Java (maven/gradle), .NET, Go
Use Manual Caching when:
- Need custom cache keys
- Caching build outputs or non-standard paths
- Implementing multi-layer cache strategies
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-deps-For optimization techniques, see references/caching-strategies.md.
Self-Hosted vs GitHub-Hosted Runners
Use GitHub-Hosted Runners when:
- Standard build environments sufficient
- No private network access required
- Within budget or free tier limits
Use Self-Hosted Runners when:
- Need specific hardware (GPU, ARM, high memory)
- Require private network/VPN access
- High usage volume (cost optimization)
- Custom software must be pre-installed
Common Patterns
Multi-Job Workflow with Dependencies
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v5
with:
name: dist
- run: npm test
deploy:
needs: [build, test]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/download-artifact@v5
- run: ./deploy.shKey Elements:
needs:creates job dependencies (sequential execution)- Artifacts pass data between jobs
if:enables conditional executionenvironment:enables protection rules and environment secrets
Matrix Builds
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm testResult: 9 jobs (3 OS × 3 Node versions)
For advanced matrix patterns, see examples/matrix-build.yml.
Concurrency Control
# Cancel in-progress runs on new push
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Single deployment per environment
jobs:
deploy:
concurrency:
group: production-deployment
cancel-in-progress: false
steps: [...]Reusable Workflows
Defining a Reusable Workflow
File: .github/workflows/reusable-build.yml
name: Reusable Build
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
secrets:
NPM_TOKEN:
required: false
outputs:
artifact-name:
value: ${{ jobs.build.outputs.artifact }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact: build-output
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/Calling a Reusable Workflow
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '20'
secrets: inherit # Same org onlyFor complete reusable workflow guide, see references/reusable-workflows.md.
Composite Actions
Defining a Composite Action
File: .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Install dependencies and setup environment'
inputs:
node-version:
description: 'Node.js version'
default: '20'
outputs:
cache-hit:
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: "composite"
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- id: cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
- if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: npm ciKey Requirements:
runs.using: "composite"marks action typeshell:required for allrunsteps- Access inputs via
${{ inputs.name }}
Using a Composite Action
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-project
with:
node-version: '20'
- run: npm run buildFor detailed composite action patterns, see references/composite-actions.md.
Security Best Practices
Secrets Management
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Uses environment secrets
steps:
- env:
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.shOIDC Authentication (No Long-Lived Credentials)
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucketMinimal Permissions
# Workflow-level
permissions:
contents: read
pull-requests: write
# Job-level
jobs:
deploy:
permissions:
contents: write
deployments: write
steps: [...]Action Pinning
# Pin to commit SHA (not tags)
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0Enable Dependabot:
File: .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"For comprehensive security guide, see references/security-practices.md.
Optimization Techniques
Use built-in caching in setup actions (cache: 'npm'), run independent jobs in parallel, add conditional execution with if:, and minimize checkout depth (fetch-depth: 1).
For detailed optimization strategies, see references/caching-strategies.md.
Context Variables
Common contexts: github.*, secrets.*, inputs.*, matrix.*, runner.*
- run: echo "Branch: ${{ github.ref }}, Event: ${{ github.event_name }}"For complete syntax reference, see references/workflow-syntax.md.
Progressive Disclosure
Detailed References
For comprehensive coverage of specific topics:
- references/workflow-syntax.md - Complete YAML syntax reference
- references/reusable-workflows.md - Advanced reusable workflow patterns
- references/composite-actions.md - Composite action deep dive
- references/caching-strategies.md - Optimization and caching techniques
- references/security-practices.md - Comprehensive security guide
- references/triggers-events.md - All trigger types and event filters
- references/marketplace-actions.md - Recommended actions catalog
Working Examples
Complete workflow templates ready to use:
- examples/basic-ci.yml - Simple CI workflow
- examples/matrix-build.yml - Matrix strategy examples
- examples/reusable-deploy.yml - Reusable deployment workflow
- examples/composite-setup/ - Composite action template
- examples/monorepo-workflow.yml - Monorepo with path filters
- examples/security-scan.yml - Security scanning workflow
Validation Scripts
- scripts/validate-workflow.sh - Validate YAML syntax
Related Skills
building-ci-pipelines- CI/CD pipeline design strategygitops-workflows- GitOps deployment patternsinfrastructure-as-code- Terraform/Pulumi integrationtesting-strategies- Test frameworks and coveragesecurity-hardening- SAST/DAST toolsgit-workflows- Understanding branches and PRs
# Basic CI Workflow Example
# Demonstrates: Simple continuous integration with testing and linting
name: CI
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
# Minimal permissions (security best practice)
permissions:
contents: read
pull-requests: write
# Cancel in-progress runs on new push (optimization)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_ENV: test
jobs:
lint:
name: Lint Code
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- 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
test:
name: Run Tests
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
if: matrix.node-version == 20
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 5
build:
name: Build Application
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 3
# Composite Action Example
# Demonstrates: Reusable step sequence for project setup
name: 'Setup Project'
description: 'Install dependencies and setup project environment with caching'
author: 'Your Organization'
inputs:
node-version:
description: 'Node.js version to use (18, 20, 22)'
required: false
default: '20'
package-manager:
description: 'Package manager to use (npm, yarn, pnpm)'
required: false
default: 'npm'
working-directory:
description: 'Working directory for project'
required: false
default: '.'
install-command:
description: 'Custom install command (overrides package manager default)'
required: false
default: ''
outputs:
cache-hit:
description: "Whether dependencies were restored from cache"
value: ${{ steps.cache.outputs.cache-hit }}
node-version:
description: "Node.js version that was installed"
value: ${{ steps.setup-node.outputs.node-version }}
runs:
using: "composite"
steps:
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: ${{ inputs.package-manager }}
cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
- name: Cache dependencies
id: cache
uses: actions/cache@v4
with:
path: ${{ inputs.working-directory }}/node_modules
key: ${{ runner.os }}-${{ inputs.package-manager }}-${{ inputs.node-version }}-${{ hashFiles(format('{0}/package-lock.json', inputs.working-directory)) }}
restore-keys: |
${{ runner.os }}-${{ inputs.package-manager }}-${{ inputs.node-version }}-
${{ runner.os }}-${{ inputs.package-manager }}-
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
if [ -n "${{ inputs.install-command }}" ]; then
echo "Using custom install command"
${{ inputs.install-command }}
elif [ "${{ inputs.package-manager }}" = "npm" ]; then
npm ci
elif [ "${{ inputs.package-manager }}" = "yarn" ]; then
yarn install --frozen-lockfile
elif [ "${{ inputs.package-manager }}" = "pnpm" ]; then
pnpm install --frozen-lockfile
else
echo "Unsupported package manager: ${{ inputs.package-manager }}"
exit 1
fi
- name: Verify installation
shell: bash
run: |
echo "Node.js version: $(node --version)"
echo "npm version: $(npm --version)"
echo "Package manager: ${{ inputs.package-manager }}"
echo "Cache hit: ${{ steps.cache.outputs.cache-hit }}"
# Matrix Build Example
# Demonstrates: Cross-platform and multi-version testing
name: Matrix Build
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Test on ${{ matrix.os }} with Node ${{ matrix.node }}
runs-on: ${{ matrix.os }}
strategy:
# Don't cancel all jobs if one fails (see all results)
fail-fast: false
# Limit concurrent jobs
max-parallel: 6
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
# Add specific configuration
include:
- os: ubuntu-latest
node: 20
experimental: true
coverage: true
# Exclude problematic combinations
exclude:
- os: windows-latest
node: 18
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
continue-on-error: ${{ matrix.experimental || false }}
- name: Generate coverage
if: matrix.coverage
run: npm test -- --coverage
- name: Upload coverage
if: matrix.coverage
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.os }}-node${{ matrix.node }}
path: coverage/
build-matrix:
name: Build for ${{ matrix.platform }}
runs-on: ubuntu-latest
strategy:
matrix:
platform: [linux, windows, darwin]
arch: [amd64, arm64]
exclude:
- platform: windows
arch: arm64
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true
- name: Build
env:
GOOS: ${{ matrix.platform }}
GOARCH: ${{ matrix.arch }}
run: go build -o bin/app-${{ matrix.platform }}-${{ matrix.arch }}
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: app-${{ matrix.platform }}-${{ matrix.arch }}
path: bin/app-${{ matrix.platform }}-${{ matrix.arch }}
# Monorepo Workflow Example
# Demonstrates: Selective builds based on changed paths
name: Monorepo CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# Detect which packages changed
changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
shared: ${{ steps.filter.outputs.shared }}
docs: ${{ steps.filter.outputs.docs }}
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Check changed paths
uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'packages/frontend/**'
backend:
- 'packages/backend/**'
shared:
- 'packages/shared/**'
docs:
- 'docs/**'
- '*.md'
# Build frontend if changed
frontend:
name: Build Frontend
needs: changes
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.shared == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/frontend
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: packages/frontend/package-lock.json
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: frontend-dist
path: packages/frontend/dist/
# Build backend if changed
backend:
name: Build Backend
needs: changes
if: needs.changes.outputs.backend == 'true' || needs.changes.outputs.shared == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/backend
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: packages/backend/package-lock.json
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run migrations
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
run: npm run migrate
- name: Test
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
run: npm test
- name: Build
run: npm run build
# Update documentation if changed
docs:
name: Build Documentation
needs: changes
if: needs.changes.outputs.docs == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build docs
run: npm run docs:build
- name: Upload docs
uses: actions/upload-artifact@v4
with:
name: documentation
path: docs/build/
# Deploy if all checks pass (main branch only)
deploy:
name: Deploy All
needs: [changes, frontend, backend]
if: github.ref == 'refs/heads/main' && always() && !contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
environment: production
steps:
- name: Download frontend
if: needs.changes.outputs.frontend == 'true'
uses: actions/download-artifact@v5
with:
name: frontend-dist
path: frontend/
- name: Deploy frontend
if: needs.changes.outputs.frontend == 'true'
run: |
echo "Deploying frontend..."
# Deploy commands here
- name: Deploy backend
if: needs.changes.outputs.backend == 'true'
run: |
echo "Deploying backend..."
# Deploy commands here
# Reusable Deployment Workflow Example
# Demonstrates: Reusable workflow with inputs, secrets, and outputs
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
description: 'Environment to deploy to'
required: true
type: string
version:
description: 'Version to deploy'
required: false
type: string
default: 'latest'
dry-run:
description: 'Perform dry run without actual deployment'
required: false
type: boolean
default: false
secrets:
aws-access-key-id:
required: true
aws-secret-access-key:
required: true
api-token:
required: false
outputs:
deployment-url:
description: "URL of the deployed application"
value: ${{ jobs.deploy.outputs.url }}
deployment-id:
description: "Deployment ID"
value: ${{ jobs.deploy.outputs.id }}
jobs:
validate:
name: Validate Deployment
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Validate environment
run: |
VALID_ENVS="dev staging production"
if [[ ! " $VALID_ENVS " =~ " ${{ inputs.environment }} " ]]; then
echo "Invalid environment: ${{ inputs.environment }}"
echo "Valid environments: $VALID_ENVS"
exit 1
fi
- name: Validate version
run: |
if [[ "${{ inputs.version }}" != "latest" && ! "${{ inputs.version }}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version format: ${{ inputs.version }}"
exit 1
fi
deploy:
name: Deploy to ${{ inputs.environment }}
needs: validate
runs-on: ubuntu-latest
# Only one deployment per environment at a time
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false
environment:
name: ${{ inputs.environment }}
url: https://${{ inputs.environment }}.example.com
outputs:
url: https://${{ inputs.environment }}.example.com
id: ${{ steps.deploy.outputs.deployment-id }}
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- 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: Download build artifacts
uses: actions/download-artifact@v5
with:
name: dist-${{ inputs.version }}
path: dist/
- name: Deploy to S3
id: deploy
run: |
if [[ "${{ inputs.dry-run }}" == "true" ]]; then
echo "DRY RUN: Would deploy to s3://my-bucket-${{ inputs.environment }}"
DEPLOYMENT_ID="dry-run-${{ github.run_id }}"
else
aws s3 sync dist/ s3://my-bucket-${{ inputs.environment }}/ --delete
DEPLOYMENT_ID=$(date +%s)
fi
echo "deployment-id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT
- name: Invalidate CloudFront cache
if: inputs.dry-run != true
run: |
DIST_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[?Aliases.Items[0]=='${{ inputs.environment }}.example.com'].Id" --output text)
aws cloudfront create-invalidation --distribution-id $DIST_ID --paths "/*"
- name: Notify deployment
if: secrets.api-token != ''
env:
API_TOKEN: ${{ secrets.api-token }}
run: |
curl -X POST https://api.example.com/deployments \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"environment": "${{ inputs.environment }}",
"version": "${{ inputs.version }}",
"url": "https://${{ inputs.environment }}.example.com",
"status": "success"
}'
smoke-test:
name: Smoke Test
needs: deploy
runs-on: ubuntu-latest
if: inputs.dry-run != true
steps:
- name: Test deployment
run: |
URL="${{ needs.deploy.outputs.url }}"
echo "Testing $URL..."
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" $URL)
if [[ "$RESPONSE" != "200" ]]; then
echo "Deployment test failed with status $RESPONSE"
exit 1
fi
echo "Deployment test passed!"
# Security Scanning Workflow Example
# Demonstrates: CodeQL, dependency scanning, container scanning
name: Security Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
# Run weekly on Monday at 6 AM UTC
- cron: '0 6 * * 1'
permissions:
contents: read
security-events: write
jobs:
codeql:
name: CodeQL Analysis
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: ['javascript', 'python']
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-extended,security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
dependency-scan:
name: Dependency Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run npm audit
run: npm audit --audit-level=moderate
continue-on-error: true
- name: Check for known vulnerabilities
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
secret-scan:
name: Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
with:
fetch-depth: 0
- name: Scan for secrets
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
container-scan:
name: Container Scan
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build container image
uses: docker/build-push-action@v5
with:
context: .
push: false
load: true
tags: myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan container image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'sarif'
output: 'trivy-container.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload container scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-container.sarif'
sbom:
name: Generate SBOM
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
format: spdx-json
output-file: sbom.spdx.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
skill: "writing-github-actions"
version: "1.0"
domain: "developer"
base_outputs:
# Core workflow files (always generated)
- path: ".github/workflows/ci.yml"
must_contain:
- "name:"
- "on:"
- "jobs:"
- "runs-on:"
- "steps:"
- "uses: actions/checkout@"
# Dependabot configuration for action updates
- path: ".github/dependabot.yml"
must_contain:
- "version: 2"
- "package-ecosystem: \"github-actions\""
- "directory: \"/\""
- "schedule:"
conditional_outputs:
maturity:
starter:
# Basic CI workflow with standard triggers
- path: ".github/workflows/ci.yml"
must_contain:
- "on: [push, pull_request]"
- "npm ci"
- "npm test"
- "actions/setup-node@"
- "cache: 'npm'"
# Basic dependabot setup
- path: ".github/dependabot.yml"
must_contain:
- "interval: \"weekly\""
intermediate:
# Multiple workflows with job dependencies
- path: ".github/workflows/ci.yml"
must_contain:
- "needs:"
- "actions/upload-artifact@"
- "actions/download-artifact@"
- "if:"
# Deployment workflow with environment
- path: ".github/workflows/deploy.yml"
must_contain:
- "environment:"
- "secrets."
- "workflow_dispatch:"
# Composite action for common setup
- path: ".github/actions/setup-project/action.yml"
must_contain:
- "name:"
- "description:"
- "inputs:"
- "runs:"
- "using: \"composite\""
- "shell:"
# Path filters for monorepo
- path: ".github/workflows/ci.yml"
must_contain:
- "paths:"
- "branches:"
advanced:
# Reusable workflow definition
- path: ".github/workflows/reusable-build.yml"
must_contain:
- "workflow_call:"
- "inputs:"
- "outputs:"
- "secrets:"
# Caller workflow using reusable workflow
- path: ".github/workflows/ci.yml"
must_contain:
- "uses: ./.github/workflows/"
- "secrets: inherit"
# Matrix builds
- path: ".github/workflows/test-matrix.yml"
must_contain:
- "strategy:"
- "matrix:"
- "fail-fast:"
- "${{ matrix."
# Concurrency control
- path: ".github/workflows/deploy.yml"
must_contain:
- "concurrency:"
- "group:"
- "cancel-in-progress:"
# OIDC authentication (no long-lived credentials)
- path: ".github/workflows/deploy.yml"
must_contain:
- "permissions:"
- "id-token: write"
- "role-to-assume:"
# Composite action with outputs
- path: ".github/actions/build-cache/action.yml"
must_contain:
- "outputs:"
- "value: ${{ steps."
- "id:"
# Security scanning workflow
- path: ".github/workflows/security.yml"
must_contain:
- "permissions:"
- "contents: read"
- "security-events: write"
infrastructure:
docker:
# Docker build and push workflow
- path: ".github/workflows/docker.yml"
must_contain:
- "docker/build-push-action@"
- "docker/login-action@"
- "docker/setup-buildx-action@"
kubernetes:
# K8s deployment workflow
- path: ".github/workflows/deploy-k8s.yml"
must_contain:
- "kubectl"
- "kubeconfig"
- "namespace:"
terraform:
# Terraform CI workflow
- path: ".github/workflows/terraform.yml"
must_contain:
- "terraform init"
- "terraform plan"
- "terraform apply"
- "working-directory:"
cloud:
# Cloud-specific deployment
- path: ".github/workflows/deploy-cloud.yml"
must_contain:
- "environment:"
- "permissions:"
- "id-token: write"
scaffolding:
# Workflow directories
- path: ".github/workflows/"
type: "directory"
- path: ".github/actions/"
type: "directory"
# Common composite actions
- path: ".github/actions/setup-project/action.yml"
template: "composite-setup"
description: "Reusable setup action for installing dependencies"
# Standard CI workflow
- path: ".github/workflows/ci.yml"
template: "basic-ci"
description: "Basic CI workflow with linting and testing"
# Deployment workflow template
- path: ".github/workflows/deploy.yml"
template: "deploy"
description: "Deployment workflow with environment protection"
# Dependabot configuration
- path: ".github/dependabot.yml"
template: "dependabot"
description: "Automated dependency updates for GitHub Actions"
# Workflow validation script
- path: "scripts/validate-workflow.sh"
template: "validate"
description: "Script to validate GitHub Actions workflow syntax"
metadata:
primary_blueprints:
- "ci-cd"
contributes_to:
- "GitHub Actions workflows"
- "Reusable workflows"
- "Composite actions"
- "CI/CD pipelines"
- "Automated deployments"
- "Security scanning"
- "Matrix builds"
- "OIDC authentication"
typical_file_count:
starter: 2-3 # Basic CI + dependabot
intermediate: 4-6 # CI + deploy + composite action
advanced: 8-12 # Multiple workflows + reusable workflows + composite actions
validation:
# Files that should be validated
file_patterns:
- "*.yml"
- "*.yaml"
# Common validation errors to check
checks:
- "Valid YAML syntax"
- "Required keys present (name, on, jobs)"
- "Action versions pinned"
- "Permissions specified (security)"
- "Secrets referenced correctly"
- "Composite actions have shell specified"
- "Reusable workflows use workflow_call trigger"
optimization_hints:
# Caching
- "Use built-in cache in setup actions (cache: 'npm')"
- "Cache key should include lock file hash"
- "Use restore-keys for partial cache matches"
# Parallelization
- "Jobs run in parallel by default"
- "Use needs: to create dependencies only when required"
- "Matrix builds for multi-version testing"
# Concurrency
- "Add concurrency control to prevent redundant runs"
- "Use cancel-in-progress: true for feature branches"
- "Use cancel-in-progress: false for deployments"
# Security
- "Pin actions to commit SHA (not tags)"
- "Use OIDC for cloud authentication (no long-lived secrets)"
- "Specify minimal permissions at workflow or job level"
- "Use environment protection rules for production"
# Performance
- "Use fetch-depth: 1 for shallow clones"
- "Run independent jobs in parallel"
- "Use conditional execution (if:) to skip unnecessary jobs"
- "Upload artifacts only when needed (they consume storage)"
common_triggers:
- "push" # Code pushed to repository
- "pull_request" # PR opened/updated
- "workflow_dispatch" # Manual trigger
- "schedule" # Cron-based scheduling
- "workflow_call" # Reusable workflow
- "release" # Release created
- "workflow_run" # Triggered by another workflow
recommended_actions:
checkout: "actions/checkout@v5"
setup_node: "actions/setup-node@v4"
setup_python: "actions/setup-python@v5"
cache: "actions/cache@v4"
upload_artifact: "actions/upload-artifact@v4"
download_artifact: "actions/download-artifact@v5"
docker_buildx: "docker/setup-buildx-action@v3"
docker_login: "docker/login-action@v3"
docker_build: "docker/build-push-action@v5"
aws_credentials: "aws-actions/configure-aws-credentials@v4"
azure_login: "azure/login@v2"
gcp_auth: "google-github-actions/auth@v2"
Caching Strategies and Optimization
Techniques for optimizing GitHub Actions workflows through caching, parallelization, and resource management.
Table of Contents
1. Caching Overview 2. Built-in Setup Action Caching 3. Manual Caching with actions/cache 4. Cache Key Strategies 5. Docker Layer Caching 6. Parallelization Strategies 7. Workflow Optimization 8. Resource Management
---
Caching Overview
What Gets Cached
Suitable for Caching:
- Package manager dependencies (npm, pip, maven, etc.)
- Build outputs (compiled code, generated files)
- Downloaded tools and binaries
- Test fixtures and data
- Docker layers
NOT Suitable for Caching:
- Secrets or sensitive data
- Very large files (>10GB limit)
- Files that change every run
- OS-specific system files
Cache Limits
- Size Limit: 10GB per repository
- Retention: 7 days for unused caches
- Eviction: Oldest caches removed when limit reached
- Access: Read-only from forks (can't save caches)
Cache Scope
- Branch: Caches created on branch available to that branch and default branch
- Default Branch: Caches available to all branches
- Pull Requests: Can restore caches from base branch
---
Built-in Setup Action Caching
Most setup actions include built-in caching. This is the recommended approach.
Node.js (npm/yarn/pnpm)
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # or 'yarn', 'pnpm'What it Caches:
npm:~/.npmyarn:~/.yarn/cachepnpm:~/.pnpm-store
Cache Key: Based on lock file hash
Python (pip/pipenv/poetry)
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip' # or 'pipenv', 'poetry'What it Caches:
pip:~/.cache/pippipenv:~/.cache/pipenvpoetry:~/.cache/pypoetry
Cache Key: Based on requirements.txt or lock file
Java (Maven/Gradle)
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven' # or 'gradle'What it Caches:
maven:~/.m2/repositorygradle:~/.gradle/caches,~/.gradle/wrapper
.NET
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.x'
cache: trueWhat it Caches: NuGet packages
Go
- uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: trueWhat it Caches: Go modules and build cache
Ruby (Bundler)
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true---
Manual Caching with actions/cache
Use actions/cache@v4 for custom caching needs.
Basic Usage
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-Parameters:
path: Directory or file(s) to cache (required)key: Unique cache identifier (required)restore-keys: Fallback keys for partial matches (optional)upload-chunk-size: Upload chunk size in bytes (optional, default: 32MB)
Multiple Paths
- uses: actions/cache@v4
with:
path: |
~/.npm
~/.cache
node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}Checking Cache Hit
- name: Cache dependencies
id: cache-deps
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
if: steps.cache-deps.outputs.cache-hit != 'true'
run: npm ci
- name: Use cached dependencies
if: steps.cache-deps.outputs.cache-hit == 'true'
run: echo "Using cached dependencies"Save Cache Only on Success
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
save-always: false # Default: saves on success onlyCache Read-Only Mode
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
lookup-only: true # Don't save, only restore---
Cache Key Strategies
Hash-Based Keys
Dependency Files:
# Single lock file
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
# Multiple lock files
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
# Multiple file types
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}Source Files (Incremental Builds):
key: ${{ runner.os }}-build-${{ hashFiles('src/**/*.ts') }}Composite Keys
# OS + Node version + Dependencies
key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
# Branch + Dependencies
key: ${{ runner.os }}-${{ github.ref_name }}-${{ hashFiles('**/package-lock.json') }}
# Date-based (weekly cache rotation)
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}-${{ github.run_number }}Restore Keys (Fallback)
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-deps-
${{ runner.os }}-Matching Logic: 1. Exact match on key 2. Prefix match on restore-keys (most recent) 3. No cache if no match
Example:
- Key:
Linux-deps-abc123 - Restore keys:
Linux-deps-,Linux- - Will match:
Linux-deps-xyz789(ifabc123doesn't exist)
Dynamic Keys
# From file content
- id: get-date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ~/.cache
key: ${{ runner.os }}-cache-${{ steps.get-date.outputs.date }}---
Docker Layer Caching
Using docker/build-push-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: false
cache-from: type=gha
cache-to: type=gha,mode=maxCache Backends:
type=gha- GitHub Actions cachetype=registry- Container registrytype=local- Local directorytype=s3- S3 bucket
Cache Mode
# Default mode (minimal layers cached)
cache-to: type=gha
# Max mode (all layers cached)
cache-to: type=gha,mode=maxMulti-Platform Builds with Cache
- uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILDKIT_INLINE_CACHE=1Registry Cache
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: user/app:latest
cache-from: type=registry,ref=user/app:buildcache
cache-to: type=registry,ref=user/app:buildcache,mode=max---
Parallelization Strategies
Independent Parallel Jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm run build
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm auditResult: All 4 jobs run simultaneously
Matrix Strategy
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 4
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm testResult: 9 jobs (3 OS × 3 Node versions)
Test Splitting
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v5
- run: npm test -- --shard=${{ matrix.shard }}/4Monorepo Parallel Builds
jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
steps:
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'packages/frontend/**'
backend:
- 'packages/backend/**'
frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm run build --workspace=frontend
backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm run build --workspace=backend---
Workflow Optimization
Minimize Checkout
Shallow Clone:
- uses: actions/checkout@v5
with:
fetch-depth: 1Sparse Checkout:
- uses: actions/checkout@v5
with:
sparse-checkout: |
src/
package.json
package-lock.jsonPartial Clone:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:noneConditional Steps
# Skip on specific branches
- name: Deploy
if: github.ref == 'refs/heads/main'
run: ./deploy.sh
# Skip on PR
- name: Publish
if: github.event_name != 'pull_request'
run: npm publish
# Run only on schedule
- name: Cleanup
if: github.event_name == 'schedule'
run: ./cleanup.sh
# Skip if files unchanged
- name: Build frontend
if: contains(github.event.head_commit.modified, 'frontend/')
run: npm run build:frontendEarly Termination
- name: Check commit message
run: |
if [[ "${{ github.event.head_commit.message }}" =~ \[skip\ ci\] ]]; then
echo "Skipping CI"
exit 78 # Neutral exit code
fiArtifacts Strategy
Minimize Retention:
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
retention-days: 1 # Delete after 1 dayCompress Before Upload:
- name: Compress artifacts
run: tar -czf dist.tar.gz dist/
- uses: actions/upload-artifact@v4
with:
name: build
path: dist.tar.gzConcurrency Control
Cancel Redundant Runs:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: truePer-Job Concurrency:
jobs:
deploy:
concurrency:
group: deploy-production
cancel-in-progress: false
steps: [...]---
Resource Management
Self-Hosted Runner Optimization
Clean Workspace:
jobs:
build:
runs-on: self-hosted
steps:
- name: Clean workspace
run: |
rm -rf ${{ github.workspace }}/*
rm -rf ${{ github.workspace }}/.??*
- uses: actions/checkout@v5Pre-installed Tools:
# Verify tools available
- name: Check tools
run: |
node --version
npm --version
docker --versionMemory and CPU Constraints
Container Resources:
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:20
options: --cpus 2 --memory 4g
steps: [...]Job Timeout:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps: [...]Step Timeout:
- name: Long running task
run: ./slow-process.sh
timeout-minutes: 15Workflow Limits
GitHub-Hosted Runners (Free Tier):
- Linux: 2-core CPU, 7GB RAM, 14GB SSD
- Windows: 2-core CPU, 7GB RAM, 14GB SSD
- macOS: 3-core CPU, 14GB RAM, 14GB SSD
- Concurrent Jobs: 20 (free), 60 (Team), 180 (Enterprise)
Usage Limits:
- Public repos: Unlimited minutes
- Private repos: 2,000 min/month (free), 3,000 (Team)
- Workflow file size: 20KB per file, 100 files per repo
- Workflow run time: 72 hours maximum
- API requests: 1,000 per hour per repo
---
Advanced Patterns
Multi-Layer Caching
jobs:
build:
runs-on: ubuntu-latest
steps:
# Layer 1: Dependencies
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-
# Layer 2: Node modules
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
# Layer 3: Build cache
- uses: actions/cache@v4
with:
path: .next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
${{ runner.os }}-nextjs-
- run: npm ci
- run: npm run buildIncremental Build Cache
- name: Cache build
uses: actions/cache@v4
with:
path: |
dist/
.cache/
key: build-${{ hashFiles('src/**') }}-${{ github.sha }}
restore-keys: |
build-${{ hashFiles('src/**') }}-
build-
- name: Incremental build
run: npm run build -- --incrementalCross-Job Caching
jobs:
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/cache@v4
id: cache
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
build:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- run: npm ci --prefer-offline
- run: npm run build---
Monitoring and Debugging
Cache Statistics
View cache usage in repository:
- Settings → Actions → Caches
- See size, creation date, last accessed
- Manually delete caches if needed
Debugging Cache Issues
Enable Debug Logging:
Add repository secret: ACTIONS_STEP_DEBUG=true
Check Cache Hits:
- name: Cache dependencies
id: cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
- name: Debug cache
run: |
echo "Cache hit: ${{ steps.cache.outputs.cache-hit }}"
echo "Cache key: ${{ steps.cache.outputs.cache-primary-key }}"List Cache Contents:
- name: List cached files
run: |
echo "=== Cached Dependencies ==="
ls -lah node_modules/ || echo "No node_modules cached"---
For security optimization, see security-practices.md.
Composite Actions Guide
Step-level reusability through composite actions in GitHub Actions.
Table of Contents
1. Overview 2. Creating Composite Actions 3. Using Composite Actions 4. Inputs and Outputs 5. Best Practices 6. Common Patterns 7. Comparison with Reusable Workflows
---
Overview
Composite actions package multiple workflow steps into a single reusable action. They enable step-level code reuse without creating separate repositories or publishing to the marketplace.
Key Benefits:
- Package common step sequences
- Distribute via repository or marketplace
- Share same runner and workspace
- Support up to 10 levels of nesting
When to Use:
- Packaging 5-20 step sequences
- Setup/teardown operations
- Utility functions (validation, formatting)
- Organization-wide tooling standards
vs Reusable Workflows:
- Composite actions: Step-level reuse
- Reusable workflows: Job-level reuse
---
Creating Composite Actions
Basic Structure
File: .github/actions/my-action/action.yml
name: 'Action Name'
description: 'What this action does'
inputs:
# Input definitions
outputs:
# Output definitions
runs:
using: "composite"
steps:
# Step definitionsMinimal Example
name: 'Hello World'
description: 'Print hello message'
runs:
using: "composite"
steps:
- run: echo "Hello from composite action"
shell: bashKey Requirements:
runs.using: "composite"is required- All
runsteps must specifyshell: - Located in
action.ymlfile
With Inputs
name: 'Setup Project'
description: 'Install dependencies and setup environment'
inputs:
node-version:
description: 'Node.js version to use'
required: false
default: '20'
install-command:
description: 'Command to install dependencies'
required: false
default: 'npm ci'
working-directory:
description: 'Working directory'
required: false
default: '.'
runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
run: ${{ inputs.install-command }}
- name: Verify installation
shell: bash
run: node --version && npm --versionWith Outputs
name: 'Get Version'
description: 'Extract version from package.json'
inputs:
package-file:
description: 'Path to package.json'
required: false
default: 'package.json'
outputs:
version:
description: "Version number"
value: ${{ steps.get-version.outputs.version }}
major:
description: "Major version"
value: ${{ steps.parse.outputs.major }}
minor:
description: "Minor version"
value: ${{ steps.parse.outputs.minor }}
runs:
using: "composite"
steps:
- id: get-version
shell: bash
run: |
VERSION=$(jq -r '.version' ${{ inputs.package-file }})
echo "version=$VERSION" >> $GITHUB_OUTPUT
- id: parse
shell: bash
run: |
VERSION="${{ steps.get-version.outputs.version }}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
echo "major=$MAJOR" >> $GITHUB_OUTPUT
echo "minor=$MINOR" >> $GITHUB_OUTPUT
echo "patch=$PATCH" >> $GITHUB_OUTPUTWith Script Execution
Directory Structure:
.github/actions/validate/
├── action.yml
└── scripts/
└── validate.shaction.yml:
name: 'Validate Project'
description: 'Run validation checks'
inputs:
strict-mode:
description: 'Enable strict validation'
required: false
default: 'false'
runs:
using: "composite"
steps:
- name: Make script executable
shell: bash
run: chmod +x ${{ github.action_path }}/scripts/validate.sh
- name: Run validation
shell: bash
run: ${{ github.action_path }}/scripts/validate.sh
env:
STRICT_MODE: ${{ inputs.strict-mode }}
ACTION_PATH: ${{ github.action_path }}scripts/validate.sh:
#!/bin/bash
set -e
echo "Running validation..."
if [ "$STRICT_MODE" = "true" ]; then
echo "Strict mode enabled"
npm run lint
npm run type-check
npm test
else
echo "Standard validation"
npm run lint
fi---
Using Composite Actions
Local Action (Same Repository)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup project
uses: ./.github/actions/setup-project
with:
node-version: '20'
install-command: 'npm ci'
- run: npm run buildPath Requirements:
- Start with
./ - Point to directory containing
action.yml - Relative to repository root
External Action (Different Repository)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup project
uses: my-org/shared-actions/setup-project@v1
with:
node-version: '20'
- run: npm run buildReference Format: {owner}/{repo}/{path}@{ref}
Marketplace Action
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'Accessing Outputs
- name: Get version
id: version
uses: ./.github/actions/get-version
with:
package-file: 'package.json'
- name: Use version
run: |
echo "Version: ${{ steps.version.outputs.version }}"
echo "Major: ${{ steps.version.outputs.major }}"
echo "Minor: ${{ steps.version.outputs.minor }}"---
Inputs and Outputs
Input Configuration
inputs:
input-name:
description: 'Human-readable description'
required: true|false
default: 'default-value'Input Types: All inputs are strings in composite actions (no type specification like workflow_call)
Accessing Inputs:
runs:
using: "composite"
steps:
- run: echo "Input value: ${{ inputs.input-name }}"
shell: bashOutput Configuration
outputs:
output-name:
description: 'Human-readable description'
value: ${{ steps.step-id.outputs.value }}Setting Outputs:
steps:
- id: step-id
shell: bash
run: echo "value=result" >> $GITHUB_OUTPUTComposite Action Output:
outputs:
result:
description: "Result value"
value: ${{ steps.compute.outputs.result }}Environment Variables
Passing to Steps:
runs:
using: "composite"
steps:
- shell: bash
env:
INPUT_VALUE: ${{ inputs.my-input }}
CUSTOM_VAR: custom-value
run: |
echo "Input: $INPUT_VALUE"
echo "Custom: $CUSTOM_VAR"From Caller:
Environment variables from the caller job are available:
# Caller
jobs:
build:
env:
BUILD_ENV: production
steps:
- uses: ./.github/actions/my-action
# BUILD_ENV available in action
# Action can access BUILD_ENV
- run: echo $BUILD_ENV
shell: bash---
Best Practices
1. Always Specify Shell
# ❌ Bad - missing shell
- run: echo "Hello"
# ✅ Good - shell specified
- run: echo "Hello"
shell: bashAvailable Shells:
bash- Bash (default on Linux/macOS)sh- Bourne shellpwsh- PowerShell Corepowershell- Windows PowerShellcmd- Windows Command Promptpython- Python interpreter
2. Use github.action_path
# Reference files relative to action directory
- run: ${{ github.action_path }}/scripts/setup.sh
shell: bash
- run: |
cat ${{ github.action_path }}/config/default.json
shell: bash3. Provide Sensible Defaults
inputs:
node-version:
description: 'Node.js version'
default: '20'
install-command:
description: 'Install command'
default: 'npm ci'
working-directory:
description: 'Working directory'
default: '.'4. Document Inputs and Outputs
name: 'Setup Project'
description: |
Install dependencies and setup project environment.
Supports npm, yarn, and pnpm package managers.
inputs:
node-version:
description: |
Node.js version to use.
Supports: 18, 20, 22
Default: 20
default: '20'
package-manager:
description: |
Package manager to use.
Options: npm, yarn, pnpm
Default: npm
default: 'npm'
outputs:
cache-hit:
description: |
Whether dependencies were restored from cache.
Values: 'true' or 'false'
value: ${{ steps.cache.outputs.cache-hit }}5. Handle Errors Gracefully
runs:
using: "composite"
steps:
- name: Setup
shell: bash
run: ./setup.sh || true
- name: Build
shell: bash
run: |
if ! npm run build; then
echo "::error::Build failed"
exit 1
fi
- if: failure()
shell: bash
run: echo "::warning::Action failed, check logs"6. Use Conditional Steps
inputs:
run-tests:
description: 'Run tests'
default: 'true'
runs:
using: "composite"
steps:
- name: Build
shell: bash
run: npm run build
- if: inputs.run-tests == 'true'
name: Test
shell: bash
run: npm test7. Pin Action Versions
# ✅ Pin to commit SHA
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
# ⚠️ Tag can be moved
- uses: actions/checkout@v5---
Common Patterns
Pattern 1: Setup and Cache
name: 'Setup Node.js with Cache'
description: 'Setup Node.js and cache dependencies'
inputs:
node-version:
description: 'Node.js version'
default: '20'
cache-key-prefix:
description: 'Cache key prefix'
default: 'deps'
outputs:
cache-hit:
description: "Cache hit status"
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: "composite"
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- id: cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-${{ inputs.cache-key-prefix }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ inputs.cache-key-prefix }}-
- if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: npm ciPattern 2: Multi-Step Validation
name: 'Validate Code Quality'
description: 'Run linting, type checking, and tests'
inputs:
skip-tests:
description: 'Skip test execution'
default: 'false'
runs:
using: "composite"
steps:
- name: Lint
shell: bash
run: npm run lint
- name: Type Check
shell: bash
run: npm run type-check
- if: inputs.skip-tests != 'true'
name: Test
shell: bash
run: npm test
- if: always()
name: Generate Report
shell: bash
run: |
echo "# Quality Report" >> $GITHUB_STEP_SUMMARY
echo "✅ Linting passed" >> $GITHUB_STEP_SUMMARY
echo "✅ Type checking passed" >> $GITHUB_STEP_SUMMARYPattern 3: Conditional Tool Installation
name: 'Install Tools'
description: 'Install required development tools'
inputs:
install-docker:
description: 'Install Docker'
default: 'false'
install-kubectl:
description: 'Install kubectl'
default: 'false'
runs:
using: "composite"
steps:
- if: inputs.install-docker == 'true'
name: Setup Docker
uses: docker/setup-buildx-action@v3
- if: inputs.install-kubectl == 'true'
name: Install kubectl
shell: bash
run: |
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/Pattern 4: Build Matrix Support
name: 'Build Project'
description: 'Build for different configurations'
inputs:
build-type:
description: 'Build type: development, production'
default: 'production'
target-platform:
description: 'Target platform'
default: 'linux'
runs:
using: "composite"
steps:
- name: Configure build
shell: bash
run: |
echo "BUILD_TYPE=${{ inputs.build-type }}" >> $GITHUB_ENV
echo "PLATFORM=${{ inputs.target-platform }}" >> $GITHUB_ENV
- name: Build
shell: bash
run: |
npm run build -- --mode $BUILD_TYPE --platform $PLATFORM
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-${{ inputs.build-type }}-${{ inputs.target-platform }}
path: dist/Pattern 5: Notification Action
name: 'Send Notification'
description: 'Send build status notifications'
inputs:
webhook-url:
description: 'Webhook URL'
required: true
status:
description: 'Build status: success, failure'
required: true
message:
description: 'Custom message'
default: ''
runs:
using: "composite"
steps:
- name: Prepare payload
id: payload
shell: bash
run: |
MESSAGE="${{ inputs.message }}"
if [ -z "$MESSAGE" ]; then
MESSAGE="Build ${{ inputs.status }} for ${{ github.repository }}"
fi
PAYLOAD=$(cat <<EOF
{
"status": "${{ inputs.status }}",
"message": "$MESSAGE",
"repository": "${{ github.repository }}",
"ref": "${{ github.ref }}",
"sha": "${{ github.sha }}"
}
EOF
)
echo "payload<<EOF" >> $GITHUB_OUTPUT
echo "$PAYLOAD" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Send notification
shell: bash
run: |
curl -X POST \
-H "Content-Type: application/json" \
-d '${{ steps.payload.outputs.payload }}' \
${{ inputs.webhook-url }}Pattern 6: Cleanup Action
name: 'Cleanup Workspace'
description: 'Clean build artifacts and caches'
inputs:
remove-node-modules:
description: 'Remove node_modules'
default: 'true'
remove-build:
description: 'Remove build directory'
default: 'true'
remove-cache:
description: 'Remove cache'
default: 'false'
runs:
using: "composite"
steps:
- if: inputs.remove-node-modules == 'true'
shell: bash
run: rm -rf node_modules
- if: inputs.remove-build == 'true'
shell: bash
run: rm -rf dist build out
- if: inputs.remove-cache == 'true'
shell: bash
run: rm -rf .cache ~/.npm ~/.yarn---
Comparison with Reusable Workflows
| Feature | Composite Actions | Reusable Workflows |
|---|---|---|
| Scope | Step-level | Job-level |
| Trigger | uses: in step | uses: in job |
| Location | action.yml | .github/workflows/*.yml |
| Secrets | Must pass explicitly | Inherit by default |
| Environment Vars | Inherit from job | Do not inherit |
| Outputs | Step outputs | Job outputs |
| File Sharing | Same workspace | Requires artifacts |
| Nesting | Up to 10 levels | Up to 10 levels |
| Best For | Utility functions | Complete CI/CD jobs |
When to Use Composite Actions:
- Packaging step sequences (5-20 steps)
- Setup/teardown operations
- Need access to same workspace
- Distributing via marketplace
When to Use Reusable Workflows:
- Standardizing entire jobs
- Multi-job orchestration
- Need job-level configuration
- Cross-repository job reuse
---
Publishing to Marketplace
1. Create Public Repository
my-action/
├── action.yml
├── README.md
├── LICENSE
└── .github/
└── workflows/
└── test.yml2. Complete action.yml Metadata
name: 'My Awesome Action'
description: 'Does something awesome'
author: 'Your Name'
branding:
icon: 'package'
color: 'blue'
inputs:
# Input definitions
outputs:
# Output definitions
runs:
using: "composite"
steps:
# Steps3. Create README.md
# My Awesome Action
Description of what your action does.
## Usage
\`\`\`yaml
- uses: username/my-action@v1
with:
input-name: value
\`\`\`
## Inputs
- `input-name` - Description
## Outputs
- `output-name` - Description
## Example
\`\`\`yaml
# Full example workflow
\`\`\`4. Tag Release
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
# Create major version tag
git tag -fa v1 -m "Update v1 to v1.0.0"
git push origin v1 --force5. Publish to Marketplace
1. Go to repository on GitHub 2. Click "Releases" → "Create a new release" 3. Select tag (v1.0.0) 4. Check "Publish this Action to the GitHub Marketplace" 5. Fill in details 6. Publish release
---
Troubleshooting
Issue: Shell Not Specified
Error: Error: Required property is missing: shell
Solution:
# Add shell to all run steps
- run: echo "Hello"
shell: bashIssue: Cannot Access Script Files
Error: Script file not found
Solution:
# Use github.action_path
- run: ${{ github.action_path }}/scripts/setup.sh
shell: bashIssue: Inputs Not Working
Problem: Input values are empty
Solution:
# Ensure inputs are defined in action.yml
inputs:
my-input:
description: 'Description'
default: 'default-value'
# Access with correct syntax
- run: echo "${{ inputs.my-input }}"
shell: bashIssue: Outputs Not Available
Problem: Cannot access step outputs
Solution:
# Step must have id
- id: my-step
run: echo "result=value" >> $GITHUB_OUTPUT
shell: bash
# Output references step id
outputs:
result:
value: ${{ steps.my-step.outputs.result }}---
For job-level reuse, see reusable-workflows.md.
GitHub Marketplace Actions
Recommended actions from GitHub Marketplace for common workflows.
Official GitHub Actions
actions/checkout@v5
- Repository: https://github.com/actions/checkout
- Purpose: Clone repository
- Trust: Official GitHub
- Pin to:
8ade135a41bc03ea155e62e844d188df1ea18608
actions/setup-node@v4
- Repository: https://github.com/actions/setup-node
- Purpose: Setup Node.js with caching
- Trust: Official GitHub
actions/cache@v4
- Repository: https://github.com/actions/cache
- Purpose: Cache dependencies and build outputs
- Trust: Official GitHub
actions/upload-artifact@v4 / download-artifact@v5
- Repository: https://github.com/actions/upload-artifact
- Purpose: Share data between jobs
- Trust: Official GitHub
Cloud Provider Actions
aws-actions/configure-aws-credentials@v4
- Purpose: Configure AWS credentials via OIDC
- Trust: Official AWS
google-github-actions/auth@v2
- Purpose: Authenticate to GCP via OIDC
- Trust: Official Google
azure/login@v1
- Purpose: Azure login via OIDC
- Trust: Official Microsoft
Docker Actions
docker/build-push-action@v5
- Purpose: Build and push Docker images
- Trust: Official Docker
docker/setup-buildx-action@v3
- Purpose: Setup Docker Buildx
- Trust: Official Docker
Security Actions
github/codeql-action
- Purpose: Code security scanning (SAST)
- Trust: Official GitHub
aquasecurity/trivy-action
- Purpose: Container and dependency scanning
- Trust: Widely trusted (5K+ stars)
gitleaks/gitleaks-action@v2
- Purpose: Secret scanning
- Trust: Widely trusted (3K+ stars)
Utility Actions
peter-evans/create-pull-request@v5
- Purpose: Create PRs from workflow changes
- Trust: Widely trusted (6K+ stars)
dorny/paths-filter@v2
- Purpose: Detect changed paths (monorepo)
- Trust: Widely trusted (2K+ stars)
Action Verification
Before using third-party actions: 1. Check repository stars (>1,000 = widely trusted) 2. Review source code 3. Verify publisher identity 4. Pin to commit SHA 5. Enable Dependabot for updates
For security best practices, see security-practices.md.
Reusable Workflows Guide
Advanced patterns and best practices for creating and using reusable workflows in GitHub Actions.
Table of Contents
1. Overview 2. Creating Reusable Workflows 3. Calling Reusable Workflows 4. Passing Data 5. Matrix Strategies with Reusable Workflows 6. Nested Reusable Workflows 7. Best Practices 8. Common Patterns
---
Overview
Reusable workflows enable job-level reuse across repositories and workflows. They standardize CI/CD processes and reduce duplication.
Key Benefits:
- Centralize CI/CD logic in single location
- Standardize workflows across organization
- Version workflows independently
- Reduce maintenance burden
When to Use:
- Standardizing build/test/deploy jobs
- Enforcing organization policies
- Sharing workflows across repositories
- Complex multi-step jobs with configuration
Limitations:
- Maximum 10 levels of nesting
- Maximum 50 workflow calls per run
- Cannot call reusable workflows from same repository in different directory
- Secrets must be explicitly passed or inherited
---
Creating Reusable Workflows
Basic Structure
File: .github/workflows/reusable-build.yml
name: Reusable Build
on:
workflow_call:
inputs:
# Define inputs here
secrets:
# Define secrets here
outputs:
# Define outputs here
jobs:
# Job definitionsWith Inputs
name: Reusable Build
on:
workflow_call:
inputs:
node-version:
description: 'Node.js version to use'
required: false
type: string
default: '20'
build-command:
description: 'Build command to run'
required: false
type: string
default: 'npm run build'
working-directory:
description: 'Working directory'
required: false
type: string
default: '.'
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
- run: ${{ inputs.build-command }}Input Types:
string- Text valuenumber- Numeric valueboolean- true/falsechoice- Predefined options (not available in workflow_call)
With Secrets
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
api-key:
required: true
npm-token:
required: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v5
- name: Deploy
env:
API_KEY: ${{ secrets.api-key }}
NPM_TOKEN: ${{ secrets.npm-token }}
run: ./deploy.shWith Outputs
name: Reusable Build with Outputs
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
outputs:
artifact-name:
description: "Name of the uploaded artifact"
value: ${{ jobs.build.outputs.artifact }}
version:
description: "Version number"
value: ${{ jobs.build.outputs.version }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact: ${{ steps.upload.outputs.artifact-name }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v5
- id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- run: npm run build
- id: upload
uses: actions/upload-artifact@v4
with:
name: build-${{ steps.version.outputs.version }}
path: dist/---
Calling Reusable Workflows
Same Repository
name: CI
on: [push]
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '20'
build-command: 'npm run build:prod'Path Requirements:
- Must start with
./ - Must reference
.github/workflows/directory - Use relative path from repository root
Different Repository (Same Organization)
name: CI
on: [push]
jobs:
build:
uses: my-org/shared-workflows/.github/workflows/reusable-build.yml@v1
with:
node-version: '20'
secrets: inheritReference Format: {owner}/{repo}/{path}@{ref}
Refs:
- Tag:
@v1,@v1.2.3 - Branch:
@main,@develop - Commit SHA:
@abc123...(most secure)
Different Repository (Public)
jobs:
build:
uses: other-org/public-workflows/.github/workflows/build.yml@v1
with:
node-version: '20'
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}Note: Cannot use secrets: inherit for external organizations
---
Passing Data
Passing Inputs
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '20'
build-command: 'npm run build'
enable-tests: truePassing Secrets (Explicit)
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
secrets:
api-key: ${{ secrets.PROD_API_KEY }}
npm-token: ${{ secrets.NPM_TOKEN }}Passing All Secrets (Inherit)
jobs:
deploy:
uses: my-org/workflows/.github/workflows/deploy.yml@v1
with:
environment: production
secrets: inheritRequirements for `secrets: inherit`:
- Same organization or enterprise
- Caller workflow has access to secrets
Using Outputs from Reusable Workflows
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '20'
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Artifact: ${{ needs.build.outputs.artifact-name }}"
- run: echo "Version: ${{ needs.build.outputs.version }}"
- uses: actions/download-artifact@v5
with:
name: ${{ needs.build.outputs.artifact-name }}---
Matrix Strategies with Reusable Workflows
Matrix in Caller Workflow
jobs:
multi-platform-build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
uses: ./.github/workflows/build.yml
with:
os: ${{ matrix.os }}
node-version: ${{ matrix.node }}Reusable Workflow:
on:
workflow_call:
inputs:
os:
required: true
type: string
node-version:
required: true
type: string
jobs:
build:
runs-on: ${{ inputs.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm run buildMatrix in Reusable Workflow
# Reusable workflow with internal matrix
on:
workflow_call:
inputs:
environments:
required: true
type: string # JSON array
jobs:
deploy:
strategy:
matrix:
environment: ${{ fromJSON(inputs.environments) }}
runs-on: ubuntu-latest
environment: ${{ matrix.environment }}
steps:
- run: ./deploy.sh ${{ matrix.environment }}Calling:
jobs:
multi-env-deploy:
uses: ./.github/workflows/deploy.yml
with:
environments: '["dev", "staging", "production"]'---
Nested Reusable Workflows
Two-Level Nesting
Level 1: Base Workflow
File: .github/workflows/base-build.yml
name: Base Build
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
outputs:
artifact-name:
value: ${{ jobs.build.outputs.artifact }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact: build-output
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/Level 2: Extended Workflow
File: .github/workflows/build-and-test.yml
name: Build and Test
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
jobs:
build:
uses: ./.github/workflows/base-build.yml
with:
node-version: ${{ inputs.node-version }}
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v5
with:
name: ${{ needs.build.outputs.artifact-name }}
- run: npm testLevel 3: Main Workflow
name: CI
on: [push]
jobs:
ci:
uses: ./.github/workflows/build-and-test.yml
with:
node-version: '20'Limits
- Maximum nesting: 10 levels
- Maximum workflow calls: 50 per run
- Each level counts toward limits
---
Best Practices
1. Version Reusable Workflows
Use Semantic Versioning:
# Pin to major version (recommended)
uses: my-org/workflows/.github/workflows/build.yml@v1
# Pin to specific version (most stable)
uses: my-org/workflows/.github/workflows/build.yml@v1.2.3
# Pin to commit SHA (most secure)
uses: my-org/workflows/.github/workflows/build.yml@abc123...Create Tags:
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
# Update major version tag
git tag -fa v1 -m "Update v1 to v1.0.0"
git push origin v1 --force2. Document Inputs and Outputs
on:
workflow_call:
inputs:
node-version:
description: |
Node.js version to use for build.
Supports: 18, 20, 22
Default: 20
required: false
type: string
default: '20'
outputs:
artifact-name:
description: |
Name of the uploaded build artifact.
Use with actions/download-artifact to retrieve.
value: ${{ jobs.build.outputs.artifact }}3. Provide Sensible Defaults
inputs:
node-version:
type: string
default: '20'
build-command:
type: string
default: 'npm run build'
test-command:
type: string
default: 'npm test'
working-directory:
type: string
default: '.'4. Use Permissions Explicitly
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps: [...]5. Handle Errors Gracefully
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Build
run: npm run build
continue-on-error: ${{ inputs.allow-build-failure || false }}
- if: failure()
uses: actions/upload-artifact@v4
with:
name: build-logs
path: logs/6. Use Concurrency Controls
jobs:
deploy:
runs-on: ubuntu-latest
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false
steps: [...]---
Common Patterns
Pattern 1: Standardized Build
name: Standard Node.js Build
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
package-manager:
type: string
default: 'npm'
outputs:
artifact-name:
value: ${{ jobs.build.outputs.artifact }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact: build-${{ github.sha }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: ${{ inputs.package-manager }}
- name: Install dependencies
run: |
if [ "${{ inputs.package-manager }}" = "npm" ]; then
npm ci
elif [ "${{ inputs.package-manager }}" = "yarn" ]; then
yarn install --frozen-lockfile
elif [ "${{ inputs.package-manager }}" = "pnpm" ]; then
pnpm install --frozen-lockfile
fi
- run: ${{ inputs.package-manager }} run build
- uses: actions/upload-artifact@v4
with:
name: build-${{ github.sha }}
path: dist/Pattern 2: Multi-Environment Deploy
name: Deploy to Environment
on:
workflow_call:
inputs:
environment:
required: true
type: string
version:
required: true
type: string
secrets:
aws-access-key-id:
required: true
aws-secret-access-key:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: ${{ inputs.environment }}
url: https://${{ inputs.environment }}.example.com
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v5
- 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
run: |
echo "Deploying version ${{ inputs.version }} to ${{ inputs.environment }}"
./deploy.shPattern 3: Test Matrix
name: Test Matrix
on:
workflow_call:
inputs:
node-versions:
type: string
default: '["18", "20", "22"]'
os-list:
type: string
default: '["ubuntu-latest"]'
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ${{ fromJSON(inputs.os-list) }}
node: ${{ fromJSON(inputs.node-versions) }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm ci
- run: npm test
- if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.os }}-${{ matrix.node }}
path: test-results/Pattern 4: Conditional Jobs
name: CI with Optional Deploy
on:
workflow_call:
inputs:
run-tests:
type: boolean
default: true
run-lint:
type: boolean
default: true
deploy:
type: boolean
default: false
environment:
type: string
default: 'dev'
jobs:
lint:
if: inputs.run-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm run lint
test:
if: inputs.run-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm test
deploy:
if: inputs.deploy
needs: [lint, test]
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- run: ./deploy.shPattern 5: Composite Build and Release
name: Build and Release
on:
workflow_call:
inputs:
create-release:
type: boolean
default: false
version:
type: string
required: true
secrets:
github-token:
required: true
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-name: build-${{ inputs.version }}
steps:
- uses: actions/checkout@v5
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-${{ inputs.version }}
path: dist/
release:
if: inputs.create-release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v5
with:
name: ${{ needs.build.outputs.artifact-name }}
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ inputs.version }}
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.github-token }}---
Troubleshooting
Common Issues
1. Secrets Not Available
Problem: Reusable workflow cannot access secrets
Solution:
# Caller must pass secrets explicitly or use inherit
jobs:
deploy:
uses: ./.github/workflows/deploy.yml
secrets: inherit # Or pass explicitly2. Cannot Reference Local Reusable Workflow
Problem: uses: ./.github/workflows/build.yml not found
Solution:
- Ensure workflow file exists in
.github/workflows/directory - Use
./prefix for same repository - Check file path is relative to repository root
3. Matrix Evaluation Errors
Problem: Matrix values from inputs not working
Solution:
# Pass as JSON string
strategy:
matrix:
version: ${{ fromJSON(inputs.versions) }}
# Caller provides JSON array
with:
versions: '["18", "20", "22"]'4. Outputs Not Available
Problem: Cannot access outputs from reusable workflow
Solution:
# Reusable workflow must define outputs
on:
workflow_call:
outputs:
result:
value: ${{ jobs.build.outputs.result }}
# Job must export outputs
jobs:
build:
outputs:
result: ${{ steps.step-id.outputs.result }}---
Migration from Regular Workflows
Before (Duplicated Workflow)
# repo-a/.github/workflows/ci.yml
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- run: npm ci && npm run build
# repo-b/.github/workflows/ci.yml
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- run: npm ci && npm run buildAfter (Reusable Workflow)
Shared Workflow:
# shared-workflows/.github/workflows/node-build.yml
name: Node.js Build
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci && npm run buildCaller Workflows:
# repo-a/.github/workflows/ci.yml
name: CI
on: [push]
jobs:
build:
uses: org/shared-workflows/.github/workflows/node-build.yml@v1
with:
node-version: '20'
# repo-b/.github/workflows/ci.yml
name: CI
on: [push]
jobs:
build:
uses: org/shared-workflows/.github/workflows/node-build.yml@v1
with:
node-version: '18'---
For composite actions (step-level reuse), see composite-actions.md.
Security Best Practices for GitHub Actions
Comprehensive security guide for GitHub Actions workflows, including secrets management, OIDC authentication, permissions, and vulnerability prevention.
Table of Contents
1. Secrets Management 2. OIDC Authentication 3. Permissions and Token Scope 4. Action Pinning and Supply Chain Security 5. Pull Request Security 6. Environment Protection 7. Script Injection Prevention 8. Security Scanning
---
Secrets Management
Using GitHub Secrets
Repository Secrets:
Settings → Secrets and variables → Actions → New repository secret
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.shEnvironment Secrets:
Settings → Environments → [environment] → Add secret
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Uses production-specific secrets
steps:
- env:
API_KEY: ${{ secrets.API_KEY }} # Environment secret overrides repository secret
run: ./deploy.shOrganization Secrets:
Organization settings → Secrets and variables → Actions
Available to all repos in organization (or selected repos).
Secret Handling Best Practices
❌ Never Log Secrets:
# BAD - exposes secret in logs
- run: echo "API_KEY=${{ secrets.API_KEY }}"
# BAD - can leak via error messages
- run: curl -H "Authorization: Bearer ${{ secrets.API_KEY }}" https://api.example.com✅ Safe Secret Usage:
# GOOD - secret not in command output
- env:
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.sh
# GOOD - mask sensitive values
- run: |
echo "::add-mask::${{ secrets.API_KEY }}"
# Now safe to referenceNever Commit Secrets:
.gitignore:
.env
.env.local
.env.*.local
secrets.yml
credentials.json
*.key
*.pemSecret Scanning:
Enable in Settings → Code security and analysis:
- Secret scanning
- Push protection (blocks commits with secrets)
---
OIDC Authentication
OIDC (OpenID Connect) enables federated identity for cloud providers without storing long-lived credentials.
AWS OIDC
Setup (AWS Side):
1. Create OIDC provider in IAM:
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
2. Create IAM role with trust policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:OWNER/REPO:*"
}
}
}
]
}Workflow:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- run: |
aws s3 sync ./dist s3://my-bucket
aws cloudfront create-invalidation --distribution-id $DIST_ID --paths "/*"Azure OIDC
Setup (Azure Side):
1. Create App Registration 2. Create Federated Credential:
- Subject identifier:
repo:OWNER/REPO:ref:refs/heads/main
Workflow:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- run: az webapp deploy --resource-group $RG --name $APP_NAME --src-path ./distGoogle Cloud OIDC
Setup (GCP Side):
1. Create Workload Identity Pool 2. Create Workload Identity Provider 3. Grant service account access
Workflow:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/PROJECT_ID/locations/global/workloadIdentityPools/POOL/providers/PROVIDER'
service_account: 'SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com'
- run: gcloud app deployOIDC Benefits
Security:
- No long-lived credentials stored as secrets
- Automatic credential rotation
- Short-lived tokens (1 hour default)
- Fine-grained access control
Compliance:
- Audit trail via cloud provider logs
- No credential exposure risk
- Meets security compliance requirements
---
Permissions and Token Scope
GITHUB_TOKEN Permissions
Default (Permissive - Legacy):
permissions: write-allRecommended (Least Privilege):
permissions:
contents: read
pull-requests: writeAvailable Permissions:
| Permission | Read | Write | Description |
|---|---|---|---|
actions | ✓ | ✓ | GitHub Actions |
checks | ✓ | ✓ | Check runs and suites |
contents | ✓ | ✓ | Repository contents |
deployments | ✓ | ✓ | Deployments |
id-token | - | ✓ | OIDC token (write only) |
issues | ✓ | ✓ | Issues and comments |
packages | ✓ | ✓ | GitHub Packages |
pull-requests | ✓ | ✓ | Pull requests |
repository-projects | ✓ | ✓ | Projects (classic) |
security-events | ✓ | ✓ | Security events |
statuses | ✓ | ✓ | Commit statuses |
Workflow-Level Permissions
name: CI
permissions:
contents: read
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps: [...]Job-Level Permissions
jobs:
test:
permissions:
contents: read
runs-on: ubuntu-latest
steps: [...]
deploy:
permissions:
contents: write
deployments: write
runs-on: ubuntu-latest
steps: [...]Disable Permissions
permissions: {} # No permissions---
Action Pinning and Supply Chain Security
Pin Actions to Commit SHAs
❌ Bad (Tags can be moved):
- uses: actions/checkout@v5
- uses: some-org/action@v1.2.3✅ Good (SHA is immutable):
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- uses: some-org/action@a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 # v1.2.3Benefits:
- Immutable reference (cannot be modified)
- Protection against tag hijacking
- Specific version for reproducibility
Get Commit SHA for Tag
# Find SHA for a tag
git ls-remote --tags https://github.com/actions/checkout refs/tags/v5
# Output: abc123... refs/tags/v5Dependabot for Actions
File: .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "github-actions"
reviewers:
- "security-team"
commit-message:
prefix: "chore"
include: "scope"Benefits:
- Automated updates for pinned actions
- Creates PRs with SHA updates
- Security vulnerability notifications
Verify Action Source
Before Using Third-Party Actions:
1. Check Repository:
- Stars (>1,000 = widely trusted)
- Activity (recent commits, maintained)
- Issues (security concerns, responsiveness)
2. Review Code:
- Read action source code
- Look for suspicious behavior
- Check for security advisories
3. Verify Publisher:
- Official organization (GitHub, AWS, Google, etc.)
- Verified publisher badge
- Known maintainer
4. Use Marketplace:
- Verified creators
- Usage statistics
- Community feedback
---
Pull Request Security
pull_request vs pull_request_target
pull_request (Safe):
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm testBehavior:
- Runs workflow from PR head (fork)
- No access to secrets
- Safe for untrusted code
- Cannot write to repository
pull_request_target (Dangerous):
on:
pull_request_target:
branches: [main]
jobs:
comment:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Thanks for the PR!'
})Behavior:
- Runs workflow from base branch (main)
- Has access to secrets
- Can write to repository
- Dangerous with untrusted code
Security Rule:
# ❌ NEVER do this with pull_request_target
on: pull_request_target
jobs:
test:
steps:
- uses: actions/checkout@v5 # Checks out untrusted PR code
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm test # Runs untrusted code with secrets access
# ✅ SAFE: Use pull_request instead
on: pull_request
jobs:
test:
steps:
- uses: actions/checkout@v5
- run: npm testFork PR Permissions
Repository Settings:
Settings → Actions → General → Fork pull request workflows
Options:
- Require approval for first-time contributors (Recommended)
- Require approval for all outside collaborators
- Run workflows from fork pull requests
---
Environment Protection
Environment Configuration
Settings → Environments → [environment name]
Protection Rules:
- Required reviewers: 1-6 reviewers must approve
- Wait timer: Delay before deployment (0-43,200 minutes)
- Branch restrictions: Only specific branches can deploy
Example:
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://prod.example.com
steps:
- run: ./deploy.shBenefits:
- Manual approval gate
- Environment-specific secrets
- Deployment history
- URL tracking
Deployment Environments
jobs:
deploy-staging:
environment: staging
steps:
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
environment: production # Requires approval
steps:
- run: ./deploy.sh production---
Script Injection Prevention
Unsafe: Direct Variable Interpolation
❌ Vulnerable to injection:
- name: Print commit message
run: echo "${{ github.event.head_commit.message }}"Attack: Commit message like "; rm -rf / #" could execute arbitrary commands.
Safe: Use Environment Variables
✅ Safe approach:
- name: Print commit message
env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: echo "$COMMIT_MSG"Safe: Use Intermediate Steps
✅ Sanitize input:
- name: Validate input
env:
USER_INPUT: ${{ github.event.inputs.version }}
run: |
if [[ ! "$USER_INPUT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version format"
exit 1
fi
echo "VERSION=$USER_INPUT" >> $GITHUB_ENV
- name: Use validated input
run: echo "Deploying version $VERSION"Safe: Use Actions for Complex Operations
Instead of inline scripts with user input:
# ✅ Use actions/github-script for safe GitHub API calls
- uses: actions/github-script@v7
with:
script: |
const title = context.payload.pull_request.title;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `PR Title: ${title}`
});---
Security Scanning
CodeQL (SAST)
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly on Monday
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
strategy:
matrix:
language: ['javascript', 'python']
steps:
- uses: actions/checkout@v5
- uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3Dependency Scanning
Dependabot (Built-in):
Settings → Code security → Dependabot
npm audit:
- name: Security audit
run: npm audit --audit-level=highOWASP Dependency Check:
- name: OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'my-project'
path: '.'
format: 'HTML'Container Scanning
Trivy:
- name: Build image
run: docker build -t myimage:${{ github.sha }} .
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myimage:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'Secret Scanning
gitleaks:
- name: Scan for secrets
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}---
Security Checklist
Workflow Security
- [ ] Pin actions to commit SHAs
- [ ] Use minimal GITHUB_TOKEN permissions
- [ ] Enable Dependabot for action updates
- [ ] Review third-party actions before use
- [ ] Use environment variables for secrets
- [ ] Never log secrets
- [ ] Enable secret scanning
- [ ] Enable push protection
Authentication
- [ ] Use OIDC for cloud providers (no long-lived credentials)
- [ ] Rotate secrets regularly
- [ ] Use environment-specific secrets
- [ ] Implement environment protection rules
Pull Requests
- [ ] Use
pull_requestfor untrusted code - [ ] Restrict
pull_request_targetusage - [ ] Require approval for fork PRs
- [ ] Never checkout untrusted code with secrets
Deployment
- [ ] Use environment protection for production
- [ ] Require manual approval
- [ ] Implement deployment gates
- [ ] Use separate environments (dev, staging, prod)
Monitoring
- [ ] Enable security scanning (CodeQL, Dependabot)
- [ ] Monitor workflow logs
- [ ] Review security advisories
- [ ] Audit GITHUB_TOKEN usage
---
For optimization techniques, see caching-strategies.md.
GitHub Actions Triggers and Events
Complete reference for workflow triggers, event types, and activity filters.
Event Categories
Code Events
push- Code pushed to repositorypull_request- Pull request activitypull_request_target- Pull request targeting base branch (safe for secrets)create- Branch or tag createddelete- Branch or tag deleted
Repository Events
release- Release published, created, editedwatch- Repository starredfork- Repository forkedissues- Issue activityissue_comment- Issue/PR comment activitydiscussion- Discussion activity
Workflow Events
workflow_dispatch- Manual triggerworkflow_call- Reusable workflowworkflow_run- Triggered by another workflowrepository_dispatch- Webhook trigger
Scheduled Events
schedule- Cron-based trigger
Deployment Events
deployment- Deployment createddeployment_status- Deployment status changed
For complete syntax examples, see workflow-syntax.md.
GitHub Actions Workflow Syntax Reference
Complete reference for GitHub Actions YAML syntax, structure, and configuration options.
Table of Contents
1. Workflow File Structure 2. Trigger Configuration (on) 3. Environment Variables 4. Jobs Configuration 5. Steps Configuration 6. Expressions and Contexts 7. Filters and Patterns
---
Workflow File Structure
Location: .github/workflows/*.yml or .github/workflows/*.yaml
Top-Level Keys:
name: Workflow Name # Display name (optional)
run-name: Custom run name # Dynamic run name (optional)
on: [push, pull_request] # Trigger events (required)
permissions: read-all # Default permissions (optional)
env: # Workflow-level environment variables
NODE_ENV: production
defaults: # Default settings for all jobs
run:
shell: bash
working-directory: ./src
concurrency: # Concurrency control
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: # Job definitions (required)
job_id:
# Job configuration---
Trigger Configuration (on)
Single Event
on: pushMultiple Events
on: [push, pull_request, workflow_dispatch]Event with Configuration
Push Event:
on:
push:
branches:
- main
- 'releases/**'
branches-ignore:
- 'experimental/**'
tags:
- v*.*.*
paths:
- 'src/**'
- '**.js'
paths-ignore:
- 'docs/**'
- '**.md'Pull Request Event:
on:
pull_request:
types:
- opened
- synchronize
- reopened
branches:
- main
paths:
- 'src/**'Available Types: opened, synchronize, reopened, closed, assigned, unassigned, labeled, unlabeled, review_requested, ready_for_review
Pull Request Target (Safe for Forks):
on:
pull_request_target:
types: [opened, synchronize]Schedule Event:
on:
schedule:
- cron: '30 5 * * 1-5' # 5:30 AM UTC, Mon-Fri
- cron: '0 0 * * 0' # Midnight UTC, SundayManual Trigger (workflow_dispatch):
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
type: choice
options:
- dev
- staging
- production
default: dev
version:
description: 'Version to deploy'
required: true
type: string
enable-debug:
description: 'Enable debug mode'
required: false
type: boolean
default: falseInput Types: string, choice, boolean, environment
Reusable Workflow (workflow_call):
on:
workflow_call:
inputs:
config-path:
required: true
type: string
node-version:
required: false
type: string
default: '20'
secrets:
api-token:
required: true
npm-token:
required: false
outputs:
build-artifact:
description: "Name of build artifact"
value: ${{ jobs.build.outputs.artifact-name }}Repository Dispatch:
on:
repository_dispatch:
types: [deploy, test-all]Other Events:
on:
release:
types: [published, created, edited]
issues:
types: [opened, labeled]
issue_comment:
types: [created, edited]
deployment:
deployment_status:
watch:
types: [started] # Repository starred---
Environment Variables
Workflow-Level
env:
NODE_ENV: production
API_URL: https://api.example.com
jobs:
build:
steps:
- run: echo $NODE_ENV # Available to all jobsJob-Level
jobs:
build:
env:
BUILD_TYPE: release
steps:
- run: echo $BUILD_TYPE # Available to all steps in jobStep-Level
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
ENVIRONMENT: production
run: ./deploy.shDefault Environment Variables
GitHub provides default variables:
GITHUB_TOKEN- Authentication tokenGITHUB_REPOSITORY- Repository name (owner/repo)GITHUB_REF- Branch or tag refGITHUB_SHA- Commit SHAGITHUB_ACTOR- Username that triggered workflowGITHUB_WORKFLOW- Workflow nameGITHUB_RUN_ID- Unique run IDRUNNER_OS- Runner OS (Linux, Windows, macOS)RUNNER_TEMP- Temporary directory path
---
Jobs Configuration
Basic Job
jobs:
build:
name: Build Application
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm run buildJob with Dependencies
jobs:
build:
runs-on: ubuntu-latest
steps: [...]
test:
needs: build
runs-on: ubuntu-latest
steps: [...]
deploy:
needs: [build, test]
runs-on: ubuntu-latest
steps: [...]Job with Outputs
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
artifact-name: build-${{ steps.get-version.outputs.version }}
steps:
- id: get-version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUTAccessing Outputs:
jobs:
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Version: ${{ needs.build.outputs.version }}"Job with Matrix Strategy
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 4
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
include:
- os: ubuntu-latest
node: 20
experimental: true
exclude:
- os: windows-latest
node: 18
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}Job with Environment
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://prod.example.com
steps:
- run: ./deploy.shEnvironment Features:
- Protection rules (required reviewers)
- Environment-specific secrets
- Deployment history
- Wait timers
Job with Container
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:20-alpine
env:
NODE_ENV: test
volumes:
- /data:/data
options: --cpus 2 --memory 4g
steps:
- run: node --versionJob with Services
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- run: psql --host localhost --port 5432Job Concurrency
jobs:
deploy:
runs-on: ubuntu-latest
concurrency:
group: production-deploy
cancel-in-progress: false
steps: [...]Job Permissions
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps: [...]Available Permissions:
actions- GitHub Actionschecks- Checks on codecontents- Repository contentsdeployments- Deploymentsid-token- OIDC tokenissues- Issues and commentspackages- GitHub Packagespull-requests- Pull requestsrepository-projects- Projectssecurity-events- Security eventsstatuses- Commit statuses
Values: read, write, none
Job Conditions
jobs:
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps: [...]---
Steps Configuration
Using Actions
- name: Checkout repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
with:
fetch-depth: 0
submodules: trueRunning Commands
- name: Build application
run: |
npm ci
npm run build
npm testRunning Scripts
- name: Run script
run: ./scripts/deploy.sh
shell: bashAvailable Shells: bash, pwsh, python, sh, cmd, powershell
Step with ID (for outputs)
- id: get-version
run: |
VERSION=$(cat VERSION)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUTAccessing Outputs:
- run: echo "Version: ${{ steps.get-version.outputs.version }}"Step with Timeout
- name: Long running task
run: ./slow-script.sh
timeout-minutes: 30Step with Continue on Error
- name: Optional check
run: npm audit
continue-on-error: trueStep Conditions
- name: Deploy to production
if: github.ref == 'refs/heads/main'
run: ./deploy.sh
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: logs
path: logs/Status Check Functions:
success()- Previous steps succeededalways()- Always run (even on cancellation)cancelled()- Workflow was cancelledfailure()- Previous step failed
---
Expressions and Contexts
Expression Syntax
${{ expression }}Context Objects
github context:
github.actor # User who triggered
github.event_name # Event type (push, pull_request, etc.)
github.ref # Branch or tag ref
github.ref_name # Branch or tag name (without refs/)
github.sha # Commit SHA
github.repository # owner/repo
github.repository_owner # Repository owner
github.workflow # Workflow name
github.run_id # Unique run ID
github.run_number # Run number
github.job # Job IDenv context:
env.NODE_ENV
env.API_KEYsecrets context:
secrets.API_TOKEN
secrets.NPM_TOKENinputs context (workflow_dispatch or workflow_call):
inputs.environment
inputs.version
inputs.enable-debugmatrix context:
matrix.os
matrix.node
matrix.experimentalsteps context:
steps.build.outputs.version
steps.build.outcome # success, failure, cancelled, skipped
steps.build.conclusion # success, failure, cancelled, skipped, neutralneeds context:
needs.build.outputs.version
needs.build.result # success, failure, cancelled, skippedrunner context:
runner.os # Linux, Windows, macOS
runner.arch # X86, X64, ARM, ARM64
runner.name # Runner name
runner.temp # Temp directory path
runner.tool_cache # Tool cache directoryjob context:
job.status # success, failure, cancelled
job.services # Service containersOperators
Comparison:
==- Equal!=- Not equal<- Less than<=- Less than or equal>- Greater than>=- Greater than or equal
Logical:
&&- AND||- OR!- NOT
Example:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'Functions
contains(search, item):
if: contains(github.event.head_commit.message, '[skip ci]')startsWith(search, prefix):
if: startsWith(github.ref, 'refs/tags/')endsWith(search, suffix):
if: endsWith(github.ref, '-beta')format(template, args):
run: echo ${{ format('Hello {0}', github.actor) }}join(array, separator):
run: echo ${{ join(github.event.commits.*.message, ', ') }}toJSON(value):
run: echo '${{ toJSON(github) }}'fromJSON(value):
strategy:
matrix:
version: ${{ fromJSON('[18, 20, 22]') }}hashFiles(pattern):
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}---
Filters and Patterns
Branch Filters
on:
push:
branches:
- main
- 'releases/**' # Matches releases/v1, releases/v1/beta
- '!releases/alpha' # Exclude patternTag Filters
on:
push:
tags:
- v*.*.* # Matches v1.0.0, v2.1.3
- 'beta-*' # Matches beta-1, beta-2Path Filters
on:
push:
paths:
- 'src/**' # Any file in src/ and subdirectories
- '**.js' # Any .js file
- 'config/*.json' # JSON files in config/ (not subdirectories)
paths-ignore:
- 'docs/**'
- '**.md'
- '.github/**'Patterns:
*- Matches zero or more characters (except/)**- Matches zero or more directories?- Matches single character!- Negates pattern (exclude)
Activity Type Filters
on:
pull_request:
types:
- opened
- synchronize
- reopened
- closed
issues:
types:
- opened
- labeled
- assigned---
Advanced Features
Reusing Workflows
Caller Workflow:
jobs:
call-workflow:
uses: octo-org/repo/.github/workflows/reusable.yml@v1
with:
input1: value1
secrets:
token: ${{ secrets.TOKEN }}Reusable Workflow:
on:
workflow_call:
inputs:
input1:
required: true
type: string
secrets:
token:
required: trueComposite Actions
action.yml:
name: 'Setup Project'
description: 'Setup project environment'
inputs:
node-version:
description: 'Node version'
required: false
default: '20'
runs:
using: "composite"
steps:
- run: echo "Setting up Node ${{ inputs.node-version }}"
shell: bashWorkflow Commands
Set output:
echo "name=value" >> $GITHUB_OUTPUTSet environment variable:
echo "VAR_NAME=value" >> $GITHUB_ENVAdd to PATH:
echo "/path/to/bin" >> $GITHUB_PATHSet step summary:
echo "## Summary" >> $GITHUB_STEP_SUMMARY
echo "Build succeeded" >> $GITHUB_STEP_SUMMARYGroup logs:
echo "::group::Group name"
echo "Content"
echo "::endgroup::"Mask value (secret):
echo "::add-mask::$SECRET_VALUE"Debugging
Enable debug logging:
Set repository secret: ACTIONS_STEP_DEBUG=true
Enable runner diagnostic logging:
Set repository secret: ACTIONS_RUNNER_DEBUG=true
---
Complete Example
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
paths:
- 'src/**'
- 'package.json'
pull_request:
branches: [main]
workflow_dispatch:
inputs:
environment:
type: choice
options: [dev, staging, production]
env:
NODE_ENV: production
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20, 22]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm ci
- run: npm test
- if: matrix.node == 20
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
permissions:
contents: write
deployments: write
steps:
- uses: actions/checkout@v5
- run: ./deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }}---
For working examples, see the examples/ directory.
Related skills
FAQ
When should I use a reusable workflow versus a composite action?
Use a reusable workflow to standardize entire CI/CD jobs across repositories, and a composite action to package a step sequence for reuse within a job.
How do I cache dependencies?
Use built-in setup-action caching like setup-node with cache: npm, or actions/cache with custom keys for non-standard paths.