Recommend the smallest upgrade that clears an advisory, not the highest
The rollup took the highest fix version across a component's advisories, which is wrong whenever an advisory is patched on more than one release branch. nanoid is fixed on both 3.3.x and 5.x, and it is transitive under a postcss range of ^3.3.11 that no 5.x release can satisfy - so the recommendation was unreachable rather than merely large. Per advisory the target is now the lowest fix above the version in use; per component, the highest of those. Fixes on older branches are skipped rather than offered as a downgrade.
This commit is contained in:
@@ -274,21 +274,43 @@ def version_key(version):
|
||||
return parts
|
||||
|
||||
|
||||
def upgrade_plan(findings):
|
||||
"""[(component, version, count)] - one row per component that has a fix.
|
||||
def minimal_fix(fixedin, current):
|
||||
"""The SMALLEST published fix above the version in use, or None.
|
||||
|
||||
The highest fix version across that component's advisories, because
|
||||
upgrading once to the top of the list clears the ones below it.
|
||||
An advisory patched on several release branches lists a fix per branch:
|
||||
nanoid's is fixed in both 3.3.18 and 5.1.16. Taking the highest recommends
|
||||
a major upgrade when a patch on the current branch would do - and where the
|
||||
package is transitive, that major may not even be reachable, because the
|
||||
parent's semver range forbids it. The smallest fix above the current
|
||||
version is the one that actually clears the advisory.
|
||||
|
||||
Fixes at or below the current version belong to older branches and are
|
||||
ignored; recommending one would be a downgrade.
|
||||
"""
|
||||
above = [v for v in fixedin if version_key(v) > version_key(current)]
|
||||
if not above:
|
||||
return None
|
||||
return min(above, key=version_key)
|
||||
|
||||
|
||||
def upgrade_plan(findings):
|
||||
"""[(component, version, count, major)] - one row per component with a fix.
|
||||
|
||||
Per advisory, the minimal upgrade that clears it; per component, the
|
||||
highest of those, since one upgrade has to satisfy every advisory against
|
||||
it.
|
||||
"""
|
||||
by_component = {}
|
||||
for f in findings:
|
||||
if f['allowlisted'] or not f['fixedin']:
|
||||
continue
|
||||
target = minimal_fix(f['fixedin'], f['version'])
|
||||
if target is None:
|
||||
continue
|
||||
name = f['component']
|
||||
best, count, current = by_component.get(name, (None, 0, f['version']))
|
||||
for version in f['fixedin']:
|
||||
if best is None or version_key(version) > version_key(best):
|
||||
best = version
|
||||
if best is None or version_key(target) > version_key(best):
|
||||
best = target
|
||||
by_component[name] = (best, count + 1, current)
|
||||
|
||||
rows = []
|
||||
@@ -337,7 +359,7 @@ def report(findings, blocking):
|
||||
if ship:
|
||||
upgrades = upgrade_plan(ship)
|
||||
if upgrades:
|
||||
print('\nWHAT TO UPGRADE - shipped components, highest fix seen')
|
||||
print('\nWHAT TO UPGRADE - shipped components, smallest upgrade that clears them')
|
||||
for component, version, count, major in upgrades:
|
||||
note = ' MAJOR - check for breaking changes' if major else ''
|
||||
print(' %-30s -> %-12s clears %-3d%s'
|
||||
|
||||
Reference in New Issue
Block a user