Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 198 additions & 34 deletions .github/workflows/opencode-review-dispatch.yml

Large diffs are not rendered by default.

284 changes: 269 additions & 15 deletions scripts/ci/materialize_base_javascript_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
import re
import subprocess
import sys
import urllib.parse
from typing import Any


SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$")
PNPM_SPEC_RE = re.compile(r"^pnpm@[0-9]+\.[0-9]+\.[0-9]+(?:[+-][A-Za-z0-9._+-]+)?$")
PNPM_BASE_INPUT_NAMES = ("package.json", "pnpm-workspace.yaml", ".pnpmfile.cjs")
NPM_LOCK_NAMES = ("npm-shrinkwrap.json", "package-lock.json")
NPM_REGISTRY_HOST = "registry.npmjs.org"
SHA512_SRI_RE = re.compile(r"^sha512-[A-Za-z0-9+/]{86}==$")


def _git(repo_root: pathlib.Path, *args: str) -> bytes:
Expand Down Expand Up @@ -104,13 +108,16 @@ def base_pnpm_projects(
if not isinstance(package_manager, str) or not PNPM_SPEC_RE.fullmatch(
package_manager
):
if str(project_root / "package-lock.json") in regular_paths:
# A sibling package-lock.json means npm owns this project and
# the pnpm-lock.yaml is a vestigial second lockfile. Skip pnpm
# materialization so the downstream npm (package-lock.json)
# install path handles it, instead of failing the whole
# coverage-evidence job. A genuine pnpm-only project (no sibling
# package-lock.json) still must pin an exact pnpm packageManager.
if any(
str(project_root / lock_name) in regular_paths
for lock_name in NPM_LOCK_NAMES
):
# A sibling npm lock means npm owns this project and the
# pnpm-lock.yaml is a vestigial second lockfile. Skip pnpm
# materialization so the downstream npm install path handles
# it, instead of failing the whole coverage-evidence job. A
# genuine pnpm-only project (no sibling npm lock) still must
# pin an exact pnpm packageManager.
continue
raise ValueError(
f"trusted base package manifest {package_path} must declare an exact pnpm packageManager version"
Expand Down Expand Up @@ -144,20 +151,255 @@ def base_pnpm_projects(
return projects


def base_npm_projects(
repo_root: pathlib.Path, base_sha: str
) -> list[tuple[str, str, dict[str, bytes]]]:
"""Return exact base npm inputs grouped by lockfile directory."""
if not SHA_RE.fullmatch(base_sha):
raise ValueError("base SHA must be exactly 40 hexadecimal characters")

repo_root = repo_root.resolve()
regular_paths = _regular_base_paths(repo_root, base_sha)
lock_by_project: dict[pathlib.PurePosixPath, pathlib.PurePosixPath] = {}
for lock_name in NPM_LOCK_NAMES:
for lock_path in sorted(
path
for path in regular_paths
if pathlib.PurePosixPath(path).name == lock_name
):
lock = pathlib.PurePosixPath(lock_path)
lock_by_project.setdefault(lock.parent, lock)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

projects: list[tuple[str, str, dict[str, bytes]]] = []
for project_root, lock in sorted(
lock_by_project.items(), key=lambda item: str(item[1])
):
lock_path = str(lock)
package_path = str(project_root / "package.json")
if package_path not in regular_paths:
raise ValueError(
f"trusted base npm lock {lock_path} has no regular sibling package.json"
)
try:
package_data: Any = json.loads(
_git(repo_root, "show", f"{base_sha}:{package_path}").decode("utf-8")
)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ValueError(
f"trusted base package manifest {package_path} is invalid JSON: {exc}"
) from exc
if not isinstance(package_data, dict):
raise ValueError(
f"trusted base package manifest {package_path} must be a JSON object"
)
package_manager = package_data.get("packageManager")
if isinstance(package_manager, str) and PNPM_SPEC_RE.fullmatch(package_manager):
# An exact pnpm declaration owns this project. A sibling npm lock
# is vestigial and must not create a second dependency cache.
continue

lock_content = _git(repo_root, "show", f"{base_sha}:{lock_path}")
if not lock_content.strip():
raise ValueError(f"trusted base npm lock {lock_path} is empty")
try:
lock_data: Any = json.loads(lock_content.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ValueError(
f"trusted base npm lock {lock_path} is invalid JSON: {exc}"
) from exc
if not isinstance(lock_data, dict):
raise ValueError(f"trusted base npm lock {lock_path} must be a JSON object")

base_inputs = {
"package.json": _git(repo_root, "show", f"{base_sha}:{package_path}"),
lock.name: lock_content,
}
lock_packages = lock_data.get("packages")
if isinstance(lock_packages, dict):
for workspace_path in sorted(lock_packages):
workspace = pathlib.PurePosixPath(str(workspace_path))
if (
not workspace_path
or workspace.is_absolute()
or ".." in workspace.parts
or "node_modules" in workspace.parts
):
continue
workspace_package = project_root / workspace / "package.json"
workspace_package_path = str(workspace_package)
if workspace_package_path in regular_paths:
base_inputs[str(workspace / "package.json")] = _git(
repo_root,
"show",
f"{base_sha}:{workspace_package_path}",
)

projects.append((lock_path, "npm", base_inputs))
return projects


def _lock_blob_sha(repo_root: pathlib.Path, revision_sha: str, lock_path: str) -> str:
"""Return the exact Git blob SHA for one validated revision lockfile."""
raw_blob = _git(repo_root, "rev-parse", f"{revision_sha}:{lock_path}")
blob_sha = raw_blob.decode("ascii", errors="strict").strip()
if not SHA_RE.fullmatch(blob_sha):
raise RuntimeError(
f"git rev-parse returned an invalid blob SHA for {lock_path}"
)
return blob_sha.lower()


def validate_head_npm_lock(lock_path: str, lock_content: bytes) -> None:
"""Fail closed unless a changed HEAD npm lock is registry- and hash-bounded."""
try:
lock_data: Any = json.loads(lock_content.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ValueError(
f"current-head npm lock {lock_path} is invalid JSON: {exc}"
) from exc
if not isinstance(lock_data, dict):
raise ValueError(f"current-head npm lock {lock_path} must be a JSON object")
lockfile_version = lock_data.get("lockfileVersion")
if (
not isinstance(lockfile_version, int)
or isinstance(lockfile_version, bool)
or lockfile_version not in (2, 3)
):
raise ValueError(
f"current-head npm lock {lock_path} must use lockfileVersion 2 or 3"
)
packages = lock_data.get("packages")
if not isinstance(packages, dict):
raise ValueError(
f"current-head npm lock {lock_path} must contain an object-valued packages map"
)

for package_path, metadata in sorted(packages.items()):
if not isinstance(package_path, str) or not isinstance(metadata, dict):
raise ValueError(
f"current-head npm lock {lock_path} contains malformed package metadata"
)
if "\\" in package_path:
raise ValueError(
f"current-head npm lock {lock_path} contains unsafe package path {package_path!r}"
)
candidate = pathlib.PurePosixPath(package_path)
if candidate.is_absolute() or ".." in candidate.parts:
raise ValueError(
f"current-head npm lock {lock_path} contains unsafe package path {package_path!r}"
)
if not package_path or "node_modules" not in candidate.parts:
continue

resolved = metadata.get("resolved")
if metadata.get("link") is True:
if not isinstance(resolved, str) or not resolved or "\\" in resolved:
raise ValueError(
f"current-head npm lock {lock_path} contains an unsafe workspace link for {package_path}"
)
link_target = pathlib.PurePosixPath(resolved)
if (
link_target.is_absolute()
or ".." in link_target.parts
or "node_modules" in link_target.parts
):
raise ValueError(
f"current-head npm lock {lock_path} contains an unsafe workspace link for {package_path}"
)
continue

integrity = metadata.get("integrity")
if not isinstance(resolved, str) or not isinstance(integrity, str):
raise ValueError(
f"current-head npm lock {lock_path} package {package_path} must pin a registry tarball and SHA-512 integrity"
)
parsed = urllib.parse.urlsplit(resolved)
try:
parsed_port = parsed.port
except ValueError as exc:
raise ValueError(
f"current-head npm lock {lock_path} package {package_path} has an invalid registry URL"
) from exc
if (
parsed.scheme != "https"
or parsed.hostname != NPM_REGISTRY_HOST
or parsed.username is not None
or parsed.password is not None
or parsed_port is not None
or parsed.query
or parsed.fragment
or not parsed.path.startswith("/")
or not parsed.path.endswith(".tgz")
):
raise ValueError(
f"current-head npm lock {lock_path} package {package_path} must resolve from https://{NPM_REGISTRY_HOST}/"
)
if not SHA512_SRI_RE.fullmatch(integrity):
raise ValueError(
f"current-head npm lock {lock_path} package {package_path} must use one SHA-512 integrity value"
)


def materialize(
repo_root: pathlib.Path,
base_sha: str,
output_dir: pathlib.Path,
head_sha: str | None = None,
) -> list[dict[str, str]]:
"""Write base pnpm inputs under generated paths safe for a Docker context."""
"""Write trusted base and bounded HEAD inputs under Docker-context-safe paths."""
if output_dir.exists() and output_dir.is_symlink():
raise ValueError("output directory must not be a symlink")
output_dir.mkdir(parents=True, exist_ok=True)

manifest: list[dict[str, str]] = []
for index, (source_path, package_manager, base_inputs) in enumerate(
base_pnpm_projects(repo_root, base_sha)
projects: list[tuple[str, str, dict[str, bytes], str, str]] = []
base_npm = base_npm_projects(repo_root, base_sha)
base_npm_paths = {source_path for source_path, _manager, _inputs in base_npm}
base_npm_blobs: dict[str, str] = {}
for source_path, package_manager, base_inputs in (
base_pnpm_projects(repo_root, base_sha) + base_npm
):
lock_blob = _lock_blob_sha(repo_root, base_sha, source_path)
projects.append(
(
source_path,
package_manager,
base_inputs,
base_sha.lower(),
lock_blob,
)
)
if source_path in base_npm_paths:
base_npm_blobs[source_path] = lock_blob

if head_sha is not None:
if not SHA_RE.fullmatch(head_sha):
raise ValueError("head SHA must be exactly 40 hexadecimal characters")
for source_path, package_manager, head_inputs in base_npm_projects(
repo_root, head_sha
):
head_blob = _lock_blob_sha(repo_root, head_sha, source_path)
if base_npm_blobs.get(source_path) == head_blob:
continue
lock_name = pathlib.PurePosixPath(source_path).name
validate_head_npm_lock(source_path, head_inputs[lock_name])
projects.append(
(
source_path,
package_manager,
head_inputs,
head_sha.lower(),
head_blob,
)
)

for index, (
source_path,
package_manager,
base_inputs,
revision_sha,
lock_blob,
) in enumerate(sorted(projects, key=lambda project: (project[0], project[3]))):
directory = f"project-{index:03d}"
project_dir = output_dir / directory
project_dir.mkdir()
Expand All @@ -168,7 +410,9 @@ def materialize(
manifest.append(
{
"directory": directory,
"lock_blob": lock_blob,
"package_manager": package_manager,
"revision_sha": revision_sha,
"source": source_path,
}
)
Expand All @@ -181,15 +425,21 @@ def materialize(


def main(argv: list[str] | None = None) -> int:
"""Materialize base pnpm locks and report the trusted inputs."""
"""Materialize trusted JavaScript locks and report their exact revisions."""
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", required=True, type=pathlib.Path)
parser.add_argument("--base-sha", required=True)
parser.add_argument("--head-sha")
parser.add_argument("--output-dir", required=True, type=pathlib.Path)
args = parser.parse_args(argv)

try:
manifest = materialize(args.repo_root, args.base_sha, args.output_dir)
manifest = materialize(
args.repo_root,
args.base_sha,
args.output_dir,
head_sha=args.head_sha,
)
except (OSError, RuntimeError, ValueError) as exc:
print(
f"::error::Could not materialize base JavaScript package locks: {exc}",
Expand All @@ -200,12 +450,16 @@ def main(argv: list[str] | None = None) -> int:
if manifest:
for entry in manifest:
print(
"Materialized trusted base pnpm lock "
"Materialized trusted JavaScript lock "
f"{entry['source']} for {entry['package_manager']} "
f"as {entry['directory']}/pnpm-lock.yaml."
f"from {entry['revision_sha']} as "
f"{entry['directory']}/{pathlib.PurePosixPath(entry['source']).name}."
)
else:
print("No tracked pnpm-lock.yaml files exist at the validated base SHA.")
print(
"No tracked supported JavaScript package lockfiles exist "
"at the validated base SHA."
)
return 0


Expand Down
Loading
Loading