feat: compare the site npm pins with the registry

The ci area of check_freshness read the PyPI and action pins only. The
exact versions website/package.json pins (astro, starlight) now get a row
each, STALE once npm publishes a newer release.
This commit is contained in:
Abdessamad Derraz committed 2026-11-06 03:17:31 +01:00
1 parent 33db4e9c83
commit ab1866fd66
3 files changed
+93 -3

No files matched your search

+52 -1
View File
@@ -79,6 +79,9 @@ TOSEC_DOWNLOADS = "https://www.tosecdev.org/downloads"
MAME_REPO = "https://github.com/mamedev/mame"
FBNEO_REPO = "https://github.com/libretro/FBNeo"
PYPI_URL = "https://pypi.org/pypi/{name}/json"
NPM_URL = "https://registry.npmjs.org/{name}/latest"
# The site's npm dependencies, pinned to exact versions and built by Deploy Site.
SITE_PACKAGE = REPO_ROOT / "website" / "package.json"
INSTALLERS = ("install.sh", "install.ps1")
SHOWN_ITEMS = 4 # items named in a diff summary before ", ..."
@@ -1355,6 +1358,53 @@ def pypi_latest(name: str) -> str | None:
return str(info.get("version") or "") or None
def parse_npm_pins(text: str) -> dict[str, str]:
"""Package -> the version a package.json names, dependencies of both kinds."""
data = json.loads(text)
pins: dict[str, str] = {}
for key in ("dependencies", "devDependencies"):
pins.update({str(k): str(v) for k, v in (data.get(key) or {}).items()})
return pins
def npm_latest(name: str) -> str | None:
payload = upstream._http_json(NPM_URL.format(name=name)) # noqa: SLF001
if not isinstance(payload, dict):
return None
return str(payload.get("version") or "") or None
def _npm_findings() -> list[Finding]:
"""One row per site dependency: an exact pin is stale once npm's latest
release is newer, since nothing else asks npm."""
findings = []
for name, pinned in sorted(parse_npm_pins(SITE_PACKAGE.read_text(encoding="utf-8")).items()):
subject = f"npm:{name}"
if not re.fullmatch(r"\d+(\.\d+)*", pinned):
findings.append(Finding("ci", subject, UNKNOWN, local=pinned, detail="a range, not an exact pin"))
continue
try:
latest = npm_latest(name)
except upstream.UpstreamError as exc:
findings.append(Finding("ci", subject, UNKNOWN, local=pinned, detail=str(exc)))
continue
if latest is None:
findings.append(Finding("ci", subject, UNKNOWN, local=pinned, detail="not on npm"))
continue
newer = _version_tuple(latest) > _version_tuple(pinned)
findings.append(
Finding(
"ci",
subject,
STALE if newer else OK,
local=pinned,
remote=latest,
detail="a newer release is out" if newer else "",
)
)
return findings
def check_ci(cache_dir: str, offline: bool) -> list[Finding]:
findings = [_check_installer_pins()]
workflows = sorted((REPO_ROOT / ".github" / "workflows").glob("*.yml"))
@@ -1366,8 +1416,9 @@ def check_ci(cache_dir: str, offline: bool) -> list[Finding]:
pins.setdefault(name, spec)
actions.update(parse_action_pins(text))
if offline:
findings.append(Finding("ci", "workflow pins", SKIPPED, detail="offline"))
findings.append(Finding("ci", "workflow and site pins", SKIPPED, detail="offline"))
return findings
findings.extend(_npm_findings())
for name, spec in sorted(pins.items()):
try:
latest = pypi_latest(name)
+38
View File
@@ -8,6 +8,7 @@ report is folded, since each of those decides whether a row says STALE.
from __future__ import annotations
import json
import os
import sys
import tempfile
@@ -353,6 +354,43 @@ class PinParsingTests(unittest.TestCase):
self.assertTrue(cf.specifier_allows(">=10.14", "12.0.1"))
class SiteNpmPins(unittest.TestCase):
"""The site's npm dependencies are pinned to exact versions in
website/package.json, and the ci area compared only the PyPI and action
pins: astro 7.4.2 stayed behind 7.4.7 with no STALE row."""
def test_both_dependency_kinds_are_read(self):
pins = cf.parse_npm_pins(
'{"dependencies": {"astro": "7.4.2"}, "devDependencies": {"x": "^1.0.0"}}'
)
self.assertEqual(pins, {"astro": "7.4.2", "x": "^1.0.0"})
def test_a_newer_release_is_stale(self):
with tempfile.TemporaryDirectory() as tmp:
package = Path(tmp) / "package.json"
package.write_text(json.dumps({"dependencies": {
"astro": "7.4.2", "@astrojs/starlight": "0.42.9", "y": "^2.0.0",
}}))
latest = {"astro": "7.4.7", "@astrojs/starlight": "0.42.9"}
def answer(url):
name = url.split("registry.npmjs.org/", 1)[1].rsplit("/latest", 1)[0]
return {"version": latest[name]}
with mock.patch.object(cf, "SITE_PACKAGE", package), \
mock.patch.object(cf.upstream, "_http_json", side_effect=answer):
rows = {f.subject: f for f in cf._npm_findings()}
self.assertEqual(rows["npm:astro"].status, cf.STALE)
self.assertEqual(rows["npm:astro"].remote, "7.4.7")
self.assertEqual(rows["npm:@astrojs/starlight"].status, cf.OK)
self.assertEqual(rows["npm:y"].status, cf.UNKNOWN)
def test_the_ci_area_reads_the_site_pins(self):
with mock.patch.object(cf, "_npm_findings", return_value=[]) as npm, \
mock.patch.object(cf, "parse_pip_pins", return_value={}), \
mock.patch.object(cf, "parse_action_pins", return_value={}):
cf.check_ci("unused", offline=False)
npm.assert_called_once()
class RomFolderPins(unittest.TestCase):
"""The rom folders of a setup were pinned by hand and nothing compared
the pins with the platform file or the project's releases."""
+3 -2
View File
@@ -688,8 +688,9 @@ directories are checked against their upstream, dump catalogs and romset
snapshots against the catalog they were read from (Redump DAT versions, the
No-Intro daily rebuild, the newest TOSEC pack, the newest MAME release, the
git blobs of the FBNeo DATs), the game catalogs of `games/catalogs/` against
the No-Intro pack and the Redump datfiles they were read from, and the CI
toolchain against PyPI and the actions' latest releases.
the No-Intro pack and the Redump datfiles they were read from, the CI
toolchain against PyPI and the actions' latest releases, and the site's exact
npm pins in `website/package.json` against the npm registry.
The `native` area needs no network. It reads the cache `export_native.py`
patches from and reports an original that is absent, that came from another