Make vitest run mode explicit in GitHub Actions
GitHub Actions sets `CI=true` in every job, and vitest reads that variable to choose run mode over watch mode, so a bare `vitest` already executes the suite once and exits. Writing `vitest run` ties that behaviour to the command instead of to the environment, and a `timeout-minutes` bound caps any test process that stops making progress.
ci.hygiene.vitest-run-modestatic · 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
vitest picks its mode at startup: it enters watch mode in a development environment and run mode in CI or a non-interactive terminal, and a truthy CI environment variable is one of the signals it reads. GitHub Actions sets CI to true in every job it runs, so on a GitHub-hosted runner the bare command already does the right thing. What the explicit run buys is that the guarantee travels with the command rather than with the environment around it. The same workflow step gets lifted into a container step that scrubs the environment, copied onto a self-hosted runner image, wrapped in a script that runs it under a shell with a cleaned environment, or reused on another CI system that never sets CI - and in each of those the implicit signal is the part that changes while the command stays identical. The second half of this page is the bound that matters regardless of mode: a job with no timeout-minutes inherits the 360-minute per-job maximum, so any test process that hangs on an open handle, a socket, or a container that never becomes ready holds a runner for six hours before anything reclaims it.
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm vitest runAvoid this
jobs:
test:
runs-on: ubuntu-latest
# No timeout-minutes: a suite that hangs holds the runner for 360 minutes.
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
# Mode is decided by the CI env var rather than by the command.
- run: pnpm vitestHow to detect it
Search workflows for vitest invocations that leave the mode implicit:
grep -rn 'vitest' .github/workflows/ | grep -vE 'vitest([[:space:]]+[^[:space:]]+)*[[:space:]]+(run|--run)([[:space:]]|$)'-vitest runandvitest --runboth pin run mode, and the flag can sit anywhere after the command (vitest --coverage --run), so the filter scans the whole invocation rather than the next token.Confirm the environment the command actually inherits. GitHub-hosted runners always export
CI=true, so a surviving match there is about explicitness; a step that runs inside acontainer:with a curatedenv:block, a self-hosted runner, or a wrapper script that resets the environment can reach vitest without it, and that is where the implicit form changes behaviour.Confirm the job has a
timeout-minutes:grep -n 'timeout-minutes' .github/workflows/*.yml. A vitest job with no timeout has the 360-minute per-job maximum as its only bound, which applies to a hung suite in run mode just as much as to a watcher.
Tradeoffs and safety
On a GitHub-hosted runner this change is about where the guarantee lives, not about wall-clock time:
CI=trueis already set, so the suite was already running once and exiting. Expect the run duration to stay the same and treat the win as portability of the workflow step.If a package script already wraps vitest (
"test": "vitest run"), pin the mode in that script and have the workflow call the script, so a developer running it locally gets the same behaviour as CI.Size
timeout-minutesa few minutes above the slowest observed passing run of that job. A bound set near the average turns a legitimately slow run into a red build, which costs more than the runaway it was meant to catch.
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 vitest invocations in CI so each one pins its own run mode,
and make sure every job that runs them is time-bounded.
1. grep .github/workflows/ for vitest commands. Flag any invocation that does not carry
`run` or `--run`. On GitHub-hosted runners these already execute once because the
runner sets `CI=true`; the change makes that independent of the environment.
2. Change each flagged invocation to `vitest run`. If a package script wraps vitest,
pin the mode in that script and leave the workflow calling the script.
3. Check each affected job for `timeout-minutes`. If it is missing, add a value a few
minutes above the slowest observed passing run of that job, not an arbitrary guess.
4. Read the Vitest CLI docs linked on this page before editing, to confirm the project's
vitest version and config do not already pin the mode some other way (a config file
`watch: false`, a custom reporter, or a wrapper script).
5. Show the full diff and open a pull request; do not apply changes blindly. In the PR
body, list each workflow file and job you changed, and state how to verify: re-run the
workflow, confirm the suite still exits on its own, and confirm the job now reports the
`timeout-minutes` you set.Confirm the change landed
Re-run the workflow and confirm the vitest step exits on its own and the job finishes shortly after the suite's reported duration.
Confirm the mode is now pinned to the command by running the same invocation with the variable cleared -
env -u CI pnpm vitest runlocally, or in a container step with noCIin itsenv:block - and checking it still executes once and exits.Read the job's configuration in the run summary and confirm
timeout-minutesis present and is the value you intended, rather than the inherited 360-minute maximum.
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 OPT55 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 (this one plus 72 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
1Vitest: Command Line Interface (opens in new tab)
2Vitest: config reference (watch) (opens in new tab)
3GitHub Actions: default environment variables (opens in new tab)
4GitHub Actions: workflow syntax (jobs.<job_id>.timeout-minutes) (opens in new tab)
Last updated 2026-08-21