Dirty workspace between jobs
A self-hosted runner reuses its work folder, so files from one job can break the next. Add cleanup steps, or use single-use runners that start each job fresh.
The short answer
GitHub self-hosted runner cleanup is your job, not GitHub's. A runner you set up by hand runs job after job in the same folder. What one job leaves, the next one finds. You can clean up with steps and hooks. Or you can give each job a new runner.
Why files survive between jobs
GitHub's runner keeps a _work folder, the GitHub Actions runner workspace. Each repo gets a folder inside it, and that folder stays. actions/checkout cleans the repo folder by default. It does not touch files outside it. Tools a job installed stay. Docker images stay. Files in your home folder stay. So do programs a job left running.
Symptoms: flaky builds and odd failures
- A test passes on GitHub's runners but fails on yours.
- A build uses old output from a past job.
- "Permission denied" on files a Docker step made as root.
- A port is busy because an old server still runs.
- The disk fills up. See no space left on device.
These bugs are hard to find. They depend on which job ran before, so they come and go. A rerun often passes, which hides the cause.
Cleanup steps you can add
Clear the workspace first, and clear temp files last, even when the job fails:
steps:
- name: Clean workspace
run: |
shopt -s dotglob
rm -rf "${GITHUB_WORKSPACE:?}"/*
- uses: actions/checkout@v4
- run: make test
- name: Clean up
if: always()
run: rm -rf "${RUNNER_TEMP:?}"/*
For GitHub Actions self-hosted runner cleanup across every workflow, use a runner hook. Add this line to the .env file in the runner's folder, then restart it:
ACTIONS_RUNNER_HOOK_JOB_STARTED=/opt/runner-hooks/clean.sh
The script runs before each job. Use it to delete leftover files and stop old processes. Keep it short, since every job waits for it.
Single-use runners
A single-use runner takes one job, then goes away. GitHub's runner supports this with ./config.sh --ephemeral. But then you must register a new runner for every job.
Codemany does that for you. Each job gets a fresh single-use runner, registered just in time. When the job ends, the runner exits, Codemany removes it, and its folder is wiped. Downloads from actions/setup-* steps stay in a shared tool cache, so jobs don't start cold. You change one line: runs-on: [self-hosted, codemany]. Details: ephemeral runners.
A fresh runner keeps jobs apart from each other, not from your machine. Jobs run as your user, so files changed outside the folder stay. Read runner security.
A fresh VM per job on Linux
On Linux with KVM, you can turn on a fresh VM per job. Host mode stays the default. Build the image once, then switch:
codemany isolation vm-image
codemany isolation vm
Each job then gets Docker and passwordless sudo in a VM laid out like GitHub's image. The VM is thrown away after the job, so nothing carries over, not even cloud keys. It is slower, since each job boots a VM first. See VM per job.
Questions
Does actions/checkout clean the workspace?
It cleans the repo folder by default, but not files outside it.
Are Codemany runners reused?
No. Each job gets a new single-use runner, and its job files are removed.
What about tools a job installs?
In VM mode on Linux, the whole VM is thrown away after the job. In the default host mode, tools a job installs stay on the machine.
Related
Read next: ephemeral runners, single-use runners, VM per job and runner security.
Start a 14-day trial $20 a month for 2 machines. No card needed.