Implera is currently offline. The blog stays up.
Back to insights

Insights

Per-PR vs Nightly Scans: When to Run Each CI Check

A team I spoke to recently ran their full security suite, Semgrep, OSV-Scanner, a licence audit and a secrets sweep, on every single push. Average PR check time: eleven minutes. Developers had quietly started batching three days of work into one branch so they only paid the tax once. The scans were thorough. They were also actively making the codebase worse, because they were training people to ship in big risky lumps.

The fix was not a faster machine. It was deciding which checks belong on a pull request and which belong on a schedule.

The question almost nobody asks

Most teams pick a cadence by accident. The first scanner gets wired into the PR workflow because that is the example in the docs, and every scanner after it inherits the same trigger. Nobody revisits it. Six months later the pipeline is eleven minutes long and nobody remembers why every check runs everywhere.

The decision is worth making deliberately. There are really only three places a check can run:

  • Per-PR (on pull_request, every push to the branch)
  • On merge to main (on push to the default branch)
  • Scheduled (a nightly or weekly cron, the schedule event)

GitHub Actions exposes all three; the full list is in the events that trigger workflows reference. The interesting work is mapping each check to the right one.

The deciding question

For any given check, ask one thing: does the result depend on the diff, or on the outside world?

If the result depends only on the code in the PR, run it per-PR. The author changed the code, the author should see the consequence immediately, while the context is fresh.

If the result depends on something outside the PR, a newly disclosed CVE, a licence that changed upstream, a dependency that went unmaintained, then running it per-PR is mostly wasted work. The same scan produces the same answer on every PR until the outside world changes. That is a job for a schedule.

This single question resolves most cadence arguments. A linter result depends on the diff: per-PR. A "is any dependency now vulnerable" result depends on the CVE feed, not the diff: nightly.

What belongs on every PR

Run a check per-PR when it is fast, deterministic and diff-dependent. The classic four from a well-designed CI pipeline all qualify:

Check Why per-PR Typical time
Lint Diff-dependent, instant feedback seconds
Type check Catches structural breakage in the change under a minute
Unit tests Logic regressions live in the diff under five minutes
Secrets scan A leaked key must never reach main seconds
New-finding security scan Catches vulns introduced by the diff under two minutes

The non-negotiable one is the secrets scan. A committed credential is a now problem, not a tomorrow problem. By the time a nightly job finds it, the secret has been in git history for hours and must be rotated regardless. Catch it before merge or you have already lost.

Note the qualifier on the security scan: new-finding. A per-PR security scan should fail only on findings the diff introduced, not on the existing baseline. Both Semgrep's CI integration and most modern scanners support diff-aware mode for exactly this reason. A scan that fails on PRs touching unrelated files is the fastest way to teach a team to bypass it.

What belongs on a schedule

Run a check nightly or weekly when the answer changes because of the outside world, not because of the code:

  • Full dependency vulnerability scan. A clean lockfile today can be vulnerable tomorrow when a CVE is published against a package you already shipped. OSV-Scanner over your full lockfile, run nightly, catches that the morning after disclosure. Running the same full scan on every PR tells you nothing new between disclosures. (Keeping that surface manageable is its own discipline, covered in keeping your dependency supply chain healthy.)
  • Licence compliance audit. Licences change upstream. A weekly sweep catches a transitive dependency that switched from MIT to a copyleft licence without you touching a line.
  • Deep SAST across the whole tree. A full OWASP Dependency-Check style pass or an exhaustive ruleset over every file is too slow for a PR. Run the deep version nightly and the diff-aware version per-PR.
  • Trend and drift signals. Codebase health trends, architecture coupling over time, documentation drift. These are dashboard inputs reviewed weekly, not merge blockers.

The mental model: per-PR checks protect the merge. Scheduled checks protect against the world moving underneath you.

The third lane: on merge to main

There is a useful middle option. Some checks are too slow for every push but you still want them tied to a specific change rather than a clock.

Run these on push to the default branch. The PR already merged, so they do not block anyone, but they run against a known commit. Good candidates: a full integration test suite, a slower end-to-end run, or a complete (not diff-aware) quality score that establishes the new baseline. If one fails, you know exactly which merge caused it, which a nightly cron cannot tell you.

Mapping it to PR gates

The cadence decision and the blocking decision are related but separate. Cadence is when a check runs. Gating is whether a failure stops the merge. Lay them on a grid:

Blocking Advisory Background
Per-PR secrets, lint, new-finding security coverage delta inline annotations
On merge broken-main alert full quality score baseline snapshot
Scheduled rarely weekly digest trend charts, full vuln scan

Most blocking checks are per-PR, because that is the only point where blocking actually prevents a problem. Most scheduled checks are background, because by the time they run there is no PR to block. Blocking a scheduled scan tends to mean "page someone", not "stop a merge".

This is also where deterministic versus AI checks matters. A flaky or non-deterministic check has no business blocking a PR; if it must run, run it advisory or on a schedule where a re-run is cheap and nobody is waiting on the result.

How Implera splits it

Worth being concrete about our own setup, since we run this model in production. Implera's PR quality gate is deliberately the fast lane: it runs a regex-and-AST security pass, accessibility pattern checks and per-domain score thresholds on the diff, and posts a result in well under the time it takes to review the PR by hand. The exhaustive work, full Semgrep rulesets, OSV-Scanner across the whole lockfile, Trivy over infrastructure files, knip dead-code analysis, runs in the scheduled analysis container, not on the merge path.

The split is not about thoroughness versus speed. It is about putting the diff-dependent answer in front of the author immediately and letting the world-dependent answer arrive on its own clock.

A migration you can do this week

If your pipeline is an everything-on-every-PR pile, you do not need to rebuild it. Do this:

  1. List every check and its current trigger.
  2. For each, ask the deciding question: diff-dependent or world-dependent?
  3. Move every world-dependent check to a nightly schedule.
  4. Add diff-aware mode to any per-PR security scan so it only fails on new findings.
  5. Measure PR check time before and after.

The team with the eleven-minute pipeline got it to under three minutes this way. Nothing got less thorough. The nightly job still runs the full suite. The difference is that developers stopped batching, started shipping small PRs again, and the reviews got easier as a result.

FAQ

If you want the per-PR fast lane and the scheduled deep scan running together without wiring it all by hand, that split is exactly what Implera is built to give you. Connect a repository and the cadence is decided for you.

FAQ

Common questions

© 2026 Implera