diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c05fca..de55837 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: Stable SDK version to publish (for example 0.1.0) + description: Stable SDK version to publish (for example 1.0.0) required: true type: string push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4815d03..da54cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +## [1.0.0] - 2026-05-31 + +### Changed +- Declared the Python SDK stable at `1.0.0` after release-hardening the public package, browser relay, framework integrations, and registry smoke coverage. + ## [0.1.9] - 2026-05-29 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index f94f52a..6826271 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "debugbundle-python" -version = "0.1.9" +version = "1.0.0" description = "DebugBundle SDK for Python" readme = "README.md" requires-python = ">=3.10" @@ -12,7 +12,7 @@ license = "AGPL-3.0-only" authors = [{ name = "DebugBundle" }] keywords = ["debugbundle", "debugging", "ai-agent", "error-tracking"] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Framework :: Django", "Framework :: FastAPI", "Framework :: Flask", diff --git a/smoke/run_app_driven_smoke.py b/smoke/run_app_driven_smoke.py index b905993..34b01a7 100644 --- a/smoke/run_app_driven_smoke.py +++ b/smoke/run_app_driven_smoke.py @@ -6,6 +6,7 @@ import sys import tempfile import threading +import time from dataclasses import dataclass from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from importlib.metadata import version @@ -19,6 +20,8 @@ SERVER_REQUEST_ID = "req-smoke-server" RELAY_TRACE_ID = "trace-smoke-relay" RELAY_REQUEST_ID = "req-smoke-relay" +PUBLISHED_INSTALL_ATTEMPTS = 12 +PUBLISHED_INSTALL_RETRY_SECONDS = 10 @dataclass @@ -79,7 +82,27 @@ def _run_subprocess(command: list[str]) -> None: subprocess.run(command, check=True) -def _bootstrap_clean_install(install_target: str, schema_path: Path) -> None: +def _install_with_retry(command: list[str], retries: int, retry_delay_seconds: int) -> None: + attempts = 0 + + while True: + attempts += 1 + try: + _run_subprocess(command) + return + except subprocess.CalledProcessError: + if attempts >= retries: + raise + + print( + "Published package not available yet; retrying in " + f"{retry_delay_seconds}s (attempt {attempts}/{retries}).", + file=sys.stderr, + ) + time.sleep(retry_delay_seconds) + + +def _bootstrap_clean_install(install_target: str, schema_path: Path, *, published_package: bool) -> None: with tempfile.TemporaryDirectory(prefix="debugbundle-python-smoke-") as temp_dir: temp_path = Path(temp_dir) venv_dir = temp_path / "venv" @@ -87,19 +110,25 @@ def _bootstrap_clean_install(install_target: str, schema_path: Path) -> None: _run_subprocess([sys.executable, "-m", "venv", str(venv_dir)]) _run_subprocess([str(python_bin), "-m", "pip", "install", "--upgrade", "pip"]) - _run_subprocess( - [ - str(python_bin), - "-m", - "pip", - "install", - install_target, - "django>=5,<6", - "fastapi>=0.115,<1", - "flask>=3,<4", - "jsonschema>=4.23,<5", - ] - ) + install_command = [ + str(python_bin), + "-m", + "pip", + "install", + install_target, + "django>=5,<6", + "fastapi>=0.115,<1", + "flask>=3,<4", + "jsonschema>=4.23,<5", + ] + if published_package: + _install_with_retry( + install_command, + retries=PUBLISHED_INSTALL_ATTEMPTS, + retry_delay_seconds=PUBLISHED_INSTALL_RETRY_SECONDS, + ) + else: + _run_subprocess(install_command) _run_subprocess( [ str(python_bin), @@ -297,11 +326,13 @@ def main() -> None: if not wheel_path.is_file(): raise SystemExit(f"Wheel not found: {wheel_path}") install_target = str(wheel_path) + published_package = False else: install_target = args.package or "" + published_package = True - _bootstrap_clean_install(install_target, args.schema.resolve()) + _bootstrap_clean_install(install_target, args.schema.resolve(), published_package=published_package) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/debugbundle/core.py b/src/debugbundle/core.py index 282567f..d0ea21d 100644 --- a/src/debugbundle/core.py +++ b/src/debugbundle/core.py @@ -891,7 +891,7 @@ def _sdk_version() -> str: try: return metadata.version("debugbundle-python") except metadata.PackageNotFoundError: - return "0.1.0" + return "1.0.0" def _sdk_config_endpoint(events_endpoint: str) -> str: diff --git a/tests/test_repository_metadata.py b/tests/test_repository_metadata.py index 1442138..f93a8f1 100644 --- a/tests/test_repository_metadata.py +++ b/tests/test_repository_metadata.py @@ -30,7 +30,7 @@ def test_standalone_changelog_and_security_policy_are_launch_ready() -> None: security = (REPO_ROOT / "SECURITY.md").read_text(encoding="utf-8") assert "## [Unreleased]" in changelog - assert "## [0.1.0] - 2026-05-07" in changelog + assert "## [1.0.0] - 2026-05-31" in changelog assert "https://github.com/debugbundle/debugbundle-python/security/advisories/new" in security diff --git a/tests/test_smoke_script.py b/tests/test_smoke_script.py new file mode 100644 index 0000000..58db66d --- /dev/null +++ b/tests/test_smoke_script.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import importlib.util +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +SMOKE_SCRIPT = REPO_ROOT / "smoke" / "run_app_driven_smoke.py" +SPEC = importlib.util.spec_from_file_location("debugbundle_python_smoke", SMOKE_SCRIPT) +assert SPEC is not None and SPEC.loader is not None +SMOKE = importlib.util.module_from_spec(SPEC) +sys.modules.setdefault("debugbundle_python_smoke", SMOKE) +SPEC.loader.exec_module(SMOKE) + + +def test_install_with_retry_retries_until_success(monkeypatch) -> None: + calls: list[list[str]] = [] + sleeps: list[int] = [] + outcomes = [ + subprocess.CalledProcessError(returncode=1, cmd=["pip", "install"]), + None, + ] + + def fake_run_subprocess(command: list[str]) -> None: + calls.append(command) + outcome = outcomes.pop(0) + if outcome is not None: + raise outcome + + monkeypatch.setattr(SMOKE, "_run_subprocess", fake_run_subprocess) + monkeypatch.setattr(SMOKE.time, "sleep", sleeps.append) + + SMOKE._install_with_retry( + ["python", "-m", "pip", "install", "debugbundle-python==1.0.0"], + retries=3, + retry_delay_seconds=7, + ) + + assert len(calls) == 2 + assert sleeps == [7] + + +def test_install_with_retry_raises_after_final_attempt(monkeypatch) -> None: + def fake_run_subprocess(command: list[str]) -> None: + raise subprocess.CalledProcessError(returncode=1, cmd=command) + + monkeypatch.setattr(SMOKE, "_run_subprocess", fake_run_subprocess) + monkeypatch.setattr(SMOKE.time, "sleep", lambda seconds: None) + + try: + SMOKE._install_with_retry( + ["python", "-m", "pip", "install", "debugbundle-python==1.0.0"], + retries=2, + retry_delay_seconds=1, + ) + except subprocess.CalledProcessError as error: + assert error.returncode == 1 + else: + raise AssertionError("Expected install retry helper to re-raise after the final attempt.")