Skip to content

feat: per-package coverage floor, and a fuzz corpus that persists - #106

Merged
Jaro-c merged 2 commits into
mainfrom
ci/per-package-coverage-and-fuzz-corpus
Jul 28, 2026
Merged

feat: per-package coverage floor, and a fuzz corpus that persists#106
Jaro-c merged 2 commits into
mainfrom
ci/per-package-coverage-and-fuzz-corpus

Conversation

@Jaro-c

@Jaro-c Jaro-c commented Jul 28, 2026

Copy link
Copy Markdown
Member

Two changes, both from decisions taken on Glyndor/authcore#218 and #225.

go-ci: an optional per-package coverage floor

An aggregate threshold on a module whose packages differ in risk rewards covering the easy ones. authcore ran at 91% against a gate of 90 while its OIDC client sat at 87.7% and the package that persists its Ed25519 keys at 85.7% — the two with the most attack surface were the two thinnest, paid for by a fully covered clock helper.

per-package-coverage-threshold defaults to 0, so no existing caller changes behaviour until it opts in.

Measured before assuming it was worth having: comparing isolated profiles against -coverpkg=./... moves those packages by +0.5 and +0.0, so the per-package numbers are honest rather than an artefact of helpers being exercised from a sibling package. Had that not held, the gate would have been enforcing noise.

The awk was dry-run against authcore's real profile and reproduces go test -cover exactly:

90.2 github.com/Glyndor/authcore/auth/oauth
91.9 github.com/Glyndor/authcore/auth/email
92.0 github.com/Glyndor/authcore/internal/keymanager
...
100.0 github.com/Glyndor/authcore/internal/clock

LC_ALL is pinned because awk formats decimals per locale, and a comma separator would truncate the comparison at the boundary — it did on my machine.

go-fuzz: the corpus persists between runs

Short fuzz runs are only worth repeating if they add up, and these did not. setup-go restores GOCACHE — where Go keeps the corpus — but on a cache hit it does not save it again:

Cache hit occurred on the primary key setup-go-…, not saving cache.

So everything a run discovered died with the runner, and the next week re-explored the same ground from the seed corpus. The key derives from go.sum as well, which a dependency bump changes, so even the accidental persistence reset every few days.

Measured on authcore before touching it: a cold 60s run of FuzzParseJWK finds 219 new interesting inputs; 300s finds 293. A single run saturates well before its budget — the budget is not the constraint, starting from zero every week is. That is also why fuzztime is left alone.

The corpus now has a key of its own, per target, with the run id in it so it never hits and is therefore always saved; restore-keys pulls the most recent one back. The path comes from go env GOCACHE rather than an assumed default.

Not tagged yet

Same rule as v1.11.0: a consumer proves these green before they get a version. authcore adopts both next, and I will cut the tag once its pull request is green.

Jaro-c added 2 commits July 27, 2026 22:20
An aggregate threshold on a module whose packages differ in risk rewards
covering the easy ones. authcore ran at 91% against a gate of 90 while its
OIDC client sat at 87.7% and the package that persists its Ed25519 keys at
85.7% — the two with the most attack surface were the two thinnest, paid
for by a fully covered clock helper.

The new input asserts the floor holds in every package rather than on
average. It defaults to 0, so no existing caller changes behaviour until
it opts in.

Measured before assuming: comparing isolated profiles against
-coverpkg=./... moves those packages by +0.5 and +0.0, so the per-package
numbers are honest rather than an artefact of helpers being exercised from
a sibling package. Without that the gate would be enforcing noise.

LC_ALL is pinned because awk formats decimals per locale, and a comma
separator would truncate the comparison at the boundary.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Short fuzz runs are only worth repeating if they add up, and these did
not. setup-go restores GOCACHE — where Go keeps the fuzzing corpus — but
on a cache hit it does not save it again, so everything a run discovered
died with the runner and the next week re-explored the same ground from
the seed corpus. Its key derives from go.sum as well, which a dependency
bump changes, so even the accidental persistence reset every few days.

Measured on authcore before changing anything: a cold 60s run of
FuzzParseJWK finds 219 new interesting inputs and 300s finds 293, so a
single run saturates well before its budget. The budget is not the
constraint — starting from zero every week is.

The corpus now has a cache key of its own, per target, with the run id in
it so it never hits and is therefore always saved; restore-keys pulls the
most recent one back. The path comes from `go env GOCACHE` rather than an
assumed default.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
@Jaro-c
Jaro-c merged commit 045903b into main Jul 28, 2026
6 checks passed
@Jaro-c
Jaro-c deleted the ci/per-package-coverage-and-fuzz-corpus branch July 28, 2026 03:22
Jaro-c added a commit to Glyndor/authcore that referenced this pull request Jul 28, 2026
…257)

Closes #218. Closes #225. Adopts both halves of Glyndor/.github#106, and
is the consumer that proves them before that repository gets a tag.

### Per-package coverage floor

This module ran at 91% against a gate of 90 while `auth/oauth` sat at
87.7% and `internal/keymanager` at 85.7%. The two with the most attack
surface were the two thinnest, and `internal/clock` at 100% was paying
for them.

Every package clears 90 today, which is exactly when a floor is cheapest
to adopt — it locks in a state that already holds rather than demanding
new work:

| package | coverage |
|---|---|
| `auth/oauth` | 90.2% |
| `auth/email` | 91.9% |
| `internal/keymanager` | 92.0% |
| `auth/apikey` | 94.6% |
| `auth/jwt` | 95.0% |
| `auth/password` | 95.8% |
| `auth/username` | 97.5% |
| `authcore` | 98.1% |
| `internal/clock` | 100.0% |

The measurement that made this decidable: comparing isolated profiles
against `-coverpkg=./...` moves those packages by +0.5 and +0.0, so the
per-package numbers are honest rather than an artefact of helpers being
exercised from a sibling package.

### The fuzz corpus survives between runs

It never did. `setup-go` restores `GOCACHE` — where Go keeps the corpus
— but on a cache hit does not save it again, so every discovery died
with the runner and the next week re-explored the same ground from the
seeds.

Measured here before changing anything: a cold 60s run of `FuzzParseJWK`
finds 219 new interesting inputs, 300s finds 293. **A single run
saturates well before its budget**, so the budget was never the
constraint — starting from zero every week was. `fuzztime` is
deliberately unchanged.

The compounding will not be visible in this pull request; the first
scheduled run after it merges seeds the cache, and the one after that is
the first to start warm.

### Pinned by SHA

`Glyndor/.github` is only tagged once a consumer has proved a workflow
green, and this is that consumer. The pin moves to the release tag
afterwards.

---------

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Jaro-c added a commit to Glyndor/authcore that referenced this pull request Jul 28, 2026
Seven callers were spread across three versions:

| caller | was | now |
|---|---|---|
| `ci.yml`, `fuzz.yml` | raw commit, no version comment | `# v1.12.0` |
| `schedule-freshness.yml` | v1.11.0 | v1.12.0 |
| `dco.yml`, `go-audit.yml`, `line-limit.yml`, `main-guard.yml` |
v1.10.1 | v1.12.0 |

`ci` and `fuzz` were pinned bare on purpose — Glyndor/.github#106 was
waiting for a consumer to prove it green before being tagged, and #257
was that consumer. Now that v1.12.0 exists they get their comment back.

That comment is not decoration. Without a version beside the SHA,
Dependabot has nothing to compare against, never proposes a bump, and
the pin rots — which is exactly how four of these ended up two releases
behind while nobody noticed.

Supersedes #250, #251, #252 and #254, which each moved one caller to
v1.11.0.

Verified with `actionlint` across all seven workflows.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant