-
Notifications
You must be signed in to change notification settings - Fork 121
fix(build): resolve OINK from candidate lock files #497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| #!/usr/bin/env python3 | ||
| # | ||
| # Licensed to the Apache Software Foundation (ASF) under one or more | ||
| # contributor license agreements. See the NOTICE file distributed with | ||
| # this work for additional information regarding copyright ownership. | ||
| # The ASF licenses this file to You under the Apache License, Version 2.0. | ||
|
|
||
| """Resolve the sole theme dependency from the checked-in Go lock files.""" | ||
| import json | ||
| import os | ||
| import re | ||
| import subprocess | ||
| from pathlib import Path | ||
|
|
||
| MODULE = "github.com/pgsty/oink" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 The contributor docs still state the hard pin this PR replaces.
Requested change: reword those three lines to describe the lock-file contract rather than a literal version, so they do not need editing again on the 1.1 bump. Separately, for #496 rather than this PR: |
||
| SITE_MODULE = "github.com/apache/hugegraph-doc" | ||
| VERSION = re.compile(r"v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?") | ||
|
|
||
|
|
||
| def command(args, root): | ||
| return subprocess.check_output([os.environ.get("GO_BIN", "go"), *args], cwd=root, text=True) | ||
|
|
||
|
|
||
| def locked_version(root): | ||
| data = json.loads(command(["mod", "edit", "-json"], root)) | ||
| requirements = data.get("Require") or [] | ||
| if (data.get("Module", {}).get("Path") != SITE_MODULE | ||
| or data.get("Replace") or data.get("Exclude") | ||
| or len(requirements) != 1 or requirements[0]["Path"] != MODULE): | ||
| raise ValueError("expected the HugeGraph module with only OINK and no replace/exclude directives") | ||
| version = requirements[0]["Version"] | ||
| if not VERSION.fullmatch(version): | ||
| raise ValueError("OINK must be pinned to an explicit release version") | ||
| return version | ||
|
|
||
|
|
||
| def download_locked(root): | ||
| root = Path(root) | ||
| version = locked_version(root) | ||
| sums = {} | ||
| for line in (root / "go.sum").read_text().splitlines(): | ||
| if not line.strip(): | ||
| continue | ||
| path, revision, digest = line.split() | ||
| sums[(path, revision)] = digest | ||
| expected = [sums.get((MODULE, version)), sums.get((MODULE, version + "/go.mod"))] | ||
| if not all(expected): | ||
| raise ValueError("OINK module and go.mod checksums must already exist in go.sum") | ||
| module = json.loads(command(["mod", "download", "-json", MODULE + "@" + version], root)) | ||
| if (module.get("Path") != MODULE or module.get("Version") != version | ||
| or module.get("Sum") != expected[0] or module.get("GoModSum") != expected[1] | ||
| or module.get("Error")): | ||
| raise ValueError("downloaded OINK identity or checksum differs from lock files") | ||
| resolved = json.loads(command(["list", "-m", "-json", MODULE], root)) | ||
| if resolved.get("Replace") or resolved.get("Version") != version or resolved.get("Path") != MODULE: | ||
| raise ValueError("resolved OINK module differs from lock files") | ||
| graph = command(["list", "-m", "all"], root).splitlines() | ||
| if graph != [SITE_MODULE, MODULE + " " + version]: | ||
| raise ValueError("module graph must contain only this site and the pinned OINK module") | ||
| command(["mod", "verify"], root) | ||
| return module | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| print(json.dumps(download_locked(Path(__file__).resolve().parents[1]), indent=2)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| #!/usr/bin/env python3 | ||
| # | ||
| # Licensed to the Apache Software Foundation (ASF) under one or more | ||
| # contributor license agreements. See the NOTICE file distributed with | ||
| # this work for additional information regarding copyright ownership. | ||
| # The ASF licenses this file to You under the Apache License, Version 2.0. | ||
|
|
||
| import json | ||
| from pathlib import Path | ||
| import tempfile | ||
| import unittest | ||
| from unittest.mock import patch | ||
|
|
||
| import oink_module | ||
|
|
||
|
|
||
| class ModuleLockTest(unittest.TestCase): | ||
| def config(self, version="v1.0.0", **changes): | ||
| return dict(Module={"Path": oink_module.SITE_MODULE}, | ||
| Require=[{"Path": oink_module.MODULE, "Version": version}], **changes) | ||
|
|
||
| def download(self, version="v1.0.0", config=None, metadata=None, resolved=None, graph=None, sums=True): | ||
| module = dict(Path=oink_module.MODULE, Version=version, Sum="h1:archive", GoModSum="h1:mod") | ||
| resolution = dict(Path=oink_module.MODULE, Version=version) | ||
| module.update(metadata or {}) | ||
| resolution.update(resolved or {}) | ||
| replies = [json.dumps(config or self.config(version)), json.dumps(module), | ||
| json.dumps(resolution), graph or f"{oink_module.SITE_MODULE}\n{oink_module.MODULE} {version}\n", "all modules verified"] | ||
| with tempfile.TemporaryDirectory() as directory: | ||
| root = Path(directory) | ||
| (root / "go.sum").write_text( | ||
| f"\n{oink_module.MODULE} {version} h1:archive\n\n{oink_module.MODULE} {version}/go.mod h1:mod\n" if sums else "") | ||
| with patch.object(oink_module, "command", side_effect=replies) as command: | ||
| result = oink_module.download_locked(root) | ||
| self.assertEqual(command.call_args.args[0], ["mod", "verify"]) | ||
| return result | ||
|
|
||
| def test_accepts_each_locked_release(self): | ||
| for version in ["v1.0.0", "v1.1.0"]: | ||
| with self.subTest(version=version): | ||
| self.assertEqual(self.download(version)["Version"], version) | ||
|
|
||
| def test_rejects_changed_dependency_boundary(self): | ||
| configurations = [self.config(Replace=[{"New": {"Path": "local"}}]), | ||
| self.config(Exclude=[{"Path": oink_module.MODULE, "Version": "v1.0.0"}])] | ||
| wrong_site = self.config() | ||
| wrong_site["Module"]["Path"] = "example.com/site" | ||
| configurations.append(wrong_site) | ||
| extra = self.config() | ||
| extra["Require"].append({"Path": "example.com/extra", "Version": "v1.0.0"}) | ||
| configurations.append(extra) | ||
| for config in configurations: | ||
| with self.subTest(config=config), self.assertRaises(ValueError): | ||
| self.download(config=config) | ||
|
|
||
| def test_missing_checksums_stop_before_download(self): | ||
| with tempfile.TemporaryDirectory() as directory: | ||
| root = Path(directory) | ||
| (root / "go.sum").write_text("") | ||
| with patch.object(oink_module, "command", return_value=json.dumps(self.config())) as command: | ||
| with self.assertRaises(ValueError): | ||
| oink_module.download_locked(root) | ||
| self.assertEqual(command.call_count, 1) | ||
|
|
||
| def test_rejects_download_identity_or_checksum_mismatch(self): | ||
| for metadata in [{"Sum": "h1:wrong"}, {"GoModSum": "h1:wrong"}, | ||
| {"Path": "example.com/theme"}, {"Version": "v9.0.0"}]: | ||
| with self.subTest(metadata=metadata), self.assertRaises(ValueError): | ||
| self.download(metadata=metadata) | ||
|
|
||
| def test_rejects_resolved_override_and_extra_dependency(self): | ||
| for resolved in [{"Replace": {"Path": "local"}}, {"Version": "v9.0.0"}, | ||
| {"Path": "example.com/theme"}]: | ||
| with self.subTest(resolved=resolved), self.assertRaises(ValueError): | ||
| self.download(resolved=resolved) | ||
| with self.assertRaises(ValueError): | ||
| self.download(graph=f"{oink_module.SITE_MODULE}\n{oink_module.MODULE} v1.0.0\nexample.com/extra v1.0.0\n") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,8 @@ | |
| import xml.etree.ElementTree as ET | ||
| from typing import NoReturn | ||
|
|
||
| from oink_module import download_locked | ||
|
|
||
|
|
||
| ROOT = pathlib.Path(__file__).resolve().parents[1] | ||
| URL_CONTRACT = ROOT / "dist/url-contract.json" | ||
|
|
@@ -4068,30 +4070,10 @@ def build(args: argparse.Namespace) -> None: | |
| json.dumps(override, ensure_ascii=False), encoding="utf-8" | ||
| ) | ||
| hugo = os.environ.get("HUGO_BIN", "hugo") | ||
| go = os.environ.get("GO_BIN", "go") | ||
| go_executable = shutil.which(go) | ||
| go_executable = shutil.which(os.environ.get("GO_BIN", "go")) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 This line keeps guarding line 4135, where Requested change: pass the resolved |
||
| if go_executable is None: | ||
| fail(f"Go executable is unavailable: {go}") | ||
| module_result = subprocess.run( | ||
| [ | ||
| go_executable, | ||
| "mod", | ||
| "download", | ||
| "-json", | ||
| "github.com/pgsty/oink@v1.0.0", | ||
| ], | ||
| cwd=assembly, | ||
| check=True, | ||
| stdout=subprocess.PIPE, | ||
| text=True, | ||
| ) | ||
| module = json.loads(module_result.stdout) | ||
| if ( | ||
| module.get("Path") != "github.com/pgsty/oink" | ||
| or module.get("Version") != "v1.0.0" | ||
| or module.get("Sum") != "h1:E+WHFP9zSRT+5RKoIkWNp+ASRGS1BKG+rDEi9by/BjE=" | ||
| ): | ||
| fail(f"unexpected OINK module metadata: {module!r}") | ||
| fail("Go executable is unavailable") | ||
| module = download_locked(assembly) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Lock-file mismatches now abort with a traceback instead of a clean The removed code ended in Reproduced by substituting and the Requested change: put the actual and expected values into each |
||
| migration_script = pathlib.Path(module["Dir"]) / "bin/migrations/oink06.py" | ||
| if not migration_script.is_file(): | ||
| fail(f"pinned OINK migration tool is absent: {migration_script}") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 On the dispatch path this step no longer asserts anything master controls.
preparerequirestest "$GITHUB_REF" = "refs/heads/master"(line 124), so this YAML is always master's, butsource_shais the resolved candidate branch head (line 159) and the build job checks that out (line 204). The five inline assertions this replaces were therefore master-authoritative against the candidate'sgo.mod;scripts/oink_module.pycomes out of the candidate checkout, so the step is now self-attested. The following step already runs candidateversioning.py, so this is not a new execution boundary - what is lost is the one cheap master-side check that would catch accidental drift in a candidate'sgo.modbefore a staging build.Requested change: keep a version-independent assertion inline alongside the helper call, so master still bounds the graph while the version floats: