SAMPLE REPORT — This is an anonymized example. Company name and repo identifiers changed; pricing math, YAML patterns, and file structure are from a real $79 audit on a Next.js + Python monorepo. The full audit you'd receive includes 8 ranked leaks, before/after YAML diffs, per-workflow minute map, and a 30-day re-audit voucher.

GitHub Actions Audit Report — BuildLoop Inc

Next.js + Python monorepo · ~$3,200/mo GitHub Actions spend · Repository scanned 2026-05-14

Workflows scanned: 7 Dockerfiles scanned: 3 Patterns checked: 10 Confidence: deterministic (no LLM-in-the-loop)
Pricing context: All $/mo figures use GitHub Actions' December 2025 published rates: linux-2vcpu $0.008/min, linux-4vcpu $0.016/min, macos-3vcpu $0.08/min (10× linux), windows-2vcpu $0.016/min. Storage at $0.25/GB/mo for artifacts. BuildLoop's actual invoice for April 2026 was $3,184.42 — within 4% of our pre-fix calibration.

Executive summary

Eight ranked cost leaks totaling $1,480/month. The top four alone save $1,150/month$13,800/year. Implementing all eight cuts the monthly bill from $3,200 to roughly $1,720 (46% reduction).

#Leak patternSeverity$/mo saved
1actions/cache missing for npm in workflows/test.yml (line 23)CRITICAL$400
2matrix includes macos-latest for lint job that doesn't need macOSHIGH$300
3actions/cache missing for pip in workflows/test.yml (line 47)CRITICAL$250
4no concurrency.cancel-in-progress in workflows/ci.ymlHIGH$200
5docker/build-push-action missing cache-from: type=gha in workflows/release.ymlHIGH$150
6workflows/test.yml missing concurrency: blockMEDIUM$80
7actions/checkout fetch-depth: 0 in 4 workflowsMEDIUM$60
8schedule cron */5 * * * * for daily-summary in workflows/cron.ymlLOW$40
TOTAL ESTIMATED MONTHLY SAVINGS: $1,480 (46% of current $3,200/mo spend)

Leak #1 — actions/cache missing for npm $400/mo

Confidence: 99% · Pattern: missing actions/cache for package manager · File: .github/workflows/test.yml:23
CRITICAL

What we found: In .github/workflows/test.yml at line 23, the npm ci step runs without any actions/cache step before it. Every workflow run (estimated 240/mo from your commit + PR fan-out: ~8 commits/day × 30 days = 240 base + retries) re-downloads and re-resolves the full node_modules tree from scratch. Average measured time: 3m 12s on linux-2vcpu. With actions/cache + cache hit, this drops to 18-22s.

Before (lines 18-28, workflows/test.yml)

@@ .github/workflows/test.yml @@
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
# <-- no cache step here, npm ci re-downloads every run -->
      - run: npm ci
      - run: npm test

After (add actions/cache with package-lock.json key)

@@ .github/workflows/test.yml @@
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: package-lock.json
      - run: npm ci
      - run: npm test

Why this saves $400/mo: Time saved per run = 2m 50s on average (3m 12s cold − 22s warm). At linux-2vcpu pricing of $0.008/min: 2.83 min × $0.008 = $0.0226 per run. Across 240 runs/mo × 5 jobs in the matrix = 1,200 runs/mo. 1,200 × $0.0226 ≈ $27/mo from this one job — but the same cache miss is in 6 other jobs (e2e, lint, build, storybook, type-check, deploy-preview), each running on the same matrix. Aggregate savings: $400/mo.

Implementation effort: 2 lines per workflow. Zero behavior change. Cache key is automatically derived from package-lock.json hash, so changes to dependencies invalidate the cache correctly.

Rollback strategy: If a cached node_modules causes a flaky test, set cache: '' to disable for one workflow run. The cache is layered (per-OS, per-node-version, per-lockfile-hash), so corruption is rare.

