The problem with delta mode in CI
hotspots analyze --mode delta is the right tool for local development. You commit, you run it, and you see what changed versus the commit before it. Simple.
In CI, the requirement is different. A PR doesn’t represent a single commit — it represents everything on a feature branch relative to main. If your PR is 12 commits deep, --mode delta compares the last commit to its immediate parent. That’s one commit. The other 11 are invisible to the policy check.
What you actually want is: compare main at the time this PR was opened against the merge commit.
That’s what hotspots diff does.
# Compare this PR against main
hotspots diff main HEAD --policy
# Compare two release tags
hotspots diff v1.0.0 v2.0.0
# Compare any two SHAs
hotspots diff abc123 def456 --format json
How it works
hotspots diff <base> <head> resolves both refs to their full SHAs, loads the corresponding snapshots from .hotspots/snapshots/, and runs the same diff engine used by --mode delta.
The key constraint: both refs need existing snapshots. Snapshots are created by hotspots analyze --mode snapshot. If a snapshot is missing, hotspots diff exits with code 3 and tells you exactly what to do:
error: no snapshot found for ref 'main' (3a8f12c)
→ run: git checkout main && hotspots analyze --mode snapshot
Once both snapshots exist, re-run: hotspots diff main HEAD
This is an intentional design choice. Snapshots include git touch metrics — how many times each function was changed in recent history. Those metrics can’t be computed correctly by checking out a ref mid-flight in an arbitrary working directory. The snapshot captures everything at the moment it was created.
Setting up the CI workflow
The workflow has two parts: create a snapshot on push to main, and diff against it on every PR.
Step 1 — Create and cache the snapshot on push to main
- name: Create snapshot
run: hotspots analyze . --mode snapshot --force
- name: Cache snapshot
uses: actions/cache/save@v4
with:
path: .hotspots/snapshots
key: hotspots-snapshot-${{ github.sha }}
The --force flag overwrites any existing snapshot for this SHA (useful on reruns). The cache key is the commit SHA, so each push to main gets its own cached snapshot.
Step 2 — Restore the base snapshot and diff on PR
- name: Restore base snapshot
id: base-snapshot
uses: actions/cache/restore@v4
with:
path: .hotspots/snapshots
key: hotspots-snapshot-${{ github.event.pull_request.base.sha }}
- name: Create HEAD snapshot
run: hotspots analyze . --mode snapshot --force
- name: Diff PR vs base
if: steps.base-snapshot.outputs.cache-hit == 'true'
run: |
hotspots diff \
${{ github.event.pull_request.base.sha }} \
${{ github.sha }} \
--format text --policy
The PR HEAD snapshot is created fresh each time (hotspots analyze --mode snapshot --force). The base snapshot comes from the cache set on the last push to main.
The if: steps.base-snapshot.outputs.cache-hit == 'true' guard handles the cold-start case: the very first PR after adopting this workflow won’t have a base snapshot yet. Subsequent PRs against the same base will.
What the output looks like
Text output (--format text) gives a ranked table of changed functions:
2 modified, 3 new, 1 deleted
====================================================================================================
STATUS FUNCTION FILE LRS CC BAND
----------------------------------------------------------------------------------------------------
new handleDiffOutput hotspots-cli/src/cmd/diff.rs — → 3.12 — → 4 low → moderate
new runPRAnalysis action/src/main.ts — → 2.84 — → 6 low → low
new generateDiffSummary action/src/main.ts — → 1.41 — → 3 low → low
modified render_policy_text_output hotspots-cli/src/output/... 2.31 → 2.74 3 → 3 low → low
modified print_policy_text_output hotspots-cli/src/output/... 1.18 → 0.41 2 → 1 low → low
deleted runFaultline (delta branch) action/src/main.ts 1.92 → — 4 → — low → —
For scripting, --format json gives you the full delta structure with before/after metrics, band transitions, and policy results. --format jsonl gives one JSON object per changed function.
The —top flag and policy interaction
--top N limits the rendered output to the N highest-risk changed functions. The sort key is:
- New functions: ranked by
after.lrs - Deleted functions: ranked by
before.lrs - Modified functions: ranked by
|Δlrs|
One important detail: policy evaluation runs on the full changed set, then --top truncates the display. This means a critical introduction in a function ranked 11th won’t escape a --top 10 run. The policy check is complete; the display is filtered.
# Show top 10 riskiest changes, but check policy against all changes
hotspots diff main HEAD --top 10 --policy
Annotated tags just work
hotspots diff v1.0.0 v2.0.0
Annotated git tags point to tag objects, not commit objects. Passing a tag object SHA to snapshot lookup would always fail, since snapshots are keyed to commit SHAs. hotspots diff handles this automatically by peeling refs through git rev-parse <ref>^{commit} before the snapshot lookup. Annotated tags, lightweight tags, branches, full SHAs, abbreviated SHAs, and HEAD~N expressions all work.
HTML reports
hotspots diff main HEAD --format html --output reports/delta.html
The HTML delta report shows the same scatter plot and risk table as a snapshot report, but oriented around the diff: band transitions are highlighted, new functions are called out, and policy violations are rendered inline.
In CI you can upload this as a workflow artifact:
- name: Generate HTML delta report
run: |
hotspots diff \
${{ github.event.pull_request.base.sha }} \
${{ github.sha }} \
--format html --output .hotspots/delta-report.html
continue-on-error: true
- name: Upload delta report
uses: actions/upload-artifact@v4
with:
name: hotspots-delta-report
path: .hotspots/delta-report.html
retention-days: 14
if-no-files-found: ignore
Exit codes
| Code | Meaning |
|---|---|
0 | Success |
1 | Blocking policy failure (--policy only) |
3 | One or both snapshots missing |
Exit code 3 is always recoverable — create the missing snapshot and rerun. It’s not a crash or a bug; it’s the diff telling you what it needs.
Upgrading from —mode delta in CI
If you’re already using hotspots analyze --mode delta in CI, the migration path is:
- Add the snapshot-on-push step to your mainline workflow
- Add the cache-restore + diff step to your PR workflow
- Keep the
--mode deltarun as a fallback while the cache warms up (the first PR against any new main SHA will miss the cache)
Once the first push to main has run, all subsequent PRs against that SHA will have the base snapshot available and the diff will run cleanly.
hotspots diff is available in v1.11.0 and later. If you’re not on that version yet, see the installation guide to upgrade. Check your current version with hotspots --version.