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
47 changes: 42 additions & 5 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,10 @@ jobs:
# environment even after shell variables are unset. Execute all
# pull-request-controlled tests in Docker's default private PID namespace with a
# read-only trusted tree and no host Docker socket. The image is
# pinned to the reviewed linux/amd64 manifest digest.
# pinned to the reviewed linux/amd64 manifest digest. JavaScript
# registry access is likewise restricted to exact package inputs
# extracted from the live-validated base commit; the PR-head sandbox
# consumes only the resulting offline store.
if [ "${OPENCODE_COVERAGE_SANDBOXED:-0}" != "1" ]; then
host_github_output="$GITHUB_OUTPUT"
sandbox_result_dir="${RUNNER_TEMP}/opencode-coverage-sandbox-result"
Expand Down Expand Up @@ -541,6 +544,10 @@ jobs:
--repo-root "$COVERAGE_SOURCE_WORKDIR" \
--base-sha "$PR_BASE_SHA" \
--output-dir "$coverage_build_dir/base-python-requirements"
python3 -I "$GITHUB_WORKSPACE/scripts/ci/materialize_base_javascript_packages.py" \
--repo-root "$COVERAGE_SOURCE_WORKDIR" \
--base-sha "$PR_BASE_SHA" \
--output-dir "$coverage_build_dir/base-javascript-packages"
cat >"$coverage_build_dir/Dockerfile" <<'DOCKERFILE'
FROM docker.io/library/python:3.14-slim@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1
ENV DEBIAN_FRONTEND=noninteractive
Expand Down Expand Up @@ -587,6 +594,25 @@ jobs:
&& ln -s /opt/pnpm/bin/pnpm.cjs /usr/local/bin/pnpm \
&& test "$(/usr/local/bin/pnpm --version)" = "11.5.3" \
&& rm -f /tmp/pnpm.tgz
COPY base-javascript-packages /tmp/base-javascript-packages
RUN set -eu; \
mkdir -p /opt/pnpm-store; \
jq -r '.[] | [.directory, .package_manager] | @tsv' \
/tmp/base-javascript-packages/manifest.json \
| while IFS="$(printf '\t')" read -r project_dir package_manager; do \
[ -n "$project_dir" ] || continue; \
if [ "$package_manager" != "pnpm@11.5.3" ]; then \
printf 'Unsupported trusted base package manager: %s\n' "$package_manager" >&2; \
exit 1; \
fi; \
cd "/tmp/base-javascript-packages/${project_dir}"; \
pnpm fetch \
--frozen-lockfile \
--ignore-scripts \
--store-dir /opt/pnpm-store; \
done; \
chmod -R a+rX /opt/pnpm-store; \
rm -rf /tmp/base-javascript-packages
COPY requirements-opencode-review-ci-hashes.txt /tmp/requirements-opencode-review-ci-hashes.txt
Comment on lines +597 to 616

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

pnpm 버전이 정확히 11.5.3이 아니면 전체 coverage-tool 이미지 빌드가 실패해, 모든 언어의 coverage-evidence가 함께 막힙니다.

if [ "$package_manager" != "pnpm@11.5.3" ]; then ... exit 1; fi은 base 커밋의 어떤 pnpm-lock.yaml/package.json이든 packageManagerpnpm@11.5.3이 아니면 즉시 exit 1합니다. 이 블록은 트러스티드 네트워크 Docker 이미지 빌드(docker build) 단계에서 실행되며, 이 이미지 빌드는 Python/R/Rust 등 언어에 관계없이 모든 PR에서 공통으로 수행됩니다. 기존에는 pnpm 버전 불일치가 sandbox 내부의 JS 전용 ensure_corepack_runner()에서만 감지되어 해당 언어의 coverage만 실패시켰지만(다른 언어 coverage는 계속 진행), 이번 변경으로 인해 image build 단계에서 하드 실패하면서 PR의 coverage-evidence 전체(Python/Rust/R 포함)가 차단됩니다. repository_dispatch로 여러 대상 저장소를 리뷰하는 구조상, pnpm 11.5.3 이외 버전을 쓰는 대상 저장소가 하나라도 있으면 해당 저장소의 모든 PR이 이 시점에서 막힐 수 있습니다.

