feat: per-package coverage floor, and a fuzz corpus that persists - #106
Merged
Conversation
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
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>
This was referenced Jul 28, 2026
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>
This was referenced Jul 28, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two changes, both from decisions taken on Glyndor/authcore#218 and #225.
go-ci: an optional per-package coverage floorAn 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-thresholddefaults to0, 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 -coverexactly:LC_ALLis 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 runsShort fuzz runs are only worth repeating if they add up, and these did not.
setup-gorestoresGOCACHE— where Go keeps the 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. The key derives from
go.sumas 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
FuzzParseJWKfinds 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 whyfuzztimeis 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-keyspulls the most recent one back. The path comes fromgo env GOCACHErather 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.