From 50d72e39b83892685e4027ce48863d1eab62f19e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 29 Jul 2026 14:36:33 +0900 Subject: [PATCH 1/3] fix(ci): regenerate OpenCode review lock for Python 3.14 --- CLAUDE.md | 5 +- requirements-opencode-review-ci-hashes.txt | 148 +++++++++++++++- ...st_materialize_base_python_requirements.py | 159 ++++++++++++++++++ tests/test_opencode_agent_contract.py | 8 + 4 files changed, 315 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c5a9b80cd..218abffd1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,9 +95,12 @@ e.g.: uv pip compile --generate-hashes --python-version 3.12 --python-platform x86_64-manylinux_2_28 requirements-bandit-ci.txt -o requirements-bandit-ci-hashes.txt uv pip compile --generate-hashes --python-version 3.12 --python-platform x86_64-manylinux_2_28 requirements-pip-audit-ci.txt -o requirements-pip-audit-ci-hashes.txt uv pip compile --generate-hashes --python-version 3.13 --python-platform x86_64-manylinux_2_28 --output-file requirements-strix-ci-hashes.txt requirements-strix-ci.txt +uv pip compile --upgrade --generate-hashes --python-version 3.14 --python-platform x86_64-manylinux_2_28 --output-file requirements-opencode-review-ci-hashes.txt requirements-opencode-review-ci.txt ``` -Note the per-file Python versions differ (bandit/pip-audit: 3.12; strix: 3.13). +Note the per-file Python versions differ (bandit/pip-audit: 3.12; strix: 3.13; OpenCode +review: 3.14). Use `--upgrade` for the OpenCode review lock so an existing output file cannot +preserve hashes from the previous Python target. ## Conventions and gotchas specific to this repo diff --git a/requirements-opencode-review-ci-hashes.txt b/requirements-opencode-review-ci-hashes.txt index 2846355f3..dd22e1c28 100644 --- a/requirements-opencode-review-ci-hashes.txt +++ b/requirements-opencode-review-ci-hashes.txt @@ -1,30 +1,170 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes --python-version 3.14 --python-platform x86_64-manylinux_2_28 --output-file requirements-opencode-review-ci-hashes.txt requirements-opencode-review-ci.txt attrs==26.1.0 \ - --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via interrogate click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 + # via interrogate colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via interrogate coverage==7.14.3 \ + --hash=sha256:0096fd7559178f0cc9cf088f2dbd2a02ef85bacaa69732c633517286b4494610 \ + --hash=sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26 \ + --hash=sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965 \ + --hash=sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a \ + --hash=sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd \ + --hash=sha256:1551b4caac3e3ec9f2bfcec6bf3776e01c0edbdd2e240431a50ca1a1aac72c27 \ + --hash=sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd \ + --hash=sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f \ + --hash=sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5 \ + --hash=sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c \ + --hash=sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e \ + --hash=sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a \ + --hash=sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b \ + --hash=sha256:2e41fd3aab806770008279a93879b0924b16247e09ab537c043d08bbca53b4ab \ + --hash=sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37 \ + --hash=sha256:360bec1f58e7243e3405d3bdf7a1a8115aa9b448d54dc7cd6f7b7e0e9406b62e \ + --hash=sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda \ + --hash=sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845 \ + --hash=sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24 \ + --hash=sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027 \ + --hash=sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92 \ + --hash=sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c \ + --hash=sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977 \ + --hash=sha256:4d310baf69a4fbe8a098ce727e4808a34866ac718a6f759ae659cbd3221358bc \ + --hash=sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889 \ + --hash=sha256:583d50d59142f8549470bd6390471d0fe8b8c8d69d6a0f28ac71e05380cef640 \ + --hash=sha256:5952f8c1bda2a5347154450379316e6dfa4d934d62ca35f6784451e6f55074fb \ + --hash=sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf \ + --hash=sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc \ + --hash=sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5 \ + --hash=sha256:6197e5a00183c11a8ce7c6abd18be1a9189fd8399084ffc95196f4f0db4f2137 \ --hash=sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727 \ - --hash=sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3 + --hash=sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f \ + --hash=sha256:64b2055bb6e0dc945af35cdeceb3633e6ed9273475ef3af85592410fd6803803 \ + --hash=sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f \ + --hash=sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9 \ + --hash=sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0 \ + --hash=sha256:74fdd718d88fe144f4579b8747873a07ec3f04cb837d5faec5a25d9e22fa31a8 \ + --hash=sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc \ + --hash=sha256:7dfe427045520d6abca33687dfef767b4f635015893a1816c5decb12eb72ce18 \ + --hash=sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d \ + --hash=sha256:830c1fca669c572dec37ce9c838224ee45aac5be0f6961edf871e82e49d6537c \ + --hash=sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949 \ + --hash=sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed \ + --hash=sha256:8ac012839ff7e396030f1e94e10553a431d14e4de2ab65cb3acb72bbd5628ca2 \ + --hash=sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336 \ + --hash=sha256:8cf0f2509acb4619e2471a1951089054dd58ebea7a912066d2ea56dd4c24ca4a \ + --hash=sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205 \ + --hash=sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3 \ + --hash=sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665 \ + --hash=sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73 \ + --hash=sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501 \ + --hash=sha256:9a3f142070eb7b82fc4085a55d887396f9c4e21250bccebe2ba22502c45b9647 \ + --hash=sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e \ + --hash=sha256:a090cbf9521e78ffdb2fcf448b72902afe9f5923ff6a12d5c0d0120200348af9 \ + --hash=sha256:a2335ea5fed26af2e831094964fa3f8fae60b45f7e37fcc2d3b615b2add3ad87 \ + --hash=sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7 \ + --hash=sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5 \ + --hash=sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3 \ + --hash=sha256:a64caee2193563601dbaaa55fe2dcf597debef04a2f8f1fa8a07aa4bb7ac7a1e \ + --hash=sha256:ac082660de8f429ba0ea363595abb838998570b9a7546777c60f413ab902bbde \ + --hash=sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994 \ + --hash=sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784 \ + --hash=sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498 \ + --hash=sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7 \ + --hash=sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de \ + --hash=sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388 \ + --hash=sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce \ + --hash=sha256:c66f9f9d4f1e9712eb9b1de5310f881d4e2188cfcba5065e1a8490f38687f2c4 \ + --hash=sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c \ + --hash=sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef \ + --hash=sha256:cc96aa922e21d4bc5d5ed3c915cef27dfcbc13686f47d5e378d647fbfba655a2 \ + --hash=sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f \ + --hash=sha256:d48400185564042287dc487c1f016a3397f18ab4f4c5d5ec36edc218f7ffa35b \ + --hash=sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891 \ + --hash=sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635 \ + --hash=sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb \ + --hash=sha256:e0bb8a6bc7015efdf8a928753b25da1b9ca2d6f24ef04d2ee0688e486f32aae7 \ + --hash=sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5 \ + --hash=sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d \ + --hash=sha256:e574801e1d643561594aa021206c46d80b257e9853087090ba97bed8b0a509d3 \ + --hash=sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150 \ + --hash=sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35 \ + --hash=sha256:eadea7aba74e40adee867a8c0eec17b820b061d308a4b014f7a0e118c2b0aa61 \ + --hash=sha256:ed68faa5e85de2f3e400bc3f122e5c82735a58c8bb24b9f63a2215954ba17b2d \ + --hash=sha256:f0a47095963cfe054e0df178daca95aec21e680d6076da807c3add28dfe920f7 \ + --hash=sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a \ + --hash=sha256:f82b6bb7d75a2613e85d07cefa3a8c973d0544a8993337f6e2728e4a1e94c305 \ + --hash=sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027 \ + --hash=sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8 \ + --hash=sha256:fbb8c3a98e779013786ae01d229662aeacbc77100efbd3f2f245219ace5af700 + # via + # -r requirements-opencode-review-ci.txt + # pytest-cov iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest interrogate==1.7.0 \ + --hash=sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0 \ --hash=sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 + # via -r requirements-opencode-review-ci.txt packaging==26.2 \ - --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via pytest pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via + # pytest + # pytest-cov py==1.11.0 \ + --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + # via interrogate pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via pytest pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via + # -r requirements-opencode-review-ci.txt + # pytest-cov pytest-cov==7.1.0 \ + --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 + # via -r requirements-opencode-review-ci.txt tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via interrogate uv==0.11.25 \ + --hash=sha256:2c1cfe97dce56c997dfa3214bdb8955b7b34cceea7505520185e22ad99c0eb6b \ + --hash=sha256:3febca65ec5bc336ddaf7e4f724704f2c894c16839723df14865ee00b4acf38d \ + --hash=sha256:41b37e724f41eb4c3794bbdd82ddeebb4b5850d4ada8cccb2906ef9e5aa0f83b \ + --hash=sha256:458e731778e7b5cc870710397859c23e766703e7bc0695f23b3eb15080745ba6 \ + --hash=sha256:560b0fbaa6356af533923a349658c21d4f410d16e835787d8a05da451d4ee859 \ + --hash=sha256:57fbd47e924242fd347d0c209d95711d8ea61db8d8780962d0f30ccde2c854a3 \ + --hash=sha256:610650cbaa0a9b18015da39d2c28d736d287a5a124e49296d8fdef5e4022e980 \ + --hash=sha256:61ef11d9967a38109e6e8e3d20d1f743fa08033c32bce274d6ccd9a9abb5d305 \ + --hash=sha256:69d14ffd0a4b050f8a70f64aacb09b8dfdfb1cb30a6351fb17b48f273f95c58c \ + --hash=sha256:79f166cd1b84f855e9d2768221d59b403869648289fd884d58ad4299edfb4d9e \ + --hash=sha256:850ba0018ff170c3a9baaf9b5fe8b23393b6b77ee4ea6b2e2315fdb8d7c388f7 \ + --hash=sha256:86d4759fec9b46f61944d6e9ef1f5eaa2c5fbe2db5ddb59492d9174b08fcf39c \ + --hash=sha256:b180b12237b4e04692491fc6796584a9a8bdf4c7332bd2a769caf096b97885d0 \ --hash=sha256:d2bc05e17ae3e1f232abf93e7dcfb3b68702dfcde34a00c29cbce7e07d1ecbfb \ - --hash=sha256:560b0fbaa6356af533923a349658c21d4f410d16e835787d8a05da451d4ee859 + --hash=sha256:d6f965a79fc7539a12139ce981caa0cbf7d9d3bd4ea3daadaf174ab4d7fb6e42 \ + --hash=sha256:e3480640983e0b8e509eeb67882837e620bdd820f8776948a5f13ebbb4481d04 \ + --hash=sha256:f42de9e7d63a28a4fe76a522077813656de38b5acda20b4db63857d260c1ff13 \ + --hash=sha256:f7a78fc8d0c5e764e9fa39c99066db47a0bc465b023feed90812e3c0a6b5eb0d \ + --hash=sha256:fbff70ae9fa4da9fb6823ae4fdaf77a65c9520e13b6d1d0241ba56e4b121b7aa + # via -r requirements-opencode-review-ci.txt diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 74f585285..c1475f4d4 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -1,6 +1,8 @@ from __future__ import annotations +import runpy import subprocess +import sys from pathlib import Path import pytest @@ -70,3 +72,160 @@ def test_rejects_invalid_base_sha(tmp_path: Path) -> None: """Git options and symbolic refs cannot cross the exact-SHA boundary.""" with pytest.raises(ValueError, match="40 hexadecimal"): materializer.base_hash_locks(tmp_path, "--help") + + +def test_git_failure_preserves_a_visible_reason(tmp_path: Path) -> None: + """A failed read-only git command reports its operation and stderr.""" + with pytest.raises(RuntimeError, match=r"git rev-parse failed: fatal"): + materializer._git(tmp_path, "rev-parse", "HEAD") + + +@pytest.mark.parametrize( + ("tree_output", "message"), + [ + (b"malformed\0", "malformed entry"), + (b"100644 blob\tfile\0", "malformed metadata"), + ], +) +def test_rejects_malformed_git_tree_entries( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + tree_output: bytes, + message: str, +) -> None: + """Malformed git output cannot be interpreted as a trusted lock blob.""" + + def fake_git(_repo_root: Path, *_args: str) -> bytes: + return tree_output + + monkeypatch.setattr(materializer, "_git", fake_git) + + with pytest.raises(RuntimeError, match=message): + materializer.base_hash_locks(tmp_path, "a" * 40) + + +def test_rejects_symlink_output_directory( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A symlink cannot redirect trusted lock materialization outside its context.""" + target = tmp_path / "target" + target.mkdir() + output = tmp_path / "output" + output.symlink_to(target, target_is_directory=True) + monkeypatch.setattr(materializer, "base_hash_locks", lambda *_args: []) + + with pytest.raises(ValueError, match="must not be a symlink"): + materializer.materialize(tmp_path, "a" * 40, output) + + +def test_main_reports_each_materialized_lock( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """The CLI identifies the exact trusted source and generated lock name.""" + + def fake_materialize( + _repo_root: Path, _base_sha: str, _output_dir: Path + ) -> list[dict[str, str]]: + return [{"file": "requirements-000.txt", "source": "backend/requirements-hashes.txt"}] + + monkeypatch.setattr(materializer, "materialize", fake_materialize) + + assert ( + materializer.main( + [ + "--repo-root", + str(tmp_path), + "--base-sha", + "a" * 40, + "--output-dir", + str(tmp_path / "output"), + ] + ) + == 0 + ) + assert ( + "Materialized trusted base Python lock backend/requirements-hashes.txt " + "as requirements-000.txt." in capsys.readouterr().out + ) + + +def test_main_reports_when_no_locks_exist( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """The CLI distinguishes an empty trusted base from a failed extraction.""" + monkeypatch.setattr(materializer, "materialize", lambda *_args: []) + + assert ( + materializer.main( + [ + "--repo-root", + str(tmp_path), + "--base-sha", + "a" * 40, + "--output-dir", + str(tmp_path / "output"), + ] + ) + == 0 + ) + assert "No tracked requirements-hashes.txt files exist" in capsys.readouterr().out + + +def test_main_fails_with_the_materialization_reason( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """A materialization exception fails closed and remains diagnosable in CI.""" + + def fail_materialize(_repo_root: Path, _base_sha: str, _output_dir: Path) -> None: + raise OSError("fixture failure") + + monkeypatch.setattr(materializer, "materialize", fail_materialize) + + assert ( + materializer.main( + [ + "--repo-root", + str(tmp_path), + "--base-sha", + "a" * 40, + "--output-dir", + str(tmp_path / "output"), + ] + ) + == 1 + ) + assert ( + "::error::Could not materialize base Python locks: fixture failure" + in capsys.readouterr().err + ) + + +def test_script_entrypoint_exits_through_main( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The executable script propagates the fail-closed CLI status.""" + module_path = Path(materializer.__file__) + monkeypatch.setattr( + sys, + "argv", + [ + str(module_path), + "--repo-root", + str(tmp_path), + "--base-sha", + "invalid", + "--output-dir", + str(tmp_path / "output"), + ], + ) + + with pytest.raises(SystemExit) as raised: + runpy.run_path(str(module_path), run_name="__main__") + + assert raised.value.code == 1 diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 4951d41a9..38fe1834e 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -425,6 +425,14 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): "a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" in trusted_requirements ) + assert ( + "--python-version 3.14 --python-platform x86_64-manylinux_2_28" + in trusted_requirements + ) + assert ( + "1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5" + in trusted_requirements + ) target_start = workflow.index(" opencode-review-target:\n") target_job = workflow[target_start:] From 5196543250f3d9a10b3a2db49f73d868349554d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 29 Jul 2026 14:42:33 +0900 Subject: [PATCH 2/3] fix(ci): ignore central review placeholder checks --- scripts/ci/pr_review_merge_scheduler.py | 5 ++++ tests/test_pr_review_merge_scheduler.py | 40 ++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 48e5c2430..b6b9b12ed 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -936,6 +936,11 @@ def context_nodes(pr: dict[str, Any]) -> list[dict[str, Any]]: def is_opencode_context(node: dict[str, Any]) -> bool: """Return whether a check or status context belongs to OpenCode Review.""" if node.get("__typename") == "CheckRun": + if (os.environ.get("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY") or "").strip(): + # Central reviews run through repository_dispatch and publish a commit + # status. Organization required-workflow CheckRuns are deliberately + # non-authoritative placeholders and must not suppress that dispatch. + return False workflow = ( ((node.get("checkSuite") or {}).get("workflowRun") or {}).get("workflow") or {} diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 30dcb4d57..5c787e51c 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -903,7 +903,8 @@ def test_cancel_stale_opencode_runs_dry_run_skips_lookup_and_mutation(monkeypatc assert calls == [] -def test_context_review_and_check_helpers(): +def test_context_review_and_check_helpers(monkeypatch): + monkeypatch.delenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", raising=False) assert sched.context_nodes({}) == [] assert sched.context_nodes(make_pr()) == [] assert sched.compare_behind_by({"compareBehindBy": "2"}) == 2 @@ -1045,6 +1046,43 @@ def test_context_review_and_check_helpers(): assert not sched.is_opencode_review(opencode_review(login="human")) +def test_central_progress_ignores_required_workflow_checkrun_placeholder( + monkeypatch, +): + """Central dispatch trusts its status context, not injected placeholder jobs.""" + monkeypatch.setenv( + "SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", + "ContextualWisdomLab/.github", + ) + placeholder = make_pr( + statusCheckRollup={"contexts": {"nodes": [opencode_check()]}} + ) + central_status = make_pr( + statusCheckRollup={ + "contexts": { + "nodes": [ + opencode_check(), + { + "__typename": "StatusContext", + "context": "opencode-review", + "state": "PENDING", + }, + ] + } + } + ) + + assert not sched.is_opencode_context(opencode_check()) + assert ( + sched.opencode_progress_state(placeholder, stale_after_minutes=45) + == "absent" + ) + assert ( + sched.opencode_progress_state(central_status, stale_after_minutes=45) + == "running" + ) + + def test_review_state_and_failed_checks(): pr = make_pr(reviews={"nodes": [opencode_review("APPROVED", "old"), opencode_review("APPROVED", "head")]}) assert sched.current_head_review_state(pr, "APPROVED") From a818e3372a23a49caaccb180a34a1a506e164cef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 29 Jul 2026 14:44:56 +0900 Subject: [PATCH 3/3] chore(ci): make OpenCode lock regeneration repeatable --- CLAUDE.md | 7 ++++--- requirements-opencode-review-ci-hashes.txt | 2 +- scripts/ci/compile_opencode_review_lock.sh | 14 ++++++++++++++ tests/test_opencode_agent_contract.py | 10 +++++++++- 4 files changed, 28 insertions(+), 5 deletions(-) create mode 100755 scripts/ci/compile_opencode_review_lock.sh diff --git a/CLAUDE.md b/CLAUDE.md index 218abffd1..1c7bdb2f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,12 +95,13 @@ e.g.: uv pip compile --generate-hashes --python-version 3.12 --python-platform x86_64-manylinux_2_28 requirements-bandit-ci.txt -o requirements-bandit-ci-hashes.txt uv pip compile --generate-hashes --python-version 3.12 --python-platform x86_64-manylinux_2_28 requirements-pip-audit-ci.txt -o requirements-pip-audit-ci-hashes.txt uv pip compile --generate-hashes --python-version 3.13 --python-platform x86_64-manylinux_2_28 --output-file requirements-strix-ci-hashes.txt requirements-strix-ci.txt -uv pip compile --upgrade --generate-hashes --python-version 3.14 --python-platform x86_64-manylinux_2_28 --output-file requirements-opencode-review-ci-hashes.txt requirements-opencode-review-ci.txt +./scripts/ci/compile_opencode_review_lock.sh ``` Note the per-file Python versions differ (bandit/pip-audit: 3.12; strix: 3.13; OpenCode -review: 3.14). Use `--upgrade` for the OpenCode review lock so an existing output file cannot -preserve hashes from the previous Python target. +review: 3.14). The OpenCode review generator always passes `--upgrade` so an existing output +file cannot preserve hashes from the previous Python target, and records itself as the lock's +repeatable compile command. ## Conventions and gotchas specific to this repo diff --git a/requirements-opencode-review-ci-hashes.txt b/requirements-opencode-review-ci-hashes.txt index dd22e1c28..60ebe9671 100644 --- a/requirements-opencode-review-ci-hashes.txt +++ b/requirements-opencode-review-ci-hashes.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --generate-hashes --python-version 3.14 --python-platform x86_64-manylinux_2_28 --output-file requirements-opencode-review-ci-hashes.txt requirements-opencode-review-ci.txt +# ./scripts/ci/compile_opencode_review_lock.sh attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 diff --git a/scripts/ci/compile_opencode_review_lock.sh b/scripts/ci/compile_opencode_review_lock.sh new file mode 100755 index 000000000..dd4aa21c8 --- /dev/null +++ b/scripts/ci/compile_opencode_review_lock.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)" +cd "$repo_root" + +uv pip compile \ + --upgrade \ + --generate-hashes \ + --python-version 3.14 \ + --python-platform x86_64-manylinux_2_28 \ + --custom-compile-command "./scripts/ci/compile_opencode_review_lock.sh" \ + --output-file requirements-opencode-review-ci-hashes.txt \ + requirements-opencode-review-ci.txt diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 38fe1834e..3934b71e3 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -420,14 +420,22 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): trusted_requirements = Path( "requirements-opencode-review-ci-hashes.txt" ).read_text(encoding="utf-8") + compile_script = Path( + "scripts/ci/compile_opencode_review_lock.sh" + ).read_text(encoding="utf-8") + normalized_compile_script = " ".join(compile_script.replace("\\\n", " ").split()) assert "pytest-cov==7.1.0" in trusted_requirements assert ( "a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" in trusted_requirements ) + assert "./scripts/ci/compile_opencode_review_lock.sh" in trusted_requirements + assert "uv pip compile" in compile_script + assert "--upgrade" in compile_script + assert "--generate-hashes" in compile_script assert ( "--python-version 3.14 --python-platform x86_64-manylinux_2_28" - in trusted_requirements + in normalized_compile_script ) assert ( "1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5"