"""Command line for ``python build/run.py release``. Commands: ``preflight`` (local gate), ``audit`` (single wheel), ``github`` (sync the public repo + publish a GitHub Release), ``pypi`` (TestPyPI then PyPI). The publish commands are dry-run unless ``--apply`` is passed. """ from __future__ import annotations import argparse import sys from pathlib import Path _BUILD_ROOT = Path(__file__).resolve().parents[1] if str(_BUILD_ROOT) not in sys.path: sys.path.insert(0, str(_BUILD_ROOT)) from common.paths import REPO_ROOT # noqa: E402 from common.verify.wheel import ( # noqa: E402 DEFAULT_RUNTIME_CONFIG, default_verification_video, verify_wheel, verify_wheel_assets, verify_wheel_runtime, ) from common.verify.wheel_contents import inspect_wheel # noqa: E402 from release import github, pypi # noqa: E402 from release.preflight import PreflightOptions, run_preflight # noqa: E402 def _cmd_preflight(argv: list[str]) -> int: parser = argparse.ArgumentParser( prog="build/run.py release preflight", description="Gate a release locally: version, tests, wheel content, runtime.", ) parser.add_argument("--version", help="release version (default: pyproject's dynamic version)") parser.add_argument("--wheel-dir", type=Path, help="directory to inspect (default: SDK/)") parser.add_argument("--video", type=Path, help="clip for the end-to-end runtime check") parser.add_argument("--config", default=DEFAULT_RUNTIME_CONFIG, help="config name for that check") parser.add_argument("--frames", type=int, default=40, help="frames to process (default: 40)") parser.add_argument("--skip-tests", action="store_true", help="skip pytest (recorded in the report)") parser.add_argument("--longrun", action="store_true", help="include the slow long-run stability test") parser.add_argument("--allow-dirty", action="store_true", help="allow a dirty working tree") parser.add_argument( "--force", action="store_true", help="accept a version that a git tag already marks as released", ) parser.add_argument("--check-pypi", action="store_true", help="read PyPI and fail if the version exists") parser.add_argument( "--all-wheel-pythons", action="store_true", help="verify cp310/cp311/cp312 wheels, each in its own conda env", ) parser.add_argument("--offline", action="store_true", help="reuse parent-env deps instead of PyPI") parser.add_argument("--no-runtime", action="store_true", help="skip the isolated runtime check") parser.add_argument("--no-assets", action="store_true", help="skip the bundled-asset check") parser.add_argument("--keep-going", action="store_true", help="collect every failure instead of stopping") parser.add_argument("--report-dir", type=Path, help="override build/release/reports") args = parser.parse_args(argv) options = PreflightOptions( version=args.version, wheel_dir=args.wheel_dir, video=args.video, config_name=args.config, frames=args.frames, skip_tests=args.skip_tests, longrun=args.longrun, allow_dirty=args.allow_dirty, force=args.force, check_pypi=args.check_pypi, all_wheel_pythons=args.all_wheel_pythons, offline=args.offline, keep_going=args.keep_going, report_dir=args.report_dir, no_runtime=args.no_runtime, no_assets=args.no_assets, ) return run_preflight(options, repo_root=REPO_ROOT) def _cmd_audit(argv: list[str]) -> int: parser = argparse.ArgumentParser( prog="build/run.py release audit", description="Inspect one built wheel without a release context.", ) parser.add_argument("wheel", type=Path, help="path to an .whl") parser.add_argument("--expected-version", help="assert the wheel reports this version") parser.add_argument( "--strict-bundle", action="store_true", help="treat bundled-file drift from the current source tree as a failure", ) parser.add_argument("--runtime", action="store_true", help="also install and run the wheel") parser.add_argument("--assets", action="store_true", help="also assert bundled assets load") parser.add_argument("--video", type=Path, help="clip for --runtime") parser.add_argument("--config", default=DEFAULT_RUNTIME_CONFIG) parser.add_argument("--frames", type=int, default=40) parser.add_argument("--offline", action="store_true") parser.add_argument( "--python", type=Path, default=None, help="interpreter to build the verification venv from (default: the running one)", ) args = parser.parse_args(argv) wheel = args.wheel if not wheel.is_file(): print(f"[ERROR] wheel not found: {wheel}", file=sys.stderr) return 2 exit_code = 0 result = inspect_wheel( wheel, expected_version=args.expected_version, repo_root=REPO_ROOT, strict_bundle=args.strict_bundle, ) print(result.render()) if not result.ok: exit_code = 1 if args.assets and args.runtime: # One venv for both checks: building it (and downloading dependencies in # online mode) is the expensive part. video = args.video or default_verification_video(REPO_ROOT) if video is None: print("[FAIL] runtime: no verification clip; pass --video or set ORISYS_VERIFY_VIDEO") return 1 try: record = verify_wheel( wheel, video=video, config_name=args.config, frames=args.frames, python_executable=args.python, offline=args.offline, ) print(f"[ok ] {record['imports']}") print(f"[ok ] {record['assets']}") print(f"[ok ] runtime: frames={record['frames']} flow={record['flow_shape']}") except Exception as exc: print(f"[FAIL] isolated verification: {type(exc).__name__}: {exc}") exit_code = 1 return exit_code if args.assets: try: print(verify_wheel_assets(wheel, python_executable=args.python)) except Exception as exc: print(f"[FAIL] bundled assets: {type(exc).__name__}: {exc}") exit_code = 1 if args.runtime: video = args.video or default_verification_video(REPO_ROOT) if video is None: print("[FAIL] runtime: no verification clip; pass --video or set ORISYS_VERIFY_VIDEO") return 1 try: info = verify_wheel_runtime( wheel, video=video, config_name=args.config, frames=args.frames, python_executable=args.python, offline=args.offline, ) print(f"[ok ] runtime: {info}") except Exception as exc: print(f"[FAIL] runtime: {type(exc).__name__}: {exc}") exit_code = 1 return exit_code def _cmd_github(argv: list[str]) -> int: parser = argparse.ArgumentParser( prog="build/run.py release github", description="Sync orisys-sdk-release, tag, and publish a GitHub Release.", ) parser.add_argument("--version", required=True, help="release version, e.g. 0.5.2") parser.add_argument( "--apply", action="store_true", help="actually mutate the release repo, push, and create the release", ) parser.add_argument("--notes-file", type=Path, default=None, help="release notes markdown") parser.add_argument( "--attach-preflight", action="store_true", help="also upload the preflight report as a release asset (contains local paths)", ) parser.add_argument( "--allow-dirty-release", action="store_true", help="proceed even if the release repo has uncommitted changes", ) parser.add_argument("--skip-fetch", action="store_true", help="skip `git fetch --tags`") parser.add_argument( "--skip-checks", action="store_true", help="skip the manifest's downstream checks" ) parser.add_argument( "--skip-release-upload", action="store_true", help="stop after pushing the tag and print the `gh release create` command", ) args = parser.parse_args(argv) return github.publish( github.Options( version=args.version, apply=args.apply, notes_file=args.notes_file, attach_preflight=args.attach_preflight, allow_dirty_release=args.allow_dirty_release, skip_fetch=args.skip_fetch, skip_checks=args.skip_checks, skip_release_upload=args.skip_release_upload, ) ) def _cmd_pypi(argv: list[str]) -> int: parser = argparse.ArgumentParser( prog="build/run.py release pypi", description="Upload the certified wheels to TestPyPI and then PyPI.", ) parser.add_argument("--version", required=True, help="release version, e.g. 0.5.2") parser.add_argument( "--apply", action="store_true", help="actually upload (uploads are permanent and cannot be overwritten)", ) parser.add_argument( "--confirm-pypi", action="store_true", help="after TestPyPI, also publish to the real PyPI", ) parser.add_argument( "--repository", default="testpypi", help="twine repository for the first phase" ) parser.add_argument( "--skip-install-verify", action="store_true", help="skip fetching the artifact back out of the index", ) parser.add_argument("--skip-check", action="store_true", help="skip `twine check`") args = parser.parse_args(argv) return pypi.publish( pypi.Options( version=args.version, apply=args.apply, confirm_pypi=args.confirm_pypi, repository=args.repository, verify_install=not args.skip_install_verify, skip_check=args.skip_check, ) ) def main(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) if not argv or argv[0] in ("-h", "--help"): print(__doc__) print("Commands: preflight | audit | github | pypi") return 0 command, rest = argv[0], argv[1:] if command == "preflight": return _cmd_preflight(rest) if command == "audit": return _cmd_audit(rest) if command == "github": return _cmd_github(rest) if command == "pypi": return _cmd_pypi(rest) print(f"Unknown release command: {command!r}", file=sys.stderr) print("Choose from: preflight, audit, github, pypi", file=sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())