diff --git a/dep_freshness/check.py b/dep_freshness/check.py index bd0b16a..e665349 100644 --- a/dep_freshness/check.py +++ b/dep_freshness/check.py @@ -72,10 +72,13 @@ def _excuse( excused: list[Finding] = [] for finding in findings: entry = by_label.get(finding.label) - if entry is None: + if entry is not None: + used.add(finding.label) # the entry applies here, even if it + # does not excuse this particular pin -- that is a stale pin to + # fix, not allowlist rot to delete. + if entry is None or not _covers(entry, finding): failing.append(finding) continue - used.add(finding.label) excused.append(Finding( finding.dep, finding.severity, finding.latest, finding.detail, excused=entry.reason, @@ -89,6 +92,19 @@ def _excuse( return failing, dead +def _covers(entry, finding: Finding) -> bool: + """Does this exception excuse THIS pin, or only the one it names? + + `pinned` is a required field: an entry is an argument about ONE version, + and excusing any version of the package turns a narrow, reasoned hold into + a blanket exemption. The fleet-wide typescript entry pins 6.0.3 for a + reason specific to 6.0.3, while dufs-cloud/web sat on `~5.8.3` -- two + majors older, for no stated reason -- and the gate called it current + because the package name matched. + """ + return entry.pinned in (finding.dep.pinned, finding.dep.constraint) + + def _unquarantine(finding: Finding) -> Finding | None: """Re-judge an npm finding against what pnpm will actually install. diff --git a/dep_freshness/tests/test_check_allowlist.py b/dep_freshness/tests/test_check_allowlist.py index a7e8570..586e7a6 100644 --- a/dep_freshness/tests/test_check_allowlist.py +++ b/dep_freshness/tests/test_check_allowlist.py @@ -129,3 +129,23 @@ exceptions: blocked_by: "transitive:x@1.0.0" """) assert run("--all") == 2 + + +def test_an_exception_excuses_only_the_version_it_names(repo, run, capsys): + """A held version is an argument about ONE version, not a blank cheque. + + The fleet-wide typescript entry pins 6.0.3 with a reason specific to it; + dufs-cloud/web sat on `~5.8.3`, two majors older for no stated reason, and + the gate reported it current because the label matched. + """ + write(repo, "pubspec.yaml", STALE_PUBSPEC.replace("1.5.0", "1.4.0")) + write(repo, ALLOWLIST, """\ +exceptions: + - ecosystem: pub + package: http + pinned: "1.5.0" + reason: "1.5.0 is held; 1.4.0 is not" + blocked_by: "transitive:some_pkg@1.0.0" +""") + assert run("--all") == 1 + assert "pub:http" in capsys.readouterr().err