LIVE REAL-REPO RUN — LOWER-DENSITY DEMO · Ran the $79 GitHub Actions Cost Audit analyzer against github.com/vercel/next.js (36 workflow files, 727 files scanned). 56 findings · $8,215/mo in estimated CI waste. Compare with huggingface/transformers demo (122 findings, $17,055/mo — 2.2x more findings, 2.1x more $/mo). Same deterministic engine, two real public repos, very different findings — proves the analyzer doesn't manufacture findings. Order your own $79 audit →
SHARE THIS HONEST DEMO
Share on X Share on LinkedIn Share on Reddit
GitHub Actions Cost Audit · by Milo Antaeus

Your GitHub Actions Cost Audit Report

Static-analysis CI/CD cost audit · https://github.com/vercel/next.js · Generated 2026-05-16 22:49 UTC

Workflows scanned: 36 Dockerfiles: 8 Patterns checked: 10 Confidence: deterministic (no LLM-in-the-loop)

Executive summary

56 ranked GitHub Actions cost-leak findings across 36 workflow file(s) (727 total files scanned, including 8 Dockerfile(s)). Implementing the top 3 could save approximately $8,215/month$98,580/year.

RECURRING GitHub Actions billing savings — verifiable in github.com/<org>/settings/billing next billing cycle. Estimates calibrated to per-minute runner rates ($0.008/min Linux, $0.016/min Windows, $0.08/min macOS) and conservative run frequency (~20 runs/day per push-triggered workflow).

#OpportunitySeverity$/mo saved
1Job `deploy-target` installs npm deps without cachingCRITICAL$250
2Job `build-native` installs npm deps without cachingCRITICAL$250
3Job `validate-docs-links` installs npm deps without cachingCRITICAL$250
4Job `start` installs npm deps without cachingCRITICAL$250
5Job `close` installs npm deps without cachingCRITICAL$250
TOTAL ESTIMATED MONTHLY SAVINGS: $8,215

Opportunity #1 — Job `deploy-target` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/build_and_deploy.yml:41

What we found: Job `deploy-target` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/build_and_deploy.yml:41)

jobs:
  deploy-target:
    runs-on: ubuntu-latest
    # Don't trigger this job on `pull_request` events from upstream branches.
    # Those would already run this job on the `push` event
    if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork }}

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #2 — Job `build-native` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/build_and_deploy.yml:183

What we found: Job `build-native` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/build_and_deploy.yml:183)

target:
              $dynamic: "`${this.arch}-${this.vendor}-${this.sys}${this.abi ? '-' + this.abi : ''}`"
            build_task: build-native-release
            os:
              mac:
                host: "['macos-15']"
                vendor: apple

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #3 — Job `validate-docs-links` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/build_and_test.yml:224

What we found: Job `validate-docs-links` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/build_and_test.yml:224)

secrets: inherit

  validate-docs-links:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #4 — Job `start` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/code_freeze.yml:24

What we found: Job `start` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/code_freeze.yml:24)

jobs:
  start:
    runs-on: ubuntu-latest

    environment: release-${{ github.event.inputs.releaseType }}
    steps:

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #5 — Job `close` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/issue_wrong_template.yml:8

What we found: Job `close` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/issue_wrong_template.yml:8)

jobs:
  close:
    if: github.event.label.name == 'please use the correct issue template'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #6 — Job `run` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/popular.yml:9

What we found: Job `run` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/popular.yml:9)

jobs:
  run:
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #7 — Job `stats-aggregate` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/pull_request_stats.yml:134

What we found: Job `stats-aggregate` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/pull_request_stats.yml:134)

retention-days: 1

  stats-aggregate:
    name: Aggregate Stats
    needs: stats
    if: always() && needs.stats.result != 'cancelled'
    runs-on: ubuntu-latest

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #8 — Job `update_dev_manifest` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/rspack-update-tests-manifest.yml:11

What we found: Job `update_dev_manifest` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/rspack-update-tests-manifest.yml:11)

jobs:
  update_dev_manifest:
    name: Update and upload Rspack development test manifest
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest
    steps:

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #9 — Job `update_build_manifest` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/rspack-update-tests-manifest.yml:72

