skills/github-actions-templates/SKILL.md
Create production-ready GitHub Actions workflows for automated testing, building, and deploying applications. Use when setting up CI/CD with GitHub Actions, automating development workflows, or creating reusable workflow templates.
npx skillsauth add ckorhonen/claude-skills github-actions-templatesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Production-ready GitHub Actions workflow patterns for testing, building, and deploying applications.
Create efficient, secure GitHub Actions workflows for continuous integration and deployment across various tech stacks.
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
Reference: See assets/test-workflow.yml
name: Build and Push
on:
push:
branches: [main]
tags: ["v*"]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Reference: See assets/deploy-workflow.yml
name: Deploy to Kubernetes
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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-west-2
- name: Update kubeconfig
run: |
aws eks update-kubeconfig --name production-cluster --region us-west-2
- name: Deploy to Kubernetes
run: |
kubectl apply -f k8s/
kubectl rollout status deployment/my-app -n production
kubectl get services -n production
- name: Verify deployment
run: |
kubectl get pods -n production
kubectl describe deployment my-app -n production
name: Matrix Build
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: pytest
Reference: See assets/matrix-build.yml
# .github/workflows/reusable-test.yml
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
required: true
type: string
secrets:
NPM_TOKEN:
required: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm test
Use reusable workflow:
jobs:
call-test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20.x"
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: "fs"
scan-ref: "."
format: "sarif"
output: "trivy-results.sarif"
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: "trivy-results.sarif"
- name: Run Snyk Security Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
name: Deploy to Production
on:
push:
tags: ["v*"]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy application
run: |
echo "Deploying to production..."
# Deployment commands here
- name: Notify Slack
if: success()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "Deployment to production completed successfully!"
}
This section documents real failure modes and anti-patterns that cause GitHub Actions workflows to fail silently or unexpectedly.
Problem: YAML indentation, quotes, and expression syntax are the most common sources of silent failures.
Common Errors:
# ❌ WRONG - Missing indentation
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
# ✅ CORRECT
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
Error message: "unexpected mapping value" or "mapping values are not allowed here"
Fix: YAML is 2-space indentation strict. Use a YAML linter like yamllint in your workflow.
# ❌ WRONG - Using $ without github context
- run: echo ${{ matrix.node-version }}
# ❌ WRONG - Missing quotes on complex expressions
env:
MY_VAR: ${{ github.event.pull_request.title }}
# ✅ CORRECT
- run: echo "${{ matrix.node-version }}"
# ✅ CORRECT with quotes for strings
env:
MY_VAR: "${{ github.event.pull_request.title }}"
Error message: "Unable to process template language" or "unrecognized named value"
Fix: Always quote expressions that will be used in shell commands or as strings. GitHub Actions requires proper quoting for safe expansion.
# ❌ WRONG - Multiline without | or >
- run: echo "Line 1
Line 2"
# ✅ CORRECT using pipe (preserves newlines)
- run: |
npm install
npm run build
npm test
# ✅ CORRECT using > (folds newlines)
- name: Summary
run: >
This is a long
command that spans
multiple lines
Error message: "expected '<block end>', but found '\\n'"
Fix: Use | for literal blocks (preserves newlines) or > for folded blocks (folds newlines to spaces).
Problem: Secrets are the most frequently mishandled element. GitHub Actions has specific rules about when secrets are available.
Critical Mistakes:
# ❌ ABSOLUTELY WRONG - Secret is now in git history
- run: |
aws s3 sync . s3://my-bucket \
--aws-access-key-id AKIA1234567890ABCDEF \
--aws-secret-access-key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Error message: None initially, but this is a critical security vulnerability.
Fix: Always use ${{ secrets.SECRET_NAME }} and define secrets in GitHub repository settings.
# ❌ WRONG - Secrets not available in if: conditionals
if: ${{ secrets.DEPLOY_KEY != '' }}
# ✅ CORRECT - Secrets work in env: blocks at job and step level
jobs:
deploy:
runs-on: ubuntu-latest
env:
MESSAGE: ${{ secrets.MY_SECRET }} # ← Works at job level
steps:
- name: Deploy
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # ← Works at step level
run: |
# Now DEPLOY_KEY is available here
./deploy.sh
Error message: Secret appears as *** (masked) but is actually empty/undefined when used in if: conditions.
Fix: Use secrets in env: blocks at job or step level. Never use secrets in if: conditions — they are not available there.
# ❌ WRONG - Logs leak credentials
- name: Get credentials
run: |
TOKEN=$(curl -s https://api.example.com/token)
echo "TOKEN=$TOKEN" >> $GITHUB_OUTPUT
echo "Got token: $TOKEN" # ← Logged!
# ✅ CORRECT - Mark as secret in action output
- name: Get credentials
id: creds
run: |
TOKEN=$(curl -s https://api.example.com/token)
echo "token=$TOKEN" >> $GITHUB_OUTPUT # ← Set output
# Then reference as:
# ${{ steps.creds.outputs.token }}
Error message: Credentials appear in plain text in workflow logs.
Fix: Use $GITHUB_OUTPUT for sensitive values, and never echo them directly.
# ❌ WRONG - Parent secrets aren't automatically passed
jobs:
call-workflow:
uses: ./.github/workflows/deploy.yml
# secrets: inherit not specified!
# ✅ CORRECT
jobs:
call-workflow:
uses: ./.github/workflows/deploy.yml
secrets: inherit # ← Pass all parent secrets
Error message: Workflow runs but secrets are undefined in called workflow.
Fix: Include secrets: inherit when calling reusable workflows that need secrets.
Problem: GITHUB_TOKEN has limited scopes by default, and permissions are easy to get wrong.
Common Problems:
# ❌ WRONG - Trying to write packages with default permissions
- name: Push to Docker registry
run: |
docker push ghcr.io/${{ github.repository }}:latest
# Fails with "permission denied" because GITHUB_TOKEN lacks 'packages: write'
# ✅ CORRECT - Explicitly grant permissions
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # Read repo code
packages: write # Write Docker images
id-token: write # For OIDC token exchange
steps:
- uses: actions/checkout@v4
- name: Push to Docker registry
run: docker push ghcr.io/${{ github.repository }}:latest
Error message: "authentication failed, permission denied" or "Insufficient permissions" from API
Fix: Always explicitly declare permissions: at job level with required scopes (contents, packages, id-token, pull-requests, issues, deployments, etc.)
# ❌ WRONG - GITHUB_TOKEN can't trigger other workflows
- name: Trigger deployment
run: |
curl -X POST https://api.github.com/repos/${{ github.repository }}/dispatches \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-d '{"event_type":"deploy"}'
# Fails silently - GITHUB_TOKEN lacks workflow permissions
# ✅ CORRECT - Use a PAT (Personal Access Token) or GitHub App token
- name: Trigger deployment
run: |
curl -X POST https://api.github.com/repos/${{ github.repository }}/dispatches \
-H "Authorization: token ${{ secrets.WORKFLOW_PAT }}" \
-d '{"event_type":"deploy"}'
Error message: "Resource not accessible by integration" (HTTP 403)
Fix: GITHUB_TOKEN can't trigger workflows (repository_dispatch). Use a PAT stored in secrets for this.
# ❌ WRONG - GITHUB_TOKEN only has access to current repo
- name: Clone another repo
run: |
git clone https://github.com/other-org/private-repo.git
# Fails - GITHUB_TOKEN has no access
# ✅ CORRECT - Use a PAT with repo access
- name: Clone another repo
run: |
git clone https://x-access-token:${{ secrets.CROSS_REPO_PAT }}@github.com/other-org/private-repo.git
Error message: "fatal: could not read Username for 'https://github.com': terminal prompts disabled"
Fix: Use a Personal Access Token (PAT) with repo or read:repo_hook scope for cross-repository access.
Problem: Matrix builds are powerful but fail silently when combinations are wrong or when exclude/include are misused.
Common Pitfalls:
# ❌ WRONG - Using matrix variables that don't exist
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
# No 'version' defined here
steps:
- run: echo "Version is ${{ matrix.version }}" # ← Empty!
# ✅ CORRECT - Define all matrix variables used
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
version: ['1.0', '2.0']
steps:
- run: echo "Building for ${{ matrix.os }} v${{ matrix.version }}"
Error message: Matrix variable appears empty or undefined in logs.
Fix: All variables referenced in steps must be defined in strategy.matrix.
# ❌ WRONG - Invalid exclude syntax
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: ['18', '20']
exclude:
- os: windows-latest, node-version: '18' # ← Comma not allowed here
# ✅ CORRECT - Proper exclude syntax
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: ['18', '20']
exclude:
- os: windows-latest
node-version: '18'
# ✅ Using include for specific combinations
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
include:
- os: windows-latest
node-version: '20' # ← Only windows + node 20
Error message: Workflow fails to parse during validation phase.
Fix: exclude and include use YAML list syntax with separate lines for each property.
# ❌ WRONG - Creates 4 × 3 × 2 × 5 = 120 jobs unexpectedly
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest, linux-arm64]
node-version: ['16', '18', '20']
npm-version: ['8', '9']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
# This is excessive and wastes CI minutes
# ✅ CORRECT - Use include for specific combinations
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
node-version: '20'
npm-version: '9'
- os: macos-latest
node-version: '20'
npm-version: '9'
- os: windows-latest
node-version: '18'
npm-version: '8'
Error message: None, but suddenly your CI spend quadruples.
Fix: Use include to define specific combinations instead of letting matrix create a Cartesian product.
Problem: GitHub Actions caching is conservative—stale caches can persist for months and cause mysterious build failures.
Critical Issues:
# ❌ WRONG - Cache key doesn't change, stale deps stay cached
- uses: actions/cache@v3
with:
path: node_modules
key: deps-${{ runner.os }} # ← Never changes!
restore-keys: |
deps-${{ runner.os }}
# ✅ CORRECT - Include lockfile hash in cache key
- uses: actions/cache@v3
with:
path: node_modules
key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
deps-${{ runner.os }}-
Error message: Build passes locally but fails in CI with cryptic module errors.
Fix: Always include a hash of your dependency lock file in the cache key: ${{ hashFiles('**/package-lock.json') }} for npm, ${{ hashFiles('**/requirements.txt') }} for pip.
# ❌ WRONG - Caching wrong path, cache misses happen
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20.x"
- uses: actions/cache@v3
with:
path: /tmp/node_modules # ← Wrong path!
key: deps-${{ hashFiles('package-lock.json') }}
# ✅ CORRECT - Let action handle caching, or use right path
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20.x"
cache: "npm" # ← Let the action handle caching
# OR if manual caching:
- uses: actions/cache@v3
with:
path: ~/.npm # ← Correct npm cache location
key: npm-${{ hashFiles('package-lock.json') }}
Error message: Workflow runs slowly, repeatedly downloading dependencies.
Fix: Use the built-in cache: parameter in setup-node@v4, or cache the language runtime's official cache directory (e.g., ~/.npm, ~/.pip-cache).
# ❌ WRONG - Multiple branches writing to same cache
branches:
- main
- develop
- '*'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v3
with:
path: dist
key: build-${{ hashFiles('package.json') }}
# develop branch builds with old deps, overwrites cache
# main branch then gets develop's stale build artifacts
# ✅ CORRECT - Include branch in cache key or read-only on non-main
- uses: actions/cache@v3
with:
path: dist
key: build-${{ github.ref }}-${{ hashFiles('package.json') }}
restore-keys: |
build-refs/heads/main-
build-refs/heads/develop-
Error message: Mysterious test failures after merging (cache was poisoned from other branch).
Fix: Include ${{ github.ref }} in cache key to isolate per-branch caches, or use read-only mode on non-main branches.
# ❌ WRONG - Rebuilds every layer despite cache-from
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myimage:latest
# cache-from: type=gha missing!
# ✅ CORRECT - Use GitHub Actions cache backend
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myimage:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Error message: Docker builds are extremely slow despite local caching working fine.
Fix: Always include cache-from: type=gha and cache-to: type=gha,mode=max in docker/build-push-action to cache layers across runs.
Useful techniques when workflows fail:
ACTIONS_STEP_DEBUG=true secret in repository settings to see detailed logspermissions: block when API calls failyamllint locally on workflow files before pushing${{ secrets.NAME }} are defined in repository settingsassets/test-workflow.yml - Testing workflow templateassets/deploy-workflow.yml - Deployment workflow templateassets/matrix-build.yml - Matrix build templatereferences/common-workflows.md - Common workflow patternsgitlab-ci-patterns - For GitLab CI workflowsdeployment-pipeline-design - For pipeline architecturesecrets-management - For secrets handlingdocumentation
Create or expand an Idea.md / IDEA.md file from a rough description, existing repo, conversation history, notes, or other early-stage product inputs. Use when the user asks to "write an Idea.md", "turn this into an idea file", "capture this product idea", "expand this concept", or wants a repo-grounded concept brief before validation, PRD, or implementation work.
development
Write structured implementation plans from specs or requirements before touching code. Use when given a spec, requirements doc, or feature description, when user says "plan this out", "write a plan for", "how should we implement", or before starting any multi-step coding task.
testing
Expert guidance for video editing with ffmpeg, encoding best practices, and quality optimization. Use when working with video files, transcoding, remuxing, encoding settings, color spaces, or troubleshooting video quality issues.
development
Opinionated constraints for building better interfaces with agents. Use when building UI components, implementing animations, designing layouts, reviewing frontend accessibility, or working with Tailwind CSS, motion/react, or accessible primitives like Radix/Base UI.