diff --git a/.github/workflows/ecosystem.yml b/.github/workflows/ecosystem.yml index 96c86d9..8eada01 100644 --- a/.github/workflows/ecosystem.yml +++ b/.github/workflows/ecosystem.yml @@ -17,6 +17,6 @@ jobs: qualification: permissions: contents: read - uses: ml4t/ecosystem/.github/workflows/qualify-library.yml@457678d8860d0c816b1dc376e7e2691daa93af4d # v0.1.0 + uses: ml4t/ecosystem/.github/workflows/qualify-library.yml@da2cbb9a9d0b4d2f166ad630470ff7d7a1aa2643 # 2026-09-19 policy snapshot with: import-package: ml4t.live diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c196c35..7b4bc92 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: needs: preflight permissions: contents: read - uses: ml4t/ecosystem/.github/workflows/qualify-library.yml@457678d8860d0c816b1dc376e7e2691daa93af4d # v0.1.0 + uses: ml4t/ecosystem/.github/workflows/qualify-library.yml@da2cbb9a9d0b4d2f166ad630470ff7d7a1aa2643 # 2026-09-19 policy snapshot with: import-package: ml4t.live diff --git a/pyproject.toml b/pyproject.toml index b249727..de2810c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ ml4t-live = "ml4t.live.cli.main:app" [project.urls] -Homepage = "https://www.ml4trading.io/docs/live/" +Homepage = "https://www.ml4trading.io/" Documentation = "https://www.ml4trading.io/docs/live/" Repository = "https://github.com/ml4t/live" Issues = "https://github.com/ml4t/live/issues" diff --git a/scripts/qualification/qualify_artifacts.py b/scripts/qualification/qualify_artifacts.py index 7899ac3..cf08c0d 100644 --- a/scripts/qualification/qualify_artifacts.py +++ b/scripts/qualification/qualify_artifacts.py @@ -48,7 +48,7 @@ "risk-management", } EXPECTED_URLS = { - "Homepage": "https://www.ml4trading.io/docs/live/", + "Homepage": "https://www.ml4trading.io/", "Documentation": "https://www.ml4trading.io/docs/live/", "Repository": "https://github.com/ml4t/live", "Issues": "https://github.com/ml4t/live/issues", diff --git a/scripts/qualification/verify_documentation_identity.py b/scripts/qualification/verify_documentation_identity.py index 906d1dd..26b69f7 100644 --- a/scripts/qualification/verify_documentation_identity.py +++ b/scripts/qualification/verify_documentation_identity.py @@ -79,7 +79,7 @@ def deployed_identity_failures( library: str, version: str, commit: str, - attempts: int = 12, + attempts: int = 24, wait_seconds: float = 10.0, ) -> list[str]: """Poll representative deployed pages until every identity matches.""" diff --git a/tests/unit/test_artifact_qualification.py b/tests/unit/test_artifact_qualification.py index b0d777d..1715023 100644 --- a/tests/unit/test_artifact_qualification.py +++ b/tests/unit/test_artifact_qualification.py @@ -29,11 +29,19 @@ run_readme_quick_start, validate_metadata, ) +from scripts.qualification.qualify_artifacts import ( + build_distributions as build_release_distributions, +) from scripts.qualification.scan_release_secrets import scan_payloads REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +@pytest.fixture(scope="module") +def built_distributions(tmp_path_factory: pytest.TempPathFactory) -> tuple[Path, Path]: + return build_release_distributions(tmp_path_factory.mktemp("distribution"), "1704067200") + + def test_release_manifest_marks_typing_and_excludes_agent_files() -> None: wheel, sdist = load_expected_manifests() @@ -74,6 +82,23 @@ def test_external_artifact_input_requires_one_pair(tmp_path: Path) -> None: distribution_pair(tmp_path) +def test_built_distributions_preserve_canonical_public_identity( + built_distributions: tuple[Path, Path], +) -> None: + wheel, sdist = built_distributions + messages = ( + email.message_from_bytes(qualify_artifacts._wheel_member(wheel, ".dist-info/METADATA")), + email.message_from_bytes(qualify_artifacts._sdist_member(sdist, "/PKG-INFO")), + ) + + for message in messages: + urls = dict(value.split(", ", maxsplit=1) for value in message.get_all("Project-URL", [])) + assert message["Author-email"] == "Stefan Jansen " + assert message["Maintainer-email"] == "Stefan Jansen " + assert urls["Homepage"] == "https://www.ml4trading.io/" + assert urls["Documentation"] == "https://www.ml4trading.io/docs/live/" + + def test_metadata_contract_accepts_declared_stable_candidate() -> None: with (REPOSITORY_ROOT / "pyproject.toml").open("rb") as stream: project = tomllib.load(stream)["project"] diff --git a/tests/unit/test_documentation_identity.py b/tests/unit/test_documentation_identity.py index 53ec77b..6a4a88e 100644 --- a/tests/unit/test_documentation_identity.py +++ b/tests/unit/test_documentation_identity.py @@ -1,11 +1,13 @@ from __future__ import annotations import json +from io import BytesIO from pathlib import Path import pytest from scripts.qualification.verify_documentation_identity import ( + deployed_identity_failures, expected_identity, html_identity_failures, site_identity_failures, @@ -51,3 +53,35 @@ def test_site_identity_checks_every_page_and_manifest(tmp_path: Path) -> None: (nested / "index.html").write_text(rendered_page(commit="b" * 40)) failures = site_identity_failures(tmp_path, library="live", version="1.2.3", commit=COMMIT) assert any("api/index.html: ml4t-commit" in failure for failure in failures) + + +def test_deployed_identity_default_allows_four_minutes_for_propagation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + requests: list[str] = [] + sleeps: list[float] = [] + + def open_url(url: str, *, timeout: int) -> BytesIO: + requests.append(url) + assert timeout == 20 + version = "1.2.3" if len(requests) <= 23 * 3 else "1.2.4" + return BytesIO(rendered_page(version=version).encode()) + + monkeypatch.setattr( + "scripts.qualification.verify_documentation_identity.urllib.request.urlopen", open_url + ) + monkeypatch.setattr( + "scripts.qualification.verify_documentation_identity.time.sleep", sleeps.append + ) + + assert ( + deployed_identity_failures( + "https://example.test/docs/live/", + library="live", + version="1.2.4", + commit=COMMIT, + ) + == [] + ) + assert len(requests) == 24 * 3 + assert sleeps == [10.0] * 23