What we found: Job `update_build_manifest` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/rspack-update-tests-manifest.yml:72)

PR_TITLE: Update Rspack development test manifest
          PR_BODY: This auto-generated PR updates the development integration test manifest used when testing Rspack.
  update_build_manifest:
    name: Update and upload Rspack production test manifest
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest
    steps:

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #10 — Job `evaluate` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/sync_backport_canary_release.yml:12

What we found: Job `evaluate` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/sync_backport_canary_release.yml:12)

inputs:
      workflowRunId:
        description: Completed build-and-deploy workflow run ID to evaluate
        required: true
        type: string
      headSha:
        description: Release commit SHA from that workflow run

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #11 — Job `test` installs pnpm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/test-turbopack-rust-bench-test.yml:1

What we found: Job `test` runs `pnpm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-pnpm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/test-turbopack-rust-bench-test.yml:1)

name: Turbopack Rust testing benchmarks
on:
  workflow_call:
    inputs:
      runner:

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #12 — Job `setup` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/test_e2e_deploy_release.yml:48

What we found: Job `setup` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/test_e2e_deploy_release.yml:48)

jobs:
  setup:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'vercel'
    outputs:
      next-version: ${{ steps.version.outputs.value }}

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #13 — Job `reset-test-project` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/test_e2e_project_reset_cron.yml:25

What we found: Job `reset-test-project` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/test_e2e_project_reset_cron.yml:25)

jobs:
  reset-test-project:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'vercel'
    steps:
      - name: Setup Node.js

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #14 — Job `testExamples` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/test_examples.yml:17

What we found: Job `testExamples` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/test_examples.yml:17)

jobs:
  testExamples:
    # Don't execute using cron on forks
    if: (github.repository == 'vercel/next.js') || (inputs.is_dispatched == true)
    name: Test Examples
    runs-on: ubuntu-latest

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #15 — Job `benchmark-small-apps` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/turbopack-benchmark.yml:34

What we found: Job `benchmark-small-apps` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/turbopack-benchmark.yml:34)

jobs:
  benchmark-small-apps:
    name: Benchmark Rust Crates (small apps)
    runs-on: ubuntu-latest-16-core-oss
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #16 — Job `benchmark-analyzer` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/turbopack-benchmark.yml:69

What we found: Job `benchmark-analyzer` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/turbopack-benchmark.yml:69)

run: cargo codspeed run -p turbopack-cli small_apps

  benchmark-analyzer:
    name: Benchmark Rust Crates (analyzer)
    runs-on: ubuntu-latest-16-core-oss
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #17 — Job `update_dev_manifest` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/turbopack-update-tests-manifest.yml:11

What we found: Job `update_dev_manifest` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/turbopack-update-tests-manifest.yml:11)

jobs:
  update_dev_manifest:
    name: Update and upload Turbopack development test manifest
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest
    steps:

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #18 — Job `update_build_manifest` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/turbopack-update-tests-manifest.yml:68

What we found: Job `update_build_manifest` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/turbopack-update-tests-manifest.yml:68)

PR_TITLE: Update Turbopack development test manifest
          PR_BODY: This auto-generated PR updates the development integration test manifest used when testing Turbopack.
  update_build_manifest:
    name: Update and upload Turbopack production test manifest
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest
    steps:

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #19 — Job `create-pull-request` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/update_fonts_data.yml:14

What we found: Job `create-pull-request` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/update_fonts_data.yml:14)

jobs:
  create-pull-request:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'vercel'
    steps:
      - name: Create GitHub App token

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #20 — Job `create-pull-request` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/update_react.yml:21

What we found: Job `create-pull-request` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/update_react.yml:21)

jobs:
  create-pull-request:
    runs-on: ubuntu-latest
    steps:
      - name: Create GitHub App token
        id: release-app-token

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #21 — Job `upload_test_results` installs npm deps without caching $250/mo

Confidence: 85% · Rule: gha_cache_missing_for_package_manager
CRITICAL

Where: .github/workflows/upload-tests-manifest.yml:17

What we found: Job `upload_test_results` runs `npm install` (or equivalent) on every workflow trigger but has neither `actions/cache@v*` nor a `setup-npm@v*` step with the `cache:` parameter. That means GitHub re-downloads and re-installs every dependency from scratch each run — typically 30-180 wasted seconds per job. At ~20 runs/day × 90s × $0.008/min for Linux, this single gap costs ~$72/mo per workflow. The fix is one step (or a `cache:` parameter on the existing setup-* action) — copy the After snippet into your workflow YAML.

Before (.github/workflows/upload-tests-manifest.yml:17)

jobs:
  upload_test_results:
    name: Upload test results
    runs-on: ubuntu-latest
    steps:
      - name: Setup node

After

# Option A — built-in caching via setup-* action (preferred):
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'    # or 'yarn' / 'pnpm'

# Option B — standalone actions/cache step:
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Opportunity #22 — Workflow runs on push/PR but doesn't cancel in-progress duplicate runs $200/mo

Confidence: 80% · Rule: gha_no_concurrency_cancel_in_progress
HIGH

Where: .github/workflows/pull_request_stats.yml:1

What we found: This workflow runs on `push` and/or `pull_request` but does not set `concurrency.cancel-in-progress: true`. Every push to a PR triggers a new run, and the older (now-obsolete) run keeps executing to completion. With ~5 PRs/day × 4 push events each, that's ~20 redundant runs/day that the next-newest push made irrelevant. Adding a 3-line `concurrency:` block at workflow scope tells GitHub to cancel earlier in-progress runs in the same group as soon as a newer one starts.

Before (.github/workflows/pull_request_stats.yml:1)

on:
  pull_request:
    types: [opened, synchronize]
  push:
    branches:

After

# Add at workflow root (above `jobs:`):
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #23 — Workflow runs on push/PR but doesn't cancel in-progress duplicate runs $200/mo

Confidence: 80% · Rule: gha_no_concurrency_cancel_in_progress
HIGH

Where: .github/workflows/turbopack-benchmark.yml:3

What we found: This workflow runs on `push` and/or `pull_request` but does not set `concurrency.cancel-in-progress: true`. Every push to a PR triggers a new run, and the older (now-obsolete) run keeps executing to completion. With ~5 PRs/day × 4 push events each, that's ~20 redundant runs/day that the next-newest push made irrelevant. Adding a 3-line `concurrency:` block at workflow scope tells GitHub to cancel earlier in-progress runs in the same group as soon as a newer one starts.

Before (.github/workflows/turbopack-benchmark.yml:3)

name: Turbopack Benchmark

on:
  workflow_dispatch:
  push:
    branches:
      - canary

After

# Add at workflow root (above `jobs:`):
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #24 — Workflow runs on push/PR but doesn't cancel in-progress duplicate runs $200/mo

Confidence: 80% · Rule: gha_no_concurrency_cancel_in_progress
HIGH

Where: .github/workflows/upload-tests-manifest.yml:6

What we found: This workflow runs on `push` and/or `pull_request` but does not set `concurrency.cancel-in-progress: true`. Every push to a PR triggers a new run, and the older (now-obsolete) run keeps executing to completion. With ~5 PRs/day × 4 push events each, that's ~20 redundant runs/day that the next-newest push made irrelevant. Adding a 3-line `concurrency:` block at workflow scope tells GitHub to cancel earlier in-progress runs in the same group as soon as a newer one starts.

Before (.github/workflows/upload-tests-manifest.yml:6)

name: Upload bundler test manifests to areweturboyet.com

on:
  schedule:
    - cron: '0 8 * * *'
  workflow_dispatch: {}
  push:

After

# Add at workflow root (above `jobs:`):
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #25 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/build_reusable.yml:148

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/build_reusable.yml:148)

BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA: 1

jobs:
  build:
    timeout-minutes: ${{ inputs.timeout_minutes }}
    runs-on: ${{ fromJson(inputs.runs_on_labels) }}

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #26 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/code_freeze.yml:23

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/code_freeze.yml:23)

NODE_LTS_VERSION: 20

jobs:
  start:
    runs-on: ubuntu-latest

    environment: release-${{ github.event.inputs.releaseType }}

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #27 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/create_release_branch.yml:21

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/create_release_branch.yml:21)

NODE_LTS_VERSION: 20

jobs:
  start:
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest
    env:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #28 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/graphite_ci_optimizer.yml:34

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/graphite_ci_optimizer.yml:34)

contains(github.event.pull_request.labels.*.name, 'CI Bypass Graphite Optimization')
    }}
jobs:
  optimize-ci:
    name: Graphite CI Optimizer
    runs-on: ubuntu-latest
    outputs:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #29 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/integration_tests_reusable.yml:40

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/integration_tests_reusable.yml:40)

default: 2

jobs:
  # First, build Next.js to execute across tests.
  build-next:
    name: build-next
    uses: ./.github/workflows/build_reusable.yml

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #30 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/issue_stale.yml:8

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/issue_stale.yml:8)

- cron: '40 23 * * *'

jobs:
  stale:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'vercel'
    steps:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #31 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/issue_wrong_template.yml:7

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/issue_wrong_template.yml:7)

types: [labeled]

jobs:
  close:
    if: github.event.label.name == 'please use the correct issue template'
    runs-on: ubuntu-latest
    steps:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #32 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/popular.yml:8

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/popular.yml:8)

workflow_dispatch:

jobs:
  run:
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest
    steps:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #33 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/pr_ci_comment.yml:13

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/pr_ci_comment.yml:13)

pull-requests: write

jobs:
  comment:
    name: Comment on PR
    if: ${{ github.event.workflow_run.event == 'pull_request' && github.repository == 'vercel/next.js' }}
    runs-on: ubuntu-latest

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #34 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/release-next-rspack.yml:30

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/release-next-rspack.yml:30)

DEBUG: napi:*

jobs:
  build:
    name: Build
    uses: rspack-contrib/rspack-toolchain/.github/workflows/build.yml@f69dc04fcae6b38d97b87acef448ed7a285b01cc
    with:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #35 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/retry_deploy_test.yml:15

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/retry_deploy_test.yml:15)

actions: write

jobs:
  retry-on-failure:
    name: retry failed jobs
    # Retry the test-e2e-deploy-release workflow once
    if: >-

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #36 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/retry_test.yml:18

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/retry_test.yml:18)

actions: write

jobs:
  retry-on-failure:
    name: retry failed jobs
    # Retry the build-and-test workflow up to 2 times
    if: >-

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #37 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/rspack-nextjs-build-integration-tests.yml:9

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/rspack-nextjs-build-integration-tests.yml:9)

workflow_dispatch: {}

jobs:
  test-dev:
    name: Rspack integration tests
    uses: ./.github/workflows/integration_tests_reusable.yml
    with:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #38 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/rspack-nextjs-dev-integration-tests.yml:9

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/rspack-nextjs-dev-integration-tests.yml:9)

workflow_dispatch: {}

jobs:
  test-dev:
    name: Rspack integration tests
    uses: ./.github/workflows/integration_tests_reusable.yml
    with:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #39 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/rspack-update-tests-manifest.yml:10

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/rspack-update-tests-manifest.yml:10)

workflow_dispatch:

jobs:
  update_dev_manifest:
    name: Update and upload Rspack development test manifest
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #40 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/test-turbopack-rust-bench-test.yml:22

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/test-turbopack-rust-bench-test.yml:22)

TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}

jobs:
  test:
    name: Test
    runs-on: ${{ fromJSON(inputs.runner) }}
    steps:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #41 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/test_e2e_deploy_release.yml:47

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/test_e2e_deploy_release.yml:47)

run-name: test-e2e-deploy ${{ inputs.nextVersion || (github.event_name == 'release' && github.event.release.tag_name) || 'canary' }}

jobs:
  setup:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'vercel'
    outputs:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #42 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/test_e2e_project_reset_cron.yml:24

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/test_e2e_project_reset_cron.yml:24)

run-name: test-e2e-project-reset (scheduled)

jobs:
  reset-test-project:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'vercel'
    steps:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #43 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/test_examples.yml:16

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/test_examples.yml:16)

name: Test examples

jobs:
  testExamples:
    # Don't execute using cron on forks
    if: (github.repository == 'vercel/next.js') || (inputs.is_dispatched == true)
    name: Test Examples

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #44 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/triage.yml:15

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/triage.yml:15)

issues: write

jobs:
  triage:
    name: Nissuer
    runs-on: ubuntu-latest
    if: >-

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #45 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/trigger_release.yml:38

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/trigger_release.yml:38)

NODE_LTS_VERSION: 20

jobs:
  start:
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest
    env:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #46 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/turbopack-nextjs-build-integration-tests.yml:8

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/turbopack-nextjs-build-integration-tests.yml:8)

workflow_dispatch: {}

jobs:
  test-dev:
    name: Next.js integration tests
    uses: ./.github/workflows/integration_tests_reusable.yml
    with:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #47 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/turbopack-nextjs-dev-integration-tests.yml:8

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/turbopack-nextjs-dev-integration-tests.yml:8)

workflow_dispatch: {}

jobs:
  test-dev:
    name: Next.js integration tests
    uses: ./.github/workflows/integration_tests_reusable.yml
    with:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #48 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/turbopack-update-tests-manifest.yml:10

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/turbopack-update-tests-manifest.yml:10)

workflow_dispatch:

jobs:
  update_dev_manifest:
    name: Update and upload Turbopack development test manifest
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #49 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/update_fonts_data.yml:13

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/update_fonts_data.yml:13)

NODE_LTS_VERSION: 20

jobs:
  create-pull-request:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'vercel'
    steps:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #50 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/update_react.yml:20

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/update_react.yml:20)

PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1

jobs:
  create-pull-request:
    runs-on: ubuntu-latest
    steps:
      - name: Create GitHub App token

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #51 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/upload-tests-manifest.yml:16

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/upload-tests-manifest.yml:16)

- 'test/*-manifest.json'

jobs:
  upload_test_results:
    name: Upload test results
    runs-on: ubuntu-latest
    steps:

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #52 — Workflow has no `concurrency:` block (no run-grouping at all) $80/mo

Confidence: 65% · Rule: gha_missing_concurrency_group
MEDIUM

Where: .github/workflows/upload_preview_tarballs.yml:19

What we found: This workflow has no `concurrency:` block. Beyond `cancel-in-progress` (covered by the gha_no_concurrency_cancel_in_progress rule), a `group:` declaration prevents two unrelated invocations of the same workflow from racing each other in cases like rapid-fire reruns or webhook duplicates. Adding a concurrency group is also a prerequisite for `cancel-in-progress: true` to have any effect. Best practice: add a workflow-scope group keyed on github.workflow + github.ref.

Before (.github/workflows/upload_preview_tarballs.yml:19)

contents: read

jobs:
  upload:
    name: Upload preview tarballs to Blob
    runs-on: ubuntu-latest
    if: github.event.workflow_run.conclusion == 'success'

After

# Add above `jobs:`:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Opportunity #53 — Cron schedule runs every 5 minute(s) without time-sensitive keyword $50/mo

Confidence: 70% · Rule: gha_schedule_too_frequent
LOW

Where: .github/workflows/update_react_poller.yml:7

What we found: This workflow runs on a cron of `*/5 * * * *` — every 5 minute(s). The steps don't mention monitoring, healthcheck, uptime, keepalive, or other time-sensitive language, so sub-hourly cadence is likely overkill. At every-5-minute frequency, the job fires 288 times/day = ~8,640/month. Even at 1-min wall time each, that's hundreds of Linux-minutes/month of recurring spend. Move to hourly (`0 * * * *`) unless you can name a specific freshness SLA below 1 hour.

Before (.github/workflows/update_react_poller.yml:7)

# GitHub runs cronjobs at most every 5 minutes.
    # Ideally we get 2 minutes or lower which is what we used to have with Vercel cronjobs.
    - cron: '*/5 * * * *'

concurrency:
  group: poll-react-sync
  cancel-in-progress: true

After

on:
  schedule:
    - cron: '0 * * * *'    # hourly — was: every 5 min

Opportunity #54 — Job `build` runs `pnpm install` 2 times in the same job $25/mo

Confidence: 55% · Rule: gha_redundant_dependency_install
LOW

Where: .github/workflows/build_and_deploy.yml:2

What we found: Job `build` runs `pnpm install` 2 times across its steps. Almost always the second + third invocations are redundant — the first install already populated node_modules / .venv / vendor/. Each redundant install is 30-60 seconds of compounded waste, billed to your account on every workflow run. Consolidate to a single install step near the top of the job, then reference the installed deps in downstream steps.

Before (.github/workflows/build_and_deploy.yml:2)

# Update all mentions of this name in vercel-packages when changing.
name: build-and-deploy

on:
  push:
    # Don't run when tags or graphite base branches are pushed

After

# Single install step at top of job:
- name: Install dependencies
  run: pnpm install

# Downstream steps use the already-installed deps:
- name: Lint
  run: npm run lint
- name: Test
  run: npm test

Opportunity #55 — Job `build-native` runs `npm install` 2 times in the same job $25/mo

Confidence: 55% · Rule: gha_redundant_dependency_install
LOW

Where: .github/workflows/build_and_deploy.yml:183

What we found: Job `build-native` runs `npm install` 2 times across its steps. Almost always the second + third invocations are redundant — the first install already populated node_modules / .venv / vendor/. Each redundant install is 30-60 seconds of compounded waste, billed to your account on every workflow run. Consolidate to a single install step near the top of the job, then reference the installed deps in downstream steps.

Before (.github/workflows/build_and_deploy.yml:183)

target:
              $dynamic: "`${this.arch}-${this.vendor}-${this.sys}${this.abi ? '-' + this.abi : ''}`"
            build_task: build-native-release
            os:
              mac:
                host: "['macos-15']"
                vendor: apple

After

# Single install step at top of job:
- name: Install dependencies
  run: npm install

# Downstream steps use the already-installed deps:
- name: Lint
  run: npm run lint
- name: Test
  run: npm test

Opportunity #56 — Job `build` runs `npm install` 2 times in the same job $25/mo

Confidence: 55% · Rule: gha_redundant_dependency_install
LOW

Where: .github/workflows/build_reusable.yml:12

What we found: Job `build` runs `npm install` 2 times across its steps. Almost always the second + third invocations are redundant — the first install already populated node_modules / .venv / vendor/. Each redundant install is 30-60 seconds of compounded waste, billed to your account on every workflow run. Consolidate to a single install step near the top of the job, then reference the installed deps in downstream steps.

Before (.github/workflows/build_reusable.yml:12)

skipInstallBuild:
        required: false
        description: 'whether to skip pnpm install && pnpm build'
        type: string
      skipNativeBuild:
        required: false
        description: 'whether to skip building native modules'

After

# Single install step at top of job:
- name: Install dependencies
  run: npm install

# Downstream steps use the already-installed deps:
- name: Lint
  run: npm run lint
- name: Test
  run: npm test

How GitHub Actions billing works

GitHub bills runner usage per-minute, scoped to the account or organization that owns the repo. For private repos beyond the included free minutes:

Public repos get unlimited free minutes on GitHub-hosted runners, but private repos and self-hosted-fallback setups still benefit from these fixes because every wasted minute is either billed or competing for runner capacity.

The fastest single-fix wins (in dollar order): cut macOS matrix axes that don't need them, cache package-manager installs, enable concurrency.cancel-in-progress on push/PR triggers, and add GHA cache to Docker builds. Verify each fix's impact in github.com/<org>/settings/billing on the next billing cycle — the line item is itemized by runner OS.

30-day re-audit voucher

Included with your $79 audit: a voucher for a free re-audit 30 days after delivery. Implement the recommended workflow changes, then re-submit the same repo URL via reply email — we re-run the analysis and confirm the cost-leak patterns are resolved. If we still flag any of the CRITICAL findings from this report, refund issued automatically.

Why this matters: GitHub Actions savings only materialize once the workflow YAML changes ship to main. The re-audit voucher creates an accountability loop — we can't claim "issue resolved" unless the v1 ruleset agrees on re-scan. Same deterministic engine, same file paths, same line numbers. No moving goalposts.