// Expert guidance for creating and maintaining GitHub Actions workflows and reusable workflows with security best practices
| name | GitHub Actions & Reusable Workflows Expert |
| description | Expert guidance for creating and maintaining GitHub Actions workflows and reusable workflows with security best practices |
| category | DevOps |
| tags | ["github-actions","ci-cd","workflows","automation","security","reusable-workflows"] |
This skill automatically activates when:
.github/workflows/*.yml filesaction.yml files (composite actions)Decision Tree: What Type of Workflow Should I Create?
Is build/test tooling required?
├─ NO → Create Universal Reusable Workflow
│ └─ Works for ALL project types
│ └─ Example: security-audit.yml, code-review.yml
│
└─ YES → Create Composite Action
└─ Caller provides build environment
└─ Example: fix-ci, improve-coverage
Universal Reusable Workflows (Analysis Tasks)
Composite Actions (Build/Test Tasks)
❌ Don't create project-specific variations of the same workflow
# BAD: Multiple similar workflows
fix-ci-foundry.yml
fix-ci-nodejs.yml
fix-ci-python.yml
✅ Instead: Create one composite action, let caller setup environment
# GOOD: One universal action
.github/actions/fix-ci/action.yml # Used by all project types
CRITICAL: You MUST read the entire workflow file before making recommendations.
Identify what type of workflow this is:
workflow_call trigger)action.yml in .github/actions/)Principle of Least Privilege: Only grant permissions that are actually needed.
# ✅ GOOD: Minimal required permissions
permissions:
contents: read # Only if reading code
issues: write # Only if creating/updating issues
pull-requests: write # Only if commenting on PRs
id-token: write # Only if using OIDC
# ❌ BAD: Over-permissive
permissions: write-all
Check for:
secrets: section (not inputs:)?GITHUB_TOKEN used instead of PAT where possible?# ✅ GOOD: Secrets are secrets
secrets:
CLAUDE_CODE_OAUTH_TOKEN:
required: true
GH_PAT:
required: false
# ❌ BAD: Secrets as inputs (logged in plain text!)
inputs:
api_token:
required: true
GITHUB_TOKEN vs Personal Access Token (PAT):
# ✅ GOOD: Use GITHUB_TOKEN when possible (explicit form)
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
# ⚠️ USE PAT ONLY WHEN NEEDED: For submodules or cross-repo access
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
submodules: recursive
Note on token syntax:
${{ secrets.GITHUB_TOKEN }} - Explicit, recommended for security clarity${{ github.token }} - Context reference (works but less explicit)secrets.GITHUB_TOKEN is the official best practiceWhen to use PAT:
Required fields for each input:
inputs:
parameter_name:
description: "Clear description of what this does" # REQUIRED
required: true/false # REQUIRED
type: string/number/boolean # REQUIRED
default: "value" # Optional
Common Issues:
description fieldstype specifications# ✅ GOOD: Well-defined inputs
inputs:
node_version:
description: "Node.js version to use"
required: false
type: string
default: "20"
create_issue:
description: "Create GitHub issue on failure"
required: false
type: boolean
default: true
# ❌ BAD: Poorly defined inputs
inputs:
version:
required: false # Missing description and type!
For composite actions, inputs work differently:
# In action.yml
inputs:
failed_run_id:
description: "ID of the failed workflow run"
required: true
github_token:
description: "GitHub token for API access"
required: true # Must be passed from caller
max_attempts:
description: "Maximum fix attempts"
required: false
default: "3"
# Access via ${{ inputs.parameter_name }}
runs:
using: "composite"
steps:
- run: echo "Failed run: ${{ inputs.failed_run_id }}"
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github_token }}
IMPORTANT: Composite actions cannot use ${{ github.token }} or ${{ secrets.* }} directly.
default: ${{ github.token }} - Does NOT work in composite actions${{ inputs.github_token }} inside the composite actionCheck for:
runs-on value (ubuntu-latest for universal workflows)needs: dependencies between jobstimeout-minutes (default 360min often too long)if: where needed# ✅ GOOD: Explicit timeout and conditions
jobs:
audit:
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.event_name == 'pull_request'
Action Version Pinning:
# ✅ GOOD: Pin to major version (gets patches)
- uses: actions/checkout@v4
# ✅ BETTER: Pin to exact SHA for security-critical workflows
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
# ❌ BAD: No version specified
- uses: actions/checkout
Step Naming:
# ✅ GOOD: Clear, descriptive names
- name: Install Foundry toolchain
uses: foundry-rs/foundry-toolchain@v1
- name: Run security audit with Claude
uses: anthropics/claude-code-action@v1
# ❌ BAD: Generic or missing names
- uses: foundry-rs/foundry-toolchain@v1 # No name
- name: Run action # Too generic
uses: anthropics/claude-code-action@v1
- name: Run Claude Code Action
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # REQUIRED
prompt: | # REQUIRED
Your detailed prompt here
claude_args: '--allowed-tools "..."' # Optional but recommended
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For gh CLI
Security Best Practice: Restrict bash commands to only what's needed.
# ✅ GOOD: Minimal tool access for audit workflow
claude_args: '--allowed-tools "Bash(gh issue create:*),Bash(gh issue list:*)"'
# ✅ GOOD: Read-only access for spec checking
claude_args: '--allowed-tools "Read,Write,Glob,Grep,Bash(gh:*),Bash(find:*),Bash(cat:*),Bash(date:*)"'
# ✅ GOOD: PR review access
claude_args: '--allowed-tools "Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"'
# ❌ BAD: No restrictions (Claude can run arbitrary bash)
# (omitting claude_args entirely)
Tool restriction patterns:
Bash(gh:*) - All gh CLI commandsBash(gh issue create:*) - Only creating issuesBash(git:*) - All git commandsRead,Write,Glob,Grep - File operations onlymcp__github_inline_comment__create_inline_comment - Inline PR commentsEffective prompts include:
/audit, /fix-ci, etc.# ✅ GOOD: Comprehensive prompt
prompt: |
/audit
Please perform a comprehensive security audit.
Generate a detailed audit report and save it to `audit-report-$(date +%Y%m%d).md`.
After completing the audit:
1. If you find any CRITICAL severity issues, create a GitHub issue using the `gh` CLI
2. Include link to this workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
3. Label with: security, critical, audit
# ❌ BAD: Vague prompt
prompt: "Please audit the code"
# ✅ GOOD: Upload artifacts even on failure
- name: Upload audit report
if: always() # Run even if previous steps failed
uses: actions/upload-artifact@v4
# ✅ GOOD: Only run on success
- name: Post success comment
if: success()
run: gh pr comment ${{ inputs.pr_number }} --body "✅ Audit passed!"
# ✅ GOOD: Only run on failure
- name: Create failure issue
if: failure()
run: gh issue create --title "Workflow failed" --label bug
Best practices:
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: audit-report-${{ github.run_number }} # Unique name with run number
path: | # Multiple paths supported
audit-report-*.md
audits/**/*.md
retention-days: 90 # Don't keep forever
if-no-files-found: warn # Don't fail if missing
# ✅ GOOD: Cache dependencies
- name: Cache Node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# ✅ GOOD: Cache Foundry installation
- name: Cache Foundry
uses: actions/cache@v4
with:
path: ~/.foundry
key: ${{ runner.os }}-foundry-${{ hashFiles('foundry.toml') }}
# ✅ GOOD: Cancel in-progress runs for PR updates
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# ⚠️ USE CAREFULLY: For deployments, don't cancel
concurrency:
group: production-deploy
cancel-in-progress: false
Use case: Security audits, code reviews, spec checking
name: Security Audit
on:
workflow_call:
secrets:
CLAUDE_CODE_OAUTH_TOKEN:
required: true
permissions:
contents: read
issues: write
id-token: write
jobs:
audit:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GH_PAT || github.token }}
- name: Run Claude Code Audit
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: '--allowed-tools "Bash(gh issue create:*),Bash(gh issue list:*)"'
prompt: |
/audit
[Detailed instructions...]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload audit report
if: always()
uses: actions/upload-artifact@v4
with:
name: audit-report-${{ github.run_number }}
path: audit-report-*.md
retention-days: 90
Use case: CI fixes, coverage improvement
# .github/actions/fix-ci/action.yml
name: 'Fix CI Failures'
description: 'Automatically fix CI failures using Claude Code'
inputs:
failed_run_id:
description: 'ID of the failed workflow run'
required: true
claude_code_oauth_token:
description: 'Claude Code OAuth token'
required: true
runs:
using: "composite"
steps:
- name: Run Claude Code to fix CI
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }}
prompt: |
/fix-ci
Failed run ID: ${{ inputs.failed_run_id }}
[Instructions for fixing CI...]
shell: bash
Caller workflow (Foundry project):
name: Auto-fix CI (Foundry)
on:
workflow_run:
workflows: ["Tests"]
types: [completed]
permissions:
contents: write
pull-requests: write
jobs:
fix-ci:
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Caller provides Foundry setup
- uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- run: forge install
# Universal action runs in this environment
- uses: ./.github/actions/fix-ci
with:
failed_run_id: ${{ github.event.workflow_run.id }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }} # Must pass explicitly
name: Code Review
on:
workflow_call:
inputs:
pr_number:
description: "Pull request number"
required: true
type: string
secrets:
CLAUDE_CODE_OAUTH_TOKEN:
required: true
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Review PR
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: '--allowed-tools "Bash(gh pr:*),mcp__github_inline_comment__create_inline_comment"'
prompt: |
/code-review
PR #${{ inputs.pr_number }}
Repository: ${{ github.repository }}
Review the changes and post inline comments using gh CLI or MCP tools.
When reviewing a workflow, systematically check:
secrets: (not inputs:)claude_args restricts bash tool accessGITHUB_TOKEN used instead of PAT where possibledescription, required, typeif: always() for artifact uploads# BAD: Conditional setup based on project detection
- name: Detect project type
id: detect
run: |
if [ -f "foundry.toml" ]; then
echo "type=foundry" >> $GITHUB_OUTPUT
elif [ -f "package.json" ]; then
echo "type=nodejs" >> $GITHUB_OUTPUT
fi
- name: Setup Foundry
if: steps.detect.outputs.type == 'foundry'
uses: foundry-rs/foundry-toolchain@v1
- name: Setup Node
if: steps.detect.outputs.type == 'nodejs'
uses: actions/setup-node@v4
Why it's bad:
Better approach:
# BAD: Hardcoded versions and paths
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18 # Hardcoded!
- run: npm ci
working-directory: ./frontend # Hardcoded path!
Better:
# GOOD: Parameterized
inputs:
node_version:
description: "Node.js version"
type: string
default: "20"
working_directory:
description: "Working directory"
type: string
default: "."
# Use in steps
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- run: npm ci
working-directory: ${{ inputs.working_directory }}
# BAD: Too many permissions
permissions: write-all
# BAD: Unrestricted bash access
- uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# No claude_args = unrestricted bash
Better:
# GOOD: Minimal permissions
permissions:
contents: read
issues: write
# GOOD: Restricted tool access
- uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: '--allowed-tools "Bash(gh issue create:*)"'
# BAD: No artifact upload on failure
- name: Run tests
run: npm test
- name: Upload coverage # Never runs if tests fail!
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
Better:
# GOOD: Always upload artifacts
- name: Run tests
run: npm test
continue-on-error: true # Or remove this to fail workflow
- name: Upload coverage
if: always() # Always run, even on failure
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
if-no-files-found: warn
Issue: "Resource not accessible by integration"
permissions:Issue: "Secret not found"
Issue: Claude Code Action timeout
timeout-minutes, break into smaller tasksIssue: Artifacts not uploaded
if: always())Issue: "Invalid workflow file"
yamllint or GitHub's schema# Enable debug logging
- name: Debug information
run: |
echo "Event name: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "Repository: ${{ github.repository }}"
echo "Runner OS: ${{ runner.os }}"
echo "Working directory: $(pwd)"
ls -la
# Add step summaries
- name: Generate summary
run: |
echo "## Audit Results" >> $GITHUB_STEP_SUMMARY
echo "- Critical issues: 2" >> $GITHUB_STEP_SUMMARY
echo "- High issues: 5" >> $GITHUB_STEP_SUMMARY
When reviewing a workflow, provide feedback in this structure:
# Workflow Review: [workflow-name.yml]
**Type**: [Universal Reusable Workflow / Composite Action / Direct Workflow]
**Purpose**: [Brief description]
---
## ✅ Strengths
- [What the workflow does well]
- [Good patterns observed]
---
## ⚠️ Issues Found
### 🔴 Critical
**Issue**: [Description]
**Location**: `line X-Y`
**Impact**: [What could go wrong]
**Fix**:
```yaml
# Current (problematic)
[current code]
# Recommended
[fixed code]
Suggestion: [Description] Benefit: [Why this improves the workflow] Implementation:
[suggested code]
[Any context-specific guidance or references]
<!-- GITHUB-WORKFLOWS:END -->