Stop reinstalling the same dependencies in GitHub Actions
Every job in a GitHub Actions workflow runs on its own fresh runner, so a lint job, a test job, and a build job that each check out and install the same dependency tree run that install three separate times. Installing once in an upstream job and handing the result to the others with actions/upload-artifact and actions/download-artifact turns three installs into one.
ci.cache.duplicate-installsstatic · 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
GitHub Actions gives every job in a workflow its own runner, and nothing on that runner survives past the job: no shared filesystem, no in-memory state, no leftover node_modules from a sibling job. Splitting a pipeline into lint, test, and build jobs is a reasonable way to get them running in parallel, but if each one independently checks out the repo, sets up the toolchain, and runs the same install command, the dependency tree gets resolved and written to disk as many times as there are jobs. That is pure duplicate work: the second and third installs cannot find anything the first one did not already produce. Passing the installed dependencies forward as a workflow artifact, or consolidating jobs that were split without a reason for the split, removes the repeats without removing the jobs.
name: CI
on:
pull_request:
jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- name: Archive installed dependencies
run: tar -cf node_modules.tar node_modules
- uses: actions/upload-artifact@v4
with:
name: node-modules
path: node_modules.tar
retention-days: 1
lint:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: node-modules
- run: tar -xf node_modules.tar
- run: npm run lint
test:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: node-modules
- run: tar -xf node_modules.tar
- run: npm test
build:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: node-modules
- run: tar -xf node_modules.tar
- run: npm run buildAvoid this
name: CI
on:
pull_request:
jobs:
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
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: npm test
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 buildHow to detect it
List every job in a workflow and its first several steps:
yq '.jobs | to_entries[] | {name: .key, steps: .value.steps[0:4]}' .github/workflows/ci.yml- jobs whose opening sequence is checkout, then a setup action, then an install command, are candidates.Compare those opening sequences across jobs in the same workflow file. Identical or near-identical sequences (same setup action, same install command, same lockfile) with no
needs:artifact handoff between them are the finding.Search for the handoff actions:
grep -rn 'actions/upload-artifact\|actions/download-artifact' .github/workflows/- a workflow with repeated installs and zero matches here has no mechanism for one job to pass its install to another.Count setup-action calls per job AND per action, across every workflow file:
yq '.jobs | to_entries[] | {"file": filename, "job": .key, "dupes": [.value.steps[] | .uses // "" | select(test("actions/setup-")) | sub("@.*"; "")] | group_by(.) | map(select(length > 1)) | map({"action": .[0], "count": length})} | select(.dupes | length > 0)' .github/workflows/*.yml- it names the file, the job, and the action for every genuine repeat. Grouping by job keeps the normal case of several jobs each calling setup once from reading as a duplicate; grouping by action keeps a polyglot job that legitimately calls setup-node and setup-python from reading as one; and the glob covers workflows named anything. A secondactions/setup-nodeinside one job simply overwrites the first, which is why the repeat is the signal.
Tradeoffs and safety
Artifact upload and download themselves take time and count against the same per-repository storage GitHub charges for. On a small dependency tree the transfer can cost more than the install it replaces; measure both before converting a fast job.
node_modules built on one runner OS or architecture is not portable to another. Only hand dependencies forward between jobs that share the same runs-on target; a workflow that installs on ubuntu-latest and needs the tree on windows-latest or macos-latest needs a separate install per platform, not a shared artifact.
needs: install makes lint, test, and build wait on the install job instead of starting immediately, which can lengthen the critical path even though it removes duplicate work. If the jobs were fast and running fully in parallel before, compare total wall-clock time, not just work done.
If the jobs were only ever split so that a failure in one would not block the others, and they always run on the same runner target anyway, consolidating them into a single job that installs once and runs lint, test, and build as sequential steps removes the duplication with no artifact handoff at all.
Workflow artifacts are scoped to a single workflow run and are the wrong tool for reuse ACROSS runs; that is what the dependency cache (actions/cache, or a setup action's built-in cache: input) is for. Use artifacts only to move data between jobs within one run.
Archive dependencies into one file before uploading
actions/upload-artifact works on any file or directory, but a dependency tree is thousands of small files, and uploading them individually is slower than uploading one archive. Tar the directory into a single file first, upload that one file, then extract it in each downstream job. This is the same shape as passing a build output forward, just applied to the installed dependency tree instead of a compiled artifact.
Artifacts move data within a run; cache moves it across runs
It is easy to reach for actions/upload-artifact and actions/download-artifact as a general caching mechanism, but they solve a different problem than actions/cache. An artifact is scoped to the workflow run that created it and exists to hand data from one job to another job in that same run. actions/cache is scoped to the repository and exists to reuse data across separate runs (today's run restoring what yesterday's run stored). Use artifacts for the cross-job handoff this page describes, and keep the lockfile-keyed dependency cache from GitHub Actions cache: dependencies, keys, and cache hits for reuse between runs; they are complementary, not interchangeable.
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.
Audit this repository's GitHub Actions workflows for dependency installs that are repeated across jobs, and remove the duplication.
1. For every workflow file, list each job's opening steps. Flag any workflow where two or
more jobs independently run checkout, a setup action (setup-node, setup-python, setup-go,
etc.), and an install command against the same lockfile, with no needs: relationship or
artifact handoff between them.
2. Within each job, also check for a setup action called more than once (for example two actions/setup-node steps); remove the redundant call rather than restructuring the job.
3. For flagged workflows, propose ONE of two fixes, whichever fits: (a) add an upstream
job that checks out, installs, archives the dependency directory, and uploads it with
actions/upload-artifact, then change the downstream jobs to needs: <upstream job> and
actions/download-artifact plus an extraction step instead of their own install; or (b) if
the jobs were split for no reason that still holds (they always run on the same runs-on
target and do not need independent pass/fail signals), consolidate them into one job that
installs once and runs the remaining steps in sequence.
4. Before proposing (a), confirm every job in the handoff uses the same runs-on target; dependencies installed on one OS or architecture are not portable to another.
5. Read the GitHub Actions artifacts documentation and the actions/upload-artifact and actions/download-artifact READMEs linked on this page before writing the workflow changes.
6. Show the full diff and open a pull request rather than applying changes directly. In
the PR body, name each workflow and job you changed, and state how to verify: re-run the
workflow and confirm the install command runs once instead of once per job.Confirm the change landed
Re-run the workflow and check the log of each downstream job: the install command should no longer appear, replaced by an actions/download-artifact step and a fast tar -xf.
Confirm the upstream install job's artifact upload succeeds and downstream jobs' download steps report a cache hit for that artifact name in the same run.
Compare the sum of time spent on install-equivalent steps across all jobs before and after: it should drop from one install per job to one install total, plus the (smaller) upload and download times.
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 OPT12 / OPT14 / OPT27 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 3 plus 70 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)
2actions/upload-artifact (opens in new tab)
3actions/download-artifact (opens in new tab)
4GitHub Actions: using jobs in a workflow (opens in new tab)
Last updated 2026-08-21