Expert guidance for designing, writing, debugging, and securing production-grade GitHub Actions workflows.
Read the detailed guide before executing this skill. It retains the complete procedure and reference material. Treat its safety, prerequisites, and validation requirements as mandatory. For focused work, load the relevant sections; for end-to-end work, read the guide completely.
.github/workflows, CI/CD pipelines, runners, jobs, steps, or actionsgitlab-ci-patterns
docker-expert
kubernetes-architect
# Workflow-level default — restrict everything
permissions:
contents: read
jobs:
publish:
# Job-level override — only expand what's needed
permissions:
contents: write # Only for release/publish jobs
packages: write # Only for container push jobs
pull-requests: write # Only for PR comment jobs
id-token: write # Only for OIDC auth jobs
# ❌ UNSAFE — tag can be mutated or hijacked
- uses: actions/checkout@v4
# ✅ SAFE — commit SHA is immutable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Tool to automate SHA pinning:
# npx pin-github-action .github/workflows/*.yml
# or: pip install ratchet && ratchet pin .github/workflows/
# ❌ UNSAFE — attacker controls PR title, which gets expanded in shell
- run: echo "${{ github.event.pull_request.title }}"
# ✅ SAFE — pass through environment variable (shell doesn't evaluate it)
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "$PR_TITLE"
# ✅ SAFE — expressions in if: conditions are evaluated by Actions, not shell
- if: github.event.pull_request.draft == false
run: echo "Not a draft"
Never place ${{ ... }} directly inside run: when the value can come from
PR metadata, workflow inputs, repository files, matrix JSON, or earlier job
outputs. Put it in env: first, validate allowlisted values where possible, and
reference the shell variable with quotes.
pull_request_target Usage# Only run when a maintainer adds a specific label — prevents untrusted execution
on:
pull_request_target:
types: [labeled]
jobs:
validate:
# Double-guard: check label name AND author_association
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')
# Add to every workflow — hardens runner, monitors outbound traffic
- uses: step-security/harden-runner@4d991eb9995541a0b71d1b66f1f98a5f1bef422c # v2.11.0
with:
egress-policy: audit # Start with 'audit', move to 'block' after confirming allowlist
allowed-endpoints: >
api.github.com:443
registry.npmjs.org:443
objects.githubusercontent.com:443