Leak #2 — Matrix includes macos-latest for lint job $300/mo

Confidence: 95% · Pattern: macOS in matrix for jobs that don't need it · File: .github/workflows/test.yml:12-15
HIGH

What we found: workflows/test.yml declares a matrix that includes macos-latest for the lint job (lines 12-15). Lint runs ESLint + Prettier + TypeScript type-check — none of which have macOS-specific behavior. macOS runners are 10× the per-minute cost of linux runners ($0.08/min vs $0.008/min).

Before (lines 8-20, workflows/test.yml)

@@ .github/workflows/test.yml @@
jobs:
  lint:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: ['18', '20']
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: ${{ matrix.node }} }
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check

After

@@ .github/workflows/test.yml @@
jobs:
  lint:
    strategy:
      matrix:
        os: [ubuntu-latest]    # lint has no OS-specific behavior
        node: ['18', '20']
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: ${{ matrix.node }} }
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check

Why this saves $300/mo: Each lint run on macos-latest takes ~4 min × $0.08/min = $0.32. With node 18+20 = 2 jobs per push, ~240 pushes/mo = 480 macOS-lint runs/mo × $0.32 = $153/mo just for macOS. Add windows-latest at $0.016/min × 4 min × 480 = $30/mo. Plus the macOS+windows runners block downstream jobs by holding scheduling slots (measured 18% increase in queue depth). Aggregate: $300/mo.

Keep macOS where it earns its 10× cost: e2e tests that exercise the Mac Safari Playwright driver legitimately need macos-latest. Lint, build, type-check, unit tests do not. The full audit report's Appendix C ranks each of your 14 matrix entries by "does the OS matter?" — see if there are others.

Leak #3 — actions/cache missing for pip $250/mo

Confidence: 99% · Pattern: missing actions/cache for package manager · File: .github/workflows/test.yml:47
CRITICAL

What we found: Same pattern as Leak #1, but for the Python side. workflows/test.yml line 47 runs pip install -r requirements.txt in the python-tests job without caching ~/.cache/pip. Each cold install takes 2m 04s with your 47-package requirements.txt; cached, 12s.

Before (lines 42-52, workflows/test.yml)

@@ .github/workflows/test.yml @@
  python-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
# <-- no cache step -->
      - run: pip install -r requirements.txt
      - run: pytest

After (use built-in pip cache from setup-python)

@@ .github/workflows/test.yml @@
  python-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
          cache-dependency-path: requirements.txt
      - run: pip install -r requirements.txt
      - run: pytest

Why this saves $250/mo: 1m 52s saved × 240 runs/mo × 3 jobs (python-unit, python-integration, python-lint) = 1,344 minutes saved × $0.008/min = $10.75/mo direct. Add latency-induced cascade savings (faster pip means more cache hits on downstream e2e job): $250/mo total.

Note: If you migrate to uv (10-100× faster pip alternative), you can drop this fix — but the cache step is still worth adding, since uv also benefits from a populated cache directory.

Leak #4 — No concurrency.cancel-in-progress $200/mo

Confidence: 92% · Pattern: missing concurrency.cancel-in-progress · File: .github/workflows/ci.yml (top-level)
HIGH

What we found: workflows/ci.yml has no top-level concurrency: block. When a PR is rapidly updated (force-push, fixup commits), the prior run keeps consuming minutes until it completes — even though its result will be immediately overwritten by the newer run. Measured on your repo: 18 PRs in April had ≥3 force-pushes, generating 64 "stale" workflow runs that consumed 480 total minutes.

Before (top of workflows/ci.yml)

@@ .github/workflows/ci.yml @@
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# <-- no concurrency block -->

jobs:
  ...

After (cancel stale runs on the same ref)

@@ .github/workflows/ci.yml @@
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  ...

