diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2a1ba81..439c6e8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -112,6 +112,9 @@ jobs: - name: Check the index against the pins run: python tools/pins.py verify build/index + - name: Check no package name is also micropython-lib's + run: python tools/pins.py overlap build/index + - name: Prepare the Pages artifact run: touch build/index/.nojekyll diff --git a/tests/test_pins.py b/tests/test_pins.py index 3b70734..2ef60ce 100644 --- a/tests/test_pins.py +++ b/tests/test_pins.py @@ -9,6 +9,8 @@ import sys import tempfile import unittest +from contextlib import redirect_stdout +from io import StringIO from pathlib import Path ROOT = Path(__file__).resolve().parent.parent @@ -210,3 +212,40 @@ def test_a_root_that_names_another_repository_is_refused(self): {"repository": "PyDevices/audioinstruments", "ref": "v0.5.0", "formerly": ["PyDevices/audiocomponents"]}, {"repository": "PyDevices/somewhere-else", "ref": "v0.4.0"}, ) + + +class Overlap(unittest.TestCase): + """overlap compares a built index with an upstream one, here a folder served from this machine.""" + + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.dir = Path(self._tmp.name) + self.index = self.dir / "index" + (self.index / "untested").mkdir(parents=True) + (self.index / "index.json").write_text(json.dumps({"packages": [{"name": "bledev"}]})) + (self.index / "untested" / "index.json").write_text(json.dumps({"packages": [{"name": "bledev"}, {"name": "gt911"}]})) + self.upstream = self.dir / "upstream" + self.upstream.mkdir() + + def theirs(self, *names): + (self.upstream / "index.json").write_text(json.dumps({"packages": [{"name": n} for n in names]})) + return self.upstream.as_uri() + + def test_no_shared_name_passes(self): + with redirect_stdout(StringIO()) as out: + pins.overlap(self.index, self.theirs("aioble", "sdcard")) + self.assertIn("none shared", out.getvalue()) + + def test_a_shared_name_fails_and_names_it(self): + with self.assertRaisesRegex(SystemExit, "bledev"): + pins.overlap(self.index, self.theirs("aioble", "bledev")) + + def test_a_name_only_on_the_untested_index_counts(self): + with self.assertRaisesRegex(SystemExit, "gt911"): + pins.overlap(self.index, self.theirs("gt911")) + + def test_an_unreadable_upstream_warns_and_passes(self): + with redirect_stdout(StringIO()) as out: + pins.overlap(self.index, (self.dir / "not-there").as_uri()) + self.assertIn("::warning::", out.getvalue()) diff --git a/tools/pins.py b/tools/pins.py index de0b538..063903c 100644 --- a/tools/pins.py +++ b/tools/pins.py @@ -28,8 +28,9 @@ GitHub later replaces in the queue is still in ``pins.json`` for the build that replaces it. ``fetch`` checks the pinned sources out, ``lock`` writes the pinned tags into -the checkout's ``lib-lock.json`` for its builder, and ``verify`` checks a built -index has every package at the version pinned. +the checkout's ``lib-lock.json`` for its builder, ``verify`` checks a built +index has every package at the version pinned, and ``overlap`` checks that none +of its names is also one of micropython-lib's. Standard library and git only. """ @@ -43,10 +44,14 @@ import subprocess import sys import time +import urllib.request from pathlib import Path PINS = Path(__file__).resolve().parent.parent / "pins.json" +#: micropython-lib's index, whose names ours must not share. +UPSTREAM_INDEX = "https://micropython.org/pi/v2" + #: Profiles whose tag names a folder of the library repository. FOLDER_PROFILES = ("palettes", "pdwidgets") @@ -227,6 +232,35 @@ def verify(index: Path) -> None: print("the index matches pins.json") +def package_names(index: Path) -> set[str]: + """Every package name a built index publishes, its untested index included.""" + names: set[str] = set() + for path in (index / "index.json", index / "untested" / "index.json"): + if path.is_file(): + names.update(p["name"] for p in json.loads(path.read_text(encoding="utf-8"))["packages"]) + return names + + +def overlap(index: Path, upstream: str = UPSTREAM_INDEX) -> None: + """No package here is named as one of micropython-lib's. + + omip looks here first, so a shared name would hide theirs. Failing to read + their index only warns: their outage shouldn't hold up a release of ours. + """ + url = f"{upstream.rstrip('/')}/index.json" + try: + with urllib.request.urlopen(url, timeout=30) as response: + theirs = {p["name"] for p in json.loads(response.read())["packages"]} + except (OSError, ValueError, KeyError) as exc: + print(f"::warning::can't read {url}, so names weren't compared with micropython-lib's: {exc}") + return + ours = package_names(index) + shared = sorted(ours & theirs) + if shared: + raise SystemExit(f"these package names are micropython-lib's too, and omip would hide theirs: {', '.join(shared)}") + print(f"{len(ours)} package names here, {len(theirs)} in micropython-lib, none shared") + + def main() -> None: parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) commands = parser.add_subparsers(dest="command", required=True) @@ -245,6 +279,9 @@ def main() -> None: k.add_argument("root", type=Path) v = commands.add_parser("verify", help="check a built index against pins.json") v.add_argument("index", type=Path) + o = commands.add_parser("overlap", help="check a built index's names against micropython-lib's") + o.add_argument("index", type=Path) + o.add_argument("--upstream", default=UPSTREAM_INDEX) args = parser.parse_args() if args.command == "apply": apply(args.profile, args.repository, args.ref) @@ -254,6 +291,8 @@ def main() -> None: fetch(args.out, args.root_ref) elif args.command == "lock": lock(args.root) + elif args.command == "overlap": + overlap(args.index, args.upstream) else: verify(args.index)