Skip to content

Fix coverage ratchet baseline freeze in generate-coverage - #353

Merged
leynos merged 1 commit into
mainfrom
fix/coverage-ratchet-baseline-freeze
Jul 16, 2026
Merged

Fix coverage ratchet baseline freeze in generate-coverage#353
leynos merged 1 commit into
mainfrom
fix/coverage-ratchet-baseline-freeze

Conversation

@leynos

@leynos leynos commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Two coherent coverage-ratchet corrections to the generate-coverage action.

1. Baseline-freeze cache-key fix

The ratchet restored the baseline with a run-id-suffixed primary cache key
(ratchet-baseline-<os>-<run_id>) plus a prefix restore-key
(ratchet-baseline-<os>-), but saved it with a constant, run-id-less key
(ratchet-baseline-<os>) guarded by cache-hit != 'true'.

GitHub Actions cache entries are immutable, so the constant save key could only
ever be written once. After the first save, every later run reported
Cache hit occurred on the primary key ratchet-baseline-<os>, not saving cache
and the baseline froze at whatever the first post-eviction run measured — it
only ever changed when the 7-day cache eviction forced a fresh save at a random
value.

Estate impact: because the baseline could not advance when coverage
improved, and drifted stale, the ratchet false-tripped "Coverage decreased" on
pull requests. This is the root cause of chutoro's ratchet false-trip: a PR
measuring 85.20% against a stale, frozen 85.23% baseline.

The fix aligns the save key with the restore step's run-id-suffixed primary key
and removes the cache-hit guard, so every main run persists a fresh baseline
that later runs recover via the restore-key prefix (newest matching entry
wins).

2. Symmetric ±1pp tolerance dead-band

ratchet_coverage.py now compares coverage within a provisional symmetric ±1
percentage-point dead-band (single named constant RATCHET_TOLERANCE_PP = 1.0):

  • current < baseline - 1.0fail ("Coverage decreased").
  • current > baseline + 1.0pass and advance the baseline to current.
  • within ±1.0 of the baseline → pass and hold (baseline unchanged).

Treating ±1pp as noise stops a nondeterministic low run false-tripping the gate
and, crucially, stops a lucky-high run inflating the baseline so the next normal
run fails. Both values are rounded to 2dp before comparison.

No inputs or user-facing contract change.

Review walkthrough

  • action.yml — the "Save baselines" step now keys on
    ratchet-baseline-${{ runner.os }}-${{ github.run_id }} (matching the restore
    primary key) and its if drops the cache-hit != 'true' guard, keeping only
    success() && inputs.with-ratchet == 'true'. A comment explains why. The
    restore step is unchanged.
  • scripts/ratchet_coverage.py — adds RATCHET_TOLERANCE_PP and the
    three-way dead-band comparison; the baseline is written only on a genuine
    improvement beyond the band.
  • tests/test_ratchet_baseline.py — new regression tests. Three assert the
    action.yml cache-key contract (save key varies per run, equals the restore
    primary key, not gated on cache-hit) and fail against the pre-fix
    action.yml. The remainder cover the dead-band: within-band dip and rise pass
    and hold the baseline (incl. the 85.23→85.20 chutoro figures and both band
    edges), a drop beyond the band fails and preserves the baseline, a rise beyond
    the band advances it, exact equality holds, and a missing baseline is treated
    as zero.
  • CHANGELOG.md / README.md — document both changes and the dead-band
    semantics.

Validation

  • uv run pytest .github/actions/generate-coverage/tests/test_ratchet_baseline.py — 12 passed.
  • Confirmed the cache-key contract tests fail against the pre-fix action.yml.
  • uv run pytest --co -q — full suite collects with no errors.
  • ruff check / ruff format --check — clean.
  • ty check on ratchet_coverage.py — clean.
  • action-validator .github/actions/generate-coverage/action.yml — exit 0.
  • markdownlint-cli2 and typos (en-GB-oxendict) on the docs — clean.

Downstream consumers must bump their generate-coverage pin to a shared-actions
SHA that includes this fix to pick up the advancing baseline and the dead-band.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 24 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6e9d2445-32dd-48fc-aa68-ee362a09d41c

📥 Commits

Reviewing files that changed from the base of the PR and between dfddcb6 and cd4ec5d.

📒 Files selected for processing (5)
  • .github/actions/generate-coverage/CHANGELOG.md
  • .github/actions/generate-coverage/README.md
  • .github/actions/generate-coverage/action.yml
  • .github/actions/generate-coverage/scripts/ratchet_coverage.py
  • .github/actions/generate-coverage/tests/test_ratchet_baseline.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/coverage-ratchet-baseline-freeze