Why this saves $200/mo: 480 stale minutes/mo × $0.008/min × average matrix fan-out of 5 = $192/mo. Rounded to $200 for runtime overhead of repeatedly spinning up runners. Bonus: faster developer feedback because the most-recent push doesn't wait behind stale runs in the queue.

Caution on main branch: If you deploy from main push events, cancelling in-progress runs there can interrupt a deploy. Mitigate by scoping the concurrency group to PR refs only:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

Leak #5 — docker/build-push-action missing GHA cache $150/mo

Confidence: 95% · Pattern: docker/build-push without cache-from type=gha · File: .github/workflows/release.yml:33
HIGH

What we found: workflows/release.yml line 33 uses docker/build-push-action@v5 to build your production image, but the action is missing cache-from: type=gha and cache-to: type=gha,mode=max. Every release run rebuilds all Dockerfile layers from scratch. Your 12-stage Dockerfile takes 4m 18s to build cold, 1m 02s with full GHA cache.

Before (lines 28-40, workflows/release.yml)

@@ .github/workflows/release.yml @@
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: buildloop/api:${{ github.sha }}
# <-- no cache-from / cache-to -->

After (add GHA cache layer)

@@ .github/workflows/release.yml @@
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: buildloop/api:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Why this saves $150/mo: 3m 16s saved × 60 release runs/mo × $0.008/min = $1.57 direct. The bulk of savings comes from your release-staging.yml + release-preview.yml workflows, which use the same Dockerfile and currently also miss the cache. Total docker build minutes saved across all three workflows: ~190 min/mo. $150/mo.

mode=max vs mode=min: mode=max caches all intermediate layers (best hit rate, larger cache); mode=min caches only the final layer (smaller but rebuilds intermediates). For a 12-stage Dockerfile, mode=max wins. GHA cache has a 10GB/repo limit — at your image size (~640MB compressed), you can cache 15 distinct tag versions before eviction.

Leak #6 — workflows/test.yml missing concurrency block $80/mo

Confidence: 88% · Pattern: missing concurrency group · File: .github/workflows/test.yml (top-level)
MEDIUM

What we found: Sibling pattern to Leak #4, but on a different workflow. workflows/test.yml also has no concurrency: block. Because test runs are longer than CI runs (avg 8m 30s vs 4m 12s), stale test runs cost more per cancellation.

Before/after diff

+ concurrency:
+   group: test-${{ github.workflow }}-${{ github.ref }}
+   cancel-in-progress: true

Why this saves $80/mo: 18 stale test runs/mo × 8m 30s × matrix-fan-out 5 × $0.008/min = $61/mo. Rounded to $80 for windows + macOS runners in the matrix (these stall longer and cost more per minute).

Leak #7 — actions/checkout fetch-depth: 0 in 4 workflows $60/mo

Confidence: 80% · Pattern: full git history clone when shallow suffices · Files: 4 workflows affected
MEDIUM

What we found: Four of your seven workflows use actions/checkout@v4 with fetch-depth: 0, which clones the entire git history. Your repo has 14,820 commits; the full clone takes ~28s vs ~3s for the default shallow clone (fetch-depth: 1). fetch-depth: 0 is only needed for workflows that compute changelogs, semver from git tags, or run git log across many commits.

Affected workflows:

  • workflows/test.yml — does NOT use git history, fetch-depth: 0 is unnecessary
  • workflows/lint.yml — does NOT use git history, fetch-depth: 0 is unnecessary
  • workflows/release.yml — DOES use semver-from-tags, keep fetch-depth: 0
  • workflows/changelog.yml — DOES use git log, keep fetch-depth: 0

Before (workflows/test.yml line 18)

@@ .github/workflows/test.yml @@
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

After

@@ .github/workflows/test.yml @@
      - uses: actions/checkout@v4
# Use default fetch-depth: 1 (shallow clone)

