From 079f6c1567bcf1e582d966f7f27ad77f6a54d54d Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Mon, 7 Sep 2026 21:25:00 +0200 Subject: [PATCH] fix(pre1): retain required build stages and bounded command diagnostics --- scripts/build_pre1_candidate_artifacts.py | 303 +++++++++++----------- scripts/pre1_artifact_common.py | 117 ++++++++- scripts/test_pre1_candidate_artifacts.py | 3 + scripts/test_pre1_command_observation.py | 55 ++++ 4 files changed, 330 insertions(+), 148 deletions(-) create mode 100644 scripts/test_pre1_command_observation.py diff --git a/scripts/build_pre1_candidate_artifacts.py b/scripts/build_pre1_candidate_artifacts.py index 4596c77..d004012 100644 --- a/scripts/build_pre1_candidate_artifacts.py +++ b/scripts/build_pre1_candidate_artifacts.py @@ -5,6 +5,7 @@ import argparse import json +import os import shutil import sys import tempfile @@ -24,6 +25,8 @@ } +REQUIRED_STEPS = ['dependencies', 'locked-tests', 'package-cache', 'online-install', 'offline-install', 'publish-fragment'] + def describe() -> dict: return { "schema": "iicp.pre1-artifact-builder-description.v1", @@ -31,6 +34,7 @@ def describe() -> dict: "targets": sorted(TARGETS), "artifact_identities": [["wheel", "any"], ["sdist", "any"]], "gates": sorted(common.GATES), + "required_steps": REQUIRED_STEPS, "requires_clean_source": True, "non_authorizing": True, } @@ -57,152 +61,159 @@ def build(destination: Path, requested_target: str | None) -> dict: staging = run_root / "fragment" staging.mkdir() try: - common.run(["uv", "sync", "--locked", "--extra", "dev"], ROOT) - common.run(["uv", "run", "--locked", "--extra", "dev", "pytest", "-q"], ROOT) - dist = run_root / "dist" - dist.mkdir() - common.run( - [ - "uv", - "run", - "--locked", - "--extra", - "dev", - "python", - "-m", - "build", - "--outdir", - str(dist), - ], - ROOT, - ) - wheels = list(dist.glob("*.whl")) - sdists = list(dist.glob("*.tar.gz")) - if len(wheels) != 1 or len(sdists) != 1: - raise ValueError("Python build did not produce exactly one wheel and one sdist") - wheel, sdist = wheels[0], sdists[0] - - requirements = run_root / "requirements.txt" - common.run( - [ - "uv", - "export", - "--locked", - "--no-dev", - "--no-emit-project", - "--format", - "requirements-txt", - "--output-file", - str(requirements), - ], - ROOT, - ) - wheelhouse = run_root / "wheelhouse" - wheelhouse.mkdir() - common.run( - [ - sys.executable, - "-m", - "pip", - "download", - "--disable-pip-version-check", - "--dest", - str(wheelhouse), - "--requirement", - str(requirements), - ], - ROOT, - ) - - online = run_root / "online" - common.run([sys.executable, "-m", "venv", str(online)], ROOT) - common.run( - [ - str(venv_python(online)), - "-m", - "pip", - "install", - "--disable-pip-version-check", - "--require-hashes", - "--requirement", - str(requirements), - ], - ROOT, - ) - common.run( - [ - str(venv_python(online)), - "-m", - "pip", - "install", - "--disable-pip-version-check", - "--no-deps", - str(wheel), - ], - ROOT, - ) - online_version = common.output([str(venv_cli(online)), "--version"], ROOT) - if version not in online_version: - raise ValueError("online Python package self-report differs") - - offline = run_root / "offline" - common.run([sys.executable, "-m", "venv", str(offline)], ROOT) - common.run( - [ - str(venv_python(offline)), - "-m", - "pip", - "install", - "--disable-pip-version-check", - "--no-index", - "--find-links", - str(wheelhouse), - "--require-hashes", - "--requirement", - str(requirements), - ], - ROOT, - ) - common.run( - [ - str(venv_python(offline)), - "-m", - "pip", - "install", - "--disable-pip-version-check", - "--no-index", - "--no-deps", - str(wheel), - ], - ROOT, - ) - offline_version = common.output([str(venv_cli(offline)), "--version"], ROOT) - if offline_version != online_version or version not in offline_version: - raise ValueError("offline Python package self-report differs") - - copied_wheel = staging / wheel.name - copied_sdist = staging / sdist.name - shutil.copyfile(wheel, copied_wheel) - shutil.copyfile(sdist, copied_sdist) - fragment = common.emit_fragment( - staging, - component=COMPONENT, - source_commit=commit, - source_version=version, - build_target=target, - artifacts=[ - common.artifact("wheel", "any", copied_wheel), - common.artifact("sdist", "any", copied_sdist), - ], - lock_inputs_sha256=common.files_sha256(ROOT, [ROOT / "pyproject.toml", ROOT / "uv.lock"]), - dependency_cache_sha256=common.tree_sha256(wheelhouse), - toolchains={ - "python": common.output([sys.executable, "--version"], ROOT), - "uv": common.output(["uv", "--version"], ROOT), - }, - ) - common.publish_staging(staging, destination) - return fragment + steps = common.RequiredSteps(Path(os.environ.get("IICP_PRE1_REQUIRED_STEP_PATH", str(run_root / "required-steps.json"))), COMPONENT, commit, target, REQUIRED_STEPS) + with steps.step("dependencies"): + common.run(["uv", "sync", "--locked", "--extra", "dev"], ROOT) + with steps.step("locked-tests"): + common.run(["uv", "run", "--locked", "--extra", "dev", "pytest", "-q"], ROOT) + with steps.step("package-cache"): + dist = run_root / "dist" + dist.mkdir() + common.run( + [ + "uv", + "run", + "--locked", + "--extra", + "dev", + "python", + "-m", + "build", + "--outdir", + str(dist), + ], + ROOT, + ) + wheels = list(dist.glob("*.whl")) + sdists = list(dist.glob("*.tar.gz")) + if len(wheels) != 1 or len(sdists) != 1: + raise ValueError("Python build did not produce exactly one wheel and one sdist") + wheel, sdist = wheels[0], sdists[0] + + requirements = run_root / "requirements.txt" + common.run( + [ + "uv", + "export", + "--locked", + "--no-dev", + "--no-emit-project", + "--format", + "requirements-txt", + "--output-file", + str(requirements), + ], + ROOT, + ) + wheelhouse = run_root / "wheelhouse" + wheelhouse.mkdir() + common.run( + [ + sys.executable, + "-m", + "pip", + "download", + "--disable-pip-version-check", + "--dest", + str(wheelhouse), + "--requirement", + str(requirements), + ], + ROOT, + ) + + with steps.step("online-install"): + online = run_root / "online" + common.run([sys.executable, "-m", "venv", str(online)], ROOT) + common.run( + [ + str(venv_python(online)), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--require-hashes", + "--requirement", + str(requirements), + ], + ROOT, + ) + common.run( + [ + str(venv_python(online)), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-deps", + str(wheel), + ], + ROOT, + ) + online_version = common.output([str(venv_cli(online)), "--version"], ROOT) + if version not in online_version: + raise ValueError("online Python package self-report differs") + + with steps.step("offline-install"): + offline = run_root / "offline" + common.run([sys.executable, "-m", "venv", str(offline)], ROOT) + common.run( + [ + str(venv_python(offline)), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-index", + "--find-links", + str(wheelhouse), + "--require-hashes", + "--requirement", + str(requirements), + ], + ROOT, + ) + common.run( + [ + str(venv_python(offline)), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-index", + "--no-deps", + str(wheel), + ], + ROOT, + ) + offline_version = common.output([str(venv_cli(offline)), "--version"], ROOT) + if offline_version != online_version or version not in offline_version: + raise ValueError("offline Python package self-report differs") + + with steps.step("publish-fragment"): + copied_wheel = staging / wheel.name + copied_sdist = staging / sdist.name + shutil.copyfile(wheel, copied_wheel) + shutil.copyfile(sdist, copied_sdist) + fragment = common.emit_fragment( + staging, + component=COMPONENT, + source_commit=commit, + source_version=version, + build_target=target, + artifacts=[ + common.artifact("wheel", "any", copied_wheel), + common.artifact("sdist", "any", copied_sdist), + ], + lock_inputs_sha256=common.files_sha256(ROOT, [ROOT / "pyproject.toml", ROOT / "uv.lock"]), + dependency_cache_sha256=common.tree_sha256(wheelhouse), + toolchains={ + "python": common.output([sys.executable, "--version"], ROOT), + "uv": common.output(["uv", "--version"], ROOT), + }, + ) + common.publish_staging(staging, destination) + return fragment finally: common.clean_failed_staging(run_root) diff --git a/scripts/pre1_artifact_common.py b/scripts/pre1_artifact_common.py index 27d3890..116c38b 100755 --- a/scripts/pre1_artifact_common.py +++ b/scripts/pre1_artifact_common.py @@ -3,6 +3,8 @@ from __future__ import annotations +import contextlib +import uuid import hashlib import json import os @@ -117,12 +119,60 @@ def safe_output(path: Path) -> None: cursor = cursor.parent +def command_operation(argv: list[str], env: dict[str, str] | None) -> str: + groups = [(('--version',), 'version'), (('pytest', 'test'), 'test'), + (('install',), 'install'), (('vendor',), 'vendor'), + (('package', 'pack'), 'package'), (('build',), 'build')] + operation = next((label for tokens, label in groups if any(token in argv for token in tokens)), 'prepare') + if operation == 'install': + offline = '--offline' in argv or '--no-index' in argv or (env or {}).get('npm_config_offline') == 'true' + return 'install-offline' if offline else 'install-online' + return operation + + +def command_step(argv: list[str], env: dict[str, str] | None = None) -> list[str]: + """Classify known builder argv without putting paths or values in events.""" + name = Path(argv[0]).stem if argv else '' + tools = {'cargo': 'cargo', 'npm': 'npm', 'node': 'npm', + 'python': 'python', 'python3': 'python', 'uv': 'python'} + return [tools.get(name, 'other'), command_operation(argv, env)] + + +def emit_command_step(identity: str, command: list[str], state: str, code=None) -> None: + from datetime import datetime, UTC + import sys + value = {"schema": "iicp.pre1-build-step-event.v1", "step_id": identity, + "command": command, "state": state, "exit_code": code, + "observed_at": datetime.now(UTC).isoformat()} + try: + print("IICP_BUILD_STEP_EVENT " + json.dumps(value), file=sys.stderr, flush=True) + except (OSError, ValueError): + pass # Optional observation cannot change native command semantics. + + +@contextlib.contextmanager +def command_observation(argv: list[str], env: dict[str, str] | None): + identity = uuid.uuid4().hex + command = command_step(argv, env) + emit_command_step(identity, command, "started") + try: + yield + except BaseException as error: + code = error.returncode if isinstance(error, subprocess.CalledProcessError) else None + emit_command_step(identity, command, "failed", code) + raise + else: + emit_command_step(identity, command, "success", 0) + + def run(argv: list[str], cwd: Path, env: dict[str, str] | None = None) -> None: - subprocess.run(argv, cwd=cwd, env=env, check=True) + with command_observation(argv, env): + subprocess.run(argv, cwd=cwd, env=env, check=True) def output(argv: list[str], cwd: Path, env: dict[str, str] | None = None) -> str: - return subprocess.check_output(argv, cwd=cwd, env=env, text=True, stderr=subprocess.STDOUT).strip() + with command_observation(argv, env): + return subprocess.check_output(argv, cwd=cwd, env=env, text=True, stderr=subprocess.STDOUT).strip() def artifact(kind: str, target: str, path: Path) -> dict: @@ -188,3 +238,66 @@ def publish_staging(staging: Path, destination: Path) -> None: def clean_failed_staging(staging: Path) -> None: shutil.rmtree(staging, ignore_errors=True) + + +class RequiredSteps: + """Bounded required evidence, separate from optional command telemetry.""" + + def __init__(self, path, component, source_commit, target, expected): + import time + self.clock = time.monotonic + self.path = Path(path) + safe_output(self.path) + if not expected or len(expected) > 32 or len(set(expected)) != len(expected): + raise ValueError("invalid required step declaration") + if any(re.fullmatch(r"[a-z][a-z0-9-]{0,63}", step) is None for step in expected): + raise ValueError("unsafe required step identity") + self.value = {"schema": "iicp.pre1-required-steps.v1", "component": component, + "source_commit": source_commit, "target": target, + "expected": list(expected), "non_authorizing": True, + "qualification_credit": 0, "steps": [ + {"id": step, "status": "NOT_RUN", "exit_code": None, + "duration_ms": 0} for step in expected]} + self.persist() + + def persist(self): + import tempfile + payload = (json.dumps(self.value, sort_keys=True) + "\n").encode() + if len(payload) > 32768: + raise ValueError("required step inventory exceeds bound") + if any(p.is_symlink() or (hasattr(p, "is_junction") and p.is_junction()) + for p in (self.path, *self.path.parents)): + raise ValueError("unsafe required step path") + fd, name = tempfile.mkstemp(prefix=".steps-", dir=self.path.parent) + try: + with os.fdopen(fd, "wb") as handle: + handle.write(payload) + handle.flush() + os.fsync(handle.fileno()) + os.replace(name, self.path) + finally: + Path(name).unlink(missing_ok=True) + + @contextlib.contextmanager + def step(self, identity): + row = next((row for row in self.value["steps"] if row["id"] == identity), None) + pending = next((row for row in self.value["steps"] if row["status"] != "PASS"), None) + if row is None or row is not pending or row["status"] != "NOT_RUN": + raise ValueError("unexpected, duplicate or out-of-order required step") + row["status"] = "INCOMPLETE" + self.persist() # No work starts without its required start record. + started = self.clock() + try: + yield + except BaseException as error: + row.update(status="FAIL", exit_code=getattr(error, "returncode", None), + duration_ms=max(0, int((self.clock() - started) * 1000))) + try: + self.persist() + except (OSError, ValueError): + pass # Preserve original failure; durable start remains INCOMPLETE. + raise + else: + row.update(status="PASS", exit_code=0, + duration_ms=max(0, int((self.clock() - started) * 1000))) + self.persist() # Required evidence failure cannot become success. diff --git a/scripts/test_pre1_candidate_artifacts.py b/scripts/test_pre1_candidate_artifacts.py index 079d0b9..8a345f4 100644 --- a/scripts/test_pre1_candidate_artifacts.py +++ b/scripts/test_pre1_candidate_artifacts.py @@ -19,5 +19,8 @@ def test_description_is_content_free_and_complete(self) -> None: self.assertTrue(value["non_authorizing"]) +from test_pre1_command_observation import CommandObservationTests + + if __name__ == "__main__": unittest.main() diff --git a/scripts/test_pre1_command_observation.py b/scripts/test_pre1_command_observation.py new file mode 100644 index 0000000..c49a21c --- /dev/null +++ b/scripts/test_pre1_command_observation.py @@ -0,0 +1,55 @@ +"""Content-free artifact command observation; no AWS or product execution.""" +import contextlib +import io +import json +from pathlib import Path +import subprocess +import unittest +from unittest import mock +import pre1_artifact_common as common + + +class CommandObservationTests(unittest.TestCase): + def events(self, stream): + return [json.loads(line.split(" ", 1)[1]) for line in stream.getvalue().splitlines()] + + def test_start_before_execution_and_output_is_unchanged(self): + stream = io.StringIO() + def execute(*args, **kwargs): + self.assertEqual(self.events(stream)[0]["state"], "started") + return "version-result\n" + with contextlib.redirect_stderr(stream), mock.patch.object(common.subprocess, "check_output", side_effect=execute): + self.assertEqual(common.output(["python", "--version"], Path(".")), "version-result") + events = self.events(stream) + self.assertEqual([e["state"] for e in events], ["started", "success"]) + self.assertEqual(events[0]["step_id"], events[1]["step_id"]) + + def test_failure_and_failed_launch_preserve_exception(self): + for error in (subprocess.CalledProcessError(101, ["cargo"]), FileNotFoundError("PRIVATE_CANARY")): + stream = io.StringIO() + with contextlib.redirect_stderr(stream), mock.patch.object(common.subprocess, "run", side_effect=error): + with self.assertRaises(type(error)) as got: + common.run(["cargo", "test", "PRIVATE_CANARY"], Path(".")) + self.assertIs(got.exception, error) + events = self.events(stream) + self.assertEqual(events[-1]["state"], "failed") + self.assertEqual(events[-1]["exit_code"], getattr(error, "returncode", None)) + self.assertNotIn("PRIVATE_CANARY", stream.getvalue()) + + def test_command_categories_and_offline_are_content_free(self): + cases = [(["uv", "run", "pytest", "-q"], {}, ["python", "test"]), + (["node", "/private/npm-cli.js", "install", "PRIVATE_CANARY"], {"npm_config_offline": "true"}, ["npm", "install-offline"]), + (["cargo", "install", "--offline"], {}, ["cargo", "install-offline"]), + (["python", "-m", "pip", "install", "--no-index"], {}, ["python", "install-offline"]), + (["npm", "install"], {}, ["npm", "install-online"])] + for argv, env, expected in cases: + self.assertEqual(common.command_step(argv, env), expected) + + def test_optional_telemetry_failure_is_nonfatal(self): + with mock.patch("builtins.print", side_effect=OSError("closed")), mock.patch.object(common.subprocess, "run") as run: + common.run(["cargo", "test"], Path(".")) + run.assert_called_once() + + +if __name__ == "__main__": + unittest.main()