
Github Actions Pipeline Builder
- 138 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Author GitHub Actions workflows that lint, test, build, and deploy on every push or PR, with caching, matrix jobs, secrets, and environment gates for reliable releases.
About
Helps design and implement GitHub Actions pipelines: trigger rules, job graphs, caching, test and build stages, deployment workflows, secret handling, and reusable workflow patterns for dependable continuous integration and delivery.
- Workflow YAML scaffolding for common stacks
- Matrix builds across OS and runtime versions
- Reusable actions and composite steps
- Secrets, environments, and approval gates
- Artifact upload and deploy job patterns
Github Actions Pipeline Builder by the numbers
- 138 all-time installs (skills.sh)
- Ranked #475 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill github-actions-pipeline-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 138 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Author GitHub Actions workflows that lint, test, build, and deploy on every push or PR, with caching, matrix jobs, secrets, and environment gates for reliable releases.
Files
GitHub Actions Pipeline Builder
Expert in building production-grade CI/CD pipelines with GitHub Actions that are fast, reliable, and secure.
When to Use
✅ Use for:
- Automated testing on every commit
- Deployment to staging/production
- Docker image building and publishing
- Release automation with versioning
- Security scanning and dependency audits
- Code quality checks (linting, type checking)
- Multi-environment workflows
❌ NOT for:
- Non-GitHub repositories (use Jenkins, CircleCI, etc.)
- Complex pipelines better suited for dedicated CI/CD tools
- Self-hosted runners (covered in advanced patterns)
Quick Decision Tree
Does your project need:
├── Testing on every PR? → GitHub Actions
├── Automated deployments? → GitHub Actions
├── Matrix builds (Node 16, 18, 20)? → GitHub Actions
├── Secrets management? → GitHub Actions secrets
├── Multi-cloud deployments? → GitHub Actions + OIDC
└── Sub-second builds? → Consider build caching---
Technology Selection
GitHub Actions vs Alternatives
Why GitHub Actions in 2024:
- Native integration: No third-party setup
- Free for public repos: 2000 minutes/month for private
- Matrix builds: Test multiple versions in parallel
- Marketplace: 10,000+ pre-built actions
- OIDC support: Keyless cloud deployments
Timeline:
- 2019: GitHub Actions released
- 2020: Became standard for OSS projects
- 2022: OIDC support for secure cloud auth
- 2024: De facto CI/CD for GitHub repos
When to Use Alternatives
| Scenario | Use | Why |
|---|---|---|
| Self-hosted GitLab | GitLab CI | Native integration |
| Complex enterprise workflows | Jenkins | More flexible |
| Bitbucket repos | Bitbucket Pipelines | Native integration |
| Extremely large repos (>10GB) | BuildKite | Better for monorepos |
---
Common Anti-Patterns
Anti-Pattern 1: No Dependency Caching
Novice thinking: "Install dependencies fresh every time for consistency"
Problem: Wastes 2-5 minutes per build installing unchanged dependencies.
Wrong approach:
# ❌ Slow: Downloads all dependencies every run
- name: Install dependencies
run: npm installCorrect approach:
# ✅ Fast: Cache dependencies, only download changes
- name: Cache node_modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci # Faster than npm installImpact: Reduces install time from 3 minutes → 30 seconds.
Timeline:
- Pre-2020: Most workflows had no caching
- 2020+: Caching became standard
- 2024: Setup actions include built-in caching
---
Anti-Pattern 2: Duplicate YAML (No Matrix Builds)
Problem: Copy-paste workflows for different Node versions.
Wrong approach:
# ❌ Duplicated workflows
jobs:
test-node-16:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm test
test-node-18:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm test
test-node-20:
# ... same steps againCorrect approach:
# ✅ DRY: Matrix build
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm testBenefits: 66% less YAML, tests run in parallel.
---
Anti-Pattern 3: Secrets in Code
Problem: Hardcoded API keys, tokens visible in repo.
Symptoms: Security scanner alerts, leaked credentials.
Correct approach:
# ✅ Use GitHub Secrets
- name: Deploy to production
env:
API_KEY: ${{ secrets.PRODUCTION_API_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }}
run: |
./deploy.shSetting secrets: 1. Repo Settings → Secrets and variables → Actions 2. New repository secret 3. Name: PRODUCTION_API_KEY, Value: sk-...
Timeline:
- Pre-2022: Some teams committed .env files
- 2022+: GitHub secret scanning blocks commits with keys
- 2024: OIDC eliminates need for long-lived credentials
---
Anti-Pattern 4: No Failure Notifications
Problem: CI fails silently, team doesn't notice for hours.
Correct approach:
# ✅ Slack notification on failure
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "❌ Build failed: ${{ github.event.head_commit.message }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Build Failed*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}---
Anti-Pattern 5: Running All Tests on Every Commit
Problem: Slow feedback loop (10+ minute test suites).
Symptom: Developers avoid committing frequently.
Correct approach:
# ✅ Fast feedback: Run subset on PR, full suite on merge
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
quick-tests:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- run: npm run test:unit # Fast: 2 minutes
full-tests:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- run: npm run test # Slow: 10 minutes (unit + integration + e2e)Alternative: Use changed-files action to run only affected tests.
---
Implementation Patterns
Pattern 1: Basic CI Pipeline
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run typecheck
- name: Run tests
run: npm test
- name: Build
run: npm run buildPattern 2: Multi-Environment Deployment
name: Deploy
on:
push:
branches:
- main # → staging
- production # → production
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref_name }} # staging or production
steps:
- uses: actions/checkout@v3
- name: Deploy to ${{ github.ref_name }}
run: |
if [ "${{ github.ref_name }}" == "production" ]; then
./deploy.sh production
else
./deploy.sh staging
fi
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}Pattern 3: Release Automation
name: Release
on:
push:
tags:
- 'v*' # Trigger on version tags (v1.0.0)
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write # Required for creating releases
steps:
- uses: actions/checkout@v3
- name: Build artifacts
run: npm run build
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
files: |
dist/**
body: |
## What's Changed
See CHANGELOG.md for details.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}Pattern 4: Docker Build & Push
name: Docker
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
myapp:latest
myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max---
Production Checklist
□ Dependency caching configured
□ Matrix builds for multiple versions
□ Secrets stored in GitHub Secrets (not code)
□ Failure notifications (Slack, email, etc.)
□ Deploy previews for pull requests
□ Staging → Production promotion workflow
□ Release automation with versioning
□ Docker layer caching enabled
□ CODEOWNERS file for required reviews
□ Branch protection rules enabled
□ Status checks required before merge
□ Security scanning (Dependabot, CodeQL)---
When to Use vs Avoid
| Scenario | Use GitHub Actions? |
|---|---|
| GitHub-hosted repo | ✅ Yes |
| Need matrix builds | ✅ Yes |
| Deploying to AWS/GCP/Azure | ✅ Yes (with OIDC) |
| GitLab repo | ❌ No - use GitLab CI |
| Extremely large monorepo | ⚠️ Maybe - consider BuildKite |
| Need GUI pipeline builder | ❌ No - use Jenkins/Azure DevOps |
---
References
/references/advanced-caching.md- Cache strategies for faster builds/references/oidc-deployments.md- Keyless cloud authentication/references/security-hardening.md- Security best practices
Scripts
scripts/workflow_validator.ts- Validate YAML syntax locallyscripts/action_usage_analyzer.ts- Find outdated actions
Assets
assets/workflows/- Ready-to-use workflow templates
---
This skill guides: CI/CD pipelines | GitHub Actions workflows | Matrix builds | Caching | Deployments | Release automation
# Full-Stack CI/CD Pipeline Template
# Copy to .github/workflows/ in your repo
name: Full-Stack CI/CD
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
env:
NODE_VERSION: 18
jobs:
lint-and-typecheck:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run TypeScript
run: npm run typecheck
test:
name: Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- name: Run tests with coverage
run: npm test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
if: matrix.node-version == 18
with:
token: ${{ secrets.CODECOV_TOKEN }}
build:
name: Build
runs-on: ubuntu-latest
needs: [lint-and-typecheck, test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build
path: dist/
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: staging
steps:
- uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build
path: dist/
- name: Deploy to staging
run: ./scripts/deploy.sh staging
env:
API_KEY: ${{ secrets.STAGING_API_KEY }}
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
- name: Run smoke tests
run: npm run test:smoke -- --url=https://staging.example.com
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: deploy-staging
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build
path: dist/
- name: Deploy to production
run: ./scripts/deploy.sh production
env:
API_KEY: ${{ secrets.PRODUCTION_API_KEY }}
DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }}
- name: Run smoke tests
run: npm run test:smoke -- --url=https://example.com
- name: Notify Slack on success
if: success()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "✅ Production deployment successful",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Deployed to Production*\n${{ github.event.head_commit.message }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "❌ Production deployment failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Deployment Failed*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Advanced Caching Strategies
Production patterns for dramatically faster GitHub Actions workflows using intelligent caching.
Why Caching Matters
Impact: Reduces build times by 50-90%
- Typical
npm install: 2-5 minutes → 30 seconds with cache - Docker builds: 10 minutes → 2 minutes with layer caching
- Test databases: 1 minute setup → 5 seconds from cache
---
Pattern 1: npm Dependencies (Basic)
- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-How it works:
key: Exact cache match (hash of package-lock.json)restore-keys: Fallback if exact match fails- Cache invalidated when package-lock.json changes
Alternative: Use setup-node built-in caching
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm' # Automatic caching---
Pattern 2: Multiple Cache Paths
Cache multiple directories for monorepos.
- uses: actions/cache@v4
with:
path: |
~/.npm
~/.cache
node_modules
**/node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-deps----
Pattern 3: Docker Layer Caching
Dramatically speed up Docker builds.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=maxmode=max: Cache all layers (not just final image)
Impact:
- First build: 10 minutes
- Cached build (no changes): 20 seconds
- Cached build (code changes): 2 minutes
---
Pattern 4: Conditional Caching (Cache Only on Main)
Save cache space by only caching on main branch.
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
- name: Save cache
if: github.ref == 'refs/heads/main' && steps.cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}---
Pattern 5: Build Output Caching
Cache compiled assets across jobs.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Check if build exists in cache
- uses: actions/cache@v4
id: build-cache
with:
path: dist/
key: build-${{ hashFiles('src/**') }}
# Only build if cache miss
- name: Build
if: steps.build-cache.outputs.cache-hit != 'true'
run: npm run build
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Restore build from cache
- uses: actions/cache@v4
with:
path: dist/
key: build-${{ hashFiles('src/**') }}
- run: npm test---
Pattern 6: Test Database Caching
Cache test database setup.
- name: Cache PostgreSQL data
uses: actions/cache@v4
with:
path: |
/var/lib/postgresql/data
~/.pgdata
key: postgres-${{ hashFiles('**/schema.sql') }}
- name: Start PostgreSQL
run: |
if [ ! -d "$HOME/.pgdata" ]; then
# First time: initialize and seed
docker run -d -p 5432:5432 \
-e POSTGRES_PASSWORD=test \
-v $HOME/.pgdata:/var/lib/postgresql/data \
postgres:15
sleep 5
psql -f schema.sql
psql -f seeds.sql
else
# Cached: just start
docker run -d -p 5432:5432 \
-v $HOME/.pgdata:/var/lib/postgresql/data \
postgres:15
fi---
Pattern 7: Turbo/Nx Incremental Builds
Cache build outputs for monorepo tools.
# Turborepo
- uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- run: npx turbo build --cache-dir=.turbo
# Nx
- uses: actions/cache@v4
with:
path: |
node_modules/.cache/nx
.nx/cache
key: nx-${{ runner.os }}-${{ github.sha }}
restore-keys: |
nx-${{ runner.os }}-
- run: npx nx run-many --target=build --all---
Pattern 8: Pip Dependencies (Python)
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
# Or manual:
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip----
Pattern 9: Cargo Dependencies (Rust)
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}---
Pattern 10: Gradle Dependencies (Java)
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
# Or manual:
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}---
Cache Management
Cache Limits
- Per repository: 10 GB total
- Per cache entry: No hard limit (but slow if >1 GB)
- Retention: 7 days if not accessed
Monitoring Cache Usage
# List caches
gh api repos/{owner}/{repo}/actions/caches
# Delete specific cache
gh api -X DELETE repos/{owner}/{repo}/actions/caches/{cache_id}Cache Eviction Strategy
Oldest caches are deleted first when limit reached.
Best practices:
- Use specific cache keys (include hash)
- Clean up old caches regularly
- Limit cache size (compress if needed)
---
Advanced Patterns
Pattern 11: Parallel Job Caching
Share cache between parallel matrix jobs.
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16, 18, 20]
steps:
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ matrix.node }}-
npm- # Fallback to any Node versionPattern 12: Compression Before Caching
For large directories.
- name: Compress node_modules
run: tar -czf node_modules.tar.gz node_modules
- uses: actions/cache@v4
with:
path: node_modules.tar.gz
key: deps-${{ hashFiles('package-lock.json') }}
- name: Extract if cache hit
if: steps.cache.outputs.cache-hit == 'true'
run: tar -xzf node_modules.tar.gzPattern 13: Warm Cache (Pre-populate)
Run a scheduled workflow to keep caches warm.
name: Warm Cache
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
warm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.npm
key: warm-${{ hashFiles('package-lock.json') }}
- run: npm ci---
Troubleshooting
Cache Not Restored
Symptom: Build always slow, cache never hits
Fixes: 1. Check cache key matches exactly 2. Verify path exists 3. Ensure cache was saved in previous run
Debug:
- name: Debug cache
run: |
echo "Cache key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}"
ls -la ~/.npm || echo "Cache directory doesn't exist"Cache Too Large
Symptom: Warning "Cache size exceeds limit"
Fixes: 1. Compress before caching 2. Exclude unnecessary files 3. Split into multiple caches
Stale Cache
Symptom: Using old dependencies despite package-lock.json change
Fix: Clear caches manually or change cache key format
---
Production Checklist
□ Dependencies cached (npm/pip/cargo/etc.)
□ Build outputs cached between jobs
□ Docker layer caching enabled
□ Cache keys include file hashes
□ Restore-keys provide fallbacks
□ Cache size monitored (< 1 GB per entry)
□ Conditional saving (main branch only)
□ Cache invalidation strategy defined
□ Compression used for large caches
□ Warm cache scheduled for main branch---
Performance Metrics
Track cache effectiveness:
- name: Cache metrics
run: |
if [ "${{ steps.cache.outputs.cache-hit }}" == "true" ]; then
echo "✅ Cache hit - saved time"
else
echo "⚠️ Cache miss - will save for next run"
fi---
Resources
#!/usr/bin/env node
/**
* GitHub Actions Usage Analyzer
*
* Analyzes workflows to find outdated actions and suggest updates.
*
* Usage: npx tsx action_usage_analyzer.ts [workflow-dir]
*
* Examples:
* npx tsx action_usage_analyzer.ts .github/workflows
*
* Dependencies: npm install yaml
*/
import * as fs from 'fs';
import * as path from 'path';
import { parse } from 'yaml';
interface ActionUsage {
action: string;
version: string;
file: string;
count: number;
}
interface UpdateSuggestion {
action: string;
currentVersion: string;
latestVersion: string;
breaking: boolean;
notes?: string;
}
// Known action versions (as of Jan 2024)
const LATEST_VERSIONS: Record<string, { version: string; breaking?: boolean; notes?: string }> = {
'actions/checkout': {
version: 'v4',
breaking: false,
notes: 'v4 uses Node.js 20'
},
'actions/setup-node': {
version: 'v4',
breaking: false,
notes: 'v4 uses Node.js 20'
},
'actions/setup-python': {
version: 'v5',
breaking: false
},
'actions/cache': {
version: 'v4',
breaking: false
},
'actions/upload-artifact': {
version: 'v4',
breaking: true,
notes: 'v4 changes artifact retention and download behavior'
},
'actions/download-artifact': {
version: 'v4',
breaking: true,
notes: 'v4 changes download location and naming'
},
'docker/build-push-action': {
version: 'v5',
breaking: false
},
'docker/login-action': {
version: 'v3',
breaking: false
},
'codecov/codecov-action': {
version: 'v4',
breaking: false
},
'slackapi/slack-github-action': {
version: 'v1.25',
breaking: false
}
};
class ActionUsageAnalyzer {
private usages: Map<string, ActionUsage[]> = new Map();
private suggestions: UpdateSuggestion[] = [];
analyzeDirectory(dir: string): void {
const files = fs.readdirSync(dir);
files.forEach(file => {
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
this.analyzeFile(path.join(dir, file));
}
});
}
analyzeFile(filePath: string): void {
const content = fs.readFileSync(filePath, 'utf-8');
try {
const workflow = parse(content);
if (!workflow?.jobs) return;
Object.entries(workflow.jobs).forEach(([_, job]: [string, any]) => {
job.steps?.forEach((step: any) => {
if (step.uses) {
this.recordAction(step.uses, filePath);
}
});
});
} catch (error) {
console.error(`Failed to parse ${filePath}:`, error);
}
}
private recordAction(usesString: string, file: string): void {
// Parse action@version format
const match = usesString.match(/^([^@]+)@(.+)$/);
if (!match) return;
const [, action, version] = match;
if (!this.usages.has(action)) {
this.usages.set(action, []);
}
const existing = this.usages.get(action)!.find(
u => u.version === version && u.file === file
);
if (existing) {
existing.count++;
} else {
this.usages.get(action)!.push({
action,
version,
file,
count: 1
});
}
// Check if update available
const latest = LATEST_VERSIONS[action];
if (latest && version !== latest.version) {
const existingSuggestion = this.suggestions.find(
s => s.action === action && s.currentVersion === version
);
if (!existingSuggestion) {
this.suggestions.push({
action,
currentVersion: version,
latestVersion: latest.version,
breaking: latest.breaking || false,
notes: latest.notes
});
}
}
}
report(): void {
console.log('\n📊 GitHub Actions Usage Report\n');
console.log('─'.repeat(70));
// Group by action
const actions = Array.from(this.usages.keys()).sort();
if (actions.length === 0) {
console.log('No actions found in workflows.');
return;
}
console.log('\nActions Used:\n');
actions.forEach(action => {
const usages = this.usages.get(action)!;
const totalCount = usages.reduce((sum, u) => sum + u.count, 0);
console.log(`📦 ${action}`);
// Group by version
const versionMap = new Map<string, number>();
usages.forEach(u => {
versionMap.set(u.version, (versionMap.get(u.version) || 0) + u.count);
});
versionMap.forEach((count, version) => {
const latest = LATEST_VERSIONS[action];
const isLatest = latest && version === latest.version;
const icon = isLatest ? '✅' : '⚠️ ';
console.log(` ${icon} ${version} (${count} usage${count > 1 ? 's' : ''})`);
});
console.log('');
});
// Update suggestions
if (this.suggestions.length > 0) {
console.log('─'.repeat(70));
console.log('\n💡 Update Suggestions:\n');
this.suggestions.forEach(suggestion => {
const icon = suggestion.breaking ? '🔴' : '🟢';
console.log(`${icon} ${suggestion.action}`);
console.log(` Current: ${suggestion.currentVersion}`);
console.log(` Latest: ${suggestion.latestVersion}`);
if (suggestion.breaking) {
console.log(' ⚠️ Breaking changes - review migration guide');
}
if (suggestion.notes) {
console.log(` ℹ️ ${suggestion.notes}`);
}
console.log('');
});
console.log('─'.repeat(70));
console.log('\nTo update an action:');
console.log(' Replace: uses: actions/checkout@v3');
console.log(' With: uses: actions/checkout@v4');
} else {
console.log('✅ All actions are up to date!\n');
}
// Security notes
console.log('\n🔒 Security Best Practices:\n');
console.log(' • Pin actions to specific SHA for security:');
console.log(' uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1');
console.log(' • Use Dependabot to auto-update actions:');
console.log(' Create .github/dependabot.yml with github-actions ecosystem');
console.log('');
}
generateDependabotConfig(): string {
return `# .github/dependabot.yml
# Auto-update GitHub Actions
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "github-actions"
`;
}
}
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
const dir = args[0] || '.github/workflows';
if (!fs.existsSync(dir)) {
console.error(`❌ Directory not found: ${dir}`);
console.error('\nUsage: npx tsx action_usage_analyzer.ts [workflow-dir]');
console.error('Example: npx tsx action_usage_analyzer.ts .github/workflows');
process.exit(1);
}
const analyzer = new ActionUsageAnalyzer();
analyzer.analyzeDirectory(dir);
analyzer.report();
// Offer to create Dependabot config
if (args.includes('--create-dependabot')) {
const config = analyzer.generateDependabotConfig();
const configPath = '.github/dependabot.yml';
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, config);
console.log(`✅ Created ${configPath}`);
}
}
export { ActionUsageAnalyzer };
#!/usr/bin/env node
/**
* GitHub Actions Workflow Validator
*
* Validates workflow YAML files for syntax errors and common issues.
*
* Usage: npx tsx workflow_validator.ts [workflow-file]
*
* Examples:
* npx tsx workflow_validator.ts .github/workflows/ci.yml
* npx tsx workflow_validator.ts .github/workflows/*.yml
*
* Dependencies: npm install yaml
*/
import * as fs from 'fs';
import * as path from 'path';
import { parse } from 'yaml';
interface ValidationIssue {
file: string;
line?: number;
severity: 'error' | 'warning' | 'info';
message: string;
suggestion?: string;
}
class WorkflowValidator {
private issues: ValidationIssue[] = [];
validateFile(filePath: string): void {
const content = fs.readFileSync(filePath, 'utf-8');
try {
const workflow = parse(content);
if (!workflow) {
this.addIssue(filePath, 'error', 'Empty or invalid YAML file');
return;
}
// Required fields
if (!workflow.name) {
this.addIssue(filePath, 'warning', 'Missing workflow name');
}
if (!workflow.on) {
this.addIssue(filePath, 'error', 'Missing trigger (on:)');
}
if (!workflow.jobs) {
this.addIssue(filePath, 'error', 'No jobs defined');
return;
}
// Check each job
Object.entries(workflow.jobs).forEach(([jobName, job]: [string, any]) => {
this.validateJob(filePath, jobName, job);
});
// Check for common issues
this.checkCommonIssues(filePath, workflow);
} catch (error: any) {
this.addIssue(filePath, 'error', `YAML parse error: ${error.message}`);
}
}
private validateJob(file: string, name: string, job: any): void {
if (!job['runs-on']) {
this.addIssue(file, 'error', `Job '${name}' missing runs-on`);
}
if (!job.steps || job.steps.length === 0) {
this.addIssue(file, 'error', `Job '${name}' has no steps`);
}
// Check each step
job.steps?.forEach((step: any, index: number) => {
if (!step.uses && !step.run) {
this.addIssue(
file,
'error',
`Job '${name}', step ${index + 1}: Must have 'uses' or 'run'`
);
}
if (!step.name) {
this.addIssue(
file,
'info',
`Job '${name}', step ${index + 1}: Consider adding a name for clarity`
);
}
});
}
private checkCommonIssues(file: string, workflow: any): void {
const content = JSON.stringify(workflow);
// Check for missing caching
if (content.includes('npm install') && !content.includes('cache')) {
this.addIssue(
file,
'warning',
'Using npm install without caching',
'Add cache: "npm" to actions/setup-node or use actions/cache'
);
}
// Check for hardcoded secrets
if (content.match(/['\"]?[A-Za-z0-9]{20,}['\"]?/) && !content.includes('secrets.')) {
this.addIssue(
file,
'warning',
'Possible hardcoded secret detected',
'Use ${{ secrets.SECRET_NAME }} instead'
);
}
// Check for npm install vs npm ci
if (content.includes('npm install') && !content.includes('npm ci')) {
this.addIssue(
file,
'info',
'Consider using npm ci instead of npm install',
'npm ci is faster and more reliable in CI environments'
);
}
// Check for checkout action version
if (content.includes('actions/checkout@v1') || content.includes('actions/checkout@v2')) {
this.addIssue(
file,
'warning',
'Using outdated actions/checkout version',
'Update to actions/checkout@v3 or later'
);
}
// Check for missing dependency on job
Object.entries(workflow.jobs).forEach(([jobName, job]: [string, any]) => {
if (job.needs && !Array.isArray(job.needs)) {
const dependency = job.needs as string;
if (!workflow.jobs[dependency]) {
this.addIssue(
file,
'error',
`Job '${jobName}' depends on non-existent job '${dependency}'`
);
}
}
});
// Check for matrix without strategy
if (content.includes('matrix.') && !content.includes('strategy:')) {
this.addIssue(
file,
'error',
'Using matrix variable without strategy.matrix defined'
);
}
// Check for environment secrets without environment
Object.entries(workflow.jobs).forEach(([jobName, job]: [string, any]) => {
const jobStr = JSON.stringify(job);
if (jobStr.includes('secrets.') && !job.environment) {
this.addIssue(
file,
'info',
`Job '${jobName}' uses secrets but no environment specified`,
'Consider using environment for better secret management'
);
}
});
}
private addIssue(
file: string,
severity: ValidationIssue['severity'],
message: string,
suggestion?: string
): void {
this.issues.push({ file, severity, message, suggestion });
}
report(): void {
if (this.issues.length === 0) {
console.log('✅ No issues found!');
return;
}
const errors = this.issues.filter(i => i.severity === 'error');
const warnings = this.issues.filter(i => i.severity === 'warning');
const info = this.issues.filter(i => i.severity === 'info');
console.log(`\n📋 Workflow Validation Report\n`);
console.log(`Found ${errors.length} errors, ${warnings.length} warnings, ${info.length} suggestions\n`);
const printIssues = (issues: ValidationIssue[], icon: string) => {
if (issues.length === 0) return;
issues.forEach(issue => {
console.log(`${icon} ${issue.file}`);
console.log(` ${issue.message}`);
if (issue.suggestion) {
console.log(` 💡 ${issue.suggestion}`);
}
console.log('');
});
};
if (errors.length > 0) {
console.log('🚨 Errors:\n');
printIssues(errors, '❌');
}
if (warnings.length > 0) {
console.log('⚠️ Warnings:\n');
printIssues(warnings, '⚠️ ');
}
if (info.length > 0) {
console.log('💡 Suggestions:\n');
printIssues(info, 'ℹ️ ');
}
if (errors.length > 0) {
process.exit(1);
}
}
}
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage: npx tsx workflow_validator.ts <workflow-file>');
console.log('\nExamples:');
console.log(' npx tsx workflow_validator.ts .github/workflows/ci.yml');
console.log(' npx tsx workflow_validator.ts .github/workflows/*.yml');
process.exit(1);
}
const validator = new WorkflowValidator();
args.forEach(pattern => {
// Handle glob patterns
if (pattern.includes('*')) {
const dir = path.dirname(pattern);
const files = fs.readdirSync(dir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
files.forEach(file => {
validator.validateFile(path.join(dir, file));
});
} else {
if (!fs.existsSync(pattern)) {
console.error(`❌ File not found: ${pattern}`);
process.exit(1);
}
validator.validateFile(pattern);
}
});
validator.report();
}
export { WorkflowValidator };