Build once and reuse it across GitHub Actions jobs

The same compile happening more than once on one commit is pure waste: a lint or typecheck job that copy-pasted a `run: npm run build` step it never uses, or two workflows on the same push each compiling the identical source from scratch.

ci.build.duplicate-compilationstatic · checkable from the repo
How StarSling works

AI agents open the PR

StarSling agents run this exact audit on your workflows, apply the fix, and open a reviewable PR automatically.

Do this

Compiling is usually the longest single step in a pipeline, so paying for it twice on one commit is one of the most expensive mistakes a workflow can make. It shows up two ways. Inside a workflow, a job copy-pasted from the build job keeps a run: npm run build step even though the job only lints or typechecks source, which never reads a build artifact, so the step burns minutes and produces nothing anyone consumes. Across workflows, a single push can trigger two or more independently-defined workflows, and if each one builds the project from the same commit, every one of them re-executes the identical compile, install, and link steps rather than the first workflow building once and the rest reusing that output.

.github/workflows/ci.yml
on:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 1

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run lint   # lints source; never needed dist/ in the first place

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx tsc --noEmit

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - run: npm test

Avoid this

.github/workflows/ci.yml
on:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build   # copy-pasted from the build job; lint never reads dist/
      - run: npm run lint

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build   # same here; tsc --noEmit checks source, not dist/
      - run: npx tsc --noEmit

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build   # rebuilt a fourth time for the same commit
      - run: npm test