빌드를 중단시키는 대신, 지원되지 않는 pnpm 버전의 프로젝트는 로그만 남기고 prefetch를 건너뛰도록(그래서 이후 JS 단계에서만 개별적으로 실패하도록) 완화하는 것을 고려해주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/opencode-review-dispatch.yml around lines 597 - 616,
Update the trusted base package prefetch loop around the package_manager
validation so unsupported pnpm versions emit a diagnostic and skip that
project’s pnpm fetch instead of exiting the Docker build. Preserve prefetching
for projects using pnpm@11.5.3 and continue processing the remaining manifest
entries.

RUN python3 -m pip install \
--break-system-packages \
Expand Down Expand Up @@ -1047,7 +1073,12 @@ jobs:
fi
;;
pnpm)
run_and_capture "JavaScript/TypeScript dependencies (pnpm install, lifecycle hooks disabled)" pnpm install --frozen-lockfile --ignore-scripts
run_and_capture "JavaScript/TypeScript dependencies (pnpm offline install, lifecycle hooks disabled)" \
pnpm install \
--offline \
--frozen-lockfile \
--ignore-scripts \
--store-dir /opt/pnpm-store
;;
yarn)
run_and_capture "JavaScript/TypeScript dependencies (yarn install, lifecycle hooks disabled)" yarn install --immutable --mode=skip-builds
Expand Down Expand Up @@ -1188,7 +1219,7 @@ jobs:
check_javascript_coverage_thresholds() {
local summary_list
summary_list="$(mktemp /tmp/javascript-coverage-summaries.XXXXXX)"
find . \
find "$COVERAGE_SOURCE_WORKDIR" \
\( -path '*/coverage/coverage-summary.json' -o -path '*/coverage/coverage-final.json' \) \
-type f \
-not -path '*/node_modules/*' \
Expand All @@ -1206,7 +1237,7 @@ jobs:

run_and_capture "JavaScript/TypeScript coverage threshold" \
python3 "$GITHUB_WORKSPACE/scripts/ci/javascript_coverage_gate.py" \
--repo-root . \
--repo-root "$COVERAGE_SOURCE_WORKDIR" \
--base-sha "$PR_BASE_SHA" \
--head-sha "$PR_HEAD_SHA" \
--summary-list "$summary_list"
Expand Down Expand Up @@ -1494,6 +1525,7 @@ jobs:
javascript_package_dirs="$(javascript_coverage_package_dirs)"
if [ -n "$javascript_package_dirs" ]; then
measured_any=1
javascript_coverage_ran_any=0
while IFS= read -r package_dir; do
[ -n "$package_dir" ] || continue
pushd "$package_dir" >/dev/null
Expand Down Expand Up @@ -1557,10 +1589,13 @@ jobs:
fi

if [ "$javascript_coverage_ran" -eq 1 ]; then
check_javascript_coverage_thresholds
javascript_coverage_ran_any=1
fi
popd >/dev/null
done <<<"$javascript_package_dirs"
if [ "$javascript_coverage_ran_any" -eq 1 ]; then
check_javascript_coverage_thresholds
fi
fi

if has_changed_tracked_files '*.R' '*.r' 'DESCRIPTION' 'renv.lock'; then
Expand Down Expand Up @@ -1932,6 +1967,7 @@ jobs:
ContextualWisdomLab/.github:opencode.jsonc | \
ContextualWisdomLab/.github:scripts/ci/changed_file_syntax_gate.py | \
ContextualWisdomLab/.github:scripts/ci/javascript_coverage_gate.py | \
ContextualWisdomLab/.github:scripts/ci/materialize_base_javascript_packages.py | \
ContextualWisdomLab/.github:scripts/ci/opencode_review_approve_gate.sh | \
ContextualWisdomLab/.github:scripts/ci/pr_head_replay_guard.py | \
ContextualWisdomLab/.github:scripts/ci/pr_review_merge_scheduler.py | \
Expand All @@ -1941,6 +1977,7 @@ jobs:
ContextualWisdomLab/.github:scripts/ci/validate_opencode_failed_check_review.sh | \
ContextualWisdomLab/.github:tests/test_changed_file_syntax_gate.py | \
ContextualWisdomLab/.github:tests/test_javascript_coverage_gate.py | \
ContextualWisdomLab/.github:tests/test_materialize_base_javascript_packages.py | \
ContextualWisdomLab/.github:tests/test_opencode_agent_contract.py | \
ContextualWisdomLab/.github:tests/test_opencode_model_pool_runner.py | \
ContextualWisdomLab/.github:tests/test_pr_head_replay_guard.py | \
Expand Down
198 changes: 198 additions & 0 deletions scripts/ci/materialize_base_javascript_packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""Materialize pnpm locks from a validated pull-request base commit."""

from __future__ import annotations

import argparse
import json
import pathlib
import re
import subprocess
import sys
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")


def _git(repo_root: pathlib.Path, *args: str) -> bytes:
"""Run one read-only git command in the materialized repository."""
completed = subprocess.run(
["git", "-C", str(repo_root), *args],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
stderr = completed.stderr.decode("utf-8", errors="replace").strip()
raise RuntimeError(f"git {args[0]} failed: {stderr}")
return completed.stdout


def _regular_base_paths(repo_root: pathlib.Path, base_sha: str) -> set[str]:
"""Return regular blob paths from the exact validated base commit."""
entries = _git(repo_root, "ls-tree", "-r", "-z", "--full-tree", base_sha)
paths: set[str] = set()
for raw_entry in entries.split(b"\0"):
if not raw_entry:
continue
metadata, separator, raw_path = raw_entry.partition(b"\t")
if not separator:
raise RuntimeError("git ls-tree returned a malformed entry")
fields = metadata.split()
if len(fields) != 3:
raise RuntimeError("git ls-tree returned malformed metadata")
mode, object_type, _object_id = (
field.decode("ascii", errors="strict") for field in fields
)
path = raw_path.decode("utf-8", errors="surrogateescape")
candidate = pathlib.PurePosixPath(path)
if (
object_type == "blob"
and mode.startswith("100")
and not candidate.is_absolute()
and ".." not in candidate.parts
):
paths.add(path)
return paths
Comment on lines +34 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the negative-filter branch (non-blob / non-100* mode) is exercised by any test.
rg -n "120000|160000|object_type|symlink|submodule" tests/test_materialize_base_javascript_packages.py

Repository: ContextualWisdomLab/.github

Length of output: 428


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the implementation around the branch in question.
sed -n '1,140p' scripts/ci/materialize_base_javascript_packages.py

printf '\n--- TEST FILE ---\n'

# Show the relevant test sections, including any symlink/submodule/tree-entry cases.
sed -n '1,340p' tests/test_materialize_base_javascript_packages.py

Repository: ContextualWisdomLab/.github

Length of output: 16622


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all references to the helper and tree-entry filtering.
rg -n "_regular_base_paths|ls-tree|120000|160000|submodule|symlink|blob" scripts/ci tests

printf '\n--- TREE OF RELEVANT TESTS ---\n'
fd -a "test_materialize_base_javascript_packages.py|materialize_base_javascript_packages.py" scripts tests

printf '\n--- TARGETED TEST SECTION ---\n'
sed -n '70,170p' tests/test_materialize_base_javascript_packages.py

Repository: ContextualWisdomLab/.github

Length of output: 21008


_regular_base_paths의 필터 제외 경로 테스트를 추가하세요. tests/test_materialize_base_javascript_packages.py는 malformed entry/metadata만 다루고 있어서, mode=120000 symlink나 mode=160000 gitlink처럼 형식은 맞지만 object_type == "blob" and mode.startswith("100")에서 제외되는 엔트리를 직접 커버하지 않습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/materialize_base_javascript_packages.py` around lines 34 - 59,
_regular_base_paths의 유효한 형식이지만 필터에서 제외되는 엔트리를 검증하는 테스트를 추가하세요.
tests/test_materialize_base_javascript_packages.py에서 mode=120000 symlink와
mode=160000 gitlink 엔트리를 포함한 git ls-tree 출력을 구성하고, object_type == "blob" 및
mode.startswith("100") 조건에 따라 해당 경로들이 결과에서 제외되는지 확인하세요.

Source: Coding guidelines



def base_pnpm_projects(
repo_root: pathlib.Path, base_sha: str
) -> list[tuple[str, str, dict[str, bytes]]]:
"""Return exact base pnpm 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)
projects: list[tuple[str, str, dict[str, bytes]]] = []
for lock_path in sorted(
path
for path in regular_paths
if pathlib.PurePosixPath(path).name == "pnpm-lock.yaml"
):
lock = pathlib.PurePosixPath(lock_path)
project_root = lock.parent
package_path = str(project_root / "package.json")
if package_path not in regular_paths:
raise ValueError(
f"trusted base pnpm 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 not isinstance(package_manager, str) or not PNPM_SPEC_RE.fullmatch(
package_manager
):
raise ValueError(
f"trusted base package manifest {package_path} must declare an exact pnpm packageManager version"
)
lock_content = _git(repo_root, "show", f"{base_sha}:{lock_path}")
if not lock_content.strip():
raise ValueError(f"trusted base pnpm lock {lock_path} is empty")