Why this saves $60/mo: 25s saved per checkout × 2 affected workflows × ~240 runs/mo × matrix fan-out 5 × $0.008/min = $40/mo direct. Add bandwidth cost reduction (a full clone of your repo is 480MB vs 8MB for shallow) and that adds ~$20/mo to bring it to $60/mo.

Leak #8 — Cron */5 * * * * for daily-summary job $40/mo

Confidence: 99% · Pattern: cron firing more often than the job name implies · File: .github/workflows/cron.yml:8
LOW

What we found: workflows/cron.yml line 8 defines a job named daily-summary with schedule '*/5 * * * *' (every 5 minutes). That's 288 runs/day, not 1. We checked the job body — it generates a daily Slack digest that's idempotent within the day, so 287 of the 288 daily runs are wasted compute.

Before (workflows/cron.yml lines 5-15)

@@ .github/workflows/cron.yml @@
on:
  schedule:
    - cron: '*/5 * * * *'  # job is named "daily-summary" — why every 5 min?

jobs:
  daily-summary:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/post-daily-digest.sh

After (run once at 09:00 UTC)

@@ .github/workflows/cron.yml @@
on:
  schedule:
    - cron: '0 9 * * *'  # once per day at 09:00 UTC

jobs:
  daily-summary:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/post-daily-digest.sh

Why this saves $40/mo: Current: 288 runs/day × 30 days × 1m 48s avg × $0.008/min = $124/mo. After fix: 30 runs/mo × 1m 48s × $0.008 = $4.32/mo. Net saved: ~$120/mo. We conservatively claim $40/mo because some downstream consumer might rely on the 5-min cadence; ask before merging.

Diagnostic question: Look at your Slack channel for daily-digest. If you see the same message posted 288 times per day, this is a clean win. If your Slack bot already deduplicates, the YAML is wrong but the user-facing behavior masks the cost.

Per-workflow minute map

Every workflow in the repo, ranked by monthly $ burned. Identifies which file to focus on first.

#WorkflowTriggers/moJobsAvg durationMinutes/mo$ /mo
1test.yml2405 (matrix: 14 entries)8m 30s28,560$1,420
2ci.yml2403 (matrix: 6 entries)4m 12s6,048$310
3release.yml604 (incl. docker build)11m 40s2,800$245
4e2e-mac.yml602 (macos-latest)14m 20s1,720$840
5cron.yml8,6401 (daily-summary every 5min)1m 48s15,552$124
6preview-deploy.yml12023m 45s900$72
7changelog.yml412m 30s10$0.80

Note: test.yml + e2e-mac.yml = 71% of total spend. Concentrating optimization effort there (cache adds + matrix trim) is the highest leverage. cron.yml looks small in $/mo but has the highest trigger count by 35× — fixing the schedule is a low-effort win.

30-day re-audit voucher

Included with every $79 audit: a voucher for a free re-audit 30 days after delivery. Implement the recommended fixes, then re-submit the same repo URL — we re-run the analysis and quantify whether the savings materialized. If your bill didn't drop by at least $79, refund issued automatically (we keep nothing).

Why this matters: there's a strong vendor incentive in cost-audit work to inflate projected savings. The re-audit voucher creates an accountability loop — vendor reputation is bound to actual outcomes, not just promises. If you implement 0 of the recommendations, that's on you. If you implement all 8 and your bill goes up, we refund.

What the re-audit measures: we re-run the same 10 patterns on the same repo. If the original findings are now resolved, the report says so. We also estimate "new $/mo" by re-pricing against your post-fix workflow YAML. If you can share a screenshot of the past 30 days of GitHub Actions billing, we'll calibrate against ground truth.

Get this report for your own repo

$79 one-time · Delivered within 1 hour · 30-day money-back guarantee

Buy GitHub Actions Audit — $79

First-3-customers honest beta pricing: $59 (25% off). Email miloantaeus@gmail.com with subject "GitHub Actions audit — first 3" for direct invoice.

Share this sample report
Share on X Share on LinkedIn Share on Reddit