GitHub Actions Advanced Skill
Expert guidance for designing, writing, debugging, and securing production-grade GitHub Actions workflows.
When to Use This Skill
- User mentions GitHub Actions,
.github/workflows, CI/CD pipelines, runners, jobs, steps, or actions - User wants to automate builds, tests, deployments, or releases via GitHub
- User asks about matrix builds, reusable workflows, composite actions, or self-hosted runners
- User needs help with OIDC authentication, caching strategies, or secrets management
- User says "my GitHub pipeline is failing" or "set up CI for my repo"
- User asks about workflow security, hardening, or environment protection rules
When NOT to Use This Skill
- The user is working with GitLab CI/CD → recommend
gitlab-ci-patterns - The user is working with CircleCI, Jenkins, or other CI platforms
- The task is purely about Docker image building without GitHub context → recommend
docker-expert - The task is about Kubernetes deployment configuration → recommend
kubernetes-architect
Step 1: Understand Context Before Responding
When invoked, first gather context:
# Discover existing workflows in the repo
find .github/workflows -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -20
# Check for composite actions
find .github/actions -name "action.yml" 2>/dev/null
# Detect tech stack (influences runner OS, language setup actions)
ls package.json requirements.txt Gemfile go.mod Cargo.toml pom.xml 2>/dev/null
Then adapt recommendations to:
- Existing workflow patterns in the repo
- The tech stack and language runtime
- Whether this is a monorepo or single-project repo
- Whether self-hosted or GitHub-hosted runners are in use
Workflow Structure Reference
name: Workflow Name
on: # Triggers (see Triggers section)
push:
branches: [main]
permissions: # Always declare — principle of least privilege
contents: read
env: # Workflow-level env vars
NODE_VERSION: '20'
concurrency: # Prevent duplicate runs
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # Cancel older runs for same branch
jobs:
job-id:
name: Human-readable name
runs-on: ubuntu-24.04 # Pin OS version — never use -latest in prod
timeout-minutes: 15 # Always set — prevents runaway jobs
environment: production # Links to GitHub Environment (approvals/secrets)
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Step name
run: echo "hello"
Triggers (on:)
Common Patterns
on:
push:
branches: [main, 'release/**']
paths-ignore: ['**.md', 'docs/**'] # Skip docs-only changes
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
workflow_dispatch: # Manual trigger with inputs
inputs:
environment:
description: 'Deploy target'
required: true
type: choice
options: [staging, production]
dry-run:
description: 'Dry run only?'
type: boolean
default: false
schedule:
- cron: '0 2 * * 1' # Monday 2am UTC
workflow_call: # Called by other workflows (reusable)
inputs:
image-tag:
type: string
required: true
secrets:
deploy-token:
required: true
release:
types: [published] # Trigger only on published releases
pull_request_target: # Runs with repo secrets — use with care!
types: [labeled] # Gate with label + author_association check
Security Warning:
pull_request_targetruns with repo secrets. Only use after a maintainer labels the PR. Never check out fork code without explicit sandboxing.
Reusable Workflows
Split large pipelines into composable units stored in .github/workflows/.
Convention: Prefix internal/reusable workflows with _ (e.g., _build.yml).
Caller (.github/workflows/deploy.yml)
jobs:
call-build:
uses: ./.github/workflows/_build.yml # Same-repo reusable
# uses: org/repo/.github/workflows/build.yml@main # Cross-repo
with:
image-tag: ${{ github.sha }}
secrets: inherit # Pass all caller secrets down
call-test:
uses: ./.github/workflows/_test.yml
with:
node-version: '20'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Explicit secret passing
Reusable Workflow (.github/workflows/_build.yml)
on:
workflow_call:
inputs:
image-tag:
type: string
required: true
push:
type: boolean
default: false
secrets:
registry-token:
required: false
outputs:
digest:
description: "Image digest"
value: ${{ jobs.build.outputs.digest }}
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 20
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: build
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
with:
push: ${{ inputs.push }}
tags: myapp:${{ inputs.image-tag }}
Matrix Builds
jobs:
test:
strategy:
fail-fast: false # Don't cancel others if one fails
max-parallel: 4 # Limit concurrent runners
matrix:
os: [ubuntu-24.04, windows-2022, macos-14]
node: ['18', '20', '22']
exclude:
- os: windows-2022
node: '18'
include:
- os: ubuntu-24.04
node: '22'
experimental: true # Custom matrix variable
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm ci
- run: npm test
continue-on-error: ${{ matrix.experimental == true }}
Dynamic Matrix via Script
jobs:
generate-matrix:
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: set-matrix
run: |
SERVICES=$(find services -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | jq -R -s -c 'split("\n")[:-1]')
printf 'matrix={"service":%s}\n' "$SERVICES" >> "$GITHUB_OUTPUT"
build:
needs: generate-matrix
strategy:
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
runs-on: ubuntu-24.04
steps:
- env:
SERVICE: ${{ matrix.service }}
run: echo "Building $SERVICE"
Caching Strategies
Language Setup Actions (Preferred — No Extra Step Needed)
# Node.js
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '20'
cache: 'npm' # or 'yarn' or 'pnpm'
# Python
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.12'
cache: 'pip'
# Go
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with:
go-version: '1.23'
cache: true
# Java / Gradle / Maven
- uses: actions/setup-java@7a6d8a8234af8eb26422e24052f73b12b0e46a27 # v4.6.0
with:
distribution: 'temurin'
java-version: '21'
cache: 'maven' # or 'gradle'
Manual Cache (Any Tool)
- uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
id: cache-deps
with:
path: |
~/.cache/pip
.v