Clean Build Artifacts in CI Without Breaking Your Pipeline
Learn how to clean generated build artifacts safely in CI by separating disposable workspace output from caches and release artifacts, then verifying the pipeline from a clean checkout.
Table of Contents10 sections

Build inputs, generated output, caches, and release artifacts need clear boundaries.
A build passes locally, fails in CI, then passes after somebody deletes a directory. That is not a cleanup inconvenience. It is evidence that the build contract depends on hidden state. For the repository boundary around deployment, see configuring automated repository access.
The usual reaction is to add a broad delete command before every build. That can hide the symptom while creating a second problem: not everything produced by automation has the same lifecycle. A compiler output directory should usually be disposable. A dependency cache is an optimization. A release artifact is evidence or a deployment input and may need to survive the job.
The safer model is therefore not delete everything. It is classify generated state, give every class an owner and lifetime, then prove a clean build works.
First, Separate Workspace Output, Cache, and Artifact
The word artifact is overloaded in CI discussions. Three categories matter:
| Category | Examples | Expected lifetime | Default action |
|---|---|---|---|
| Disposable workspace output | build/, dist/, generated bundles, temporary reports |
Current build | Recreate freely |
| Cache | downloaded dependencies, reusable intermediate data | Multiple runs while cache key remains valid | Restore opportunistically; never require it for correctness |
| Durable workflow artifact | APK/AAB, deployable bundle, test report, crash evidence | After the producing job or run | Upload intentionally and retain by policy |
GitHub Actions makes the last distinction explicit: workflow artifacts persist Configuring Mcp Json Files Ai Agents after a job and can move build or test output between jobs. GitHub also treats dependency caching as a different use case. That distinction is useful even if your pipeline runs somewhere else.
If a cleanup script treats all three categories as disposable, it can destroy a release input. If a build treats all three as persistent, stale output can influence the next run.
The Real Goal Is a Reproducible Clean Build
A reliable pipeline should be able to start from tracked source plus declared external inputs and recreate its generated output.
That gives you a stronger test than “did the cleanup command succeed?”:
- Start from a fresh checkout or an intentionally cleaned workspace.
- Restore only caches whose keys match the current dependency/build contract.
- Run dependency installation and generation steps.
- Build and test.
- Package the exact outputs required downstream.
- Upload durable artifacts only after verification succeeds.
If step 3 or 4 needs a file left behind by yesterday’s run, the missing dependency belongs in the build definition, not in the workspace.
This same least-surprise principle matters when automation reads repositories remotely. RayLabs covers the access boundary separately in Configuring Automated Repository Access; cleanup should not compensate for an unclear repository or credential contract.
Put Generated Files Behind Explicit Directory Boundaries
Cleanup becomes safer when generated output has predictable roots.
Instead of allowing generators to scatter files beside source and configuration, direct them into known locations such as:
project/
├── src/ # tracked source
├── config/ # tracked shared configuration
├── build/ # disposable compiler output
├── dist/ # packaged output for this build
└── reports/ # test/lint output
The exact names do not matter. The boundary does.
A cleanup step that removes build/ is understandable. A cleanup step that recursively deletes “everything untracked” is much harder to reason about because untracked files may include local configuration, diagnostic evidence, or files another step expects.
The rule I use is simple: if automation is allowed to delete a path, automation should also be able to recreate that path from declared inputs.
Do Not Use Cleanup to Fix a Bad Cache Key
Stale state often gets blamed on generated files when the actual problem is cache invalidation.
Suppose dependencies change but the cache key does not. Restoring that cache can produce behavior that looks like an uncleared build directory. Deleting more files may temporarily fix the run, but the cache contract is still wrong.
Before adding a purge step, ask:
- Which input makes this generated state invalid?
- Is that input represented in the cache key?
- Can the build succeed with the cache completely disabled?
- Is the cached content an optimization or an undeclared dependency?
A healthy cache changes speed, not correctness.
A Safer GitHub Actions Pattern
A pipeline does not need a dramatic workspace wipe to enforce clean output. Make the disposable paths explicit and upload only the outputs that deserve to survive:
steps:
- uses: actions/checkout@v4
- name: Remove disposable build output
run: rm -rf build dist
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Test
run: npm test
- name: Upload verified build output
uses: actions/upload-artifact@v4
with:
name: web-dist
path: dist/
retention-days: 7
The important part is not the shell command. It is that build and dist have been declared disposable before the job starts, while the verified dist/ output becomes a named workflow artifact only after build and test complete.
GitHub Actions also supports artifact retention policies, including per-artifact retention-days. That is a storage lifecycle decision, not a workspace-cleanup decision.
Use a Cleanup Decision Matrix Before Deleting Anything
When a suspicious file or directory appears in CI, classify it before writing the delete command:
| Question | If yes | If no |
|---|---|---|
| Is it tracked source/config? | Never purge as build output | Continue |
| Can the pipeline recreate it deterministically? | Candidate for cleanup | Find the missing source/input first |
| Is it only a performance optimization? | Treat as cache | Continue |
| Is another job or deployment consuming it? | Publish/version it as an artifact | Continue |
| Does it contain credentials or machine-local config? | Fix generation/storage boundary | Continue |
| Is its presence changing build correctness? | Add a clean-build regression test | Cleanup alone is probably not the root fix |
This prevents a common anti-pattern: expanding the deletion scope every time a flaky pipeline produces another unexplained file.
Verify Both the Clean Path and the Dirty Path
A cleanup strategy is not proven until you test two conditions.
Clean path: start with no generated output and confirm the full build succeeds.
Dirty path: deliberately leave old output behind, run the pipeline, and confirm stale state cannot leak into the new artifact.
For a release pipeline, I would also verify:
- the packaged artifact contains only expected files;
- a failed build cannot publish a partial output;
- deleting local build output does not delete uploaded workflow artifacts;
- cache misses make the run slower, not incorrect;
- required license/notices are recreated or packaged intentionally rather than surviving by accident.
The dirty-path test is especially valuable because it checks whether your cleanup boundary is actually complete.
Failure Signals Should Tell You Which Lifecycle Broke
“Build failed” is not enough context when the pipeline owns several classes of generated state.
Logs should make it possible to distinguish:
- cleanup could not remove a disposable path;
- dependency restoration returned an invalid cache;
- generation did not produce an expected output;
- packaging included an unexpected stale file;
- artifact upload failed after a valid build;
- a downstream job downloaded the wrong artifact version.
Those are different failures with different fixes. Collapsing them into one generic cleanup phase makes incident diagnosis slower.
When Should You Purge More Aggressively?
Aggressive cleanup is justified when the workspace is intentionally reusable, such as a self-hosted runner, and previous jobs can leave state behind. Even there, prefer a documented workspace-reset contract over an ever-growing list of ad hoc deletes.
For ephemeral runners, repeated stale-output bugs are more likely to reveal an incorrect cache, a generator writing outside its expected directory, or a workflow step consuming the wrong artifact. The runner may already be fresh; deleting more of it does not solve the dependency error.
Practical Takeaway
The best CI cleanup strategy is one you rarely have to think about because lifecycle boundaries are explicit.
Keep source and configuration durable. Keep generated workspace output disposable. Treat caches as optional acceleration. Promote only verified output into durable workflow artifacts. Then test the pipeline from both a clean workspace and a deliberately dirty one.
That turns “delete stale files until CI works” into a reproducibility contract you can reason about, test, and maintain.
Continue Exploring
You Might Also Like

Speed Up Node.js CI: Cache Dependencies Without Shipping node_modules
Learn how to speed up Node.js CI and template deployments with reproducible installs, lockfile-aware dependency caching, and zero-build runtime patterns.

Software Project Submission Checklist: Make Your Work Easy to Verify
A practical software project submission checklist for making a repository reproducible, reviewable, and easy to evaluate from a clean checkout.

Configuring Automated Repository Access
Learn how to establish automated repository access for remote assistants and continuous integration pipelines while managing security boundaries.