Publish complete signed module catalogs
This commit is contained in:
@@ -64,17 +64,26 @@ def resolve(args: argparse.Namespace) -> dict[str, object]:
|
||||
python=args.python,
|
||||
index_url=str(package_set["registries"]["python"]),
|
||||
)
|
||||
_download_webui(
|
||||
webui_registry_metadata = _download_webui(
|
||||
packages=tuple(package_set["webui"]),
|
||||
destination=webui,
|
||||
npm=args.npm,
|
||||
registry=str(package_set["registries"]["npm"]),
|
||||
)
|
||||
python_rows = _verify_wheels(tuple(package_set["python"]), wheels)
|
||||
webui_rows = _verify_webui(tuple(package_set["webui"]), webui)
|
||||
python_rows = _verify_wheels(
|
||||
tuple(package_set["python"]),
|
||||
wheels,
|
||||
registry=str(package_set["registries"]["python"]),
|
||||
)
|
||||
webui_rows = _verify_webui(
|
||||
tuple(package_set["webui"]),
|
||||
webui,
|
||||
registry_metadata=webui_registry_metadata,
|
||||
)
|
||||
lock: dict[str, object] = {
|
||||
"schema_version": "1",
|
||||
"release_version": package_set["release_version"],
|
||||
"profile": package_set.get("profile", "base"),
|
||||
"package_set_sha256": package_set["package_set_sha256"],
|
||||
"registries": package_set["registries"],
|
||||
"python": python_rows,
|
||||
@@ -169,7 +178,7 @@ def _download_wheels(
|
||||
|
||||
def _download_webui(
|
||||
*, packages: tuple[dict[str, object], ...], destination: Path, npm: str, registry: str
|
||||
) -> None:
|
||||
) -> dict[str, dict[str, str]]:
|
||||
environment = dict(os.environ)
|
||||
npmrc: tempfile.NamedTemporaryFile[bytes] | None = None
|
||||
token = os.environ.get("GOVOPLAN_PACKAGE_TOKEN", "")
|
||||
@@ -182,7 +191,31 @@ def _download_webui(
|
||||
os.chmod(npmrc.name, 0o600)
|
||||
environment["NPM_CONFIG_USERCONFIG"] = npmrc.name
|
||||
try:
|
||||
metadata: dict[str, dict[str, str]] = {}
|
||||
for item in packages:
|
||||
view = subprocess.run(
|
||||
[npm, "view", f"{item['name']}@{item['version']}", "dist", "--json", "--registry", registry],
|
||||
check=True,
|
||||
env=environment,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
dist = json.loads(view.stdout)
|
||||
if not isinstance(dist, dict):
|
||||
raise PackageArtifactError(f"npm registry returned no distribution metadata for {item['name']}")
|
||||
tarball = dist.get("tarball")
|
||||
integrity = dist.get("integrity")
|
||||
parsed = urlsplit(str(tarball or ""))
|
||||
if (
|
||||
parsed.scheme != "https"
|
||||
or not parsed.netloc
|
||||
or parsed.username
|
||||
or parsed.password
|
||||
or not isinstance(integrity, str)
|
||||
or not integrity.startswith("sha512-")
|
||||
):
|
||||
raise PackageArtifactError(f"npm registry returned unsafe distribution metadata for {item['name']}")
|
||||
metadata[str(item["name"])] = {"url": str(tarball), "integrity": integrity}
|
||||
subprocess.run(
|
||||
[npm, "pack", f"{item['name']}@{item['version']}", "--ignore-scripts", "--pack-destination", str(destination), "--registry", registry],
|
||||
check=True,
|
||||
@@ -191,9 +224,15 @@ def _download_webui(
|
||||
finally:
|
||||
if npmrc is not None:
|
||||
Path(npmrc.name).unlink(missing_ok=True)
|
||||
return metadata
|
||||
|
||||
|
||||
def _verify_wheels(packages: tuple[dict[str, object], ...], root: Path) -> list[dict[str, object]]:
|
||||
def _verify_wheels(
|
||||
packages: tuple[dict[str, object], ...],
|
||||
root: Path,
|
||||
*,
|
||||
registry: str | None = None,
|
||||
) -> list[dict[str, object]]:
|
||||
expected = {_normalize(str(item["name"])): item for item in packages}
|
||||
rows: list[dict[str, object]] = []
|
||||
seen: set[str] = set()
|
||||
@@ -204,13 +243,21 @@ def _verify_wheels(packages: tuple[dict[str, object], ...], root: Path) -> list[
|
||||
if package is None or identity["version"] != package["version"] or name in seen:
|
||||
raise PackageArtifactError(f"unexpected wheel artifact: {path.name}")
|
||||
seen.add(name)
|
||||
rows.append(_artifact_row(path, package))
|
||||
row = _artifact_row(path, package)
|
||||
if registry:
|
||||
row["url"] = _python_artifact_url(registry, package=package, filename=path.name)
|
||||
rows.append(row)
|
||||
if seen != set(expected):
|
||||
raise PackageArtifactError("registry did not return every selected Python wheel")
|
||||
return sorted(rows, key=lambda item: str(item["name"]))
|
||||
|
||||
|
||||
def _verify_webui(packages: tuple[dict[str, object], ...], root: Path) -> list[dict[str, object]]:
|
||||
def _verify_webui(
|
||||
packages: tuple[dict[str, object], ...],
|
||||
root: Path,
|
||||
*,
|
||||
registry_metadata: dict[str, dict[str, str]] | None = None,
|
||||
) -> list[dict[str, object]]:
|
||||
expected = {str(item["name"]): item for item in packages}
|
||||
rows: list[dict[str, object]] = []
|
||||
seen: set[str] = set()
|
||||
@@ -222,7 +269,13 @@ def _verify_webui(packages: tuple[dict[str, object], ...], root: Path) -> list[d
|
||||
raise PackageArtifactError(f"unexpected WebUI artifact: {path.name}")
|
||||
seen.add(name)
|
||||
row = _artifact_row(path, package)
|
||||
row["integrity"] = "sha512-" + base64.b64encode(hashlib.sha512(path.read_bytes()).digest()).decode("ascii")
|
||||
integrity = "sha512-" + base64.b64encode(hashlib.sha512(path.read_bytes()).digest()).decode("ascii")
|
||||
row["integrity"] = integrity
|
||||
metadata = (registry_metadata or {}).get(name)
|
||||
if metadata:
|
||||
if metadata.get("integrity") != integrity:
|
||||
raise PackageArtifactError(f"npm registry integrity does not match downloaded package: {name}")
|
||||
row["url"] = metadata["url"]
|
||||
rows.append(row)
|
||||
if seen != set(expected):
|
||||
raise PackageArtifactError("registry did not return every selected WebUI package")
|
||||
@@ -283,6 +336,25 @@ def _artifact_row(path: Path, package: dict[str, object]) -> dict[str, object]:
|
||||
return row
|
||||
|
||||
|
||||
def _python_artifact_url(
|
||||
registry: str,
|
||||
*,
|
||||
package: dict[str, object],
|
||||
filename: str,
|
||||
) -> str:
|
||||
parsed = urlsplit(registry.rstrip("/"))
|
||||
path = parsed.path.rstrip("/")
|
||||
if not path.endswith("/simple"):
|
||||
raise PackageArtifactError("Python registry URL must end in /simple to derive immutable artifacts")
|
||||
artifact_path = (
|
||||
f"{path.removesuffix('/simple')}/files/"
|
||||
f"{quote(str(package['name']), safe='')}/"
|
||||
f"{quote(str(package['version']), safe='')}/"
|
||||
f"{quote(filename, safe='')}"
|
||||
)
|
||||
return urlunsplit((parsed.scheme, parsed.netloc, artifact_path, "", ""))
|
||||
|
||||
|
||||
def _write_requirements(path: Path, rows: list[dict[str, object]]) -> None:
|
||||
lines = ["--no-index", "--find-links ./local-wheels", "--require-hashes"]
|
||||
for row in rows:
|
||||
|
||||
Reference in New Issue
Block a user