From claude-resources
Provides GitHub Actions best practices and pitfalls for timeouts, concurrency, permissions, action pinning, and caching in .yml workflows. Useful for writing, reviewing, or debugging CI/CD pipelines.
npx claudepluginhub takazudo/claude-resourcesThis skill uses the workspace's default tool permissions.
Reference best practices before writing or reviewing any GitHub Actions workflow.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Reference best practices before writing or reviewing any GitHub Actions workflow.
Load topic-specific references as needed from references/.
timeout-minutesThe default timeout is 360 minutes (6 hours). A stuck job silently burns runner minutes.
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15 # ALWAYS set this
Recommended values:
| Job type | timeout-minutes |
|---|---|
| Lint / typecheck | 5-10 |
| Unit tests | 10-15 |
| Build | 15-30 |
| E2E tests | 30-60 |
| Docker build | 15-30 |
| Deploy | 10-15 |
| Notification | 5 |
Prevent redundant runs and protect production deploys.
# PR checks: cancel previous runs on new push
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Production deploy: never cancel in-progress
concurrency:
group: deploy-production
cancel-in-progress: false
Never rely on default permissions. Declare explicitly per workflow or per job.
permissions:
contents: read
jobs:
deploy:
permissions:
contents: read
deployments: write
Tags are mutable. The March 2025 tj-actions/changed-files supply chain attack (CVE-2025-30066) compromised 23,000+ repos via rewritten tags.
# Bad - tag can be rewritten
- uses: actions/checkout@v4
# Good - immutable SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Caveat: Some repos (e.g., pnpm/action-setup) have force-pushed, invalidating previously pinned SHAs. If CI fails with Unable to resolve action ... unable to find version, look up the current SHA via gh api repos/OWNER/REPO/git/ref/tags/vX.Y.Z. See references/security.md for the full diagnostic procedure.
Do not use cache: 'pnpm' (or cache: 'npm', cache: 'yarn') in actions/setup-node. GitHub Actions cache restore is often slower than a fresh pnpm install from npm's CDN. npm's CDN is highly optimized for package downloads, while GitHub's cache API has significant overhead for large stores (especially 1GB+). Benchmarking confirmed: direct install from CDN consistently beats cache restore + install.
# BAD - cache restore adds overhead, slower than fresh install
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm # REMOVE THIS
# GOOD - just install directly
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- run: pnpm install
This is especially true for self-hosted runners where the pnpm store is already local — caching to GitHub's remote cache and restoring it is pointless overhead.
set-safe-directory: false on Self-Hosted Runnersactions/checkout defaults set-safe-directory to true, which runs git config --global --add safe.directory on every CI run. On self-hosted runners this appends duplicate entries to ~/.gitconfig indefinitely, polluting the shared gitconfig across all repos on that machine.
# GOOD — prevent gitconfig pollution on self-hosted runners
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
set-safe-directory: false
The safe.directory setting is unnecessary when the runner user owns the workspace directory.
actions/cache for Build Tools on Self-Hosted RunnersOn self-hosted runners, build tool caches (Cargo, Go modules, Gradle, etc.) already persist on disk. Using actions/cache uploads them to GitHub's remote cache API on every run and creates duplicate entries, wasting storage.
# BAD on self-hosted — uploads local cache to remote on every run
- uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: cargo-${{ hashFiles('Cargo.lock') }}
# GOOD on self-hosted — just use the local disk cache directly
# (no actions/cache step needed)
curl | sh Installers — Use Prebuilt-Binary ActionsInstaller scripts like curl https://.../init.sh | sh (wasm-pack, rustup, many language toolchains) do one HTTP request with no retry. A single transient 5xx from the redirect target (e.g., a GitHub release asset) kills the entire workflow. Seen in the wild: rustwasm.github.io → github.com/rustwasm/wasm-pack/releases/... returning 504 mid-deploy.
# BAD — one curl, no retry, fails on any 5xx
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# GOOD — prebuilt binary from GitHub releases, with retries + runner caching
- uses: taiki-e/install-action@v2
with:
tool: wasm-pack
taiki-e/install-action covers most Rust/Go/Node tools (wasm-pack, cargo-nextest, just, mdbook, etc.). For tools it doesn't cover, use actions/cache on a pinned-version binary, or wrap the curl in a retry loop with curl --retry 5 --retry-all-errors --retry-delay 5.
upload-artifact/download-artifact counts toward shared org storage (often limited). For passing build output between jobs in the same workflow, use actions/cache instead — it has a separate 10 GB per-repo limit.
# BAD — artifacts accumulate in shared org storage
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 1
# GOOD — cache uses separate per-repo quota
- uses: actions/cache/save@v4
with:
path: dist/
key: build-${{ github.run_id }}
# In the downstream job:
- uses: actions/cache/restore@v4
with:
path: dist/
key: build-${{ github.run_id }}
If the build and deploy steps can run on the same runner, merging them into a single job is even simpler.
For detailed guidance, read the appropriate reference file:
pull_request_target, script injection, secrets, OIDCNever debug CI issues by pushing and waiting. CI runs consume time (10-15 min per cycle) and runner minutes. Always verify locally first:
# Run the same checks CI runs, locally
pnpm check # typecheck + lint + format
pnpm build # production build
pnpm test # unit tests
# Only after ALL pass locally:
git push
# Then monitor:
/watch-ci
The workflow: fix locally → verify locally → push once → /watch-ci. If CI fails after local verification, it's either an environment difference (Node version, missing env vars) or a path/dependency issue specific to CI — much easier to diagnose than a code bug.
When reviewing or writing a workflow, verify:
timeout-minutesconcurrency group is set with appropriate cancel-in-progresspermissions are declared (least privilege)pull_request_target is NOT used with PR code checkoutrun: blockssecrets: inheritcache: parameter in setup-node (fresh install from CDN is faster — see rule 5)actions/checkout has set-safe-directory: false on self-hosted runners (see rule 6)actions/cache for build tools on self-hosted runners — disk cache is already local (see rule 7)curl | sh installers — use taiki-e/install-action or similar with retries (see rule 8)actions/cache not upload-artifact to avoid org storage limits (see rule 9)