base_inputs = {
input_name: _git(
repo_root,
"show",
f"{base_sha}:{project_root / input_name}",
)
for input_name in PNPM_BASE_INPUT_NAMES
if str(project_root / input_name) in regular_paths
}
base_inputs["pnpm-lock.yaml"] = lock_content

patches_root = project_root / "patches"
for base_path in sorted(regular_paths):
candidate = pathlib.PurePosixPath(base_path)
if candidate == patches_root or patches_root not in candidate.parents:
continue
relative_path = str(candidate.relative_to(project_root))
base_inputs[relative_path] = _git(
repo_root, "show", f"{base_sha}:{base_path}"
)

projects.append((lock_path, package_manager, base_inputs))
return projects


def materialize(
repo_root: pathlib.Path,
base_sha: str,
output_dir: pathlib.Path,
) -> list[dict[str, str]]:
"""Write base pnpm inputs under generated paths safe for a Docker context."""
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)
):
directory = f"project-{index:03d}"
project_dir = output_dir / directory
project_dir.mkdir()
for relative_path, content in sorted(base_inputs.items()):
destination = project_dir / relative_path
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(content)
manifest.append(
{
"directory": directory,
"package_manager": package_manager,
"source": source_path,
}
)

(output_dir / "manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
return manifest


def main(argv: list[str] | None = None) -> int:
"""Materialize base pnpm locks and report the trusted inputs."""
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", required=True, type=pathlib.Path)
parser.add_argument("--base-sha", required=True)
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)
except (OSError, RuntimeError, ValueError) as exc:
print(
f"::error::Could not materialize base JavaScript package locks: {exc}",
file=sys.stderr,
)
return 1

