From 97604777c092dfca1bd555be179462707eab71ca Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:23:51 +0000 Subject: [PATCH 1/2] Add a BUILD-VERIFIED packaging check without publishing release_check.py runs the library dependency test, builds a wheel, and imports the new export from site-packages with PYTHONPATH stripped. GrokBot DevOps reads that JSON; it does not run python -m build. Co-authored-by: ale93.moro --- .gitignore | 1 + projections/devops/ci-gate.md | 11 +- tests/test_tooling.py | 72 +++++++- tools/build_projections.py | 11 +- tools/grokbot_sim.py | 43 ++++- tools/release_check.py | 321 ++++++++++++++++++++++++++++++++++ 6 files changed, 452 insertions(+), 7 deletions(-) create mode 100644 tools/release_check.py diff --git a/.gitignore b/.gitignore index bb887ad..c3b0c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ __pycache__/ venv/ *.egg-info/ convention-report.json +dist/ .DS_Store diff --git a/projections/devops/ci-gate.md b/projections/devops/ci-gate.md index 9e06b61..224cf95 100644 --- a/projections/devops/ci-gate.md +++ b/projections/devops/ci-gate.md @@ -17,8 +17,15 @@ python tools/convention_check.py --all # human-readable, sets exit code - `--json` output is stable for dashboards / PR annotations. - No GPU, no model downloads, no network — runs on the cheapest runner. - Same script runs in the editor hook and pre-PR, so CI surprises are rare. -- Green gate = merge-eligible (release clearance). This kit does not - deploy; it is the check that a change is allowed to move toward release. +- Green gate = merge-eligible. Packaging is a separate BUILD-VERIFIED / + PACKAGING-ELIGIBLE check (`tools/release_check.py`) that runs only on a + library checkout: pytest tests/others/test_dependencies.py, `python -m build + --wheel`, then import the new export from the installed wheel with + PYTHONPATH stripped (site-packages, not src/). Same spirit as + overlay_pr_gate.py — invoke the team's tooling; not a new product. + GrokBot DevOps reads that JSON via `--release-json`; it does not run + `python -m build`. Stops at build-verified. Handoff: customer index, + creds, and tag. Not CD. Not a required GitHub check on first-contribution PRs. - **Projection drift:** `python tools/build_projections.py && git diff --exit-code` fails if a generated surface was hand-edited instead of `rules.yaml`. - **Scheduler contract re-verify:** `python tools/verify_scheduler_contract.py` diff --git a/tests/test_tooling.py b/tests/test_tooling.py index b61c40d..b5cec25 100644 --- a/tests/test_tooling.py +++ b/tests/test_tooling.py @@ -390,10 +390,18 @@ def _gate_json(self, example_dir: str) -> str: self.assertIn('"findings"', proc.stdout) return proc.stdout - def _sim(self, role: str, gate_json: str | None = None, context: bool = True) -> str: + def _sim( + self, + role: str, + gate_json: str | None = None, + context: bool = True, + release_json: Path | None = None, + ) -> str: cmd = [sys.executable, str(ROOT / "tools" / "grokbot_sim.py"), "--role", role] if context: cmd.extend(["--context", str(self.CONTEXT)]) + if release_json is not None: + cmd.extend(["--release-json", str(release_json)]) if gate_json is None: gate_json = self._gate_json("scaffolded_scheduler") proc = subprocess.run( @@ -493,6 +501,68 @@ def test_change_context_example_schema(self): self.assertIn(data["state"], ("scaffolded", "gate-green", "tests-pass", "merge-eligible")) self.assertIn("EulerLite", data["pr"]["title"] + data["pr"]["issue"]) + def test_devops_packaging_not_wired_without_json(self): + out = self._sim("devops") + self.assertIn("## Packaging", out) + self.assertIn("packaging check not wired", out) + self.assertNotIn("BUILD-VERIFIED", out) + + def test_devops_packaging_fed_by_release_json(self): + with tempfile.TemporaryDirectory() as td: + report = Path(td) / "packaging.json" + report.write_text(json.dumps({ + "verdict": "BUILD-VERIFIED / PACKAGING-ELIGIBLE", + "object": "PNDMLiteScheduler", + "diffusers_file": "/tmp/site-packages/diffusers/__init__.py", + "steps": [ + {"name": "dependency_contract", "status": "pass", + "command": "pytest tests/others/test_dependencies.py -q"}, + ], + "advisories": ["step() still TODO(engineer); this is a scaffold, not a product release"], + "note": "Stops at build-verified. Handoff: the customer's index, creds, and tag. Not CD.", + })) + out = self._sim("devops", release_json=report) + self.assertIn("## Packaging", out) + self.assertIn("BUILD-VERIFIED / PACKAGING-ELIGIBLE", out) + self.assertIn("PNDMLiteScheduler", out) + self.assertIn("site-packages", out) + self.assertIn("TODO(engineer)", out) + self.assertNotIn("release-eligible", out.lower()) + self.assertNotIn("packaging check not wired", out) + pm = self._sim("pm") + self.assertNotIn("## Packaging", pm) + + def test_grokbot_sim_does_not_invoke_build(self): + src = (ROOT / "tools" / "grokbot_sim.py").read_text() + self.assertNotIn("python -m build", src.replace("never runs python -m build", "")) + from grokbot_sim import VALID_STATES # noqa: WPS433 + self.assertEqual( + VALID_STATES, + ("scaffolded", "gate-green", "tests-pass", "merge-eligible"), + ) + + +class TestReleaseCheckWrapper(unittest.TestCase): + def test_refuses_kit_standin(self): + proc = subprocess.run( + [sys.executable, str(ROOT / "tools" / "release_check.py"), + "--object", "PNDMLiteScheduler", "--json"], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + env={**os.environ, "DIFFUSERS_ROOT": str(ROOT)}, + ) + self.assertEqual(proc.returncode, 2, proc.stdout + proc.stderr) + self.assertIn("not the kit stand-in", proc.stderr) + + def test_verdict_wording(self): + src = (ROOT / "tools" / "release_check.py").read_text() + self.assertIn("BUILD-VERIFIED / PACKAGING-ELIGIBLE", src) + self.assertNotIn("release-eligible", src.lower()) + self.assertNotIn('"ship"', src.lower()) + self.assertIn("TODO(engineer)", src) + class TestGrokbotIphonePack(unittest.TestCase): def test_profiles_cover_three_roles_and_never_gate(self): diff --git a/tools/build_projections.py b/tools/build_projections.py index 55aa336..984f514 100644 --- a/tools/build_projections.py +++ b/tools/build_projections.py @@ -511,8 +511,15 @@ def build_devops(): "- `--json` output is stable for dashboards / PR annotations.", "- No GPU, no model downloads, no network — runs on the cheapest runner.", "- Same script runs in the editor hook and pre-PR, so CI surprises are rare.", - "- Green gate = merge-eligible (release clearance). This kit does not", - " deploy; it is the check that a change is allowed to move toward release.", + "- Green gate = merge-eligible. Packaging is a separate BUILD-VERIFIED /", + " PACKAGING-ELIGIBLE check (`tools/release_check.py`) that runs only on a", + " library checkout: pytest tests/others/test_dependencies.py, `python -m build", + " --wheel`, then import the new export from the installed wheel with", + " PYTHONPATH stripped (site-packages, not src/). Same spirit as", + " overlay_pr_gate.py — invoke the team's tooling; not a new product.", + " GrokBot DevOps reads that JSON via `--release-json`; it does not run", + " `python -m build`. Stops at build-verified. Handoff: customer index,", + " creds, and tag. Not CD. Not a required GitHub check on first-contribution PRs.", "- **Projection drift:** `python tools/build_projections.py && git diff --exit-code`", " fails if a generated surface was hand-edited instead of `rules.yaml`.", "- **Scheduler contract re-verify:** `python tools/verify_scheduler_contract.py`", diff --git a/tools/grokbot_sim.py b/tools/grokbot_sim.py index 1cab55f..53d1d74 100644 --- a/tools/grokbot_sim.py +++ b/tools/grokbot_sim.py @@ -132,7 +132,13 @@ def _finding_line(f: dict, rec: dict | None = None) -> str: ) -def brief(role: str, gate: dict, rules: dict, context: dict | None = None) -> str: +def brief( + role: str, + gate: dict, + rules: dict, + context: dict | None = None, + packaging: dict | None = None, +) -> str: context = context or {} findings = gate.get("findings") or [] blocking = gate.get( @@ -226,6 +232,30 @@ def brief(role: str, gate: dict, rules: dict, context: dict | None = None) -> st "this briefing never fails a job and never merges.", ) ) + lines.append("") + lines.append("## Packaging") + if packaging and packaging.get("verdict"): + lines.append( + _bullet("ci", f"verdict: {packaging.get('verdict')}") + ) + if packaging.get("object"): + lines.append(_bullet("ci", f"object: {packaging.get('object')}")) + proven = packaging.get("diffusers_file") or "" + if proven: + lines.append(_bullet("ci", f"diffusers.__file__: {proven}")) + for st in packaging.get("steps") or []: + lines.append( + _bullet( + "ci", + f"{st.get('name')}: {st.get('status')} ({st.get('command')})", + ) + ) + for adv in packaging.get("advisories") or []: + lines.append(_bullet("ci", f"advisory: {adv}")) + if packaging.get("note"): + lines.append(_bullet("ci", packaging["note"])) + else: + lines.append(_bullet("ci", "packaging check not wired")) elif role == "pm": lines.append("## DoD state") if declared_state in VALID_STATES: @@ -401,6 +431,12 @@ def main(argv=None) -> int: metavar="FILE.json", help="optional change-context JSON (pr / ci / state)", ) + ap.add_argument( + "--release-json", + metavar="FILE.json", + help="JSON from a prior `release_check.py --object --json` " + "run on the fork. Reads that report; never runs python -m build.", + ) ap.add_argument( "gate_json", nargs="?", @@ -410,7 +446,10 @@ def main(argv=None) -> int: args = ap.parse_args(argv) gate = read_gate(args.gate_json) context = read_context(args.context) - sys.stdout.write(brief(args.role, gate, load_rules(), context)) + packaging = None + if args.release_json: + packaging = json.loads(Path(args.release_json).read_text(encoding="utf-8")) + sys.stdout.write(brief(args.role, gate, load_rules(), context, packaging)) return 0 diff --git a/tools/release_check.py b/tools/release_check.py new file mode 100644 index 0000000..3a3d027 --- /dev/null +++ b/tools/release_check.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +"""Thin wrapper: BUILD-VERIFIED / PACKAGING-ELIGIBLE on a library checkout. + +Invokes the library's own tests and `python -m build`. Does not publish, +tag, twine-upload, or run `make pre-release`. Not a new product — same +idea as overlay_pr_gate.py (invoke team tooling, report). + +Run ONLY on a huggingface/diffusers checkout (the fork), never the kit +stand-in: + + python ramp-kit/tools/release_check.py --object PNDMLiteScheduler + python ramp-kit/tools/release_check.py --json # detect new export vs origin/main +""" +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +KIT = Path(__file__).resolve().parent.parent +VERDICT_PASS = "BUILD-VERIFIED / PACKAGING-ELIGIBLE" +VERDICT_FAIL = "NOT PACKAGING-ELIGIBLE" +NOTE = ( + "Stops at build-verified. Handoff: the customer's index, creds, and tag. " + "Not CD. This wrapper does not publish." +) + + +def _is_library_checkout(root: Path) -> bool: + return ( + (root / "setup.py").is_file() + and (root / "tests" / "others" / "test_dependencies.py").is_file() + and (root / "src" / "diffusers" / "__init__.py").is_file() + ) + + +def resolve_library_root() -> Path: + cwd = Path.cwd().resolve() + if _is_library_checkout(cwd): + return cwd + parent = KIT.parent + if _is_library_checkout(parent): + return parent + env = os.environ.get("DIFFUSERS_ROOT") + if env: + p = Path(env).expanduser().resolve() + if _is_library_checkout(p): + return p + sys.stderr.write( + "release_check: run on a huggingface/diffusers checkout (fork), " + "not the kit stand-in. Need setup.py + tests/others/test_dependencies.py.\n" + ) + sys.exit(2) + + +def snake_from_export(name: str) -> str: + name = name.removesuffix("Scheduler") + s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower() + + +def _run(cmd: list[str], cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess: + return subprocess.run( + cmd, + cwd=cwd, + env=env, + capture_output=True, + text=True, + check=False, + ) + + +def detect_new_export(lib: Path) -> str: + proc = _run( + ["git", "diff", "origin/main", "--", "src/diffusers/__init__.py"], + cwd=lib, + ) + if proc.returncode != 0: + proc = _run( + ["git", "diff", "main", "--", "src/diffusers/__init__.py"], + cwd=lib, + ) + diff = proc.stdout or "" + added = re.findall(r'^\+\s+"([A-Za-z_][A-Za-z0-9_]+)",?\s*$', diff, re.M) + added += re.findall(r"^\+\s+([A-Z][A-Za-z0-9]+),?\s*$", diff, re.M) + removed = set(re.findall(r'^\-\s+"([A-Za-z_][A-Za-z0-9_]+)",?\s*$', diff, re.M)) + removed |= set(re.findall(r"^\-\s+([A-Z][A-Za-z0-9]+),?\s*$", diff, re.M)) + new = [] + for name in added: + if name in removed or name in new: + continue + new.append(name) + sched = [n for n in new if n.endswith("Scheduler")] + pick = sched if sched else new + if len(pick) == 1: + return pick[0] + if not pick: + sys.stderr.write( + "release_check: could not detect a new export in " + "src/diffusers/__init__.py vs origin/main; pass --object\n" + ) + sys.exit(2) + sys.stderr.write( + "release_check: multiple new exports " + f"{pick}; pass --object\n" + ) + sys.exit(2) + + +def _step(name: str, command: str, status: str, detail: str = "") -> dict: + return {"name": name, "command": command, "status": status, "detail": detail} + + +def advisories(lib: Path, obj: str) -> list[str]: + notes: list[str] = [] + names = _run( + ["git", "diff", "--name-only", "origin/main"], + cwd=lib, + ).stdout + if "changelog" not in names.lower() and "release note" not in names.lower(): + notes.append("no changelog/release-notes entry for the change") + version_diff = _run( + ["git", "diff", "origin/main", "--", "setup.py", "src/diffusers/__init__.py"], + cwd=lib, + ).stdout + if not re.search(r"^[-+].*__version__|^[-+].*version", version_diff, re.M | re.I): + notes.append( + "public API surface changed; confirm a version/semver decision" + ) + stem = f"scheduling_{snake_from_export(obj)}.py" + impl = lib / "src" / "diffusers" / "schedulers" / stem + if impl.is_file() and "TODO(engineer)" in impl.read_text(encoding="utf-8"): + notes.append( + "step() still TODO(engineer); this is a scaffold, not a product release" + ) + return notes + + +def ensure_build_module() -> None: + proc = subprocess.run( + [sys.executable, "-c", "import build"], + capture_output=True, + check=False, + ) + if proc.returncode == 0: + return + inst = subprocess.run( + [sys.executable, "-m", "pip", "install", "build"], + capture_output=True, + text=True, + check=False, + ) + if inst.returncode != 0: + sys.stderr.write(inst.stdout + inst.stderr) + sys.exit(2) + + +def restore_editable(lib: Path) -> None: + _run([sys.executable, "-m", "pip", "install", "-e", ".", "-q"], cwd=lib) + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--object", help="public export to import from the wheel") + ap.add_argument("--json", action="store_true", help="print a JSON report") + args = ap.parse_args(argv) + + lib = resolve_library_root() + obj = args.object or detect_new_export(lib) + steps: list[dict] = [] + ok = True + wheel_path: Path | None = None + import_file = "" + + # 1. Dependency contract (library's own pytest job). + dep_cmd = [sys.executable, "-m", "pytest", "tests/others/test_dependencies.py", "-q"] + dep = _run(dep_cmd, cwd=lib) + dep_status = "pass" if dep.returncode == 0 else "fail" + steps.append( + _step( + "dependency_contract", + " ".join(dep_cmd), + dep_status, + (dep.stdout + dep.stderr)[-2000:], + ) + ) + if dep_status != "pass": + ok = False + + # 2. Build a wheel (wipe dist/ first). + if ok: + ensure_build_module() + dist = lib / "dist" + if dist.exists(): + shutil.rmtree(dist) + build_cmd = [sys.executable, "-m", "build", "--wheel"] + built = _run(build_cmd, cwd=lib) + wheels = sorted(dist.glob("*.whl")) if dist.is_dir() else [] + if built.returncode == 0 and len(wheels) == 1: + wheel_path = wheels[0] + steps.append( + _step("build", " ".join(build_cmd), "pass", str(wheel_path)) + ) + else: + ok = False + steps.append( + _step( + "build", + " ".join(build_cmd), + "fail", + (built.stdout + built.stderr)[-2000:], + ) + ) + + # 3. Install the exact wheel and import with PYTHONPATH stripped. + if ok and wheel_path is not None: + inst_cmd = [ + sys.executable, + "-m", + "pip", + "install", + "--force-reinstall", + str(wheel_path), + ] + inst = _run(inst_cmd, cwd=lib) + if inst.returncode != 0: + ok = False + steps.append( + _step( + "install_and_import", + " ".join(inst_cmd), + "fail", + (inst.stdout + inst.stderr)[-2000:], + ) + ) + else: + prove = ( + "import diffusers, sys; " + "assert 'site-packages' in diffusers.__file__, diffusers.__file__; " + f"from diffusers import {obj}; " + "print(diffusers.__file__)" + ) + prove_cmd = [ + "env", + "-u", + "PYTHONPATH", + sys.executable, + "-c", + prove, + ] + proved = subprocess.run( + prove_cmd, + cwd="/tmp", + capture_output=True, + text=True, + check=False, + ) + out = (proved.stdout or "").strip() + import_file = out.splitlines()[-1] if out else "" + cmd_shown = ( + f"env -u PYTHONPATH {sys.executable} -c " + f"\"import diffusers, sys; " + f"assert 'site-packages' in diffusers.__file__, diffusers.__file__; " + f"from diffusers import {obj}; print(diffusers.__file__)\"" + ) + src_root = str((lib / "src").resolve()) + if ( + proved.returncode == 0 + and "site-packages" in import_file + and src_root not in import_file + ): + steps.append(_step("install_and_import", cmd_shown, "pass", import_file)) + else: + ok = False + steps.append( + _step( + "install_and_import", + cmd_shown, + "fail", + (proved.stdout + proved.stderr + import_file)[-2000:], + ) + ) + + restore_editable(lib) + + verdict = VERDICT_PASS if ok else VERDICT_FAIL + report = { + "verdict": verdict, + "object": obj, + "library_root": str(lib), + "steps": steps, + "advisories": advisories(lib, obj), + "note": NOTE, + "diffusers_file": import_file, + } + + if args.json: + print(json.dumps(report, indent=2)) + else: + print(f"object: {obj}") + print(f"library: {lib}") + for st in steps: + print(f"{st['status'].upper()} {st['name']}: {st['command']}") + if st["detail"]: + print(f" {st['detail'][:500]}") + print(f"verdict: {verdict}") + if report["advisories"]: + print("advisories (not blocking):") + for a in report["advisories"]: + print(f" - {a}") + print(NOTE) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 3f1ace2f3636f75c6a82f1eb7b31b5f645dc0b59 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:25:08 +0000 Subject: [PATCH 2/2] Detect a just-merged export against origin/main~1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the working tree already matches origin/main, origin/main has no new symbol. Fall back to origin/main~1 so --json still finds the landed export without hardcoding a scheduler name. Skip local main — on a fork that ref may be upstream. Co-authored-by: ale93.moro --- tools/release_check.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/tools/release_check.py b/tools/release_check.py index 3a3d027..cfd9021 100644 --- a/tools/release_check.py +++ b/tools/release_check.py @@ -75,17 +75,25 @@ def _run(cmd: list[str], cwd: Path, env: dict | None = None) -> subprocess.Compl ) +def compare_spec(lib: Path) -> str: + """Prefer origin/main. If the tree already matches (change landed), use ~1. + + Do not fall back to local `main` — on a fork that ref may be upstream + and would report every overlay export as new. + """ + for spec in ("origin/main", "origin/main~1"): + proc = _run(["git", "diff", "--name-only", spec], cwd=lib) + if proc.returncode == 0 and (proc.stdout or "").strip(): + return spec + return "origin/main" + + def detect_new_export(lib: Path) -> str: - proc = _run( - ["git", "diff", "origin/main", "--", "src/diffusers/__init__.py"], + spec = compare_spec(lib) + diff = _run( + ["git", "diff", spec, "--", "src/diffusers/__init__.py"], cwd=lib, - ) - if proc.returncode != 0: - proc = _run( - ["git", "diff", "main", "--", "src/diffusers/__init__.py"], - cwd=lib, - ) - diff = proc.stdout or "" + ).stdout or "" added = re.findall(r'^\+\s+"([A-Za-z_][A-Za-z0-9_]+)",?\s*$', diff, re.M) added += re.findall(r"^\+\s+([A-Z][A-Za-z0-9]+),?\s*$", diff, re.M) removed = set(re.findall(r'^\-\s+"([A-Za-z_][A-Za-z0-9_]+)",?\s*$', diff, re.M)) @@ -118,16 +126,17 @@ def _step(name: str, command: str, status: str, detail: str = "") -> dict: def advisories(lib: Path, obj: str) -> list[str]: notes: list[str] = [] + spec = compare_spec(lib) names = _run( - ["git", "diff", "--name-only", "origin/main"], + ["git", "diff", "--name-only", spec], cwd=lib, - ).stdout + ).stdout or "" if "changelog" not in names.lower() and "release note" not in names.lower(): notes.append("no changelog/release-notes entry for the change") version_diff = _run( - ["git", "diff", "origin/main", "--", "setup.py", "src/diffusers/__init__.py"], + ["git", "diff", spec, "--", "setup.py", "src/diffusers/__init__.py"], cwd=lib, - ).stdout + ).stdout or "" if not re.search(r"^[-+].*__version__|^[-+].*version", version_diff, re.M | re.I): notes.append( "public API surface changed; confirm a version/semver decision"