Static-analysis CI/CD cost audit · https://github.com/vercel/next.js · Generated 2026-05-16 22:49 UTC
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).
| # | Opportunity | Severity | $/mo saved |
|---|---|---|---|
| 1 | Job `deploy-target` installs npm deps without caching | CRITICAL | $250 |
| 2 | Job `build-native` installs npm deps without caching | CRITICAL | $250 |
| 3 | Job `validate-docs-links` installs npm deps without caching | CRITICAL | $250 |
| 4 | Job `start` installs npm deps without caching | CRITICAL | $250 |
| 5 | Job `close` installs npm deps without caching | CRITICAL | $250 |
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.
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 }}
# 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') }}
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.
target:
$dynamic: "`${this.arch}-${this.vendor}-${this.sys}${this.abi ? '-' + this.abi : ''}`"
build_task: build-native-release
os:
mac:
host: "['macos-15']"
vendor: apple
# 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') }}
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.
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
# 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') }}
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.
jobs:
start:
runs-on: ubuntu-latest
environment: release-${{ github.event.inputs.releaseType }}
steps:
# 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') }}
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.
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
# 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') }}
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.
jobs:
run:
if: github.repository_owner == 'vercel'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# 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') }}
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.
retention-days: 1
stats-aggregate:
name: Aggregate Stats
needs: stats
if: always() && needs.stats.result != 'cancelled'
runs-on: ubuntu-latest
# 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') }}
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.
jobs:
update_dev_manifest:
name: Update and upload Rspack development test manifest
if: github.repository_owner == 'vercel'
runs-on: ubuntu-latest
steps:
# 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') }}
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.
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:
# 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') }}
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.
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
# 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') }}
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.
name: Turbopack Rust testing benchmarks
on:
workflow_call:
inputs:
runner:
# 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') }}
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.
jobs:
setup:
runs-on: ubuntu-latest
if: github.repository_owner == 'vercel'
outputs:
next-version: ${{ steps.version.outputs.value }}
# 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') }}
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.
jobs:
reset-test-project:
runs-on: ubuntu-latest
if: github.repository_owner == 'vercel'
steps:
- name: Setup Node.js
# 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') }}
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.
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
# 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') }}
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.
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
# 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') }}
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.
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
# 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') }}
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.
jobs:
update_dev_manifest:
name: Update and upload Turbopack development test manifest
if: github.repository_owner == 'vercel'
runs-on: ubuntu-latest
steps:
# 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') }}
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.
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:
# 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') }}
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.
jobs:
create-pull-request:
runs-on: ubuntu-latest
if: github.repository_owner == 'vercel'
steps:
- name: Create GitHub App token
# 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') }}
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.
jobs:
create-pull-request:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: release-app-token
# 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') }}
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.
jobs:
upload_test_results:
name: Upload test results
runs-on: ubuntu-latest
steps:
- name: Setup node
# 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') }}
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.
on:
pull_request:
types: [opened, synchronize]
push:
branches:
# Add at workflow root (above `jobs:`):
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
name: Turbopack Benchmark
on:
workflow_dispatch:
push:
branches:
- canary
# Add at workflow root (above `jobs:`):
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
name: Upload bundler test manifests to areweturboyet.com
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch: {}
push:
# Add at workflow root (above `jobs:`):
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA: 1
jobs:
build:
timeout-minutes: ${{ inputs.timeout_minutes }}
runs-on: ${{ fromJson(inputs.runs_on_labels) }}
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
NODE_LTS_VERSION: 20
jobs:
start:
runs-on: ubuntu-latest
environment: release-${{ github.event.inputs.releaseType }}
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
NODE_LTS_VERSION: 20
jobs:
start:
if: github.repository_owner == 'vercel'
runs-on: ubuntu-latest
env:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
contains(github.event.pull_request.labels.*.name, 'CI Bypass Graphite Optimization')
}}
jobs:
optimize-ci:
name: Graphite CI Optimizer
runs-on: ubuntu-latest
outputs:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
default: 2
jobs:
# First, build Next.js to execute across tests.
build-next:
name: build-next
uses: ./.github/workflows/build_reusable.yml
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
- cron: '40 23 * * *'
jobs:
stale:
runs-on: ubuntu-latest
if: github.repository_owner == 'vercel'
steps:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
types: [labeled]
jobs:
close:
if: github.event.label.name == 'please use the correct issue template'
runs-on: ubuntu-latest
steps:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
workflow_dispatch:
jobs:
run:
if: github.repository_owner == 'vercel'
runs-on: ubuntu-latest
steps:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
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
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
DEBUG: napi:*
jobs:
build:
name: Build
uses: rspack-contrib/rspack-toolchain/.github/workflows/build.yml@f69dc04fcae6b38d97b87acef448ed7a285b01cc
with:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
actions: write
jobs:
retry-on-failure:
name: retry failed jobs
# Retry the test-e2e-deploy-release workflow once
if: >-
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
actions: write
jobs:
retry-on-failure:
name: retry failed jobs
# Retry the build-and-test workflow up to 2 times
if: >-
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
workflow_dispatch: {}
jobs:
test-dev:
name: Rspack integration tests
uses: ./.github/workflows/integration_tests_reusable.yml
with:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
workflow_dispatch: {}
jobs:
test-dev:
name: Rspack integration tests
uses: ./.github/workflows/integration_tests_reusable.yml
with:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
workflow_dispatch:
jobs:
update_dev_manifest:
name: Update and upload Rspack development test manifest
if: github.repository_owner == 'vercel'
runs-on: ubuntu-latest
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
jobs:
test:
name: Test
runs-on: ${{ fromJSON(inputs.runner) }}
steps:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
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:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
run-name: test-e2e-project-reset (scheduled)
jobs:
reset-test-project:
runs-on: ubuntu-latest
if: github.repository_owner == 'vercel'
steps:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
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
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
issues: write
jobs:
triage:
name: Nissuer
runs-on: ubuntu-latest
if: >-
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
NODE_LTS_VERSION: 20
jobs:
start:
if: github.repository_owner == 'vercel'
runs-on: ubuntu-latest
env:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
workflow_dispatch: {}
jobs:
test-dev:
name: Next.js integration tests
uses: ./.github/workflows/integration_tests_reusable.yml
with:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
workflow_dispatch: {}
jobs:
test-dev:
name: Next.js integration tests
uses: ./.github/workflows/integration_tests_reusable.yml
with:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
workflow_dispatch:
jobs:
update_dev_manifest:
name: Update and upload Turbopack development test manifest
if: github.repository_owner == 'vercel'
runs-on: ubuntu-latest
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
NODE_LTS_VERSION: 20
jobs:
create-pull-request:
runs-on: ubuntu-latest
if: github.repository_owner == 'vercel'
steps:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
jobs:
create-pull-request:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
- 'test/*-manifest.json'
jobs:
upload_test_results:
name: Upload test results
runs-on: ubuntu-latest
steps:
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
contents: read
jobs:
upload:
name: Upload preview tarballs to Blob
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
# Add above `jobs:`:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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.
# 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
on:
schedule:
- cron: '0 * * * *' # hourly — was: every 5 min
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.
# 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
# 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
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.
target:
$dynamic: "`${this.arch}-${this.vendor}-${this.sys}${this.abi ? '-' + this.abi : ''}`"
build_task: build-native-release
os:
mac:
host: "['macos-15']"
vendor: apple
# 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
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.
skipInstallBuild:
required: false
description: 'whether to skip pnpm install && pnpm build'
type: string
skipNativeBuild:
required: false
description: 'whether to skip building native modules'
# 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
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.
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.