if manifest:
for entry in manifest:
print(
"Materialized trusted base pnpm lock "
f"{entry['source']} for {entry['package_manager']} "
f"as {entry['directory']}/pnpm-lock.yaml."
)
else:
print("No tracked pnpm-lock.yaml files exist at the validated base SHA.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
5 changes: 4 additions & 1 deletion scripts/ci/test_strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$workflow_file" "or fall back to npm" "coverage evidence logs package-runner activation failures instead of silently using npm"
assert_file_not_contains "$workflow_file" '@latest' "coverage evidence refuses mutable package-manager toolchains"
assert_file_contains "$workflow_file" "npm ci --ignore-scripts" "coverage dependency installation suppresses npm lifecycle hooks"
assert_file_contains "$workflow_file" "pnpm install --frozen-lockfile --ignore-scripts" "coverage dependency installation suppresses pnpm lifecycle hooks"
assert_file_contains "$workflow_file" "pnpm offline install" "coverage dependency installation uses a prefetched trusted pnpm store"
assert_file_contains "$workflow_file" "--offline" "coverage dependency installation refuses pnpm registry access"
assert_file_contains "$workflow_file" "--ignore-scripts" "coverage dependency installation suppresses pnpm lifecycle hooks"
assert_file_contains "$workflow_file" "--store-dir /opt/pnpm-store" "coverage dependency installation uses the trusted pnpm store"
assert_file_contains "$workflow_file" "yarn install --immutable --mode=skip-builds" "coverage dependency installation suppresses Yarn build hooks"
assert_file_contains "$workflow_file" "PR-selected dependency manifests are never resolved" "coverage refuses PR-controlled Python dependency resolution entirely"
assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'STRIX_EXECUTABLE_PATH=%s' "Strix workflow captures the pinned installation executable before scanning"
Expand Down
Loading
Loading