Comment @coderabbitai help to get the list of available commands.

@leynos
leynos enabled auto-merge (squash) July 16, 2026 09:10
codescene-access[bot]

This comment was marked as outdated.

The generate-coverage action's ratchet restored the baseline with a
run-id-suffixed primary cache key (ratchet-baseline-<os>-<run_id>) and a
prefix restore-key, but saved it with a constant, run-id-less key
(ratchet-baseline-<os>) guarded by cache-hit != 'true'. GitHub Actions
cache entries are immutable, so the constant key could only be written
once; every later run reported a cache hit on the constant primary key and
skipped the save, freezing the baseline at whatever the first
post-eviction run measured. The baseline only ever changed when the 7-day
cache eviction forced a fresh save at a random value.

Consequently the ratchet could not advance when coverage improved, and for
repositories with any coverage nondeterminism it false-tripped "Coverage
decreased" on pull requests. For example, chutoro measured 85.20% on a
pull request against a stale, frozen 85.23% baseline.

Align the save key with the restore step's run-id-suffixed primary key
(ratchet-baseline-<os>-<run_id>) and drop the cache-hit guard, so every
main run persists a fresh baseline that later runs recover via the
ratchet-baseline-<os>- restore-key prefix (newest matching entry wins).

Add a provisional symmetric +/-1 percentage-point dead-band to the ratchet
comparison in ratchet_coverage.py. Coverage within one absolute percentage
point of the baseline is treated as noise: the run passes and the baseline
is held. A drop of more than one point below the baseline fails; a rise of
more than one point above the baseline advances the baseline. Holding the
baseline within the band stops a nondeterministic low run false-tripping
the gate and a lucky-high run inflating the baseline so the next normal run
fails. The tolerance is a single named constant (RATCHET_TOLERANCE_PP =
1.0). The action's inputs and contract are unchanged.

Add tests covering the cache-key contract (the save key varies per run,
matches the restore primary key, and is not gated on cache-hit) and the
dead-band comparison (within-band dip and rise pass and hold the baseline,
a drop beyond the band fails, a rise beyond the band advances the
baseline, and exact equality holds).
@leynos
leynos force-pushed the fix/coverage-ratchet-baseline-freeze branch from 305dfaf to cd4ec5d Compare July 16, 2026 09:29
codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No quality gates enabled for this code.

@leynos
leynos merged commit 4977418 into main Jul 16, 2026
29 checks passed
@leynos
leynos deleted the fix/coverage-ratchet-baseline-freeze branch July 16, 2026 11:05
leynos pushed a commit to leynos/podbot that referenced this pull request Jul 22, 2026
The lint-test job's coverage ratchet false-tripped on a 0.07pp dip below a
stale cached baseline (86.39% vs 86.46%), and the failure surfaced as a
confusing empty-artefact-name upload error that masked it. Bump the
`generate-coverage` and `upload-codescene-coverage` pins from 927edd45 to
1c1a46f0, which brings the ±1pp ratchet tolerance dead-band
(leynos/shared-actions#353) that absorbs measurement jitter and the
archive-masking fix (leynos/shared-actions#380) that surfaces a genuine
ratchet failure instead of an empty-name error.

The `setup-rust` pin is left unchanged.
leynos added a commit to leynos/podbot that referenced this pull request Jul 23, 2026
…het fix (#147)

* Bump whitaker-installer to 0.2.6

installer 0.2.5 provisions cargo-dylint 4.1.0, whose dylint driver cannot
build on the suite's new nightly-2026-05-28 pin; 0.2.6 provisions
cargo-dylint 6.0.1.

* Bump whitaker-installer to 0.2.7

0.2.7 fixes the dylint-link install verification defect (leynos/whitaker#299)
that made the lint gate fall back to an impossible source build.

* Bump generate-coverage pin to clear the ratchet dead-band

The lint-test job's coverage ratchet false-tripped on a 0.07pp dip below a
stale cached baseline (86.39% vs 86.46%), and the failure surfaced as a
confusing empty-artefact-name upload error that masked it. Bump the
`generate-coverage` and `upload-codescene-coverage` pins from 927edd45 to
1c1a46f0, which brings the ±1pp ratchet tolerance dead-band
(leynos/shared-actions#353) that absorbs measurement jitter and the
archive-masking fix (leynos/shared-actions#380) that surfaces a genuine
ratchet failure instead of an empty-name error.

The `setup-rust` pin is left unchanged.

---------

Co-authored-by: leynos <leynos@rohga>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant