| name | github-actions-advanced |
| description | Design, debug, and harden GitHub Actions CI/CD workflows, including reusable workflows, matrix builds, self-hosted runners, OIDC authentication, caching, environments, secrets, and release automation.
|
| category | devops |
| risk | safe |
| source | community |
| date_added | 2026-05-30 |
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:
find .github/workflows -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -20
find .github/actions -name "action.yml" 2>/dev/null
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:
push:
branches: [main]
permissions:
contents: read
env:
NODE_VERSION: '20'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
job-id:
name: Human-readable name
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: production
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Step name
run: echo "hello"
Triggers (on:)
Common Patterns
on:
push:
branches: [main, 'release/**']
paths-ignore: ['**.md', 'docs/**']
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
workflow_dispatch:
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'
workflow_call:
inputs:
image-tag:
type: string
required: true
secrets:
deploy-token:
required: true
release:
types: [published]
pull_request_target:
types: [labeled]
Security Warning: pull_request_target runs 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
with:
image-tag: ${{ github.sha }}
secrets: inherit
call-test:
uses: ./.github/workflows/_test.yml
with:
node-version: '20'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
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
- id: build
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75
with:
push: ${{ inputs.push }}
tags: myapp:${{ inputs.image-tag }}
Matrix Builds
jobs:
test:
strategy:
fail-fast: false
max-parallel: 4
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
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
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
- id: set-matrix
run: |
SERVICES=$(ls services/ | jq -R -s -c 'split("\n")[:-1]')
echo "matrix={\"service\":$SERVICES}" >> $GITHUB_OUTPUT
build:
needs: generate-matrix
strategy:
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
runs-on: ubuntu-24.04
steps:
- run: echo "Building ${{ matrix.service }}"
Caching Strategies
Language Setup Actions (Preferred — No Extra Step Needed)
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with:
node-version: '20'
cache: 'npm'
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
with:
python-version: '3.12'
cache: 'pip'
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a
with:
go-version: '1.23'
cache: true
- uses: actions/setup-java@7a6d8a8234af8eb26422e24052f73b12b0e46a27
with:
distribution: 'temurin'
java-version: '21'
cache: 'maven'
Manual Cache (Any Tool)
- uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
id: cache-deps
with:
path: |
~/.cache/pip
.venv
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
${{ runner.os }}-pip-
- name: Install deps (only on cache miss)
if: steps.cache-deps.outputs.cache-hit != 'true'
run: pip install -r requirements.txt
Docker Layer Caching
- uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75
with:
cache-from: type=gha
cache-to: type=gha,mode=max
OIDC Authentication (Keyless Cloud Auth)
Never store long-lived cloud credentials as secrets. Use OIDC to get short-lived tokens that expire automatically.
AWS
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
role-session-name: GitHubActions-${{ github.run_id }}
GCP (Workload Identity Federation)
permissions:
id-token: write
contents: read
steps:
- uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f
with:
workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider
service_account: github-actions@my-project.iam.gserviceaccount.com
token_format: access_token
Azure (Federated Identity)
permissions:
id-token: write
contents: read
steps:
- uses: azure/login@a65d910e8af852a8061c627c456678983e180302
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
Environments & Deployment Protection
jobs:
deploy-staging:
environment:
name: staging
url: https://staging.myapp.com
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- run: ./scripts/deploy.sh staging
deploy-production:
needs: deploy-staging
environment:
name: production
url: https://myapp.com
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- run: ./scripts/deploy.sh production
Configure in Settings → Environments:
- Required reviewers — manual approval gate before run
- Wait timer — delay after approval (e.g., 10-minute buffer)
- Branch/tag restrictions — only
main or v* tags can deploy to prod
- Environment-specific secrets — override repo-level secrets per environment
- Deployment branches — whitelist which branches can target this environment
Secrets Management
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
Masking Dynamic Values
- name: Generate and mask dynamic token
run: |
TOKEN=$(./scripts/generate-token.sh)
echo "::add-mask::$TOKEN" # Mask in all subsequent logs
echo "DEPLOY_TOKEN=$TOKEN" >> $GITHUB_ENV
Secrets in Composite Actions
- uses: ./.github/actions/my-action
env:
SECRET_VALUE: ${{ secrets.MY_SECRET }}
Composite Actions
Package reusable step sequences into local actions. No container spin-up, no separate workflow file needed.
Action Definition (.github/actions/setup-app/action.yml)
name: Setup App
description: Install and configure application dependencies
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
install-flags:
description: 'Additional npm install flags'
required: false
default: ''
outputs:
cache-hit:
description: 'Whether the dependency cache was hit'
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: composite
steps:
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with:
node-version: ${{ inputs.node-version }}
cache: npm
- id: cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
with:
path: node_modules
key: ${{ runner.os }}-node-${{ inputs.node-version }}-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: npm ci ${{ inputs.install-flags }}
- name: Build
shell: bash
run: npm run build
Usage in a Workflow
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: ./.github/actions/setup-app
with:
node-version: '22'
install-flags: '--ignore-scripts'
Self-Hosted Runners
jobs:
build-gpu:
runs-on: [self-hosted, linux, x64, gpu]
timeout-minutes: 60
build-arm:
runs-on: [self-hosted, linux, arm64]
Runner Best Practices
| Practice | Details |
|---|
| Ephemeral runners | Use Actions Runner Controller (ARC) on Kubernetes for fresh runners per job |
| Isolation | Never share prod runners with untrusted/fork PR workflows |
| Cleanup hooks | Set ACTIONS_RUNNER_HOOK_JOB_COMPLETED to reset environment |
| Runner groups | Use groups to restrict which repos/workflows can access which runners |
| Labels | Use custom labels (e.g., gpu, high-memory) for precise targeting |
| Security | Disable fork PR access to self-hosted runners in Settings |
helm install arc \
--namespace arc-systems \
--create-namespace \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
Conditional Execution & Flow Control
- run: ./scripts/deploy.sh
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
- run: ./scripts/lint.sh
continue-on-error: true
jobs:
test:
runs-on: ubuntu-24.04
outputs:
result: ${{ steps.run-tests.outcome }}
deploy:
needs: [test, build]
if: |
needs.test.result == 'success' &&
needs.build.result == 'success' &&
github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
notify-failure:
needs: [test, deploy]
if: failure()
runs-on: ubuntu-24.04
steps:
- run: ./scripts/notify-slack.sh "Pipeline failed!"
Passing Data Between Jobs
jobs:
prepare:
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.get-version.outputs.version }}
should-deploy: ${{ steps.check.outputs.deploy }}
steps:
- id: get-version
run: |
VERSION=$(cat VERSION)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- id: check
run: |
if git log -1 --pretty=%B | grep -q '\[deploy\]'; then
echo "deploy=true" >> $GITHUB_OUTPUT
else
echo "deploy=false" >> $GITHUB_OUTPUT
fi
build:
needs: prepare
if: needs.prepare.outputs.should-deploy == 'true'
runs-on: ubuntu-24.04
steps:
- run: echo "Building version ${{ needs.prepare.outputs.version }}"
Security Hardening
1. Always Declare Permissions (Least Privilege)
permissions:
contents: read
jobs:
publish:
permissions:
contents: write
packages: write
pull-requests: write
id-token: write
2. Pin Third-Party Actions to Full Commit SHA
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
3. Prevent Script Injection
- run: echo "${{ github.event.pull_request.title }}"
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "$PR_TITLE"
- if: github.event.pull_request.draft == false
run: echo "Not a draft"
4. Restrict pull_request_target Usage
on:
pull_request_target:
types: [labeled]
jobs:
validate:
if: |
github.event.label.name == 'safe-to-test' &&
(github.event.pull_request.author_association == 'COLLABORATOR' ||
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'OWNER')
5. Harden with StepSecurity
- uses: step-security/harden-runner@4d991eb9995541a0b71d1b66f1f98a5f1bef422c
with:
egress-policy: audit
allowed-endpoints: >
api.github.com:443
registry.npmjs.org:443
objects.githubusercontent.com:443
Debugging Techniques
- name: Debug — dump github context
if: runner.debug == '1'
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" | jq '.'
- name: Debug — dump all contexts
if: runner.debug == '1'
run: |
echo "github: ${{ toJson(github) }}"
echo "env: ${{ toJson(env) }}"
echo "vars: ${{ toJson(vars) }}"
echo "runner: ${{ toJson(runner) }}"
- uses: mxschmitt/action-tmate@7b04f3521e6b0a9fc56fa8f9f50da4bcfb5fc7b5
if: failure() && runner.debug == '1'
with:
limit-access-to-actor: true
timeout-minutes: 30
- run: |
echo "=== Tool Versions ==="
node --version
python3 --version
go version
docker --version
echo "=== Disk Space ==="
df -h
echo "=== Memory ==="
free -h
Complete Pipeline Patterns
Pattern 1: Build → Test → Push → Deploy
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
permissions:
contents: read
jobs:
build-test:
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
checks: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test -- --coverage
- run: npm run build
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
with:
name: build-artifacts
path: dist/
retention-days: 7
push-image:
needs: build-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
packages: write
id-token: write
outputs:
image-digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@70b2cdc6480c1a8b86edf1777157f8f437de2166
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,format=long
type=raw,value=latest
- id: push
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
deploy-staging:
needs: push-image
runs-on: ubuntu-24.04
timeout-minutes: 30
environment:
name: staging
url: https://staging.myapp.com
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: ./scripts/deploy.sh staging ${{ needs.push-image.outputs.image-digest }}
deploy-production:
needs: deploy-staging
runs-on: ubuntu-24.04
timeout-minutes: 30
environment:
name: production
url: https://myapp.com
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: ./scripts/deploy.sh production ${{ needs.push-image.outputs.image-digest }}
Pattern 2: Automated Release with Changelog
name: Release
on:
push:
tags: ['v[0-9]+.[0-9]+.[0-9]+']
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0
- uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8
with:
generate_release_notes: true
make_latest: true
fail_on_unmatched_files: true
files: |
dist/**/*.tar.gz
dist/**/*.zip
Pattern 3: Dependency Auto-Update with PR
name: Dependency Updates
on:
schedule:
- cron: '0 9 * * 1'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-deps:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with:
node-version: '20'
- run: npx npm-check-updates -u
- run: npm install
- uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f
with:
commit-message: 'chore: update npm dependencies'
title: 'chore: update npm dependencies'
branch: 'chore/npm-updates'
delete-branch: true
body: |
Automated dependency updates generated by the dependency update workflow.
Please review and test before merging.
Pattern 4: Security Scanning Pipeline
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * *'
permissions:
contents: read
security-events: write
jobs:
codeql:
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: github/codeql-action/init@4f3212b61783c3c68e8309a0f18a699764811cda
with:
languages: javascript-typescript
- uses: github/codeql-action/autobuild@4f3212b61783c3c68e8309a0f18a699764811cda
- uses: github/codeql-action/analyze@4f3212b61783c3c68e8309a0f18a699764811cda
container-scan:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8
with:
scan-type: 'fs'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda
with:
sarif_file: 'trivy-results.sarif'
Common Pitfalls & Fixes
| Problem | Cause | Fix |
|---|
| Workflow doesn't trigger on PR from fork | Fork PRs use restricted GITHUB_TOKEN | Use pull_request not pull_request_target; avoid repo secrets in fork context |
Secret is *** in logs but exposed | Dynamic value not masked | Use echo "::add-mask::$VALUE" before using it |
| Cache never hits across branches | Cache key too specific | Add restore-keys fallback without branch or hash segment |
| Matrix job fails silently | fail-fast: true (default) cancels siblings | Set fail-fast: false during debugging |
| Job hangs indefinitely | No timeout-minutes set | Always set timeout-minutes on every job |
$GITHUB_OUTPUT not set | Old set-output command used | Use echo "key=value" >> $GITHUB_OUTPUT |
| OIDC token request fails | Missing id-token: write permission | Add to job-level permissions block |
| Reusable workflow can't access caller secrets | No secrets: inherit | Add secrets: inherit or explicitly pass secrets |
GitHub Actions Expressions Reference
${{ github.sha }}
${{ github.ref }}
${{ github.ref_name }}
${{ github.event_name }}
${{ github.actor }}
${{ github.repository }}
${{ github.run_id }}
${{ runner.os }}
${{ toJson(github) }}
${{ fromJson(needs.job.outputs.matrix) }}
${{ hashFiles('**/package-lock.json') }}
${{ format('{0}/{1}', var1, var2) }}
${{ join(matrix.items, ',') }}
${{ success() }}
${{ failure() }}
${{ cancelled() }}
${{ always() }}
Production Readiness Checklist
Before merging any workflow to main, verify:
Security
Reliability
Performance
Maintainability
Related Skills
gha-security-review — Deep security audit of existing workflow files
github-actions-templates — Copy-paste ready workflow templates
docker-expert — Container build optimization and Dockerfile best practices
kubernetes-architect — Deploying to Kubernetes from GitHub Actions
gitlab-ci-patterns — GitLab CI/CD equivalent patterns
Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Always test reusable workflows in a feature branch before merging to main.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.