Skip to content

Commit 4c508ed

Browse files
fix(release): reject ignored runtime source and bytecode
1 parent 1ce23c0 commit 4c508ed

2 files changed

Lines changed: 61 additions & 19 deletions

File tree

‎scripts/check_release_readiness.py‎

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@
3232
_COMMIT = re.compile(r"[a-f0-9]{40}\Z")
3333
_MAX_BYTES = 8 * 1024 * 1024
3434
_EXECUTABLE_SUFFIXES = frozenset({
35-
".dll", ".dylib", ".exe", ".node", ".pyd", ".pyc", ".pyo", ".so",
35+
# Source files are included because an ignored module such as
36+
# ``sitecustomize.py`` executes before the candidate package and can alter
37+
# imports even when the tracked tree is clean.
38+
".bat", ".cjs", ".cmd", ".css", ".dll", ".dylib", ".exe", ".html",
39+
".js", ".jsx", ".mjs", ".node", ".ps1", ".pyd", ".py", ".pyc", ".pyo",
40+
".pyw", ".sh", ".so", ".ts", ".tsx",
3641
})
3742
_IGNORED_RUNTIME_DIRS = frozenset({
3843
".codex-pytest-tmp", ".hosted-eval-results", ".playwright", ".private-eval",
@@ -87,18 +92,12 @@ def _ignored_executable_paths(root: Path) -> list[str]:
8792
"""Return ignored executable artifacts that can affect a source checkout.
8893
8994
``git status`` deliberately hides ignored files. Release qualification may
90-
import source from the checkout, so an ignored bytecode/native artifact can
91-
change behavior even when the tracked tree is clean. Standard tool caches
92-
are excluded; bytecode under ``__pycache__`` is allowed only when it maps to
93-
a tracked Python source file.
95+
import source from the checkout, so an ignored source, bytecode, native
96+
artifact or runtime script can change behavior even when the tracked tree
97+
is clean. Standard tool/runtime directories are excluded because they are
98+
not candidate source paths; all matching artifacts elsewhere fail closed,
99+
including bytecode under ``__pycache__``.
94100
"""
95-
tracked = {
96-
Path(os.fsdecode(entry)).as_posix()
97-
for entry in subprocess.check_output(
98-
["git", "ls-files", "-z"], cwd=root, stderr=subprocess.DEVNULL
99-
).split(b"\0")
100-
if entry
101-
}
102101
ignored = subprocess.check_output(
103102
["git", "ls-files", "--others", "--ignored", "--exclude-standard", "-z"],
104103
cwd=root, stderr=subprocess.DEVNULL,
@@ -113,13 +112,6 @@ def _ignored_executable_paths(root: Path) -> list[str]:
113112
parts = {part.lower() for part in relative.parts}
114113
if parts & _IGNORED_RUNTIME_DIRS:
115114
continue
116-
if "__pycache__" in parts:
117-
cache_index = next(index for index, part in enumerate(relative.parts)
118-
if part.lower() == "__pycache__")
119-
stem = relative.name.split(".", 1)[0]
120-
source = Path(*relative.parts[:cache_index]) / (stem + ".py")
121-
if source.as_posix() in tracked:
122-
continue
123115
suspicious.append(relative.as_posix())
124116
return suspicious
125117

‎tests/test_product_release_readiness.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""A checklist label cannot bypass missing, stale or contradictory evidence."""
22
import hashlib
3+
import importlib.util
34
import json
5+
import py_compile
46
import subprocess
57

68
import pytest
@@ -271,6 +273,54 @@ def test_ignored_executable_artifact_cannot_qualify_candidate(ledger, tmp_path):
271273
assert not result["valid"]
272274

273275

276+
def test_ignored_importable_source_cannot_qualify_candidate(ledger, tmp_path):
277+
repository = tmp_path / "ignored-source-repo"
278+
repository.mkdir()
279+
(repository / ".gitignore").write_text("sitecustomize.py\n", encoding="utf-8")
280+
(repository / "engine.py").write_text("original", encoding="utf-8")
281+
subprocess.run(["git", "init", "--quiet", str(repository)], check=True)
282+
subprocess.run(["git", "add", ".gitignore", "engine.py"], cwd=repository, check=True)
283+
subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid",
284+
"commit", "--quiet", "-m", "fixture"], cwd=repository, check=True)
285+
ledger["components"]["engine"]["commit"] = subprocess.check_output(
286+
["git", "rev-parse", "HEAD"], cwd=repository, text=True).strip()
287+
ledger["candidate_id"] = candidate_id(ledger["components"])
288+
for name in RELEASE_GATES:
289+
pass_gate(ledger, tmp_path, name)
290+
(repository / "sitecustomize.py").write_text("raise RuntimeError('ignored')\n", encoding="utf-8")
291+
result = validate(ledger, tmp_path, engine_root=repository)
292+
assert not result["engine_checkout_verified"]
293+
assert any("sitecustomize.py" in error for error in result["errors"])
294+
assert not result["valid"]
295+
296+
297+
def test_unchecked_hash_bytecode_cannot_qualify_candidate(ledger, tmp_path):
298+
repository = tmp_path / "unchecked-bytecode-repo"
299+
repository.mkdir()
300+
(repository / ".gitignore").write_text("__pycache__/\n", encoding="utf-8")
301+
source = repository / "engine.py"
302+
source.write_text("value = 'tracked'\n", encoding="utf-8")
303+
subprocess.run(["git", "init", "--quiet", str(repository)], check=True)
304+
subprocess.run(["git", "add", ".gitignore", "engine.py"], cwd=repository, check=True)
305+
subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid",
306+
"commit", "--quiet", "-m", "fixture"], cwd=repository, check=True)
307+
ledger["components"]["engine"]["commit"] = subprocess.check_output(
308+
["git", "rev-parse", "HEAD"], cwd=repository, text=True).strip()
309+
ledger["candidate_id"] = candidate_id(ledger["components"])
310+
for name in RELEASE_GATES:
311+
pass_gate(ledger, tmp_path, name)
312+
py_compile.compile(
313+
str(source),
314+
cfile=importlib.util.cache_from_source(str(source)),
315+
doraise=True,
316+
invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH,
317+
)
318+
result = validate(ledger, tmp_path, engine_root=repository)
319+
assert not result["engine_checkout_verified"]
320+
assert any("__pycache__" in error for error in result["errors"])
321+
assert not result["valid"]
322+
323+
274324
@pytest.mark.parametrize("flag", ["--assume-unchanged", "--skip-worktree"])
275325
def test_hidden_git_changes_cannot_qualify_candidate(ledger, tmp_path, flag):
276326
repository = tmp_path / "hidden-repo"

0 commit comments

Comments
 (0)