Developer Tools

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
A hand writing a checklist in a notebook during workflow planning.
Text-free hero visual supporting Clean Build Artifacts in CI Without Breaking Your Pipeline.

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?”:

  1. Start from a fresh checkout or an intentionally cleaned workspace.
  2. Restore only caches whose keys match the current dependency/build contract.
  3. Run dependency installation and generation steps.
  4. Build and test.
  5. Package the exact outputs required downstream.
  6. 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:

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 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:

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

View all articles
Configuring Automated Repository Access
5 min read

Configuring Automated Repository Access

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