From 5d1ca2d2061c46fd931125682df3bbd41d690683 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:40:43 +0000 Subject: [PATCH 1/2] chore: skip ux enhancement due to no ui codebase From 04d173a35dc3dc4d3c799c635e745b38ac005ca9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 4 Jul 2026 04:09:18 +0000 Subject: [PATCH 2/2] fix(opencode): accept empty changed files for empty PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이 커밋은 변경된 파일이 없는 PR(Empty PR)을 처리할 때 OpenCode 리뷰 모델 결과가 정상적으로 정규화되도록 `opencode_review_normalize_output.py` 스크립트를 수정합니다. 기존 코드는 `OPENCODE_CHANGED_FILES_FILE` 환경변수가 주어지더라도 변경 파일이 없는 경우, 변경 파일 정보가 부족하다며 `valid_control` 과정에서 None을 반환하고 `NO_CONCLUSION` 오류를 유발하여 리뷰가 시간 초과되는 문제가 있었습니다. 이번 패치에서는 파일이 없거나 비어있는 경우를 정확히 식별(None 반환 vs 빈 set 반환)하도록 `current_changed_files`를 개선하고, `mentions_actual_changed_file`과 `build_approval_repair_summary` 함수에서 예외 처리를 통해 에러 없이 `APPROVE`를 허용하도록 보완했습니다. 관련 테스트 케이스도 100% 커버리지를 보장하도록 추가되었습니다. --- .../ci/opencode_review_normalize_output.py | 34 +++++++++----- .../test_opencode_review_normalize_output.py | 45 ++++++++++++++++++- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index c7cbeb63..c6cc0450 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -315,21 +315,26 @@ def mentions_changed_file_evidence(reason: str, summary: str) -> bool: return bool(CHANGED_FILE_EVIDENCE_PATTERN.search(f"{reason}\n{summary}")) -def current_changed_files() -> set[str]: - """Return the exact current-head changed files when the workflow provides them.""" +def current_changed_files() -> set[str] | None: + """Return the exact current-head changed files when the workflow provides them, or None if unavailable.""" changed_files_path = os.environ.get("OPENCODE_CHANGED_FILES_FILE") if not changed_files_path: - return set() + return None + + path = Path(changed_files_path) + if not path.is_file(): + return None + try: return { line.strip() - for line in Path(changed_files_path) + for line in path .read_text(encoding="utf-8") .splitlines() if line.strip() } except OSError: - return set() + return None def changed_file_is_source_like(path: str) -> bool: @@ -404,8 +409,10 @@ def contradicts_material_changed_file_scope(reason: str, summary: str) -> bool: def mentions_actual_changed_file(reason: str, summary: str) -> bool: """Return whether an approval names an exact current-head changed file.""" changed_files = current_changed_files() - if not changed_files: + if changed_files is None: return mentions_changed_file_evidence(reason, summary) + if not changed_files: + return True combined = f"{reason}\n{summary}" return any(changed_file in combined for changed_file in changed_files) @@ -572,13 +579,18 @@ def build_approval_repair_summary(summary: str, evidence_text: str) -> str | Non """Append missing approval labels from bounded current-head evidence.""" changed_files = changed_files_from_evidence(evidence_text) coverage_mode = evidence_coverage_mode(evidence_text) - if not changed_files or coverage_mode is None: + if coverage_mode is None: return None - first_file = changed_files[0] - file_list = ", ".join(changed_files[:5]) - if len(changed_files) > 5: - file_list += f", and {len(changed_files) - 5} more" + if not changed_files: + first_file = "no-files-changed" + file_list = "no files changed" + else: + first_file = changed_files[0] + file_list = ", ".join(changed_files[:5]) + if len(changed_files) > 5: + file_list += f", and {len(changed_files) - 5} more" + if coverage_mode == "not_applicable": coverage_line = ( "Coverage: coverage execution evidence reports test coverage as not applicable " diff --git a/tests/test_opencode_review_normalize_output.py b/tests/test_opencode_review_normalize_output.py index fcc7d37d..b7e3ac54 100644 --- a/tests/test_opencode_review_normalize_output.py +++ b/tests/test_opencode_review_normalize_output.py @@ -92,7 +92,7 @@ def test_changed_file_and_verification_posture_detection(): def test_actual_changed_file_detection_prefers_current_head_file_list(tmp_path, monkeypatch): monkeypatch.delenv("OPENCODE_CHANGED_FILES_FILE", raising=False) - assert norm.current_changed_files() == set() + assert norm.current_changed_files() is None assert norm.mentions_actual_changed_file("scripts/ci/example.py", "") changed_files = tmp_path / "changed-files.txt" @@ -126,9 +126,27 @@ def test_actual_changed_file_detection_prefers_current_head_file_list(tmp_path, ) monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(tmp_path / "missing.txt")) - assert norm.current_changed_files() == set() + assert norm.current_changed_files() is None assert norm.mentions_actual_changed_file("scripts/ci/example.py", "") + empty_files = tmp_path / "empty-files.txt" + empty_files.write_text("", encoding="utf-8") + monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(empty_files)) + assert norm.current_changed_files() == set() + assert norm.mentions_actual_changed_file("No files changed", "Empty PR") + + dir_path = tmp_path / "a_directory" + dir_path.mkdir() + monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(dir_path)) + assert norm.current_changed_files() is None + + def raise_os_error(*args, **kwargs): + raise OSError("Permission denied") + + monkeypatch.setattr(norm.Path, "read_text", raise_os_error) + monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(changed_files)) + assert norm.current_changed_files() is None + def test_preferred_review_language_handles_unreadable_and_unknown_evidence(tmp_path, monkeypatch): evidence = tmp_path / "evidence.md" @@ -485,6 +503,29 @@ def test_valid_control_filters_shape_head_and_review_contract(): def test_valid_control_repairs_approval_summary_from_bounded_evidence(tmp_path, monkeypatch): + evidence = tmp_path / "bounded-review-evidence-empty.md" + evidence.write_text( + """\ +# OpenCode bounded PR review evidence + +## Coverage execution evidence + +# Coverage Evidence + +## Coverage Decision + +- Result: PASS +- Test coverage: not applicable because no supported changed source files or package manifests were found. +- Docstring coverage: not applicable + +## Changed files +""", + encoding="utf-8", + ) + monkeypatch.setenv("OPENCODE_EVIDENCE_FILE", str(evidence)) + monkeypatch.delenv("OPENCODE_CHANGED_FILES_FILE", raising=False) + assert norm.build_approval_repair_summary("", evidence.read_text(encoding="utf-8")) is not None + evidence = tmp_path / "bounded-review-evidence.md" evidence.write_text( """\