writing-github-actions
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.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install ancoleman-ai-design-components-writing-github-actions
Repository
Skill path: skills/writing-github-actions
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.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, DevOps, Tech Writer, Security.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: ancoleman.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install writing-github-actions into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/ancoleman/ai-design-components before adding writing-github-actions to shared team environments
- Use writing-github-actions for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: writing-github-actions
description: 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.
---
# 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
```yaml
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 test
```
**Key Components:**
- `name`: Workflow display name
- `on`: 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
```yaml
# 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 UTC
```
For 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):**
```yaml
- 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
```yaml
- 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
```yaml
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.sh
```
**Key Elements:**
- `needs:` creates job dependencies (sequential execution)
- Artifacts pass data between jobs
- `if:` enables conditional execution
- `environment:` enables protection rules and environment secrets
### Matrix Builds
```yaml
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 test
```
Result: 9 jobs (3 OS × 3 Node versions)
For advanced matrix patterns, see `examples/matrix-build.yml`.
### Concurrency Control
```yaml
# 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`
```yaml
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
```yaml
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '20'
secrets: inherit # Same org only
```
For complete reusable workflow guide, see `references/reusable-workflows.md`.
## Composite Actions
### Defining a Composite Action
File: `.github/actions/setup-project/action.yml`
```yaml
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 ci
```
**Key Requirements:**
- `runs.using: "composite"` marks action type
- `shell:` required for all `run` steps
- Access inputs via `${{ inputs.name }}`
### Using a Composite Action
```yaml
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-project
with:
node-version: '20'
- run: npm run build
```
For detailed composite action patterns, see `references/composite-actions.md`.
## Security Best Practices
### Secrets Management
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Uses environment secrets
steps:
- env:
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.sh
```
### OIDC Authentication (No Long-Lived Credentials)
```yaml
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
```
### Minimal Permissions
```yaml
# Workflow-level
permissions:
contents: read
pull-requests: write
# Job-level
jobs:
deploy:
permissions:
contents: write
deployments: write
steps: [...]
```
### Action Pinning
```yaml
# Pin to commit SHA (not tags)
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
```
**Enable Dependabot:**
File: `.github/dependabot.yml`
```yaml
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.*`
```yaml
- 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 strategy
- `gitops-workflows` - GitOps deployment patterns
- `infrastructure-as-code` - Terraform/Pulumi integration
- `testing-strategies` - Test frameworks and coverage
- `security-hardening` - SAST/DAST tools
- `git-workflows` - Understanding branches and PRs
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/triggers-events.md
```markdown
# GitHub Actions Triggers and Events
Complete reference for workflow triggers, event types, and activity filters.
## Event Categories
### Code Events
- `push` - Code pushed to repository
- `pull_request` - Pull request activity
- `pull_request_target` - Pull request targeting base branch (safe for secrets)
- `create` - Branch or tag created
- `delete` - Branch or tag deleted
### Repository Events
- `release` - Release published, created, edited
- `watch` - Repository starred
- `fork` - Repository forked
- `issues` - Issue activity
- `issue_comment` - Issue/PR comment activity
- `discussion` - Discussion activity
### Workflow Events
- `workflow_dispatch` - Manual trigger
- `workflow_call` - Reusable workflow
- `workflow_run` - Triggered by another workflow
- `repository_dispatch` - Webhook trigger
### Scheduled Events
- `schedule` - Cron-based trigger
### Deployment Events
- `deployment` - Deployment created
- `deployment_status` - Deployment status changed
For complete syntax examples, see `workflow-syntax.md`.
```
### references/reusable-workflows.md
```markdown
# Reusable Workflows Guide
Advanced patterns and best practices for creating and using reusable workflows in GitHub Actions.
## Table of Contents
1. [Overview](#overview)
2. [Creating Reusable Workflows](#creating-reusable-workflows)
3. [Calling Reusable Workflows](#calling-reusable-workflows)
4. [Passing Data](#passing-data)
5. [Matrix Strategies with Reusable Workflows](#matrix-strategies-with-reusable-workflows)
6. [Nested Reusable Workflows](#nested-reusable-workflows)
7. [Best Practices](#best-practices)
8. [Common Patterns](#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`
```yaml
name: Reusable Build
on:
workflow_call:
inputs:
# Define inputs here
secrets:
# Define secrets here
outputs:
# Define outputs here
jobs:
# Job definitions
```
### With Inputs
```yaml
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 value
- `number` - Numeric value
- `boolean` - true/false
- `choice` - Predefined options (not available in workflow_call)
### With Secrets
```yaml
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.sh
```
### With Outputs
```yaml
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
```yaml
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)
```yaml
name: CI
on: [push]
jobs:
build:
uses: my-org/shared-workflows/.github/workflows/reusable-build.yml@v1
with:
node-version: '20'
secrets: inherit
```
**Reference Format:** `{owner}/{repo}/{path}@{ref}`
**Refs:**
- Tag: `@v1`, `@v1.2.3`
- Branch: `@main`, `@develop`
- Commit SHA: `@abc123...` (most secure)
### Different Repository (Public)
```yaml
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
```yaml
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '20'
build-command: 'npm run build'
enable-tests: true
```
### Passing Secrets (Explicit)
```yaml
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)
```yaml
jobs:
deploy:
uses: my-org/workflows/.github/workflows/deploy.yml@v1
with:
environment: production
secrets: inherit
```
**Requirements for `secrets: inherit`:**
- Same organization or enterprise
- Caller workflow has access to secrets
### Using Outputs from Reusable Workflows
```yaml
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
```yaml
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:**
```yaml
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 build
```
### Matrix in Reusable Workflow
```yaml
# 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:**
```yaml
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`
```yaml
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`
```yaml
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 test
```
**Level 3: Main Workflow**
```yaml
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:**
```yaml
# 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/[email protected]
# Pin to commit SHA (most secure)
uses: my-org/workflows/.github/workflows/build.yml@abc123...
```
**Create Tags:**
```bash
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 --force
```
### 2. Document Inputs and Outputs
```yaml
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
```yaml
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
```yaml
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps: [...]
```
### 5. Handle Errors Gracefully
```yaml
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
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false
steps: [...]
```
---
## Common Patterns
### Pattern 1: Standardized Build
```yaml
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
```yaml
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.sh
```
### Pattern 3: Test Matrix
```yaml
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
```yaml
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.sh
```
### Pattern 5: Composite Build and Release
```yaml
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:**
```yaml
# Caller must pass secrets explicitly or use inherit
jobs:
deploy:
uses: ./.github/workflows/deploy.yml
secrets: inherit # Or pass explicitly
```
**2. 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:**
```yaml
# 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:**
```yaml
# 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)
```yaml
# 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 build
```
### After (Reusable Workflow)
**Shared Workflow:**
```yaml
# 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 build
```
**Caller Workflows:**
```yaml
# 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`.
```
### references/composite-actions.md
```markdown
# Composite Actions Guide
Step-level reusability through composite actions in GitHub Actions.
## Table of Contents
1. [Overview](#overview)
2. [Creating Composite Actions](#creating-composite-actions)
3. [Using Composite Actions](#using-composite-actions)
4. [Inputs and Outputs](#inputs-and-outputs)
5. [Best Practices](#best-practices)
6. [Common Patterns](#common-patterns)
7. [Comparison with Reusable Workflows](#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`
```yaml
name: 'Action Name'
description: 'What this action does'
inputs:
# Input definitions
outputs:
# Output definitions
runs:
using: "composite"
steps:
# Step definitions
```
### Minimal Example
```yaml
name: 'Hello World'
description: 'Print hello message'
runs:
using: "composite"
steps:
- run: echo "Hello from composite action"
shell: bash
```
**Key Requirements:**
- `runs.using: "composite"` is required
- All `run` steps must specify `shell:`
- Located in `action.yml` file
### With Inputs
```yaml
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 --version
```
### With Outputs
```yaml
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_OUTPUT
```
### With Script Execution
**Directory Structure:**
```
.github/actions/validate/
├── action.yml
└── scripts/
└── validate.sh
```
**action.yml:**
```yaml
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:**
```bash
#!/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)
```yaml
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 build
```
**Path Requirements:**
- Start with `./`
- Point to directory containing `action.yml`
- Relative to repository root
### External Action (Different Repository)
```yaml
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 build
```
**Reference Format:** `{owner}/{repo}/{path}@{ref}`
### Marketplace Action
```yaml
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
```
### Accessing Outputs
```yaml
- 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
```yaml
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:**
```yaml
runs:
using: "composite"
steps:
- run: echo "Input value: ${{ inputs.input-name }}"
shell: bash
```
### Output Configuration
```yaml
outputs:
output-name:
description: 'Human-readable description'
value: ${{ steps.step-id.outputs.value }}
```
**Setting Outputs:**
```yaml
steps:
- id: step-id
shell: bash
run: echo "value=result" >> $GITHUB_OUTPUT
```
**Composite Action Output:**
```yaml
outputs:
result:
description: "Result value"
value: ${{ steps.compute.outputs.result }}
```
### Environment Variables
**Passing to Steps:**
```yaml
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:
```yaml
# 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
```yaml
# ❌ Bad - missing shell
- run: echo "Hello"
# ✅ Good - shell specified
- run: echo "Hello"
shell: bash
```
**Available Shells:**
- `bash` - Bash (default on Linux/macOS)
- `sh` - Bourne shell
- `pwsh` - PowerShell Core
- `powershell` - Windows PowerShell
- `cmd` - Windows Command Prompt
- `python` - Python interpreter
### 2. Use github.action_path
```yaml
# Reference files relative to action directory
- run: ${{ github.action_path }}/scripts/setup.sh
shell: bash
- run: |
cat ${{ github.action_path }}/config/default.json
shell: bash
```
### 3. Provide Sensible Defaults
```yaml
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
```yaml
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
```yaml
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
```yaml
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 test
```
### 7. Pin Action Versions
```yaml
# ✅ 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
```yaml
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 ci
```
### Pattern 2: Multi-Step Validation
```yaml
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_SUMMARY
```
### Pattern 3: Conditional Tool Installation
```yaml
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
```yaml
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
```yaml
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
```yaml
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.yml
```
### 2. Complete action.yml Metadata
```yaml
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:
# Steps
```
### 3. Create README.md
```markdown
# 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
```bash
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 --force
```
### 5. 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:**
```yaml
# Add shell to all run steps
- run: echo "Hello"
shell: bash
```
### Issue: Cannot Access Script Files
**Error:** Script file not found
**Solution:**
```yaml
# Use github.action_path
- run: ${{ github.action_path }}/scripts/setup.sh
shell: bash
```
### Issue: Inputs Not Working
**Problem:** Input values are empty
**Solution:**
```yaml
# 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: bash
```
### Issue: Outputs Not Available
**Problem:** Cannot access step outputs
**Solution:**
```yaml
# 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`.
```
### references/caching-strategies.md
```markdown
# Caching Strategies and Optimization
Techniques for optimizing GitHub Actions workflows through caching, parallelization, and resource management.
## Table of Contents
1. [Caching Overview](#caching-overview)
2. [Built-in Setup Action Caching](#built-in-setup-action-caching)
3. [Manual Caching with actions/cache](#manual-caching-with-actionscache)
4. [Cache Key Strategies](#cache-key-strategies)
5. [Docker Layer Caching](#docker-layer-caching)
6. [Parallelization Strategies](#parallelization-strategies)
7. [Workflow Optimization](#workflow-optimization)
8. [Resource Management](#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)
```yaml
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # or 'yarn', 'pnpm'
```
**What it Caches:**
- `npm`: `~/.npm`
- `yarn`: `~/.yarn/cache`
- `pnpm`: `~/.pnpm-store`
**Cache Key:** Based on lock file hash
### Python (pip/pipenv/poetry)
```yaml
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip' # or 'pipenv', 'poetry'
```
**What it Caches:**
- `pip`: `~/.cache/pip`
- `pipenv`: `~/.cache/pipenv`
- `poetry`: `~/.cache/pypoetry`
**Cache Key:** Based on requirements.txt or lock file
### Java (Maven/Gradle)
```yaml
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven' # or 'gradle'
```
**What it Caches:**
- `maven`: `~/.m2/repository`
- `gradle`: `~/.gradle/caches`, `~/.gradle/wrapper`
### .NET
```yaml
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.x'
cache: true
```
**What it Caches:** NuGet packages
### Go
```yaml
- uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true
```
**What it Caches:** Go modules and build cache
### Ruby (Bundler)
```yaml
- 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
```yaml
- 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
```yaml
- uses: actions/cache@v4
with:
path: |
~/.npm
~/.cache
node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
```
### Checking Cache Hit
```yaml
- 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
```yaml
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
save-always: false # Default: saves on success only
```
### Cache Read-Only Mode
```yaml
- 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:**
```yaml
# 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):**
```yaml
key: ${{ runner.os }}-build-${{ hashFiles('src/**/*.ts') }}
```
### Composite Keys
```yaml
# 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)
```yaml
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` (if `abc123` doesn't exist)
### Dynamic Keys
```yaml
# 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
```yaml
- 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=max
```
**Cache Backends:**
- `type=gha` - GitHub Actions cache
- `type=registry` - Container registry
- `type=local` - Local directory
- `type=s3` - S3 bucket
### Cache Mode
```yaml
# Default mode (minimal layers cached)
cache-to: type=gha
# Max mode (all layers cached)
cache-to: type=gha,mode=max
```
### Multi-Platform Builds with Cache
```yaml
- 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=1
```
### Registry Cache
```yaml
- 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
```yaml
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 audit
```
**Result:** All 4 jobs run simultaneously
### Matrix Strategy
```yaml
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 test
```
**Result:** 9 jobs (3 OS × 3 Node versions)
### Test Splitting
```yaml
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v5
- run: npm test -- --shard=${{ matrix.shard }}/4
```
### Monorepo Parallel Builds
```yaml
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:**
```yaml
- uses: actions/checkout@v5
with:
fetch-depth: 1
```
**Sparse Checkout:**
```yaml
- uses: actions/checkout@v5
with:
sparse-checkout: |
src/
package.json
package-lock.json
```
**Partial Clone:**
```yaml
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none
```
### Conditional Steps
```yaml
# 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:frontend
```
### Early Termination
```yaml
- name: Check commit message
run: |
if [[ "${{ github.event.head_commit.message }}" =~ \[skip\ ci\] ]]; then
echo "Skipping CI"
exit 78 # Neutral exit code
fi
```
### Artifacts Strategy
**Minimize Retention:**
```yaml
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
retention-days: 1 # Delete after 1 day
```
**Compress Before Upload:**
```yaml
- name: Compress artifacts
run: tar -czf dist.tar.gz dist/
- uses: actions/upload-artifact@v4
with:
name: build
path: dist.tar.gz
```
### Concurrency Control
**Cancel Redundant Runs:**
```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
```
**Per-Job Concurrency:**
```yaml
jobs:
deploy:
concurrency:
group: deploy-production
cancel-in-progress: false
steps: [...]
```
---
## Resource Management
### Self-Hosted Runner Optimization
**Clean Workspace:**
```yaml
jobs:
build:
runs-on: self-hosted
steps:
- name: Clean workspace
run: |
rm -rf ${{ github.workspace }}/*
rm -rf ${{ github.workspace }}/.??*
- uses: actions/checkout@v5
```
**Pre-installed Tools:**
```yaml
# Verify tools available
- name: Check tools
run: |
node --version
npm --version
docker --version
```
### Memory and CPU Constraints
**Container Resources:**
```yaml
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:20
options: --cpus 2 --memory 4g
steps: [...]
```
**Job Timeout:**
```yaml
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps: [...]
```
**Step Timeout:**
```yaml
- name: Long running task
run: ./slow-process.sh
timeout-minutes: 15
```
### Workflow 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
```yaml
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 build
```
### Incremental Build Cache
```yaml
- 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 -- --incremental
```
### Cross-Job Caching
```yaml
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:**
```yaml
- 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:**
```yaml
- name: List cached files
run: |
echo "=== Cached Dependencies ==="
ls -lah node_modules/ || echo "No node_modules cached"
```
---
For security optimization, see `security-practices.md`.
```
### examples/matrix-build.yml
```yaml
# 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 }}
```
### references/security-practices.md
```markdown
# 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](#secrets-management)
2. [OIDC Authentication](#oidc-authentication)
3. [Permissions and Token Scope](#permissions-and-token-scope)
4. [Action Pinning and Supply Chain Security](#action-pinning-and-supply-chain-security)
5. [Pull Request Security](#pull-request-security)
6. [Environment Protection](#environment-protection)
7. [Script Injection Prevention](#script-injection-prevention)
8. [Security Scanning](#security-scanning)
---
## Secrets Management
### Using GitHub Secrets
**Repository Secrets:**
Settings → Secrets and variables → Actions → New repository secret
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.sh
```
**Environment Secrets:**
Settings → Environments → [environment] → Add secret
```yaml
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.sh
```
**Organization Secrets:**
Organization settings → Secrets and variables → Actions
Available to all repos in organization (or selected repos).
### Secret Handling Best Practices
**❌ Never Log Secrets:**
```yaml
# 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:**
```yaml
# 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 reference
```
**Never Commit Secrets:**
`.gitignore`:
```
.env
.env.local
.env.*.local
secrets.yml
credentials.json
*.key
*.pem
```
**Secret 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:
```json
{
"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:**
```yaml
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:**
```yaml
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 ./dist
```
### Google Cloud OIDC
**Setup (GCP Side):**
1. Create Workload Identity Pool
2. Create Workload Identity Provider
3. Grant service account access
**Workflow:**
```yaml
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 deploy
```
### OIDC 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):**
```yaml
permissions: write-all
```
**Recommended (Least Privilege):**
```yaml
permissions:
contents: read
pull-requests: write
```
**Available 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
```yaml
name: CI
permissions:
contents: read
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps: [...]
```
### Job-Level Permissions
```yaml
jobs:
test:
permissions:
contents: read
runs-on: ubuntu-latest
steps: [...]
deploy:
permissions:
contents: write
deployments: write
runs-on: ubuntu-latest
steps: [...]
```
### Disable Permissions
```yaml
permissions: {} # No permissions
```
---
## Action Pinning and Supply Chain Security
### Pin Actions to Commit SHAs
**❌ Bad (Tags can be moved):**
```yaml
- uses: actions/checkout@v5
- uses: some-org/[email protected]
```
**✅ Good (SHA is immutable):**
```yaml
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
- uses: some-org/action@a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 # v1.2.3
```
**Benefits:**
- Immutable reference (cannot be modified)
- Protection against tag hijacking
- Specific version for reproducibility
### Get Commit SHA for Tag
```bash
# Find SHA for a tag
git ls-remote --tags https://github.com/actions/checkout refs/tags/v5
# Output: abc123... refs/tags/v5
```
### Dependabot for Actions
**File:** `.github/dependabot.yml`
```yaml
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):**
```yaml
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm test
```
**Behavior:**
- Runs workflow from PR head (fork)
- No access to secrets
- Safe for untrusted code
- Cannot write to repository
**pull_request_target (Dangerous):**
```yaml
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:**
```yaml
# ❌ 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 test
```
### Fork 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:**
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://prod.example.com
steps:
- run: ./deploy.sh
```
**Benefits:**
- Manual approval gate
- Environment-specific secrets
- Deployment history
- URL tracking
### Deployment Environments
```yaml
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:**
```yaml
- 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:**
```yaml
- name: Print commit message
env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: echo "$COMMIT_MSG"
```
### Safe: Use Intermediate Steps
**✅ Sanitize input:**
```yaml
- 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:
```yaml
# ✅ 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)
```yaml
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@v3
```
### Dependency Scanning
**Dependabot (Built-in):**
Settings → Code security → Dependabot
**npm audit:**
```yaml
- name: Security audit
run: npm audit --audit-level=high
```
**OWASP Dependency Check:**
```yaml
- name: OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'my-project'
path: '.'
format: 'HTML'
```
### Container Scanning
**Trivy:**
```yaml
- 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:**
```yaml
- 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_request` for untrusted code
- [ ] Restrict `pull_request_target` usage
- [ ] 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`.
```
### references/workflow-syntax.md
```markdown
# GitHub Actions Workflow Syntax Reference
Complete reference for GitHub Actions YAML syntax, structure, and configuration options.
## Table of Contents
1. [Workflow File Structure](#workflow-file-structure)
2. [Trigger Configuration (on)](#trigger-configuration-on)
3. [Environment Variables](#environment-variables)
4. [Jobs Configuration](#jobs-configuration)
5. [Steps Configuration](#steps-configuration)
6. [Expressions and Contexts](#expressions-and-contexts)
7. [Filters and Patterns](#filters-and-patterns)
---
## Workflow File Structure
**Location:** `.github/workflows/*.yml` or `.github/workflows/*.yaml`
**Top-Level Keys:**
```yaml
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
```yaml
on: push
```
### Multiple Events
```yaml
on: [push, pull_request, workflow_dispatch]
```
### Event with Configuration
**Push Event:**
```yaml
on:
push:
branches:
- main
- 'releases/**'
branches-ignore:
- 'experimental/**'
tags:
- v*.*.*
paths:
- 'src/**'
- '**.js'
paths-ignore:
- 'docs/**'
- '**.md'
```
**Pull Request Event:**
```yaml
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):**
```yaml
on:
pull_request_target:
types: [opened, synchronize]
```
**Schedule Event:**
```yaml
on:
schedule:
- cron: '30 5 * * 1-5' # 5:30 AM UTC, Mon-Fri
- cron: '0 0 * * 0' # Midnight UTC, Sunday
```
**Manual Trigger (workflow_dispatch):**
```yaml
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: false
```
**Input Types:** `string`, `choice`, `boolean`, `environment`
**Reusable Workflow (workflow_call):**
```yaml
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:**
```yaml
on:
repository_dispatch:
types: [deploy, test-all]
```
**Other Events:**
```yaml
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
```yaml
env:
NODE_ENV: production
API_URL: https://api.example.com
jobs:
build:
steps:
- run: echo $NODE_ENV # Available to all jobs
```
### Job-Level
```yaml
jobs:
build:
env:
BUILD_TYPE: release
steps:
- run: echo $BUILD_TYPE # Available to all steps in job
```
### Step-Level
```yaml
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
ENVIRONMENT: production
run: ./deploy.sh
```
### Default Environment Variables
GitHub provides default variables:
- `GITHUB_TOKEN` - Authentication token
- `GITHUB_REPOSITORY` - Repository name (owner/repo)
- `GITHUB_REF` - Branch or tag ref
- `GITHUB_SHA` - Commit SHA
- `GITHUB_ACTOR` - Username that triggered workflow
- `GITHUB_WORKFLOW` - Workflow name
- `GITHUB_RUN_ID` - Unique run ID
- `RUNNER_OS` - Runner OS (Linux, Windows, macOS)
- `RUNNER_TEMP` - Temporary directory path
---
## Jobs Configuration
### Basic Job
```yaml
jobs:
build:
name: Build Application
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm run build
```
### Job with Dependencies
```yaml
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
```yaml
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_OUTPUT
```
**Accessing Outputs:**
```yaml
jobs:
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Version: ${{ needs.build.outputs.version }}"
```
### Job with Matrix Strategy
```yaml
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
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://prod.example.com
steps:
- run: ./deploy.sh
```
**Environment Features:**
- Protection rules (required reviewers)
- Environment-specific secrets
- Deployment history
- Wait timers
### Job with Container
```yaml
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 --version
```
### Job with Services
```yaml
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 5432
```
### Job Concurrency
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
concurrency:
group: production-deploy
cancel-in-progress: false
steps: [...]
```
### Job Permissions
```yaml
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps: [...]
```
**Available Permissions:**
- `actions` - GitHub Actions
- `checks` - Checks on code
- `contents` - Repository contents
- `deployments` - Deployments
- `id-token` - OIDC token
- `issues` - Issues and comments
- `packages` - GitHub Packages
- `pull-requests` - Pull requests
- `repository-projects` - Projects
- `security-events` - Security events
- `statuses` - Commit statuses
**Values:** `read`, `write`, `none`
### Job Conditions
```yaml
jobs:
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps: [...]
```
---
## Steps Configuration
### Using Actions
```yaml
- name: Checkout repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v5.0.0
with:
fetch-depth: 0
submodules: true
```
### Running Commands
```yaml
- name: Build application
run: |
npm ci
npm run build
npm test
```
### Running Scripts
```yaml
- name: Run script
run: ./scripts/deploy.sh
shell: bash
```
**Available Shells:** `bash`, `pwsh`, `python`, `sh`, `cmd`, `powershell`
### Step with ID (for outputs)
```yaml
- id: get-version
run: |
VERSION=$(cat VERSION)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
```
**Accessing Outputs:**
```yaml
- run: echo "Version: ${{ steps.get-version.outputs.version }}"
```
### Step with Timeout
```yaml
- name: Long running task
run: ./slow-script.sh
timeout-minutes: 30
```
### Step with Continue on Error
```yaml
- name: Optional check
run: npm audit
continue-on-error: true
```
### Step Conditions
```yaml
- 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 succeeded
- `always()` - Always run (even on cancellation)
- `cancelled()` - Workflow was cancelled
- `failure()` - Previous step failed
---
## Expressions and Contexts
### Expression Syntax
```yaml
${{ expression }}
```
### Context Objects
**github context:**
```yaml
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 ID
```
**env context:**
```yaml
env.NODE_ENV
env.API_KEY
```
**secrets context:**
```yaml
secrets.API_TOKEN
secrets.NPM_TOKEN
```
**inputs context (workflow_dispatch or workflow_call):**
```yaml
inputs.environment
inputs.version
inputs.enable-debug
```
**matrix context:**
```yaml
matrix.os
matrix.node
matrix.experimental
```
**steps context:**
```yaml
steps.build.outputs.version
steps.build.outcome # success, failure, cancelled, skipped
steps.build.conclusion # success, failure, cancelled, skipped, neutral
```
**needs context:**
```yaml
needs.build.outputs.version
needs.build.result # success, failure, cancelled, skipped
```
**runner context:**
```yaml
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 directory
```
**job context:**
```yaml
job.status # success, failure, cancelled
job.services # Service containers
```
### Operators
**Comparison:**
- `==` - Equal
- `!=` - Not equal
- `<` - Less than
- `<=` - Less than or equal
- `>` - Greater than
- `>=` - Greater than or equal
**Logical:**
- `&&` - AND
- `||` - OR
- `!` - NOT
**Example:**
```yaml
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
```
### Functions
**contains(search, item):**
```yaml
if: contains(github.event.head_commit.message, '[skip ci]')
```
**startsWith(search, prefix):**
```yaml
if: startsWith(github.ref, 'refs/tags/')
```
**endsWith(search, suffix):**
```yaml
if: endsWith(github.ref, '-beta')
```
**format(template, args):**
```yaml
run: echo ${{ format('Hello {0}', github.actor) }}
```
**join(array, separator):**
```yaml
run: echo ${{ join(github.event.commits.*.message, ', ') }}
```
**toJSON(value):**
```yaml
run: echo '${{ toJSON(github) }}'
```
**fromJSON(value):**
```yaml
strategy:
matrix:
version: ${{ fromJSON('[18, 20, 22]') }}
```
**hashFiles(pattern):**
```yaml
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
```
---
## Filters and Patterns
### Branch Filters
```yaml
on:
push:
branches:
- main
- 'releases/**' # Matches releases/v1, releases/v1/beta
- '!releases/alpha' # Exclude pattern
```
### Tag Filters
```yaml
on:
push:
tags:
- v*.*.* # Matches v1.0.0, v2.1.3
- 'beta-*' # Matches beta-1, beta-2
```
### Path Filters
```yaml
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
```yaml
on:
pull_request:
types:
- opened
- synchronize
- reopened
- closed
issues:
types:
- opened
- labeled
- assigned
```
---
## Advanced Features
### Reusing Workflows
**Caller Workflow:**
```yaml
jobs:
call-workflow:
uses: octo-org/repo/.github/workflows/reusable.yml@v1
with:
input1: value1
secrets:
token: ${{ secrets.TOKEN }}
```
**Reusable Workflow:**
```yaml
on:
workflow_call:
inputs:
input1:
required: true
type: string
secrets:
token:
required: true
```
### Composite Actions
**action.yml:**
```yaml
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: bash
```
### Workflow Commands
**Set output:**
```bash
echo "name=value" >> $GITHUB_OUTPUT
```
**Set environment variable:**
```bash
echo "VAR_NAME=value" >> $GITHUB_ENV
```
**Add to PATH:**
```bash
echo "/path/to/bin" >> $GITHUB_PATH
```
**Set step summary:**
```bash
echo "## Summary" >> $GITHUB_STEP_SUMMARY
echo "Build succeeded" >> $GITHUB_STEP_SUMMARY
```
**Group logs:**
```bash
echo "::group::Group name"
echo "Content"
echo "::endgroup::"
```
**Mask value (secret):**
```bash
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
```yaml
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.
```