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 repoAI 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.
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 testAvoid this
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 testHow to detect it
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/*.ymlfinds jobs with a build step, then read the job's remaining steps - a job whose only other steps arelint,typecheck,tsc --noEmit, or similar never touches the build output and does not need the step.If the repo uses Turborepo or Nx, confirm from the task graph rather than guessing:
jq '.tasks // .pipeline | .lint, .typecheck' turbo.json- iflintortypecheckdoes not listbuildas a dependsOn, a job that runs build before them is running unused work.List every workflow triggered by the same event on the same ref:
grep -rl 'on:' .github/workflows/*.ymlthen check each file'son.pushoron.pull_requestblock for overlapping branches.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.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/orbuild/still needs the artifact - useneeds:andactions/download-artifactfor 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.
# 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 publishVerify 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.
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
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.
Confirm the test job's log shows a download-artifact step retrieving
distrather than a build step re-running the compiler.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.
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.
Fix this one thing
Copy the prompt above
Hand OPT13 / OPT15 to your coding agent and fix it in your repo today.
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.
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)
Last updated 2026-08-21