How to detect it

  1. List every job's steps and check what each one actually consumes: grep -rln 'run:.*\(npm run build\|yarn build\|pnpm build\|pnpm run build\)' .github/workflows/*.yml finds jobs with a build step, then read the job's remaining steps - a job whose only other steps are lint, typecheck, tsc --noEmit, or similar never touches the build output and does not need the step.

  2. If the repo uses Turborepo or Nx, confirm from the task graph rather than guessing: jq '.tasks // .pipeline | .lint, .typecheck' turbo.json - if lint or typecheck does not list build as a dependsOn, a job that runs build before them is running unused work.

  3. List every workflow triggered by the same event on the same ref: grep -rl 'on:' .github/workflows/*.yml then check each file's on.push or on.pull_request block for overlapping branches.

  4. For each workflow found above, extract its build command(s), normalized (strip env vars, working-directory, and paths): grep -rn 'build\.sh\|cmake\|cargo build\|npm run build\|yarn build\|pnpm build\|make ' .github/workflows/*.yml.

  5. Compare the normalized commands across workflows that share a trigger and a ref. Two or more workflows running the same build command on the same push, with no needs: or artifact hand-off between them, means every one of them recompiles the same commit from zero.

Tradeoffs and safety

  • Only drop a build step after confirming the job truly never reads the output. A test suite that imports compiled output (not source) via dist/ or build/ still needs the artifact - use needs: and actions/download-artifact for that job instead of removing the step.

  • actions/upload-artifact and actions/download-artifact add their own upload and download time; for a very small build output this can approach the cost of just rebuilding, so this fix pays off most on projects where compiling is minutes, not seconds.

  • Artifacts uploaded with actions/upload-artifact count against the repository's storage; set a short retention-days on build-only-for-this-run artifacts instead of the default.

  • Consolidating two workflows into one changes the check names GitHub reports on the PR (job names become the source of truth instead of workflow names) - update branch protection required-status-check names in the same change so the merge gate does not silently stop firing.

  • If two workflows build for genuinely different targets from the same source (for example, a Linux build and a Windows build), that is not duplicate compilation - only flag builds that produce the same artifact from the same commit.

The same build across two workflows

The job-level case above is one workflow file with redundant jobs. The more expensive case is two SEPARATE workflow files that both trigger on the same push and both build the project from the same commit, with no shared job between them - GitHub Actions has no built-in awareness that two independently-triggered workflows are compiling the same source, so it happily runs both to completion.

.github/workflows/*.yml
# ci.yml
on:
  push:
    branches: [main]
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - run: npm test

# release.yml - triggers on the same push, builds the same commit again
on:
  push:
    branches: [main]
jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build   # identical build, paid for a second time
      - run: npm publish

Consolidate the workflows, or share the artifact

There are two valid fixes, and which one applies depends on whether the workflows are really one pipeline that got split by accident, or two genuinely separate concerns that happen to share a build. If they are one pipeline, merge them into a single workflow file with one build job and needs: for everything downstream - this page's earlier example. If they are legitimately separate (different owners, different trigger philosophy, different audiences for the check), keep them as separate workflows but have the build run in only one of them and have the other pull that build's artifact with actions/download-artifact, which supports fetching an artifact from a different workflow run via its run-id and github-token inputs rather than only from jobs in the same run.

Verify it worked

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to run this audit and open the fix as a reviewable PR.

Prompt for your coding agent
Audit this repository's GitHub Actions workflows for the same code being compiled more than once on one commit, and fix what you find.

1. For every job in .github/workflows/*.yml, list its steps. For any job that runs a build
   command (npm run build, yarn build, pnpm build, cargo build, cmake, make, etc.) alongside
   steps that only lint, typecheck, or run tests against source rather than compiled output,
   confirm the build step is genuinely unused by that job (check tool config such as
   turbo.json or nx.json for a real dependsOn: build relationship first) before removing it.
2. Across all workflow files, find workflows that trigger on the same event and ref (for
   example, more than one workflow on pull_request) and extract each one's build command,
   normalized (ignore env vars, working-directory, and paths). Flag any build command that
   appears more than once across workflows that fire on the same push.
3. For jobs whose build step is unused: remove it.
4. For jobs that genuinely need the compiled output but do not need to produce it themselves:
   add actions/upload-artifact to the job that builds and actions/download-artifact plus a
   needs: dependency to the jobs that consume it, so the build runs exactly once per commit.
5. For duplicate cross-workflow builds: propose consolidating the workflows so only one job
   builds, with the rest depending on it via needs: and artifacts - or merging the workflows
   outright if they serve the same trigger and audience.
6. Read the GitHub Actions artifacts and needs: docs linked on this page before editing.
7. Show the full diff and open a pull request; do not apply changes blindly. In the PR body,
   list each job or workflow where compilation was duplicated, and state how to verify: open
   the next PR's Actions run and confirm the build command executes exactly once.

Confirm the change landed

  1. Open the workflow run for a PR push and confirm each job's step list: lint and typecheck jobs should show no build step, only their own check.

  2. Confirm the test job's log shows a download-artifact step retrieving dist rather than a build step re-running the compiler.

  3. Compare total runner-minutes for the same PR's checks before and after: with the build step removed from three jobs, that job's build time should disappear from lint, typecheck, and test entirely, leaving it only in the one job that produces it.

  4. For cross-workflow consolidation, confirm in the Actions run list that only one job across the entire push actually executes the build command; every other job that needs the output should show a download-artifact step instead.

Go further

One fix, all of them, or forever.

You have the prompt for this one practice. Here is how much further you can take it, each step doing more for you than the last.

  1. Fix this one thing

    Copy the prompt above

    Hand OPT13 / OPT15 to your coding agent and fix it in your repo today.

  2. Fix everything, once

    Install the ci-speedup skill

    One prompt audits your whole repo against all 73 ci-speedup patterns (these 2 plus 71 more) and hands your agent every fix at once. Open source, MIT, runs locally.

  3. Keep it fixed, forever

    Install the StarSling GitHub App

    Connect GitHub and the fixes stay applied as your CI evolves, with agents that keep inspecting your workflows and opening optimization PRs you review.

Sources

1GitHub Actions: storing workflow data as artifacts (opens in new tab)

2GitHub Actions: using jobs in a workflow (needs) (opens in new tab)

3actions/upload-artifact (opens in new tab)

4actions/download-artifact (opens in new tab)

Last updated 2026-08-21