START FROM A TESTED COMPOSITION
From empty directory to passing production checks.
- uvx --from create-hayate==0.13.0 create-hayate my-app --template workers --preset production
+ uvx --from create-hayate==0.13.1 create-hayate my-app --template workers --preset production
Follow the verified path
diff --git a/scripts/check_release_freshness.py b/scripts/check_release_freshness.py
new file mode 100644
index 0000000..edb5c22
--- /dev/null
+++ b/scripts/check_release_freshness.py
@@ -0,0 +1,108 @@
+"""Fail when the public ecosystem manifest or quickstart pins lag behind PyPI."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import tomllib
+from collections.abc import Callable, Mapping
+from pathlib import Path
+from typing import Any
+from urllib.request import Request, urlopen
+
+ROOT = Path(__file__).resolve().parents[1]
+MANIFEST = ROOT / "data" / "ecosystem.toml"
+CREATE_HAYATE_PIN = re.compile(r"create-hayate==(\d+\.\d+\.\d+)")
+PYPI_ORIGIN = "https://pypi.org"
+USER_AGENT = "hayatepy-docs-release-freshness/1"
+
+FetchProject = Callable[[str], Mapping[str, Any]]
+
+
+def _normalize_project_name(name: str) -> str:
+ return re.sub(r"[-_.]+", "-", name).lower()
+
+
+def _public_packages(data: Mapping[str, Any]) -> list[Mapping[str, Any]]:
+ packages = data.get("packages")
+ if not isinstance(packages, list):
+ raise ValueError("ecosystem manifest needs a package list")
+ return [package for package in packages if isinstance(package, Mapping) and package.get("pypi")]
+
+
+def fetch_pypi_project(name: str) -> Mapping[str, Any]:
+ request = Request(
+ f"{PYPI_ORIGIN}/pypi/{name}/json",
+ headers={"Accept": "application/json", "User-Agent": USER_AGENT},
+ )
+ with urlopen(request, timeout=10) as response:
+ payload = json.load(response)
+ if not isinstance(payload, Mapping):
+ raise ValueError(f"PyPI returned a non-object payload for {name}")
+ return payload
+
+
+def validate_pypi_versions(
+ data: Mapping[str, Any], fetch_project: FetchProject = fetch_pypi_project
+) -> list[str]:
+ failures: list[str] = []
+ for package in _public_packages(data):
+ expected_name = str(package["name"])
+ expected_version = str(package["version"])
+ payload = fetch_project(expected_name)
+ info = payload.get("info")
+ if not isinstance(info, Mapping):
+ failures.append(f"{expected_name}: PyPI payload has no info object")
+ continue
+ published_name = str(info.get("name", ""))
+ published_version = str(info.get("version", ""))
+ if _normalize_project_name(published_name) != _normalize_project_name(expected_name):
+ failures.append(
+ f"{expected_name}: PyPI returned project {published_name or '