About actions-ci-tuning
A skill for auditing and improving GitHub Actions workflows, aimed at teams maintaining CI. It prioritizes correctness before checking caching, parallel tests, artifact guards, reliability, and security configuration, then proposes minimal changes.
Key features
- 01Inventory workflows and run durations
- 02Check dependency caching and installation
- 03Review parallel jobs and test sharding
- 04Guard artifact report merging
- 05Check concurrency, triggers, and version pins
Use cases
- 01Diagnose failing or slow CI runs
- 02Tune Playwright test sharding and reports
- 03Reduce cache misses and lockfile drift
- 04Review GitHub Actions security settings
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ mizchi/ skills --skill actions-ci-tuningAfter installing, type/actions-ci-tuningin Claude Code to confirm it loaded.
Bundled filesREADME.md
Details
From the project READMEGitHub Actions CI Tuning
Audit and improve GitHub Actions workflows. Focus on correctness first (jobs that error rather than fail), then speed (cache, parallelism), then reliability (flakiness, artifact guards).
Workflow
- Inventory — list all
.github/workflows/*.ymlfiles and their trigger events, jobs, and rough durations (gh run list --workflow <file> --limit 5). - Audit against checklist — run through each section below and flag every gap.
- Prioritise — group findings by impact: correctness bugs > cache misses > parallelism gains > cosmetic.
- Propose changes — draft minimal diffs; do not refactor unrelated parts.
- Verify — after applying, confirm via
gh run listthat the next run is green and faster.
Checklist
Package Manager Cache
A cache miss means re-downloading hundreds of packages on every run. This is the single highest-ROI fix in most repos.
pnpm
- uses: pnpm/action-setup@v6
with:
version: latest # or pin to a specific version
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc # or node-version: '24' (current LTS)
cache: 'pnpm' # ← must be present; omitting it silently skips caching
Common mistake: calling corepack enable instead of pnpm/action-setup. corepack enable does NOT set up the pnpm store cache — actions/setup-node cache: 'pnpm' requires pnpm/action-setup to have run first.
npm
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: 'npm'
yarn (classic / berry)
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: 'yarn'
Verify cache is working: look for Cache restored successfully in setup-node logs. If absent, cache: key is likely missing or the lockfile path is wrong.
Dependency Install
- Always use
--frozen-lockfile(pnpm) /--ci(npm) /--immutable(yarn berry) in CI. Prevents lockfile drift from silently changing the installed tree. - Never use
npm installorpnpm installwithout the frozen flag in CI.
Parallel Jobs vs. Steps
- Independent jobs (lint, typecheck, test) should be separate jobs so they run in parallel, not sequential steps.
- Sequential steps are fine for setup within a single job (install → build → test).
- If a repo has lint + typecheck + unit test all in one job, split them.
jobs:
lint:
runs-on: ubuntu-latest
steps: [checkout, setup, install, run lint]
typecheck:
runs-on: ubuntu-latest
steps: [checkout, setup, install, run typecheck]
test:
runs-on: ubuntu-latest
steps: [checkout, setup, install, run test]
Test Sharding (Playwright / Vitest)
Use sharding to cut long test suites. Each shard runs a subset of tests in parallel.
Playwright example — 4 shards:
jobs:
e2e:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- run: pnpm exec playwright test --shard=${{ matrix.shard }}/4
env:
CI: true
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }} # ← upload even on test failure
with:
name: blob-report-${{ matrix.shard }}
path: blob-report/
retention-days: 1
Pitfalls:
fail-fast: falseis required — otherwise one failing shard cancels the rest before they upload blob reports.if: ${{ !cancelled() }}on the upload step ensures blob reports are uploaded even when tests fail. Without this, the merge-reports job gets no artifacts.
Artifact Guard in merge-reports
When shards can fail before uploading blob reports, the all-blob-reports directory may not exist. Guard the merge command:
- name: Merge reports
run: |
if [ -d all-blob-reports ] && [ "$(ls -A all-blob-reports 2>/dev/null)" ]; then
pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
else
echo "No blob reports found, skipping merge"
mkdir -p playwright-report
echo "<html><body><p>No test results available</p></body></html>" > playwright-report/index.html
fi
Without this guard, the merge-reports job errors with Error: Directory does not exist: ./all-blob-reports even when the overall workflow should gracefully report "no results."
Playwright Docker Image
For VRT (Visual Regression Testing), snapshots must be generated in the same environment as CI (Linux + specific fonts). Use the official Playwright Docker image instead of a plain node:* image:
mcr.microsoft.com/playwright:v1.59.1-noble # pin to the same version as @playwright/test
Why not node:24?
node:24requiresplaywright install --with-deps chromiumwhich triggersapt-getand can hang for hours in Docker Desktop on macOS (known issue with apt in QEMU-emulated environments).- The
mcr.microsoft.com/playwrightimage ships Chromium and all system dependencies pre-installed — no apt-get needed, starts in seconds.
Version pinning: keep the image version in sync with @playwright/test in package.json. Mismatch causes browser-not-found errors.
Concurrency Control
Cancel in-progress runs for the same branch/PR to avoid queue buildup:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Exception: do NOT cancel release/deploy workflows on main. Scope it to PR branches only:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
Scheduled Workflow Reliability
- Scheduled workflows on GitHub Actions can silently stop firing if the repo has no activity for 60 days.
- Add a
workflow_dispatch:trigger alongsideschedule:so it can be manually re-triggered without a code change. - For critical scheduled jobs (e.g., nightly E2E), add a failure notification step (GitHub issue creation, Slack, etc.) so silent failures are visible.
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch: # ← always add this
Action Version Pinning
- Pin third-party actions to a full commit SHA, not a tag. Tags can be moved.
- Use a comment with the human-readable version for readability:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- Dependabot or Renovate can keep SHA pins up to date automatically. Check that
.github/dependabot.ymlincludes thegithub-actionsecosystem.
Node.js 20 deprecation warning: GitHub started issuing "Node.js 20 actions are deprecated" warnings in 2025/2026. This refers to the action's own runtime (runs.using: node20), not the project's Node version. Fix by upgrading to the first major version that uses node24:
| action | first node24 major | current major (2026-09) |
|---|---|---|
actions/checkout | v6.0.0 | v7 |
actions/setup-node | v6.0.0 | v7 |
actions/cache | v5.0.0 | v6 |
actions/upload-artifact | v7.0.0 | v7 |
actions/download-artifact | v8.0.0 | v8 |
aws-actions/configure-aws-credentials | v6.0.0 | v6 |
Latest pinned SHAs (verified 2026-09):
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
Note: SHAs drift — always verify with gh release view --repo <owner>/<action> before pinning.
Environment Variables
CI: trueshould be set at the job or workflow level, not only in individual run steps. Many tools (Playwright, Vite, etc.) change behavior based on this flag.- Secrets should use
${{ secrets.NAME }}— never hardcode tokens. NODE_OPTIONS: --max-old-space-size=4096is sometimes needed for large builds in constrained runners (default heap is ~1.5 GB for a 7 GB runner).
Quick Audit Commands
# List recent run durations for a workflow
gh run list --workflow e2e.yml --limit 10 --json databaseId,displayTitle,createdAt,updatedAt,conclusion \
| jq '.[] | {title: .displayTitle, duration: (.updatedAt | fromdate) - (.createdAt | fromdate), conclusion: .conclusion}'
# Find jobs that always time out
gh run list --workflow e2e.yml --status failure --limit 20 --json databaseId \
| jq -r '.[].databaseId' \
| xargs -I{} gh run view {} --json jobs \
| jq '.jobs[] | select(.conclusion == "timed_out") | .name'
# Check cache usage in a run
gh run view <run_id> --log | grep -i "cache"
Common Anti-Patterns
| Anti-pattern | Problem | Fix |
|---|---|---|
corepack enable only (no pnpm/action-setup) | pnpm store is not cached | Add pnpm/action-setup@v6 before setup-node |
pnpm install without --frozen-lockfile | Lockfile can silently drift | Use --frozen-lockfile always in CI |
fail-fast: true on test matrix | Shards cancel before uploading artifacts | Set fail-fast: false |
Upload artifact without if: ${{ !cancelled() }} | Blob reports lost on test failure | Add the if condition |
playwright merge-reports without directory guard | Job errors when no shards uploaded | Guard with [ -d all-blob-reports ] check |
node:* image for Playwright VRT | apt-get hangs in Docker Desktop on macOS | Use mcr.microsoft.com/playwright image |
Scheduled workflow without workflow_dispatch | Can't manually re-trigger | Always add workflow_dispatch: |
| Action pinned to tag not SHA | Tag can be moved (supply-chain risk) | Pin to full commit SHA |