
Github Actions Validator
- 397 installs
- 286 repo stars
- Updated July 26, 2026
- akin-ozer/cc-devops-skills
github-actions-validator is a Claude Code DevOps skill that validates GitHub Actions workflows with actionlint and act for syntax, security, runner labels, and job dependencies before merge.
About
github-actions-validator is one of 14 validators in akin-ozer/cc-devops-skills (31 skills total) that lint and test .github/workflows/*.yml using actionlint for schema, expression, CRON, runner-label, and script-injection checks, plus act for local job execution when Docker is available. A seven-step flow runs validate_workflow.sh, maps errors to six reference files (common_errors, runners, action_versions, act_usage, actionlint_usage, modern_features), applies minimal fixes, and mandates a post-fix rerun. Use github-actions-validator before committing workflow changes, debugging actionlint failures, or verifying Marketplace action versions and deprecations offline via references/action_versions.md.
- YAML and schema validation
- Permissions and secrets hygiene checks
- Job dependency sanity review
- Action version and pinning guidance
Github Actions Validator by the numbers
- 397 all-time installs (skills.sh)
- Ranked #302 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/akin-ozer/cc-devops-skills --skill github-actions-validatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 397 |
|---|---|
| repo stars | ★ 286 |
| Last updated | July 26, 2026 |
| Repository | akin-ozer/cc-devops-skills ↗ |
How do you validate GitHub Actions workflows locally?
Validate existing GitHub Actions workflows for syntax errors, insecure patterns, missing permissions, and brittle job dependencies before merge.
Who is it for?
DevOps engineers editing GitHub Actions workflows who want actionlint and act validation with reference-mapped fixes before push.
Skip if: Teams managing Kubernetes manifests or non-GitHub CI systems—this skill targets .github/workflows YAML only.
When should I use this skill?
User asks to validate, lint, debug, or locally test GitHub Actions workflow YAML with actionlint or act
What you get
Lint-clean workflow files, mapped fix notes, post-fix validation log, and UNMAPPED or UNVERIFIED-OFFLINE flags
- Validated workflow YAML
- Error-to-fix mapping report
- Post-fix rerun confirmation
By the numbers
- Uses 2 validation tools: actionlint and act
- Ships 6 reference files for error mapping and version checks
- Part of a 31-skill DevOps pack with 14 validators
Files
GitHub Actions Validator
Overview
Validate and test GitHub Actions workflows, custom actions, and public actions using industry-standard tools (actionlint and act). This skill provides comprehensive validation including syntax checking, static analysis, local workflow execution testing, and action verification with version-aware documentation lookup.
Trigger Phrases
Use this skill when the request includes phrases like:
- "validate this GitHub Actions workflow"
- "check my
.github/workflows/*.ymlfile" - "debug actionlint errors"
- "test this workflow locally with act"
- "verify GitHub Action versions or deprecations"
When to Use This Skill
Use this skill when:
- Validating workflow files: Checking
.github/workflows/*.ymlfor syntax errors and best practices - Testing workflows locally: Running workflows with
actbefore pushing to GitHub - Debugging workflow failures: Identifying issues in workflow configuration
- Validating custom actions: Checking composite, Docker, or JavaScript actions
- Verifying public actions: Validating usage of actions from GitHub Marketplace
- Pre-commit validation: Ensuring workflows are valid before committing
Required Execution Flow
Every validation run should follow these steps in order.
Step 1: Set Skill Path and Run Validation
Run commands from the repository root that contains .github/workflows/.
SKILL_DIR="devops-skills-plugin/skills/github-actions-validator"
bash "$SKILL_DIR/scripts/validate_workflow.sh" <workflow-file-or-directory>Step 2: Map Each Error to a Reference
For each actionlint/act error, consult the mapping table below, then extract the matching fix pattern.
Step 3: Apply Minimal-Quote Policy
For each issue: 1. Include the exact error line from tool output. 2. Quote only the smallest useful snippet from references/ (prefer <=8 lines). 3. Paraphrase the rest and cite the source file/section. 4. Show corrected workflow code.
Step 4: Handle Unmapped Errors Explicitly
If an error does not match any mapping: 1. Label it as UNMAPPED. 2. Capture exact tool output, workflow file, and line number (if available). 3. Check references/common_errors.md general sections first. 4. If still unresolved, search official docs with the exact error string. 5. Mark the fix as provisional until post-fix rerun passes.
Step 5: Verify Public Action Versions
For each uses: owner/action@version: 1. Check references/action_versions.md. 2. For unknown actions, verify against official docs. 3. Confirm required inputs and deprecations.
Offline mode behavior:
- If network/doc lookup is unavailable, rely on
references/action_versions.mdonly. - Mark unknown actions as
UNVERIFIED-OFFLINE. - Do not claim "latest" version without an online verification pass.
Step 6: Mandatory Post-Fix Rerun
After applying fixes, rerun validation before finalizing:
SKILL_DIR="devops-skills-plugin/skills/github-actions-validator"
bash "$SKILL_DIR/scripts/validate_workflow.sh" <workflow-file-or-directory>Step 7: Provide Final Summary
Final output should include:
- Issues found and fixes applied
- Any
UNMAPPEDorUNVERIFIED-OFFLINEitems - Post-fix rerun command and result
- Remaining warnings/risk notes
Error Type to Reference File Mapping
| Error Pattern in Output | Reference File to Read | Section to Quote |
|---|---|---|
runs-on:, runner, ubuntu, macos, windows | references/runners.md | Runner labels |
cron, schedule | references/common_errors.md | Schedule Errors |
${{, expression, if: | references/common_errors.md | Expression Errors |
needs:, job, dependency | references/common_errors.md | Job Configuration Errors |
uses:, action, input | references/common_errors.md | Action Errors |
untrusted, injection, security | references/common_errors.md | Script Injection section |
syntax, yaml, unexpected | references/common_errors.md | Syntax Errors |
docker, container | references/act_usage.md | Troubleshooting |
@v3, @v4, deprecated, outdated | references/action_versions.md | Version table |
workflow_call, reusable, oidc | references/modern_features.md | Relevant section |
glob, path, paths:, pattern | references/common_errors.md | Path Filter Errors |
Example: Complete Error Handling Workflow
User's workflow has this error:
runs-on: ubuntu-lastestStep 1 - Script output:
label "ubuntu-lastest" is unknownStep 2 - Read `references/runners.md` or `references/common_errors.md`: Find the "Invalid Runner Label" section.
Step 3 - Quote the fix to user:
Error: label "ubuntu-lastest" is unknown>
Cause: Typo in runner label (from references/common_errors.md):```yaml
# Bad
runs-on: ubuntu-lastest # Typo
```
>
Fix (from references/common_errors.md):```yaml
# Good
runs-on: ubuntu-latest
```
>
Valid runner labels (from references/runners.md):-ubuntu-latest,ubuntu-24.04,ubuntu-22.04
-windows-latest,windows-2025,windows-2022
-macos-latest,macos-15,macos-14
Step 4 - Provide corrected code:
runs-on: ubuntu-latestQuick Start
Set once per shell session:
SKILL_DIR="devops-skills-plugin/skills/github-actions-validator"Initial Setup
bash "$SKILL_DIR/scripts/install_tools.sh"This installs act (local workflow execution) and actionlint (static analysis) to scripts/.tools/.
Basic Validation
# Validate a single workflow
bash "$SKILL_DIR/scripts/validate_workflow.sh" .github/workflows/ci.yml
# Validate all workflows
bash "$SKILL_DIR/scripts/validate_workflow.sh" .github/workflows/
# Lint-only (fastest)
bash "$SKILL_DIR/scripts/validate_workflow.sh" --lint-only .github/workflows/ci.yml
# Test-only with act (requires Docker)
bash "$SKILL_DIR/scripts/validate_workflow.sh" --test-only .github/workflows/Core Validation Workflow
1. Static Analysis with actionlint
Start with static analysis to catch syntax errors and common issues:
bash "$SKILL_DIR/scripts/validate_workflow.sh" --lint-only .github/workflows/ci.ymlWhat actionlint checks: YAML syntax, schema compliance, expression syntax, runner labels, action inputs/outputs, job dependencies, CRON syntax, glob patterns, shell scripts, security vulnerabilities.
2. Local Testing with act
After passing static analysis, test workflow execution:
bash "$SKILL_DIR/scripts/validate_workflow.sh" --test-only .github/workflows/Note: act has limitations - see references/act_usage.md.
3. Full Validation
bash "$SKILL_DIR/scripts/validate_workflow.sh" .github/workflows/ci.ymlDefault behavior if tools/runtime are unavailable:
- If
actis missing, full validation falls back to actionlint-only. - If Docker is unavailable, full validation skips act and continues with actionlint.
--check-versionsworks in offline/local mode usingreferences/action_versions.md.
Validating Resource Types
Workflows
# Single workflow
bash "$SKILL_DIR/scripts/validate_workflow.sh" .github/workflows/ci.yml
# All workflows
bash "$SKILL_DIR/scripts/validate_workflow.sh" .github/workflows/Key validation points: triggers, job configurations, runner labels, environment variables, secrets, conditionals, matrix strategies.
Custom Local Actions
Create a test workflow that uses the custom action, then validate:
bash "$SKILL_DIR/scripts/validate_workflow.sh" .github/workflows/test-custom-action.ymlPublic Actions
When workflows use public actions (e.g., actions/checkout@v6):
1. Check references/action_versions.md first 2. Use official docs (or web search) for unknown actions 3. Verify required inputs and version 4. Check for deprecation warnings 5. Run validation script
If offline:
- Mark unknown versions as
UNVERIFIED-OFFLINE - Avoid "latest/current" claims until online verification is possible
Search format: "[action-name] [version] github action documentation"
Reference File Consultation Guide
Mandatory Reference Consultation
| Situation | Reference File | Action |
|---|---|---|
| actionlint reports any mapped error | references/common_errors.md | Find matching error and apply minimal quote policy |
| actionlint reports unmapped error | references/common_errors.md + official docs | Label as UNMAPPED, capture exact output and verify by rerun |
| act fails with Docker/runtime error | references/act_usage.md | Check Troubleshooting section |
| act fails but workflow works on GitHub | references/act_usage.md | Read Limitations section |
| User asks about actionlint config | references/actionlint_usage.md | Provide examples |
| User asks about act options | references/act_usage.md | Read Advanced Options |
| Security vulnerability detected | references/common_errors.md | Quote minimal safe fix snippet |
| Validating action versions | references/action_versions.md | Check version table and offline note |
| Using modern features | references/modern_features.md | Check syntax examples |
| Runner questions/errors | references/runners.md | Check labels and availability |
Script Output to Reference Mapping
| Output Pattern | Reference File |
|---|---|
[syntax-check], parse, YAML errors | common_errors.md - Syntax Errors |
[expression], ${{, condition parsing | common_errors.md - Expression Errors |
[action], uses:, input/output mismatch | common_errors.md - Action Errors |
[events] with CRON/schedule text | common_errors.md - Schedule Errors |
potentially untrusted, injection warnings | common_errors.md - Security section |
[runner-label] or unknown runs-on label | runners.md |
[job-needs] dependency errors | common_errors.md - Job Configuration Errors |
[glob], paths, pattern errors | common_errors.md - Path Filter Errors |
| Docker/pull/image errors from act | act_usage.md - Troubleshooting |
| No pattern match | common_errors.md + official docs (label UNMAPPED) |
Reference Files Summary
| File | Content |
|---|---|
references/act_usage.md | Act tool usage, commands, options, limitations, troubleshooting |
references/actionlint_usage.md | Actionlint validation categories, configuration, integration |
references/common_errors.md | Common errors catalog with fixes |
references/action_versions.md | Current action versions, deprecation timeline, SHA pinning |
references/modern_features.md | Reusable workflows, SBOM, OIDC, environments, containers |
references/runners.md | GitHub-hosted runners (ARM64, GPU, M2 Pro, deprecations) |
Troubleshooting
| Issue | Solution |
|---|---|
| "Tools not found" | Run bash "$SKILL_DIR/scripts/install_tools.sh" |
| "Docker daemon not running" | Start Docker or use --lint-only |
| "Permission denied" | Run chmod +x "$SKILL_DIR"/scripts/*.sh |
| act fails but GitHub works | See references/act_usage.md Limitations |
Debug Mode
actionlint -verbose .github/workflows/ci.yml # Verbose actionlint
act -v # Verbose act
act -n # Dry-run (no execution)Best Practices
1. Always validate locally first - Catch errors before pushing 2. Use actionlint in CI/CD - Automate validation in pipelines 3. Pin action versions - Use @v6 not @main for stability; SHA pinning for security 4. Keep tools updated - Regularly update actionlint and act 5. Use official docs for unknown actions - Verify usage and versions 6. Check version compatibility - See references/action_versions.md 7. Enable shellcheck - Catch shell script issues early 8. Review security warnings - Address script injection issues
Limitations
- act limitations: Not all GitHub Actions features work locally
- Docker requirement: act requires Docker to be running
- Network actions: Some GitHub API actions may fail locally
- Private actions: Cannot validate without access
- Runtime behavior: Static analysis cannot catch all issues
- File location: act can only validate workflows in
.github/workflows/directory; files outside (likeexamples/) can only be validated with actionlint
Quick Examples
Example 1: Pre-commit Validation
SKILL_DIR="devops-skills-plugin/skills/github-actions-validator"
bash "$SKILL_DIR/scripts/validate_workflow.sh" .github/workflows/
git add .github/workflows/ && git commit -m "Update workflows"Example 2: Debug Failing Workflow
bash "$SKILL_DIR/scripts/validate_workflow.sh" --lint-only .github/workflows/failing.yml
# Fix issues
bash "$SKILL_DIR/scripts/validate_workflow.sh" .github/workflows/failing.ymlComplete Worked Example: Multi-Error Workflow
This example demonstrates the full assistant workflow for handling multiple errors.
User's Problematic Workflow
name: Broken CI
on:
schedule:
- cron: '0 0 * * 8' # ERROR 1
jobs:
build:
runs-on: ubuntu-lastest # ERROR 2
steps:
- uses: actions/checkout@v3 # ERROR 3 (outdated)
- run: echo ${{ github.event.issue.title }} # ERROR 4 (security)
deploy:
needs: biuld # ERROR 5 (typo)
runs-on: ubuntu-latest
steps:
- run: echo "Deploying"Step 1: Run Validation
bash "$SKILL_DIR/scripts/validate_workflow.sh" --lint-only workflow.ymlOutput:
[ERROR] invalid CRON format "0 0 * * 8"
[ERROR] label "ubuntu-lastest" is unknown
[WARN] "github.event.issue.title" is potentially untrusted
[ERROR] job "deploy" needs job "biuld" which does not existStep 2-3: Consult References and Quote Fixes
---
Error 1: Invalid CRON Expression
From `references/common_errors.md` - Schedule Errors:
Cause: Day of week 8 doesn't exist (valid: 0-6, where 0 = Sunday)
>
```yaml
# Bad
schedule:
- cron: '0 0 8' # Day 8 doesn't exist
>
# Good
schedule:
- cron: '0 0 0' # Sunday
```
>
CRON format: minute hour day month weekday- Weekday: 0-6 (0 = Sunday)
Fix: Change 8 to 0 (Sunday) or 1-6 for Monday-Saturday.
---
Error 2: Invalid Runner Label
From `references/common_errors.md` - Job Configuration Errors and `references/runners.md`:
Cause: Typo in runner label
>
```yaml
# Bad
runs-on: ubuntu-lastest # Typo
>
# Good
runs-on: ubuntu-latest
```
>
Valid labels (from `references/runners.md`):
- ubuntu-latest (Ubuntu 24.04)-ubuntu-24.04,ubuntu-22.04
- macos-latest (macOS 15)- windows-latest (Windows Server 2022)Fix: Change ubuntu-lastest to ubuntu-latest.
---
Error 3: Outdated Action Version
From `references/action_versions.md`:
| Action | Current Version | Minimum Supported |
|--------|----------------|-------------------|
| actions/checkout | v6 | v4 |>
SHA Pinning Best Practice:
```yaml
# Recommended: SHA pinning with version comment
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
```
Fix: Update from @v3 to @v6 (or SHA-pinned version).
---
Error 4: Security Vulnerability (Script Injection)
From `references/common_errors.md` - Expression Errors:
Warning: Potential script injection via untrusted input
>
```yaml
# Bad - vulnerable to injection
run: echo ${{ github.event.issue.title }}
>
# Good - use environment variables
env:
TITLE: ${{ github.event.issue.title }}
run: echo "$TITLE"
```
>
Why: Untrusted input (issue titles, PR bodies, commit messages) can contain malicious commands. Using environment variables sanitizes the input.
Fix: Pass untrusted input through environment variable.
---
Error 5: Undefined Job Dependency
From `references/common_errors.md` - Job Configuration Errors:
Error: Job 'deploy' depends on job 'biuld' which does not exist
>
```yaml
# Bad
jobs:
build:
runs-on: ubuntu-latest
deploy:
needs: biuld # Typo
>
# Good
jobs:
build:
runs-on: ubuntu-latest
deploy:
needs: build
```
Fix: Change biuld to build.
---
Step 4: Provide Corrected Workflow
name: Fixed CI
on:
schedule:
- cron: '0 0 * * 0' # Fixed: Sunday (0-6 valid)
jobs:
build:
runs-on: ubuntu-latest # Fixed: typo corrected
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - Fixed: updated version
- name: Process issue
env:
TITLE: ${{ github.event.issue.title }} # Fixed: use env var
run: echo "$TITLE"
deploy:
needs: build # Fixed: typo corrected
runs-on: ubuntu-latest
steps:
- run: echo "Deploying"Step 5: Mandatory Rerun
bash "$SKILL_DIR/scripts/validate_workflow.sh" --lint-only workflow.ymlExpected rerun result:
- No previous errors reproduced
- Remaining warnings, if any, are documented explicitly
Step 6: Summary
| Error | Type | Fix Applied |
|---|---|---|
CRON 0 0 * * 8 | Schedule | Changed to 0 0 * * 0 |
ubuntu-lastest | Runner | Changed to ubuntu-latest |
checkout@v3 | Outdated Action | Updated to @v6.0.0 (SHA-pinned) |
Direct ${{ }} in run | Security | Wrapped in environment variable |
needs: biuld | Job Dependency | Changed to needs: build |
Recommendations:
- Run
bash "$SKILL_DIR/scripts/validate_workflow.sh" --check-versionsregularly - Use SHA pinning for all actions in production workflows
- Always pass untrusted input through environment variables
Done Criteria
Validation work is complete when all are true:
- Trigger matched and correct validation mode selected.
- Each mapped error includes source reference and minimal quote.
- Each unmapped error is labeled
UNMAPPEDwith exact output captured. - Public action versions are verified, or marked
UNVERIFIED-OFFLINE. - Post-fix rerun executed and result reported.
Summary
1. Setup: Install tools with install_tools.sh 2. Validate: Run validate_workflow.sh on workflow files 3. Fix: Address issues using reference documentation 4. Rerun: Verify fixes with a mandatory post-fix validation run 5. Search: Use official docs to verify unknown actions 6. Commit: Push validated workflows with confidence
For detailed information, consult the appropriate reference file in references/.
# Ignore installed tools directory
scripts/.tools/# Example: Workflow with Outdated Action Versions
# This workflow uses older action versions for testing version validation
# Use for testing version checks: bash scripts/validate_workflow.sh --check-versions examples/outdated-versions.yml
#
# VERSION ISSUES IN THIS FILE:
# 1. actions/checkout@v4 - OUTDATED (current: v6)
# 2. actions/setup-node@v4 - OUTDATED (current: v6)
# 3. actions/upload-artifact@v3 - DEPRECATED (minimum: v4)
# 4. docker/build-push-action@v5 - OUTDATED (current: v6)
name: Workflow with Outdated Versions
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
name: Build Application
runs-on: ubuntu-latest
steps:
# OUTDATED: Using v4, current is v6
- name: Checkout code
uses: actions/checkout@v4
# OUTDATED: Using v4, current is v6
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install and build
run: |
npm ci
npm run build
# DEPRECATED: Using v3, minimum supported is v4
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: build-output
path: dist/
docker:
name: Build Docker Image
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# OUTDATED: Using v5, current is v6
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latestGitHub Actions Validator - Example Workflows
This directory contains example workflow files for testing the GitHub Actions Validator skill.
Files
valid-ci.yml
A complete, valid CI pipeline that passes all validation checks.
Purpose: Test successful validation flow
Usage:
bash scripts/validate_workflow.sh examples/valid-ci.ymlExpected Result: All validations pass
---
with-errors.yml
A workflow containing common intentional errors for testing error detection.
Purpose: Test error detection and reference file consultation
Errors included (4 total, all caught by actionlint): 1. Invalid CRON expression (day 8 doesn't exist) — [events] 2. Typo in runner label (ubuntu-lastest instead of ubuntu-latest) — [runner-label] 3. Script injection vulnerability (untrusted input in script) — [expression] 4. Undefined job dependency (biuld instead of build) — [job-needs]
Usage:
bash scripts/validate_workflow.sh examples/with-errors.ymlExpected Result: Multiple errors reported by actionlint
---
outdated-versions.yml
A workflow using older action versions to test version validation.
Purpose: Test action version checking
Version issues included: 1. actions/checkout@v4 - OUTDATED (current: v6) 2. actions/setup-node@v4 - OUTDATED (current: v6) 3. actions/upload-artifact@v3 - DEPRECATED (minimum: v4) 4. docker/build-push-action@v5 - OUTDATED (current: v6)
Usage:
bash scripts/validate_workflow.sh --check-versions examples/outdated-versions.ymlExpected Result: Version warnings for outdated actions
---
Testing Workflow
1. Test successful validation:
bash scripts/validate_workflow.sh examples/valid-ci.yml2. Test error detection:
bash scripts/validate_workflow.sh examples/with-errors.yml3. Test version checking:
bash scripts/validate_workflow.sh --check-versions examples/outdated-versions.yml4. Test all examples:
for file in examples/*.yml; do
echo "=== Testing: $file ==="
bash scripts/validate_workflow.sh --lint-only "$file"
echo ""
done# Example: Valid CI Workflow
# This workflow passes all validation checks
# Use for testing successful validation: bash scripts/validate_workflow.sh examples/valid-ci.yml
name: Valid CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: '20'
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-output
path: dist/
retention-days: 7
security-scan:
name: Security Scan
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Run security scan
run: |
echo "Running security scan..."
# Security scan commands here
- name: Generate summary
run: |
echo "## Security Scan Results" >> $GITHUB_STEP_SUMMARY
echo "All checks passed!" >> $GITHUB_STEP_SUMMARY# Example: Workflow with Intentional Errors
# This workflow contains common errors for testing validation
# Use for testing error detection: bash scripts/validate_workflow.sh --lint-only examples/with-errors.yml
#
# ERRORS IN THIS FILE (4 total, all caught by actionlint):
# 1. Line 18: Invalid CRON expression (day 8 doesn't exist) [events]
# 2. Line 25: Typo in runner label (ubuntu-lastest) [runner-label]
# 3. Line 37: Script injection vulnerability [expression]
# 4. Line 43: Undefined job dependency (biuld instead of build) [job-needs]
name: Workflow with Errors
on:
push:
branches: [main]
schedule:
# ERROR: Invalid CRON - day of week 8 doesn't exist (should be 0-6)
- cron: '0 0 * * 8'
pull_request:
jobs:
build:
name: Build
# ERROR: Typo in runner label (should be ubuntu-latest)
runs-on: ubuntu-lastest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
run: npm run build
# ERROR: Script injection vulnerability - untrusted input directly in script
- name: Process issue
run: |
echo "Processing: ${{ github.event.issue.title }}"
deploy:
name: Deploy
runs-on: ubuntu-latest
# ERROR: Undefined job dependency (typo: biuld instead of build)
needs: biuld
steps:
- name: Deploy
run: echo "Deploying..."
Act (nektos/act) - Usage Reference
Act is a tool that allows you to run your GitHub Actions locally, providing fast feedback and acting as a local task runner.
Installation
# Install act using the official script
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | bash
# Or use the skill's installation script
bash scripts/install_tools.shCore Commands
List Workflows
List all available workflows in the repository:
act -lList workflows for a specific event:
act -l pull_request
act -l push
act -l workflow_dispatchDry Run (No Execution)
Validate workflows without executing them, useful for inspection and validation:
act -n
# or (long form)
act --dryrunThis performs a dry run that:
- Parses all workflow files
- Validates syntax
- Shows what would be executed
- Does NOT actually run any jobs
- Returns exit code 0 on success, non-zero on errors
Important for Validation: The dry-run mode is perfect for validating workflow syntax before pushing to GitHub.
Run Workflows
Run the default workflow:
actRun workflows for a specific event:
act push
act pull_request
act workflow_dispatchRun a specific job:
act -j <job-id>Run a specific workflow file:
act -W .github/workflows/ci.ymlCommon Use Cases
1. Validate Workflow Syntax
Use dry run to check if workflows parse correctly:
act -nIf there are syntax errors, act will report them immediately.
2. Test Workflows Locally
Before pushing to GitHub, test workflows locally:
# Test push event workflows
act push
# Test pull request workflows
act pull_request3. Debug Workflow Issues
Run workflows with verbose output:
act -v4. List Available Events
See which events have workflows configured:
act -lOutput format:
Stage Job ID Job name Workflow name Workflow file Events
0 build build CI ci.yml push,pull_request
0 test test CI ci.yml push,pull_requestAdvanced Options
Container Architecture
Ensure consistent platform behavior across different machines:
act --container-architecture linux/amd64This is especially important on ARM-based Macs (M1/M2/M3) to ensure workflows run in the same environment as GitHub's x64 runners.
Using Specific Docker Images
Act uses Docker containers to run jobs. Specify custom images:
act -P ubuntu-latest=node:16-busterConfiguration File
Create .actrc file in your project or home directory to set default options:
# .actrc
--container-architecture=linux/amd64
--action-offline-modeOptions are loaded in this order: 1. XDG spec .actrc 2. HOME directory .actrc 3. Current directory .actrc 4. CLI arguments
Passing Secrets
Provide secrets for testing:
act -s GITHUB_TOKEN=ghp_xxxOr use a secrets file:
act --secret-file .secretsEnvironment Variables
Set environment variables:
act --env MY_VAR=valueInput Variables (for workflow_dispatch)
Pass input variables:
act workflow_dispatch --input myInput=myValueLimitations
Be aware of act's limitations:
1. Not 100% Compatible: Some GitHub Actions features may not work exactly as on GitHub 2. Docker Required: act requires Docker to be installed and running 3. Network Actions: Some actions that interact with GitHub's API may fail 4. Runner Images: Default runner images may differ from GitHub's hosted runners 5. Secrets: Local testing requires manually providing secrets
Exit Codes
0: Success - all jobs passed1: Failure - at least one job failed2: Error - workflow parsing or execution error
Best Practices for Validation
1. Always run dry-run first: act -n to catch syntax errors 2. Test specific events: Don't run all workflows, target the event you care about 3. Use verbose mode for debugging: act -v when troubleshooting 4. Check Docker availability: Ensure Docker is running before using act 5. Consider limitations: Not all features work locally - use for syntax and basic logic validation
Troubleshooting
Issue: "Cannot connect to Docker daemon"
Solution: Start Docker Desktop or Docker daemon
Issue: "Workflow file not found"
Solution: Ensure you're in the repository root or use -W to specify the workflow file path
Issue: "Action not found"
Solution: Some actions may not be available locally. Use -P to specify alternative Docker images or skip the problematic action for validation purposes
Issue: "Out of disk space"
Solution: Clean up Docker images: docker system prune -a
Action Version Validation Reference
This reference provides current recommended action versions and validation procedures for GitHub Actions workflows.
Current Recommended Versions (December 2025)
| Action | Current Version | Minimum Supported | Notes |
|---|---|---|---|
actions/checkout | v6 | v4 | v6 stores credentials in $RUNNER_TEMP |
actions/setup-node | v6 | v4 | v6 adds Node 24 support |
actions/setup-python | v5 | v4 | v5 adds Python 3.13 support |
actions/setup-java | v4 | v4 | Current latest |
actions/setup-go | v5 | v4 | v5 adds Go 1.23 support |
actions/cache | v4 | v4 | v4.2.0+ required as of Feb 2025 |
actions/upload-artifact | v4 | v4 | v3 deprecated |
actions/download-artifact | v4 | v4 | v3 deprecated |
docker/setup-buildx-action | v3 | v3 | Current latest |
docker/login-action | v3 | v3 | Current latest |
docker/build-push-action | v6 | v5 | v6 adds provenance attestation |
docker/metadata-action | v5 | v5 | Current latest |
aws-actions/configure-aws-credentials | v4 | v4 | OIDC support improved |
Version Validation Process
Step 1: Extract Action References
For each uses: statement in the workflow, extract:
- Action name (e.g.,
actions/checkout) - Version (e.g.,
v4,v4.1.1, or SHA likeb4ffde65f46...)
Step 2: Compare Against Recommended Versions
For each action found: 1. Look up the action in the table above 2. Compare the workflow version against the Current Version 3. Flag if using a version older than Minimum Supported
Step 3: Report Findings
Generate warnings for:
- OUTDATED: Action using older major version (e.g., checkout@v4 when v6 is current)
- DEPRECATED: Action using version below minimum supported
- UP-TO-DATE: Action using current or acceptable version
Example Version Validation Output
=== Action Version Check ===
actions/checkout@v6.0.0 - UP-TO-DATE (current: v6)
actions/setup-java@v4.2.1 - UP-TO-DATE (current: v4)
docker/build-push-action@v5.3.0 - OUTDATED (current: v6, using: v5)
actions/upload-artifact@v3 - DEPRECATED (minimum: v4, using: v3)
Recommendation: Update docker/build-push-action to v6 for provenance attestation support
Recommendation: Update actions/upload-artifact to v4 (v3 is deprecated)Using the Version Check Flag
# Check action versions in workflow
bash scripts/validate_workflow.sh --check-versions .github/workflows/ci.yml
# Full validation including version check
bash scripts/validate_workflow.sh .github/workflows/ci.ymlNode.js Runtime Deprecation Timeline
GitHub Actions runtime requirements:
- Node.js 12: EOL April 2022 - Actions using this are deprecated
- Node.js 16: EOL September 2023 - Actions using this are deprecated
- Node.js 20: EOL April 2026 - Current runtime for most actions
- Node.js 22/24: Current LTS - Newer actions support these
SHA Pinning Best Practice
For security, pin actions to specific commit SHAs:
# Recommended: SHA pinning with version comment
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
# Acceptable: Major version tag
- uses: actions/checkout@v6
# Not recommended: Branch reference
- uses: actions/checkout@mainCache Storage Updates (November 2025)
GitHub Actions cache storage expanded beyond the 10 GB limit:
New Features:
- Pay-as-you-go model: Repositories can store more than 10 GB of cache data
- Free tier: All repositories continue to receive 10 GB at no additional cost
- New management policies:
- Cache size eviction limit (GB): Control maximum cache size
- Cache retention limit (days): Set how long caches are retained
Pricing:
- First 10 GB per repository: FREE
- Additional storage: Comparable to Git LFS and Codespaces storage
- Requires Pro, Team, or Enterprise account to exceed 10 GB limit
Cache best practices:
- Monitor cache usage in repository settings
- Configure eviction limits to control costs
- Use appropriate retention periods for your workflow
- Clean up old caches regularly
- Consider cache key strategies to avoid cache bloat
Validation Checklist
When validating workflows, ALWAYS: 1. Run the validation script 2. Manually review uses: statements against the version table 3. Warn about any outdated or deprecated versions 4. Suggest specific upgrade paths with SHA pinning
Actionlint (rhysd/actionlint) - Usage Reference
Actionlint is a static checker for GitHub Actions workflow files that catches errors before they cause CI failures.
Installation
# Download and install using the official script
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
# Or use the skill's installation script
bash scripts/install_tools.shCore Usage
Basic Validation
Validate a single workflow file:
actionlint .github/workflows/ci.ymlValidate all workflow files in a directory:
actionlint .github/workflows/*.ymlValidate all workflows in the default location:
actionlintOutput Formats
Default Format (human-readable)
actionlintOutput example:
.github/workflows/ci.yml:5:7: unexpected key "job" for "workflow" section [syntax-check]
.github/workflows/ci.yml:10:15: invalid CRON format "0 0 * * 8" in schedule event [events]JSON Format
actionlint -format '{{json .}}'Useful for programmatic processing and integration with other tools.
Sarif Format
actionlint -format sarifFor integration with GitHub Code Scanning and other security tools.
Validation Categories
1. Syntax Checking
Validates YAML syntax and GitHub Actions schema:
- Required fields
- Valid keys and values
- Proper nesting
- Type correctness
2. Expression Validation
Validates GitHub Actions expressions ${{ }}:
- Syntax errors
- Type checking (string, number, boolean)
- Function calls
- Context access
Example caught errors:
# Error: Boolean expression expected
if: ${{ 'true' }} # String, not boolean
# Error: Unknown function
run: echo ${{ unknown() }}
# Error: Type mismatch
if: ${{ 42 }} # Number, not boolean3. Runner Label Validation
Validates runner labels against known GitHub-hosted runners:
Ubuntu:
ubuntu-latest(currently ubuntu-24.04)ubuntu-24.04,ubuntu-22.04,ubuntu-20.04
Windows:
windows-latest(currently windows-2022)windows-2025(NEW - recently added)windows-2022,windows-2019
macOS:
macos-latest(currently macos-15)macos-15(Apple Silicon M1/M2/M3)macos-14(Apple Silicon M1)macos-26(preview)macos-13(Intel - RETIRED November 14, 2025)macos-12(Intel - RETIRED)
Example:
runs-on: ubuntu-lastest # Error: Did you mean "ubuntu-latest"?4. Action Validation
Validates action references:
- Action exists
- Valid version/ref
- Required inputs provided
- No unknown inputs
Example:
# Error: Missing required input "path"
- uses: actions/checkout@v5
# Error: Unknown input "invalid_input"
- uses: actions/checkout@v5
with:
invalid_input: value5. Job Dependencies
Validates needs: dependencies:
- Referenced jobs exist
- No circular dependencies
- Valid job IDs
6. CRON Syntax
Validates schedule event CRON expressions:
# Error: Day of week must be 0-6
schedule:
- cron: '0 0 * * 8'7. Shell Script Validation
Integrates with shellcheck to validate shell scripts in run: steps:
# Warning: Quote to prevent word splitting
run: echo $VARIABLE8. Glob Pattern Validation
Validates glob patterns in paths: and paths-ignore: filters for structural errors (e.g., empty patterns or malformed syntax).
Note: The pattern **.js (double-star without a slash) is not flagged by actionlint as of v1.7.x. It is functionally equivalent to **/*.js in GitHub's glob engine but **/*.js is more explicit and widely understood. Use **/*.js as a best practice, not because actionlint will warn about the alternative.
# Best practice (clear intent)
on:
push:
paths:
- '**/*.js' # Matches any .js file in any subdirectory
- 'src/**' # Matches everything under src/9. Security Checks
Detects potential security issues:
- Injection vulnerabilities
- Insecure credential handling
- Dangerous patterns
Example:
# Warning: Potential script injection
run: echo ${{ github.event.issue.title }}Configuration
Create .github/actionlint.yaml or .github/actionlint.yml:
# Configure shellcheck
shellcheck:
enable: true
shell: bash
# Configure pyflakes for Python
pyflakes:
enable: true
executable: pyflakes
# Ignore specific rules
ignore:
- 'SC2086' # Ignore shellcheck rule
- 'action-validation' # Ignore action validation
# Custom runner labels
self-hosted-runner:
labels:
- my-custom-runner
- gpu-runnerExit Codes
0: Success - no errors found1: Validation errors found2: Fatal error (invalid file, config error, etc.)
Integration
Pre-commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/rhysd/actionlint
rev: v1.7.9 # Check https://github.com/rhysd/actionlint/releases for latest version
hooks:
- id: actionlintNote: Always use the latest version of actionlint. Check the releases page for the most recent version.
GitHub Actions Workflow
name: Lint GitHub Actions workflows
on: [push, pull_request]
jobs:
actionlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Download actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
- name: Run actionlint
run: ./actionlintVS Code Integration
Install the "actionlint" extension for real-time validation in VS Code.
Common Error Examples
1. Typo in Runner Label
# Error
runs-on: ubuntu-lastest
# Fix
runs-on: ubuntu-latest2. Invalid CRON Expression
# Error
schedule:
- cron: '0 0 * * 8' # Day of week 8 doesn't exist
# Fix
schedule:
- cron: '0 0 * * 0' # Sunday = 03. Missing Required Input
# Error
- uses: actions/checkout@v4
# Fix (if repository input is required)
- uses: actions/checkout@v4
with:
repository: owner/repo4. Invalid Expression
# Error
if: ${{ success() && 'true' }} # Mixing boolean and string
# Fix
if: ${{ success() && true }}5. Undefined Job in needs
# Error
jobs:
deploy:
needs: biuld # Typo
# Fix
jobs:
deploy:
needs: buildBest Practices
1. Run locally before pushing: Catch errors early 2. Use in CI/CD: Add actionlint to your workflow 3. Configure for custom runners: Update config for self-hosted runners 4. Enable shellcheck: Catch shell script issues 5. Review all warnings: Even non-fatal warnings can indicate issues 6. Keep actionlint updated: New rules and features are added regularly
Limitations
- Cannot validate runtime behavior (only static analysis)
- Cannot access private actions (must be public to validate)
- May not catch all possible issues (e.g., environment-specific problems)
- Custom actions may require manual verification
Common GitHub Actions Errors and Solutions
This reference lists common errors encountered when working with GitHub Actions and how to fix them.
Syntax Errors
1. Invalid YAML Syntax
Error:
Error: Unable to process file command 'workflow' successfully.Common Causes:
- Incorrect indentation (YAML is whitespace-sensitive)
- Missing colons
- Unquoted strings containing special characters
- Tabs instead of spaces
Fix:
# Bad
name:My Workflow
jobs:
build:
runs-on: ubuntu-latest
# Good
name: My Workflow
jobs:
build:
runs-on: ubuntu-latest2. Missing Required Fields
Error:
Required property is missing: nameFix:
# Every workflow needs a name
name: CI Pipeline
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.03. Invalid Workflow Triggers
Error:
The workflow is not valid. Unexpected value 'on'Fix:
# Bad - wrong event name
on:
pull-request: # Should be pull_request
# Good
on:
pull_request:
push:Expression Errors
1. Incorrect Expression Syntax
Error:
Unrecognized named-value: 'github'. Located at position 1 within expression: github.refFix:
# Bad - missing ${{ }}
if: github.ref == 'refs/heads/main'
# Good
if: ${{ github.ref == 'refs/heads/main' }}
# Even better (GitHub Actions auto-evaluates if conditions)
if: github.ref == 'refs/heads/main'2. Type Mismatches
Error:
Expected boolean value, got stringFix:
# Bad
if: ${{ 'true' }} # String, not boolean
# Good
if: ${{ true }}
if: ${{ success() }}
if: ${{ github.event_name == 'push' }}3. Script Injection Vulnerabilities
Warning:
Potential script injection via untrusted inputFix:
# Bad - vulnerable to injection
run: echo ${{ github.event.issue.title }}
# Good - use environment variables
env:
TITLE: ${{ github.event.issue.title }}
run: echo "$TITLE"Action Errors
1. Action Not Found
Error:
Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '/home/runner/work/_actions/actions/chekout/v4'Common Causes:
- Typo in action name
- Invalid action reference
- Action doesn't exist or was removed
Fix:
# Bad
- uses: actions/chekout@v4 # Typo
# Good
- uses: actions/checkout@v42. Missing Required Inputs
Error:
Input required and not supplied: pathFix:
# Bad
- uses: some-action@v1
# Good
- uses: some-action@v1
with:
path: ./my-path3. Unknown Action Inputs
Error:
Unexpected input 'invalid_input'Fix:
# Check the action's documentation for valid inputs
- uses: actions/checkout@v4
with:
# Only use documented inputs
ref: main
# Remove undocumented inputs4. Deprecated Action Versions
Warning:
Node.js 12/16 actions are deprecatedFix:
# Deprecated - Node.js 12 (EOL April 2022)
- uses: actions/checkout@v2
# Deprecated - Node.js 16 (EOL September 2023)
- uses: actions/checkout@v3
# Older - Node.js 20 (EOL April 2026)
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# Current - Node.js 20+/24 (v6)
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
# Note: Node.js 20 EOL is April 2026, Node.js 22 and 24 are currentJob Configuration Errors
1. Invalid Runner Label
Error:
Unable to locate executable file: ubuntu-lastestFix:
# Bad
runs-on: ubuntu-lastest # Typo
# Good
runs-on: ubuntu-latestValid runner labels:
ubuntu-latest,ubuntu-22.04,ubuntu-20.04windows-latest,windows-2025,windows-2022,windows-2019macos-latest(now macOS 15),macos-15,macos-14,macos-26(preview)macos-13(RETIRED November 14, 2025 - no longer available)macos-15-intel,macos-15-large(Intel x86_64, long-term deprecated)macos-15-xlarge,macos-14-xlarge(M2 Pro with GPU)gpu-t4-4-core(GPU runners for ML/AI)- ARM64 runners (free for public repos)
2. Undefined Job Dependency
Error:
Job 'deploy' depends on job 'biuld' which does not existFix:
# Bad
jobs:
build:
runs-on: ubuntu-latest
deploy:
needs: biuld # Typo
# Good
jobs:
build:
runs-on: ubuntu-latest
deploy:
needs: build3. Circular Job Dependencies
Error:
Circular dependency detectedFix:
# Bad
jobs:
job1:
needs: job2
job2:
needs: job1 # Circular!
# Good
jobs:
job1:
runs-on: ubuntu-latest
job2:
needs: job1Schedule Errors
1. Invalid CRON Syntax
Error:
Invalid CRON expression: '0 0 * * 8'Fix:
# Bad
schedule:
- cron: '0 0 * * 8' # Day 8 doesn't exist
# Good
schedule:
- cron: '0 0 * * 0' # Sunday
# CRON format: minute hour day month weekday
# Minute: 0-59
# Hour: 0-23
# Day: 1-31
# Month: 1-12
# Weekday: 0-6 (0 = Sunday)2. Multiple Schedule Entries
# Correct way to define multiple schedules
on:
schedule:
- cron: '0 0 * * 1' # Monday at midnight
- cron: '0 12 * * 5' # Friday at noonPath Filter Errors
1. Glob Pattern Best Practices
Note: **.js (double-star without a slash) is not flagged by actionlint as an error. GitHub's glob engine treats it similarly to **/*.js, but **/*.js is the conventional and explicit form. Prefer it for clarity.
# Not recommended (ambiguous intent, but accepted by actionlint and GitHub)
on:
push:
paths:
- '**.js'
# Recommended (explicit and conventional)
on:
push:
paths:
- '**/*.js'
- 'src/**'Environment and Secrets
1. Secret Not Found
Error:
Secret MY_SECRET not foundFix:
- Ensure the secret is defined in repository settings
- Check secret name spelling (case-sensitive)
- Verify secret scope (repository vs organization vs environment)
# Use secrets correctly
env:
API_KEY: ${{ secrets.MY_SECRET }} # Must match name in settings2. Environment Variables in run
Common Issue:
# Bad - environment variable not accessible
steps:
- run: echo $MY_VAR # May not work on Windows
# Good - use env
steps:
- name: Print variable
env:
MY_VAR: ${{ secrets.MY_SECRET }}
run: echo "$MY_VAR" # Unix
# or
run: echo $env:MY_VAR # Windows PowerShellMatrix Strategy Errors
1. Invalid Matrix Configuration
Error:
Matrix configuration is invalidFix:
# Bad
strategy:
matrix:
os: ubuntu-latest # Should be an array
# Good
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [20, 22, 24] # Node 16 EOL Sep 2023, Node 20 EOL Apr 20262. Matrix Variable Reference
# Correct way to reference matrix variables
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
jobs:
test:
runs-on: ${{ matrix.os }}Conditional Execution Errors
1. Always/Cancelled/Failure Conditions
# Understanding conditions
steps:
- name: Run on success (default)
run: echo "Runs only if previous steps succeeded"
- name: Run always
if: always()
run: echo "Runs whether previous steps succeeded or failed"
- name: Run on failure
if: failure()
run: echo "Runs only if a previous step failed"
- name: Run on success
if: success()
run: echo "Runs only if all previous steps succeeded"Debugging Tips
1. Enable Debug Logging
Set secrets in repository settings:
ACTIONS_STEP_DEBUG=true(detailed step logs)ACTIONS_RUNNER_DEBUG=true(runner diagnostic logs)
2. Use tmate for Interactive Debugging
steps:
- name: Setup tmate session
if: failure()
uses: mxschmitt/action-tmate@v33. Print Context Information
steps:
- name: Dump GitHub context
run: echo '${{ toJSON(github) }}'
- name: Dump job context
run: echo '${{ toJSON(job) }}'
- name: Dump runner context
run: echo '${{ toJSON(runner) }}'Best Practices
1. Always use specific action versions: actions/checkout@v6 not actions/checkout@main 2. Quote strings with special characters: name: "My: Workflow" 3. Use shellcheck: Enable shell script linting 4. Validate locally: Use act and actionlint before pushing 5. Use env for secrets: Never put secrets directly in run commands 6. Keep workflows DRY: Use reusable workflows and composite actions 7. Set timeouts: Prevent runaway jobs with timeout-minutes 8. Use concurrency: Cancel redundant runs with concurrency groups
# Example of good practices
name: Production Deployment
on:
push:
branches: [main]
concurrency:
group: production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: |
./deploy.shModern GitHub Actions Features Reference
This reference covers validation of modern GitHub Actions features including reusable workflows, attestations, OIDC authentication, and more.
Reusable Workflows
Validation Points
workflow_calltrigger configuration- Required and optional inputs with correct types
- Secrets declaration and usage
- Outputs definition
Example
# Reusable workflow (.github/workflows/reusable-deploy.yml)
on:
workflow_call:
inputs:
environment:
required: true
type: string
deploy-version:
required: false
type: string
default: 'latest'
secrets:
deploy-token:
required: true
outputs:
deployment-url:
description: "The URL of the deployment"
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- name: Deploy
id: deploy
run: echo "url=https://example.com" >> $GITHUB_OUTPUTCommon Errors
- Incorrect input types (string, number, boolean)
- Missing required secrets
- Invalid output references
Workflow Limits (November 2025)
GitHub Actions increased reusable workflow limits:
- Nested workflows: Up to 10 levels (previously 4)
- Total workflows per run: Up to 50 workflows (previously 20)
This enables complex workflow compositions and better code reuse.
---
SBOM and Build Provenance Attestations
Validation Points
- Correct permissions (
id-token: write,attestations: write) - Valid artifact paths
- Proper attestation action usage
Example
permissions:
id-token: write
contents: read
attestations: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build artifact
run: |
mkdir -p dist
tar -czvf dist/app.tar.gz ./src
- name: Generate SBOM
run: |
# Generate SPDX SBOM
syft ./src -o spdx-json > sbom.spdx.json
- uses: actions/attest-sbom@v3
with:
subject-path: '${{ github.workspace }}/dist/*.tar.gz'
sbom-path: '${{ github.workspace }}/sbom.spdx.json'
- uses: actions/attest-build-provenance@v3
with:
subject-path: '${{ github.workspace }}/dist/*.tar.gz'Common Errors
- Missing required permissions
- Invalid subject-path glob patterns
- Incorrect SBOM format
---
OIDC Authentication
Validation Points
- Correct permissions (
id-token: write) - Valid audience claims
- Proper OIDC provider configuration
- Token claim validation in receiving systems
Available Token Claims (November 2025)
| Claim | Description |
|---|---|
repository | Repository name |
ref | Git ref (branch/tag) |
sha | Commit SHA |
workflow | Workflow name |
run_id | Workflow run ID |
run_attempt | Attempt number |
check_run_id | NEW - Specific check run ID for the job |
actor | User who triggered the workflow |
environment | Deployment environment (if applicable) |
Example: AWS OIDC
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
# Token now includes check_run_id for granular tracking
- name: Deploy to AWS
run: aws s3 sync ./build s3://my-bucket/AWS IAM Policy with check_run_id
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:org/repo:ref:refs/heads/main",
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:check_run_id": "*"
}
}
}]
}Benefits of check_run_id
- Fine-grained access control: Trace tokens to exact job and compute
- Improved auditability: Track which specific check run made API calls
- Least-privilege policies: Attribute-based access control without enumerating repositories
- Faster revocation: Reduce secret exposure risk
---
Deployment Environments
Validation Points
- Environment name configuration
- Protection rules compatibility
- Required reviewers setup
- Environment variables and secrets scope
Example
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v6
- run: ./deploy.sh staging
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment:
name: production
url: https://prod.example.com
steps:
- uses: actions/checkout@v6
- run: ./deploy.sh productionCommon Errors
- Undefined environment names
- Missing URL for environment tracking
- Incorrect environment variable scope
---
Job Summaries
Validation Points
- Correct usage of
$GITHUB_STEP_SUMMARY - Valid Markdown formatting
- Proper escaping of dynamic content
Example
steps:
- name: Run tests
id: tests
run: |
# Run tests and capture results
npm test 2>&1 | tee test-output.txt
PASSED=$(grep -c "PASS" test-output.txt || echo 0)
FAILED=$(grep -c "FAIL" test-output.txt || echo 0)
echo "passed=$PASSED" >> $GITHUB_OUTPUT
echo "failed=$FAILED" >> $GITHUB_OUTPUT
- name: Generate summary
run: |
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Status | Count |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Passed | ${{ steps.tests.outputs.passed }} |" >> $GITHUB_STEP_SUMMARY
echo "| Failed | ${{ steps.tests.outputs.failed }} |" >> $GITHUB_STEP_SUMMARYNote: Job summaries are runtime features - actionlint validates script syntax but not summary content.
---
Container Jobs
Validation Points
- Valid container image references
- Correct volume mounts
- Environment variable configuration
- Service container networking
Example
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:24
env:
NODE_ENV: test
volumes:
- /data:/data
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v6
- name: Install dependencies
run: npm ci
- name: Run tests
env:
DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb
REDIS_URL: redis://redis:6379
run: npm testCommon Errors
- Invalid image tags
- Incorrect volume mount syntax
- Service container networking issues
- Missing health checks for services
---
Matrix Strategies
Validation Points
- Matrix values must be arrays
- Valid matrix variable references
- Proper include/exclude syntax
Example
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [20, 22, 24]
exclude:
- os: macos-latest
node: 20
include:
- os: ubuntu-latest
node: 24
experimental: true
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- run: npm test---
Concurrency Control
Validation Points
- Valid concurrency group names
- Proper cancel-in-progress usage
Example
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npm ci && npm run buildThis prevents redundant runs while protecting main branch runs from cancellation.
GitHub-Hosted Runners Reference (2025)
This reference covers all GitHub-hosted runner types, including recent additions and deprecations.
Standard Runner Labels
Ubuntu
runs-on: ubuntu-latest # Ubuntu 24.04 (default)
runs-on: ubuntu-24.04 # Ubuntu 24.04
runs-on: ubuntu-22.04 # Ubuntu 22.04
runs-on: ubuntu-20.04 # Ubuntu 20.04Windows
runs-on: windows-latest # Windows Server 2022 (default)
runs-on: windows-2025 # Windows Server 2025 (NEW)
runs-on: windows-2022 # Windows Server 2022
runs-on: windows-2019 # Windows Server 2019macOS
runs-on: macos-latest # macOS 15 (as of Aug 2025)
runs-on: macos-15 # macOS 15 Sequoia (Apple Silicon)
runs-on: macos-14 # macOS 14 Sonoma (Apple Silicon)
runs-on: macos-26 # macOS 26 (PREVIEW)---
macOS Runner Updates and Deprecations
Current Status
| Label | Status | Architecture | Notes |
|---|---|---|---|
macos-latest | Active | ARM64 (Apple Silicon) | Points to macOS 15 |
macos-15 | Active | ARM64 (Apple Silicon) | M1/M2/M3 |
macos-14 | Active | ARM64 (Apple Silicon) | M1/M2 |
macos-26 | Preview | ARM64 (Apple Silicon) | Beta |
macos-13 | RETIRED | Intel x86_64 | Retired November 14, 2025 |
macos-12 | RETIRED | Intel x86_64 | Retired |
Intel-Specific Labels (Long-term Deprecated)
runs-on: macos-15-intel # Intel x86_64 (NEW but deprecated long-term)
runs-on: macos-14-large # Intel x86_64
runs-on: macos-15-large # Intel x86_64Important: Apple Silicon (ARM64) will be required after Fall 2027. Plan migration now.
Migration Example
jobs:
build:
# BAD - macos-13 retired Nov 14, 2025 (WILL FAIL)
# runs-on: macos-13
# GOOD - Use macos-15 or macos-latest
runs-on: macos-15
steps:
- uses: actions/checkout@v6
- run: ./build.sh---
ARM64 Runners
Availability
- Generally available as of August 2025
- Free for public repositories
- Private repositories require GitHub Enterprise Cloud plan
Labels
runs-on: ubuntu-latest-arm64 # Free for public repos
runs-on: ubuntu-24.04-arm64
runs-on: windows-latest-arm64 # ARM WindowsExample
jobs:
build:
runs-on: ubuntu-latest-arm64 # Free for public repos
steps:
- uses: actions/checkout@v6
- name: Build on ARM64
run: |
uname -m # Should output: aarch64
./build.shSpecifications
- 4 vCPU ARM64 processors
- Available for Linux and Windows
- Native ARM execution (no virtualization needed)
- Ideal for multi-architecture builds
Common Validation Issues
- Using ARM64 runners in private repos without Enterprise Cloud
- Assuming all community actions work on ARM64
- Not testing ARM-specific compilation issues
---
GPU Runners
Availability
- Generally available for Windows and Linux
- Requires Team or Enterprise Cloud plan
Labels
runs-on: gpu-t4-4-core # NVIDIA Tesla T4Specifications
- GPU: NVIDIA Tesla T4 with 16GB VRAM
- CPU: 4 vCPUs
- RAM: 28GB
- Pricing: $0.07/minute
Example
jobs:
ml-training:
runs-on: gpu-t4-4-core
steps:
- uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install ML dependencies
run: |
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install -r requirements.txt
- name: Train model
run: python train.py --use-gpu
- name: Run inference
run: python inference.pyUse Cases
- ML model training
- GPU-accelerated testing
- CUDA development
- Image/video processing
Common Validation Issues
- Missing CUDA setup
- Incorrect GPU driver versions
- Not utilizing GPU in workloads (CPU fallback)
- Missing GPU-specific dependencies
---
M2 Pro macOS Runners (Larger Runners)
Availability
- Generally available with M2 Pro powered runners
Labels
runs-on: macos-latest-xlarge # macOS 15, M2 Pro
runs-on: macos-15-xlarge # macOS 15, M2 Pro
runs-on: macos-14-xlarge # macOS 14, M2 ProSpecifications
- CPU: 5-core (vs 3-core standard)
- GPU: 8-core with hardware acceleration (enabled by default)
- RAM: 14GB
- Storage: 14GB
- Performance: Up to 15% faster than M1 runners
- Pricing: $0.16/minute
Example
jobs:
ios-build:
runs-on: macos-15-xlarge # M2 Pro with GPU acceleration
steps:
- uses: actions/checkout@v6
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Build iOS app
run: |
xcodebuild -workspace App.xcworkspace \
-scheme Production \
-configuration Release \
-archivePath build/App.xcarchive \
archive
- name: Run GPU-accelerated tests
run: |
# GPU acceleration automatically available
xcodebuild test -scheme AppTestsBenefits
- GPU hardware acceleration for Metal-based workloads
- Improved build times for iOS/macOS apps
- Better performance for Xcode builds
- Native Apple Silicon performance
---
Runner Selection Best Practices
Decision Criteria
1. Architecture compatibility: ARM64 vs Intel x86_64 2. Cost optimization: Standard vs larger runners 3. GPU requirements: ML/AI workloads need GPU runners 4. Operating system: Latest versions recommended 5. Deprecation timelines: Avoid retired runners 6. Public vs private repos: ARM64 free only for public repos
Validation Checklist
# Check these in your workflows:
- [ ] Using latest runner versions (macos-15, windows-2025, ubuntu-latest)
- [ ] Not using deprecated runners (macos-13)
- [ ] Architecture-appropriate runners (ARM64 vs Intel)
- [ ] GPU runners for ML workloads
- [ ] Cost-effective runner selection
- [ ] ARM64 compatibility tested (if using ARM64 runners)Cost Comparison
| Runner Type | Pricing | Best For |
|---|---|---|
| Standard (Linux/Windows) | Included | Most workloads |
| Standard (macOS) | Included | iOS/macOS builds |
| ARM64 (public repos) | Free | Multi-arch builds |
| ARM64 (private repos) | Enterprise | ARM-native builds |
| GPU (T4) | $0.07/min | ML/AI workloads |
| M2 Pro (xlarge) | $0.16/min | Heavy iOS builds |
---
Multi-Architecture Builds
Example: Building for Multiple Architectures
jobs:
build:
strategy:
matrix:
include:
- runner: ubuntu-latest
arch: x64
- runner: ubuntu-latest-arm64
arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v6
- name: Build
run: |
echo "Building for ${{ matrix.arch }}"
./build.sh --arch ${{ matrix.arch }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.arch }}
path: dist/---
Self-Hosted Runner Configuration
When using self-hosted runners, configure actionlint to recognize custom labels:
# .github/actionlint.yaml
self-hosted-runner:
labels:
- my-custom-runner
- gpu-runner
- arm-runner
- on-premisesThis prevents actionlint from reporting unknown runner label errors.
#!/bin/bash
# GitHub Actions Validator - Tool Installation Script
# Installs act and actionlint tools locally in the current directory
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TOOLS_DIR="${SCRIPT_DIR}/.tools"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Create tools directory if it doesn't exist
mkdir -p "${TOOLS_DIR}"
# Function to install act
install_act() {
log_info "Installing act (nektos/act)..."
# Check if act already exists in tools directory
if [ -f "${TOOLS_DIR}/act" ]; then
log_warn "act already exists in ${TOOLS_DIR}, removing..."
rm -f "${TOOLS_DIR}/act"
fi
# Check if act exists in PATH
if command -v act &> /dev/null; then
log_info "act found in PATH, creating symlink..."
ln -sf "$(command -v act)" "${TOOLS_DIR}/act"
else
log_info "Downloading act..."
# Install act to the tools directory
cd "${TOOLS_DIR}"
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | bash -s -- -b "${TOOLS_DIR}"
cd - > /dev/null
fi
if [ -f "${TOOLS_DIR}/act" ]; then
log_info "act installed successfully at ${TOOLS_DIR}/act"
"${TOOLS_DIR}/act" --version
else
log_error "Failed to install act"
return 1
fi
}
# Function to install actionlint
install_actionlint() {
log_info "Installing actionlint (rhysd/actionlint)..."
# Check if actionlint already exists in tools directory
if [ -f "${TOOLS_DIR}/actionlint" ]; then
log_warn "actionlint already exists in ${TOOLS_DIR}, removing..."
rm -f "${TOOLS_DIR}/actionlint"
fi
# Check if actionlint exists in PATH
if command -v actionlint &> /dev/null; then
log_info "actionlint found in PATH, creating symlink..."
ln -sf "$(command -v actionlint)" "${TOOLS_DIR}/actionlint"
else
log_info "Downloading actionlint..."
# Download and install actionlint
cd "${TOOLS_DIR}"
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
cd - > /dev/null
fi
if [ -f "${TOOLS_DIR}/actionlint" ]; then
log_info "actionlint installed successfully at ${TOOLS_DIR}/actionlint"
"${TOOLS_DIR}/actionlint" --version
else
log_error "Failed to install actionlint"
return 1
fi
}
# Main installation
main() {
log_info "=== GitHub Actions Validator - Tool Installation ==="
log_info "Installing tools to: ${TOOLS_DIR}"
echo ""
install_act
echo ""
install_actionlint
echo ""
log_info "=== Installation Complete ==="
log_info "Tools installed at: ${TOOLS_DIR}"
log_info "act: ${TOOLS_DIR}/act"
log_info "actionlint: ${TOOLS_DIR}/actionlint"
echo ""
log_info "Add ${TOOLS_DIR} to your PATH or use absolute paths:"
echo " export PATH=\"${TOOLS_DIR}:\$PATH\""
}
main "$@"
#!/bin/bash
# GitHub Actions Validator - Workflow Validation Script
# Validates GitHub Actions workflows using actionlint and act
# Includes version checking and reference file hints
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TOOLS_DIR="${SCRIPT_DIR}/.tools"
SKILL_DIR="$(dirname "${SCRIPT_DIR}")"
REFERENCES_DIR="${SKILL_DIR}/references"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_section() {
echo ""
echo -e "${BLUE}=== $1 ===${NC}"
echo ""
}
log_reference() {
echo -e "${CYAN}[REF]${NC} $1"
}
# Check if a tool is available either in scripts/.tools or on PATH
tool_exists() {
local tool_name=$1
[ -x "${TOOLS_DIR}/${tool_name}" ] || command -v "${tool_name}" &> /dev/null
}
# Check if Docker is running
check_docker() {
if ! docker info &> /dev/null 2>&1; then
return 1
fi
return 0
}
# Pre-check Docker status and inform user early
precheck_docker() {
if ! check_docker; then
log_warn "Docker is not running - act testing will be skipped"
log_info "To enable full validation, start Docker Desktop or Docker daemon"
log_info "Continuing with actionlint validation only..."
echo ""
return 1
fi
return 0
}
# Current recommended action versions (December 2025)
# Format: action_name:current_version:minimum_version
declare -A ACTION_VERSIONS=(
["actions/checkout"]="v6:v4"
["actions/setup-node"]="v6:v4"
["actions/setup-python"]="v5:v4"
["actions/setup-java"]="v4:v4"
["actions/setup-go"]="v5:v4"
["actions/cache"]="v4:v4"
["actions/upload-artifact"]="v4:v4"
["actions/download-artifact"]="v4:v4"
["docker/setup-buildx-action"]="v3:v3"
["docker/login-action"]="v3:v3"
["docker/build-push-action"]="v6:v5"
["docker/metadata-action"]="v5:v5"
["aws-actions/configure-aws-credentials"]="v4:v4"
)
# Extract major version from version string (v4.1.1 -> 4, v4 -> 4)
get_major_version() {
local version=$1
# Remove 'v' prefix and get first number
echo "$version" | sed 's/^v//' | cut -d'.' -f1
}
# Check action versions in workflow files
check_action_versions() {
local workflow_path=$1
log_section "Action Version Check"
local files_to_check=()
if [ -f "$workflow_path" ]; then
files_to_check+=("$workflow_path")
elif [ -d "$workflow_path" ]; then
while IFS= read -r -d '' file; do
files_to_check+=("$file")
done < <(find "$workflow_path" -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) -print0 2>/dev/null)
fi
if [ ${#files_to_check[@]} -eq 0 ]; then
log_warn "No workflow files found to check"
return 0
fi
local has_issues=0
local outdated_count=0
local deprecated_count=0
local uptodate_count=0
for file in "${files_to_check[@]}"; do
log_info "Checking: $file"
# Extract all 'uses:' statements
while IFS= read -r line; do
# Skip empty lines and comments
[[ -z "$line" ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
# Extract action reference (e.g., actions/checkout@v4 or actions/checkout@sha)
if [[ "$line" =~ uses:[[:space:]]*([^@]+)@([^[:space:]#]+) ]]; then
local action="${BASH_REMATCH[1]}"
local version="${BASH_REMATCH[2]}"
# Clean up action name (remove quotes if present)
action=$(echo "$action" | tr -d '"' | tr -d "'" | xargs)
# Check if this action is in our version database
if [[ -v ACTION_VERSIONS["$action"] ]]; then
local version_info="${ACTION_VERSIONS[$action]}"
local current_version="${version_info%%:*}"
local minimum_version="${version_info##*:}"
local current_major=$(get_major_version "$current_version")
local minimum_major=$(get_major_version "$minimum_version")
# Handle SHA pinning - extract version from comment if present
local used_major=""
if [[ "$version" =~ ^[0-9a-f]{40}$ ]] || [[ "$version" =~ ^[0-9a-f]{7,}$ ]]; then
# SHA pinning - try to find version in the same line or comment
if [[ "$line" =~ v([0-9]+) ]]; then
used_major="${BASH_REMATCH[1]}"
else
# Can't determine version from SHA, skip
echo " ⚪ ${action}@${version:0:12}... - SHA pinned (version unknown)"
continue
fi
else
used_major=$(get_major_version "$version")
fi
if [ -z "$used_major" ] || ! [[ "$used_major" =~ ^[0-9]+$ ]]; then
echo " ⚪ ${action}@${version} - Unable to parse version"
continue
fi
if [ "$used_major" -lt "$minimum_major" ]; then
echo -e " ${RED}❌${NC} ${action}@${version} - ${RED}DEPRECATED${NC} (minimum: ${minimum_version}, using: v${used_major})"
((deprecated_count++))
has_issues=1
elif [ "$used_major" -lt "$current_major" ]; then
echo -e " ${YELLOW}⚠️${NC} ${action}@${version} - ${YELLOW}OUTDATED${NC} (current: ${current_version}, using: v${used_major})"
((outdated_count++))
else
echo -e " ${GREEN}✅${NC} ${action}@${version} - UP-TO-DATE (current: ${current_version})"
((uptodate_count++))
fi
fi
fi
done < "$file"
done
echo ""
log_info "Version Check Summary:"
log_info " Up-to-date: $uptodate_count"
if [ $outdated_count -gt 0 ]; then
log_warn " Outdated: $outdated_count"
fi
if [ $deprecated_count -gt 0 ]; then
log_error " Deprecated: $deprecated_count"
fi
if [ $outdated_count -gt 0 ] || [ $deprecated_count -gt 0 ]; then
echo ""
log_info "Recommendations:"
if [ $deprecated_count -gt 0 ]; then
log_error " - Update deprecated actions to current versions (see references/action_versions.md)"
fi
if [ $outdated_count -gt 0 ]; then
log_warn " - Consider updating outdated actions for latest features"
fi
log_info " - Use SHA pinning for security: action@SHA # vX.Y.Z"
fi
return $has_issues
}
# Advisory security hardening checks.
# These checks emit warnings and do not change overall pass/fail status.
check_security_policies() {
local workflow_path=$1
log_section "Security Policy Checks (Advisory)"
local files_to_check=()
if [ -f "$workflow_path" ]; then
files_to_check+=("$workflow_path")
elif [ -d "$workflow_path" ]; then
while IFS= read -r -d '' file; do
files_to_check+=("$file")
done < <(find "$workflow_path" -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) -print0 2>/dev/null)
else
log_error "Path not found: $workflow_path"
return 1
fi
if [ ${#files_to_check[@]} -eq 0 ]; then
log_warn "No workflow files found for security checks"
return 0
fi
local warning_count=0
for file in "${files_to_check[@]}"; do
log_info "Checking policies in: $file"
# 1) Third-party actions should be pinned to full SHA.
while IFS= read -r match; do
local line_no="${match%%:*}"
local line="${match#*:}"
if [[ "$line" =~ uses:[[:space:]]*([^@[:space:]]+)@([^[:space:]#]+) ]]; then
local action="${BASH_REMATCH[1]}"
local version="${BASH_REMATCH[2]}"
action=$(echo "$action" | tr -d '"' | tr -d "'" | xargs)
version=$(echo "$version" | tr -d '"' | tr -d "'" | sed 's/[[:space:],]*$//')
# Skip local actions/reusable workflows and docker:// references.
if [[ "$action" == ./* ]] || [[ "$action" == ../* ]] || [[ "$action" == docker://* ]]; then
continue
fi
# Only enforce SHA pinning for third-party actions.
if [[ "$action" == */* ]]; then
local owner="${action%%/*}"
if [ "$owner" != "actions" ] && [ "$owner" != "github" ]; then
if ! [[ "$version" =~ ^[0-9a-fA-F]{40}$ ]]; then
log_warn "$file:$line_no third-party action is not SHA pinned: ${action}@${version}"
((warning_count++))
fi
fi
fi
fi
done < <(grep -nE '^[[:space:]]*(-[[:space:]]*)?uses:[[:space:]]*[^[:space:]]+@[^[:space:]#]+' "$file" || true)
# 2) Explicit least-privilege permissions.
if ! grep -qE '^[[:space:]]*permissions:[[:space:]]*' "$file"; then
log_warn "$file missing explicit permissions block. Add workflow/job-level permissions (or permissions: {})."
((warning_count++))
fi
if grep -qE '^[[:space:]]*permissions:[[:space:]]*write-all([[:space:]]*(#.*)?)?$' "$file"; then
log_warn "$file uses permissions: write-all. Prefer least-privilege scopes."
((warning_count++))
fi
# 3) Heuristic detection for untrusted github context in run scripts.
while IFS= read -r risky_line; do
log_warn "$file:$risky_line potential script injection risk in run step. Move untrusted input to env and quote it."
((warning_count++))
done < <(
awk '
function indent_of(s, t) { t=s; sub(/^[ ]*/, "", t); return length(s)-length(t) }
BEGIN { in_run_block=0; run_indent=-1 }
{
line=$0
indent=indent_of(line)
if (in_run_block && indent <= run_indent && line !~ /^[[:space:]]*$/) {
in_run_block=0
}
if (line ~ /^[[:space:]]*run:[[:space:]]*[|>][[:space:]]*$/) {
in_run_block=1
run_indent=indent
next
}
if (line ~ /^[[:space:]]*run:[[:space:]]*.*\$\{\{[[:space:]]*github\.(event|head_ref|ref_name|actor|triggering_actor|repository_owner|base_ref)/) {
print NR
next
}
if (in_run_block && line ~ /\$\{\{[[:space:]]*github\.(event|head_ref|ref_name|actor|triggering_actor|repository_owner|base_ref)/) {
print NR
}
}
' "$file"
)
# 4) OIDC-integrated actions should explicitly request id-token: write.
if grep -qiE 'uses:[[:space:]]*(aws-actions/configure-aws-credentials|azure/login|google-github-actions/auth|hashicorp/vault-action|actions/attest-build-provenance)@' "$file"; then
if ! grep -qiE 'id-token:[[:space:]]*write' "$file"; then
log_warn "$file uses an OIDC-related action but does not declare id-token: write in permissions."
((warning_count++))
fi
fi
done
echo ""
if [ $warning_count -eq 0 ]; then
log_info "✓ No security policy warnings found"
else
log_warn "Security policy warnings: $warning_count (advisory only; exit code unchanged)"
log_info "See references/common_errors.md (Security section) and references/modern_features.md"
fi
return 0
}
# Show reference file hints based on error type
show_reference_hints() {
local error_output=$1
echo ""
log_section "Reference Documentation"
local showed_hint=0
# Check for various error patterns and suggest references
if echo "$error_output" | grep -qi "syntax\|yaml\|unexpected"; then
log_reference "Syntax errors detected - see references/common_errors.md (Syntax Errors section)"
showed_hint=1
fi
if echo "$error_output" | grep -qi "expression\|\${{"; then
log_reference "Expression errors detected - see references/common_errors.md (Expression Errors section)"
showed_hint=1
fi
if echo "$error_output" | grep -qi "cron\|schedule"; then
log_reference "Schedule errors detected - see references/common_errors.md (Schedule Errors section)"
showed_hint=1
fi
if echo "$error_output" | grep -qi "runner\|runs-on\|ubuntu\|macos\|windows"; then
log_reference "Runner label issues - see references/runners.md"
showed_hint=1
fi
if echo "$error_output" | grep -qi "action\|uses:"; then
log_reference "Action issues detected - see references/common_errors.md (Action Errors section)"
showed_hint=1
fi
if echo "$error_output" | grep -qi "docker\|container"; then
log_reference "Docker/container issues - see references/act_usage.md (Troubleshooting section)"
showed_hint=1
fi
if echo "$error_output" | grep -qi "needs:\|dependency\|job"; then
log_reference "Job dependency issues - see references/common_errors.md (Job Configuration Errors section)"
showed_hint=1
fi
if echo "$error_output" | grep -qi "injection\|security\|secret\|untrusted"; then
log_reference "Security issues detected - see references/common_errors.md (Security section)"
showed_hint=1
fi
if echo "$error_output" | grep -qi "workflow_call\|reusable\|oidc\|id-token\|attestation\|environment:\|permissions:"; then
log_reference "Modern features - see references/modern_features.md"
showed_hint=1
fi
if echo "$error_output" | grep -qi "version\|deprecated\|outdated\|v[0-9]"; then
log_reference "Action versions - see references/action_versions.md"
showed_hint=1
fi
if [ $showed_hint -eq 0 ]; then
log_reference "No direct mapping found for this error output"
log_reference "Fallback: check references/common_errors.md, then search the exact error text in official docs"
log_reference "Include exact tool output, workflow file, and line number in your report"
fi
}
# Validate required tools for selected execution mode
check_tools() {
local run_actionlint=$1
local run_act=$2
local allow_fallback=$3
local missing_tools=0
if [ "$run_actionlint" = true ] && ! tool_exists "actionlint"; then
if [ "$allow_fallback" = true ] && [ "$run_act" = true ] && tool_exists "act"; then
log_warn "actionlint not found. Falling back to act-only validation."
run_actionlint=false
else
log_error "actionlint not found. Please run install_tools.sh first."
missing_tools=1
fi
fi
if [ "$run_act" = true ] && ! tool_exists "act"; then
if [ "$allow_fallback" = true ] && [ "$run_actionlint" = true ] && tool_exists "actionlint"; then
log_warn "act not found. Falling back to actionlint-only validation."
run_act=false
else
log_error "act not found. Please run install_tools.sh first."
missing_tools=1
fi
fi
if [ $missing_tools -eq 1 ]; then
log_info "Run: bash ${SCRIPT_DIR}/install_tools.sh"
exit 1
fi
CHECK_TOOLS_RUN_ACTIONLINT=$run_actionlint
CHECK_TOOLS_RUN_ACT=$run_act
}
# Get the appropriate tool path
get_tool_path() {
local tool_name=$1
if [ -f "${TOOLS_DIR}/${tool_name}" ]; then
echo "${TOOLS_DIR}/${tool_name}"
elif command -v "${tool_name}" &> /dev/null; then
command -v "${tool_name}"
else
log_error "${tool_name} not found"
exit 1
fi
}
# Validate workflow with actionlint.
# Captures output to global ACTIONLINT_OUTPUT (for reference hints) while printing it.
validate_with_actionlint() {
local workflow_path=$1
log_section "Running actionlint"
local actionlint_path
actionlint_path=$(get_tool_path "actionlint")
local output=""
local actionlint_exit=0
if [ -f "$workflow_path" ]; then
log_info "Validating: $workflow_path"
output=$("${actionlint_path}" "$workflow_path" 2>&1) || actionlint_exit=$?
[ -n "$output" ] && echo "$output"
ACTIONLINT_OUTPUT="$output"
if [ $actionlint_exit -eq 0 ]; then
log_info "✓ actionlint validation passed"
return 0
else
log_error "✗ actionlint found issues"
return 1
fi
elif [ -d "$workflow_path" ]; then
log_info "Validating all workflows in: $workflow_path"
# Find all .yml and .yaml files
local workflow_files=()
while IFS= read -r -d '' file; do
workflow_files+=("$file")
done < <(find "$workflow_path" -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) -print0 2>/dev/null)
if [ ${#workflow_files[@]} -eq 0 ]; then
log_warn "No workflow files found in: $workflow_path"
return 0
fi
output=$("${actionlint_path}" "${workflow_files[@]}" 2>&1) || actionlint_exit=$?
[ -n "$output" ] && echo "$output"
ACTIONLINT_OUTPUT="$output"
if [ $actionlint_exit -eq 0 ]; then
log_info "✓ actionlint validation passed for ${#workflow_files[@]} file(s)"
return 0
else
log_error "✗ actionlint found issues"
return 1
fi
else
log_error "Path not found: $workflow_path"
return 1
fi
}
# Test workflow with act
test_with_act() {
local workflow_path=$1
log_section "Running act (validation)"
ACT_SKIP_REASON=""
# Check if Docker is running
if ! check_docker; then
log_error "Docker is not running!"
log_warn "act requires Docker to validate and test workflows."
log_warn ""
log_warn "Solutions:"
log_warn " 1. Start Docker Desktop or Docker daemon"
log_warn " 2. Use --lint-only flag to skip act testing"
log_warn ""
return 1
fi
local act_path=$(get_tool_path "act")
# Convert workflow_path to absolute path
local abs_workflow_path="$workflow_path"
if [[ ! "$abs_workflow_path" = /* ]]; then
abs_workflow_path="$(cd "$(dirname "$workflow_path")" 2>/dev/null && pwd)/$(basename "$workflow_path")" || abs_workflow_path="$workflow_path"
fi
# Find the repository root (where .github/workflows exists)
local repo_root=""
local search_path="$abs_workflow_path"
# If workflow_path is a file, get its directory for searching
if [ -f "$search_path" ]; then
search_path="$(dirname "$search_path")"
fi
# Search upwards for .github/workflows directory
local current_dir="$search_path"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/.github/workflows" ]; then
repo_root="$current_dir"
break
fi
# Also check if we're inside .github/workflows
if [[ "$current_dir" == *"/.github/workflows"* ]] || [[ "$current_dir" == *"/.github/workflows" ]]; then
# Extract the part before .github
repo_root="${current_dir%%/.github/workflows*}"
if [ -d "$repo_root/.github/workflows" ]; then
break
fi
fi
current_dir="$(dirname "$current_dir")"
done
# Fallback to current directory
if [ -z "$repo_root" ] && [ -d "./.github/workflows" ]; then
repo_root="$(pwd)"
fi
if [ -z "$repo_root" ]; then
log_warn "No .github/workflows directory found in path hierarchy"
log_warn "Skipping act validation - workflows must be in .github/workflows/ directory"
log_info "Searched from: $workflow_path"
ACT_SKIP_REASON="no .github/workflows directory found in path hierarchy"
return 2
fi
log_info "Repository root: $repo_root"
# Determine the workflow file(s) to validate with act
# act requires workflows to be in .github/workflows/ directory
local workflow_flag=""
local target_description=""
if [ -f "$abs_workflow_path" ]; then
# Check if the file is inside .github/workflows
if [[ "$abs_workflow_path" == *"/.github/workflows/"* ]]; then
# File is in .github/workflows - use -W flag with relative path
workflow_flag="-W ${abs_workflow_path#$repo_root/}"
target_description="workflow: $(basename "$abs_workflow_path")"
else
# File is outside .github/workflows (e.g., examples/)
# act cannot directly validate files outside .github/workflows
log_warn "Target file is outside .github/workflows/: $abs_workflow_path"
log_warn "act can only validate workflows in .github/workflows/ directory"
log_info "Skipping act validation for this file"
log_info "Note: actionlint validation still applies to this file"
ACT_SKIP_REASON="target file is outside .github/workflows"
return 2
fi
elif [ -d "$abs_workflow_path" ]; then
# Directory specified
if [[ "$abs_workflow_path" == *"/.github/workflows"* ]] || [[ "$abs_workflow_path" == "$repo_root/.github/workflows" ]]; then
# It's the .github/workflows directory - validate all workflows
workflow_flag=""
target_description="all workflows in .github/workflows/"
else
# Directory outside .github/workflows
log_warn "Target directory is outside .github/workflows/: $abs_workflow_path"
log_warn "act can only validate workflows in .github/workflows/ directory"
log_info "Skipping act validation for this directory"
ACT_SKIP_REASON="target directory is outside .github/workflows"
return 2
fi
fi
# Save current directory
local original_dir="$(pwd)"
# Change to repository root for act
cd "$repo_root" || {
log_error "Failed to change to repository root: $repo_root"
return 1
}
log_info "Target: $target_description"
log_info "Step 1: Listing workflows..."
echo ""
# Define default runner images to avoid interactive prompts
# Using medium-sized images for good compatibility without huge downloads
local runner_images=(
"-P" "ubuntu-latest=catthehacker/ubuntu:act-latest"
"-P" "ubuntu-22.04=catthehacker/ubuntu:act-22.04"
"-P" "ubuntu-20.04=catthehacker/ubuntu:act-20.04"
)
# Use --list to show available workflows
# This validates that workflows can be parsed
local list_cmd="${act_path} --list ${workflow_flag} ${runner_images[*]}"
log_info "Running: act --list ${workflow_flag}"
if ! eval "${list_cmd}" 2>&1 | head -30; then
log_warn "Could not list workflows - this may indicate parsing issues"
echo ""
else
echo ""
log_info "✓ Workflow listing successful"
fi
echo ""
log_info "Step 2: Validating workflow syntax with dry-run..."
log_info "Note: This validates workflow structure without executing jobs"
log_info "Using medium-sized runner images (catthehacker/ubuntu:act-*)"
echo ""
# Run act in dry-run mode
# --dryrun: validates without executing
# --container-architecture: ensures consistent platform
# -W: specifies the workflow file to validate
# -P: specifies runner images to avoid interactive prompt
# 2>&1: capture both stdout and stderr
local act_output
local act_exit_code
local dryrun_cmd="${act_path} --dryrun ${workflow_flag} --container-architecture linux/amd64 ${runner_images[*]}"
log_info "Running: act --dryrun ${workflow_flag} --container-architecture linux/amd64"
act_output=$(eval "${dryrun_cmd}" 2>&1)
act_exit_code=$?
# Display output
echo "$act_output"
echo ""
# Interpret results
if [ $act_exit_code -eq 0 ]; then
log_info "✓ act validation passed"
cd "$original_dir"
return 0
else
# Check for specific error conditions
if echo "$act_output" | grep -qi "EOF"; then
log_error "✗ act encountered EOF error"
log_warn "This should not happen with -P flags set"
log_info "Try running: act --list manually to diagnose"
cd "$original_dir"
return 1
elif echo "$act_output" | grep -q "unable to get git repo"; then
log_warn "Not a git repository - some act features limited"
log_info "act validation completed with warnings"
cd "$original_dir"
return 0
elif echo "$act_output" | grep -qi "pull access denied\|image.*not found"; then
log_error "✗ Docker image pull failed"
log_warn "Cannot pull runner images. This may be due to:"
log_warn " - Docker registry connectivity issues"
log_warn " - Rate limiting"
log_warn "First-time run will download ~500MB of images"
cd "$original_dir"
return 1
elif echo "$act_output" | grep -qi "error\|failed"; then
log_error "✗ act validation failed (exit code: $act_exit_code)"
log_warn "This may indicate:"
log_warn " - Workflow syntax errors"
log_warn " - Invalid action references"
log_warn " - Docker image issues"
log_warn " - Configuration problems"
cd "$original_dir"
return 1
else
log_warn "act completed with warnings (exit code: $act_exit_code)"
cd "$original_dir"
return 0
fi
fi
}
# Display usage
# Optional first argument: exit code (default 0, pass 1 for error paths)
usage() {
local exit_code="${1:-0}"
echo "Usage: $0 [OPTIONS] <workflow-file-or-directory>"
echo ""
echo "Options:"
echo " --lint-only Run only actionlint validation"
echo " --test-only Run only act testing (requires Docker)"
echo " --check-versions Check action versions against recommended versions"
echo " --policy-checks Run advisory security policy checks (warnings only)"
echo " --help Display this help message"
echo ""
echo "Examples:"
echo " $0 .github/workflows/ci.yml"
echo " $0 .github/workflows/"
echo " $0 --lint-only .github/workflows/ci.yml"
echo " $0 --test-only .github/workflows/"
echo " $0 --check-versions .github/workflows/ci.yml"
echo " $0 --lint-only --policy-checks .github/workflows/ci.yml"
echo ""
echo "Requirements:"
echo " - actionlint: For static analysis (installed via install_tools.sh)"
echo " - act: For workflow testing (installed via install_tools.sh)"
echo " - Docker: Required for act to run (must be running)"
echo ""
exit "$exit_code"
}
# Main validation
main() {
local workflow_path=""
local lint_only=false
local test_only=false
local check_versions=false
local policy_checks=false
local version_only=false
local run_actionlint=true
local run_act=true
local allow_tool_fallback=false
local docker_available=true
local did_actionlint=false
local did_act=false
local act_skip_reason=""
local act_result=0
CHECK_TOOLS_RUN_ACTIONLINT=true
CHECK_TOOLS_RUN_ACT=true
ACT_SKIP_REASON=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--lint-only)
lint_only=true
shift
;;
--test-only)
test_only=true
shift
;;
--check-versions)
check_versions=true
shift
;;
--policy-checks)
policy_checks=true
shift
;;
--help)
usage
;;
*)
workflow_path=$1
shift
;;
esac
done
if [ -z "$workflow_path" ]; then
log_error "No workflow file or directory specified"
echo ""
usage 1
fi
if [ "$lint_only" = true ] && [ "$test_only" = true ]; then
log_error "Cannot combine --lint-only and --test-only"
exit 1
fi
# Determine mode-specific execution
if [ "$test_only" = true ]; then
run_actionlint=false
run_act=true
elif [ "$lint_only" = true ]; then
run_actionlint=true
run_act=false
else
run_actionlint=true
run_act=true
allow_tool_fallback=true
fi
if [ "$check_versions" = true ] && [ "$lint_only" = false ] && [ "$test_only" = false ] && [ "$policy_checks" = false ]; then
version_only=true
run_actionlint=false
run_act=false
allow_tool_fallback=false
fi
log_section "GitHub Actions Validator"
log_info "Target: $workflow_path"
check_tools "$run_actionlint" "$run_act" "$allow_tool_fallback"
run_actionlint=$CHECK_TOOLS_RUN_ACTIONLINT
run_act=$CHECK_TOOLS_RUN_ACT
# Pre-check Docker status if act testing is enabled
if [ "$run_act" = true ]; then
if ! precheck_docker; then
if [ "$test_only" = true ]; then
log_error "Docker is required for --test-only mode"
exit 1
fi
if [ "$run_actionlint" = true ]; then
docker_available=false
run_act=false
log_warn "Proceeding without act because Docker is unavailable"
else
log_error "Docker is required for act validation in the selected mode"
exit 1
fi
fi
fi
local exit_code=0
ACTIONLINT_OUTPUT=""
# Run version check if requested
if [ "$check_versions" = true ]; then
if ! check_action_versions "$workflow_path"; then
exit_code=1
fi
# If only checking versions, exit here
if [ "$version_only" = true ]; then
log_section "Version Check Complete"
exit $exit_code
fi
fi
# Run actionlint (output is captured and printed inside validate_with_actionlint,
# and stored in ACTIONLINT_OUTPUT for reference hint routing)
if [ "$run_actionlint" = true ]; then
did_actionlint=true
if ! validate_with_actionlint "$workflow_path"; then
exit_code=1
fi
fi
# Run advisory security checks if requested.
if [ "$policy_checks" = true ]; then
if ! check_security_policies "$workflow_path"; then
exit_code=1
fi
fi
# Run act if enabled and Docker is available
if [ "$run_act" = true ] && [ "$docker_available" = true ]; then
test_with_act "$workflow_path"
act_result=$?
if [ $act_result -eq 0 ]; then
did_act=true
elif [ $act_result -eq 2 ]; then
act_skip_reason="$ACT_SKIP_REASON"
log_warn "act validation skipped: $act_skip_reason"
if [ "$did_actionlint" = false ]; then
log_error "No effective validator executed: act was skipped and actionlint did not run"
exit_code=1
fi
else
exit_code=1
fi
fi
if [ "$version_only" = false ] && [ "$did_actionlint" = false ] && [ "$did_act" = false ]; then
log_error "No validator executed; refusing to report success"
exit_code=1
fi
log_section "Validation Summary"
if [ $exit_code -eq 0 ]; then
if [ -n "$act_skip_reason" ]; then
log_info "✓ Validation passed (actionlint completed; act skipped: $act_skip_reason)"
else
log_info "✓ All validations passed"
fi
else
log_error "✗ Some validations failed"
# Show reference hints based on errors
if [ -n "$ACTIONLINT_OUTPUT" ]; then
show_reference_hints "$ACTIONLINT_OUTPUT"
fi
echo ""
log_info "Tips:"
log_info " - Review error messages above"
log_info " - Use --lint-only to skip Docker-dependent tests"
log_info " - Use --check-versions to check for outdated actions"
log_info " - Use --policy-checks for security hardening warnings"
log_info " - Check references/common_errors.md for solutions"
fi
exit $exit_code
}
main "$@"
#!/usr/bin/env bash
#
# Regression test suite for scripts/validate_workflow.sh
#
# Covers:
# - P0: no false-success when actionlint is missing and act cannot validate target
# - P1: advisory policy checks (SHA pinning, permissions, script injection, OIDC)
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
readonly SKILL_DIR
VALIDATOR_SOURCE="$SKILL_DIR/scripts/validate_workflow.sh"
readonly VALIDATOR_SOURCE
TMP_ROOT="$(mktemp -d)"
cleanup() {
rm -rf "$TMP_ROOT"
}
trap cleanup EXIT
PASS=0
FAIL=0
OUTPUT=""
EXIT_CODE=0
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
new_sandbox() {
SANDBOX="$(mktemp -d "$TMP_ROOT/case-XXXXXX")"
mkdir -p "$SANDBOX/skill/scripts/.tools"
mkdir -p "$SANDBOX/repo/.github/workflows"
mkdir -p "$SANDBOX/repo/examples"
mkdir -p "$SANDBOX/bin"
cp "$VALIDATOR_SOURCE" "$SANDBOX/skill/scripts/validate_workflow.sh"
chmod +x "$SANDBOX/skill/scripts/validate_workflow.sh"
cat > "$SANDBOX/bin/docker" <<'EOF'
#!/usr/bin/env bash
if [[ "${1:-}" == "info" ]]; then
exit "${DOCKER_INFO_STUB_EXIT:-0}"
fi
exit 0
EOF
chmod +x "$SANDBOX/bin/docker"
}
create_act_stub() {
cat > "$SANDBOX/skill/scripts/.tools/act" <<'EOF'
#!/usr/bin/env bash
if [[ "$*" == *"--list"* ]]; then
exit "${ACT_LIST_STUB_EXIT:-0}"
fi
if [[ "$*" == *"--dryrun"* ]]; then
exit "${ACT_DRYRUN_STUB_EXIT:-0}"
fi
exit "${ACT_STUB_EXIT:-0}"
EOF
chmod +x "$SANDBOX/skill/scripts/.tools/act"
}
create_actionlint_stub() {
cat > "$SANDBOX/skill/scripts/.tools/actionlint" <<'EOF'
#!/usr/bin/env bash
exit "${ACTIONLINT_STUB_EXIT:-0}"
EOF
chmod +x "$SANDBOX/skill/scripts/.tools/actionlint"
}
run_validator() {
local -a args=("$@")
OUTPUT=""
EXIT_CODE=0
OUTPUT=$(
cd "$SANDBOX/repo" && \
PATH="$SANDBOX/bin:$PATH" bash "$SANDBOX/skill/scripts/validate_workflow.sh" "${args[@]}" 2>&1
) || EXIT_CODE=$?
}
assert_exit() {
local label="$1"
local expected="$2"
if [[ "$EXIT_CODE" -eq "$expected" ]]; then
pass "$label (exit $EXIT_CODE)"
else
fail "$label (expected exit $expected, got $EXIT_CODE)"
echo "$OUTPUT" | sed 's/^/ /'
fi
}
assert_contains() {
local label="$1"
local pattern="$2"
if echo "$OUTPUT" | grep -qE "$pattern"; then
pass "$label"
else
fail "$label (pattern not found: $pattern)"
echo "$OUTPUT" | sed 's/^/ /'
fi
}
assert_not_contains() {
local label="$1"
local pattern="$2"
if echo "$OUTPUT" | grep -qE "$pattern"; then
fail "$label (unexpected pattern found: $pattern)"
echo "$OUTPUT" | sed 's/^/ /'
else
pass "$label"
fi
}
echo "Running github-actions-validator regression tests..."
echo ""
echo "[P0] actionlint missing + target outside .github/workflows must fail"
new_sandbox
create_act_stub
cat > "$SANDBOX/repo/examples/outside.yml" <<'EOF'
name: Outside
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
EOF
run_validator "$SANDBOX/repo/examples/outside.yml"
if [[ "$EXIT_CODE" -ne 0 ]]; then
pass "returns non-zero when no effective validator executed"
else
fail "returns non-zero when no effective validator executed (expected non-zero, got 0)"
echo "$OUTPUT" | sed 's/^/ /'
fi
assert_contains "reports skipped act path" "act validation skipped"
assert_contains "reports no effective validator" "No effective validator executed|No validator executed; refusing to report success"
echo ""
echo "[P0] actionlint run + act skip should still pass"
new_sandbox
create_act_stub
create_actionlint_stub
cat > "$SANDBOX/repo/examples/outside.yml" <<'EOF'
name: Outside
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
EOF
run_validator "$SANDBOX/repo/examples/outside.yml"
assert_exit "passes when actionlint runs and act skips unsupported target" 0
assert_contains "shows actionlint success" "actionlint validation passed"
assert_contains "shows act skip message" "act validation skipped: target file is outside \\.github/workflows"
echo ""
echo "[P0] fallback to act-only still works for real workflow paths"
new_sandbox
create_act_stub
cat > "$SANDBOX/repo/.github/workflows/ci.yml" <<'EOF'
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
EOF
run_validator "$SANDBOX/repo/.github/workflows/ci.yml"
assert_exit "passes in act-only fallback mode for workflow under .github/workflows" 0
assert_contains "warns about actionlint fallback" "actionlint not found\\. Falling back to act-only validation"
assert_contains "act dry-run success is reported" "act validation passed"
echo ""
echo "[P1] policy checks report hardening warnings (advisory)"
new_sandbox
create_actionlint_stub
cat > "$SANDBOX/repo/examples/policy-bad.yml" <<'EOF'
name: Policy Bad
on: pull_request
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: docker/build-push-action@v6
- uses: aws-actions/configure-aws-credentials@v4
- name: Unsafe run usage
run: echo "${{ github.event.pull_request.title }}"
EOF
run_validator --lint-only --policy-checks "$SANDBOX/repo/examples/policy-bad.yml"
assert_exit "policy warnings do not change exit code" 0
assert_contains "warns for unpinned third-party action" "third-party action is not SHA pinned: docker/build-push-action@v6"
assert_contains "warns for missing permissions" "missing explicit permissions block"
assert_contains "warns for script injection pattern" "potential script injection risk in run step"
assert_contains "warns for missing id-token with OIDC action" "OIDC-related action but does not declare id-token: write"
assert_contains "prints policy warning summary" "Security policy warnings:"
echo ""
echo "[P1] policy checks accept hardened workflow"
new_sandbox
create_actionlint_stub
cat > "$SANDBOX/repo/examples/policy-good.yml" <<'EOF'
name: Policy Good
on: pull_request
permissions:
contents: read
id-token: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: docker/build-push-action@0123456789abcdef0123456789abcdef01234567
- uses: aws-actions/configure-aws-credentials@0123456789abcdef0123456789abcdef01234567
- name: Safe run usage
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "$PR_TITLE"
EOF
run_validator --lint-only --policy-checks "$SANDBOX/repo/examples/policy-good.yml"
assert_exit "hardened workflow remains successful" 0
assert_contains "prints clean policy summary" "No security policy warnings found"
assert_not_contains "does not print warning summary when clean" "Security policy warnings:"
echo ""
echo "Test summary: PASS=$PASS FAIL=$FAIL"
if [[ "$FAIL" -ne 0 ]]; then
exit 1
fi
echo "All tests passed."
Related skills
How it compares
Use github-actions-validator for existing workflow lint and local act runs; use github-actions template generators when scaffolding new pipelines from scratch.
FAQ
Which tools does github-actions-validator use?
github-actions-validator uses actionlint for static YAML, expression, runner-label, and security analysis, and act for local workflow execution when Docker runs. install_tools.sh installs both into scripts/.tools; --lint-only skips act when Docker is unavailable.
What workflow errors does github-actions-validator catch?
github-actions-validator catches invalid CRON schedules, unknown runs-on labels, outdated action versions, script-injection via untrusted expressions, broken needs: dependencies, and glob/path filter mistakes—mapping each to references/common_errors.md or runners.md fixes.