diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 1cad151..bf7b2cb 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -5,6 +5,8 @@ on: branches: [main] pull_request: workflow_dispatch: + schedule: + - cron: "17 5 * * *" permissions: contents: read @@ -30,6 +32,7 @@ jobs: - run: uv run ruff format --check scripts tests - run: uv run pytest -q - run: uv run python scripts/export_ecosystem.py --check + - run: uv run python scripts/check_release_freshness.py - run: uv run zensical build --clean --strict - run: uv run python scripts/write_headers.py site - run: uv run python scripts/check_site.py site @@ -71,7 +74,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} deploy: - if: github.event_name != 'pull_request' + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' needs: [build, audit-dependencies, lint-workflows] runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/data/ecosystem.toml b/data/ecosystem.toml index a685eee..ad64272 100644 --- a/data/ecosystem.toml +++ b/data/ecosystem.toml @@ -1,8 +1,8 @@ schema_version = 1 as_of = "2026-07-30" source_repository = "hayatepy/.github" -source_commit = "975173da2c1be720a4f902cde36ba3822b9b6e56" -compatibility_url = "https://github.com/hayatepy/.github/blob/975173da2c1be720a4f902cde36ba3822b9b6e56/docs/COMPATIBILITY.md" +source_commit = "9320a766fd5eab03298ae04a39ac39a68799dc48" +compatibility_url = "https://github.com/hayatepy/.github/blob/9320a766fd5eab03298ae04a39ac39a68799dc48/docs/COMPATIBILITY.md" [[packages]] name = "hayate" @@ -16,23 +16,23 @@ source_commit = "5592276315c9b75e736d3fc204b4900e15f8867a" [[packages]] name = "create-hayate" -version = "0.13.0" +version = "0.13.1" status = "Published alpha" role = "Composable applications with drift-checked first-party TypeScript clients" use_when = "Start with a tested API, Workers, MCP, auth, SQL, admin, or frontend composition and keep its generated client in sync." repository = "https://github.com/hayatepy/create-hayate" pypi = "https://pypi.org/project/create-hayate/" -source_commit = "9fdb98e5642ff047e04e24251be1c2747fc7572a" +source_commit = "97d19b59e1256ec6d50e9ecb725296442fda4b20" [[packages]] name = "hayate-auth" -version = "0.10.1" +version = "0.10.3" status = "Published alpha; independent audit pending" role = "Sessions, API keys, OAuth, passkeys, and authorization server" use_when = "Add first-party identity or protect an HTTP or MCP resource server." repository = "https://github.com/hayatepy/hayate-auth" pypi = "https://pypi.org/project/hayate-auth/" -source_commit = "eff705c335d0903084988edea5b783be258f42e8" +source_commit = "20dd7ae08c12051d23fdbe4b242578f800dacdfc" [[packages]] name = "hayate-mcp" @@ -99,4 +99,4 @@ status = "Reference application" role = "Executable production integration" use_when = "Review tested trust boundaries and deployment choices before shipping." repository = "https://github.com/hayatepy/golden-app" -source_commit = "842fd2486f3d8bd5c284e44e4f8528b0441a574a" +source_commit = "cfa07c1d129b588b3c539a6d6c9fcdd443cefbf7" diff --git a/docs/ecosystem/compose.md b/docs/ecosystem/compose.md index 461491b..274279c 100644 --- a/docs/ecosystem/compose.md +++ b/docs/ecosystem/compose.md @@ -62,7 +62,7 @@ customer workflows. them: ```sh -uvx --from create-hayate==0.13.0 create-hayate --help +uvx --from create-hayate==0.13.1 create-hayate --help ``` Use the [family map](index.md) for current release state and package links. diff --git a/docs/ecosystem/index.md b/docs/ecosystem/index.md index 311e39f..8388064 100644 --- a/docs/ecosystem/index.md +++ b/docs/ecosystem/index.md @@ -4,13 +4,13 @@ Choose packages by the boundary your application needs. The core stays small; capabilities are explicit packages with their own release and evidence. -This snapshot is dated **2026-07-30** and is pinned to [`hayatepy/.github` at `975173da2c1b`](https://github.com/hayatepy/.github/tree/975173da2c1be720a4f902cde36ba3822b9b6e56). Every package is pre-1.0; check the compatibility snapshot before combining versions. +This snapshot is dated **2026-07-30** and is pinned to [`hayatepy/.github` at `9320a766fd5e`](https://github.com/hayatepy/.github/tree/9320a766fd5eab03298ae04a39ac39a68799dc48). Every package is pre-1.0; check the compatibility snapshot before combining versions. | Project | Version | Responsibility | Release state | |---|---:|---|---| | [`hayate`](https://github.com/hayatepy/hayate) | [`0.15.1`](https://pypi.org/project/hayate/) | Core framework and native ASGI, Workers, and Lambda adapters | Published alpha | -| [`create-hayate`](https://github.com/hayatepy/create-hayate) | [`0.13.0`](https://pypi.org/project/create-hayate/) | Composable applications with drift-checked first-party TypeScript clients | Published alpha | -| [`hayate-auth`](https://github.com/hayatepy/hayate-auth) | [`0.10.1`](https://pypi.org/project/hayate-auth/) | Sessions, API keys, OAuth, passkeys, and authorization server | Published alpha; independent audit pending | +| [`create-hayate`](https://github.com/hayatepy/create-hayate) | [`0.13.1`](https://pypi.org/project/create-hayate/) | Composable applications with drift-checked first-party TypeScript clients | Published alpha | +| [`hayate-auth`](https://github.com/hayatepy/hayate-auth) | [`0.10.3`](https://pypi.org/project/hayate-auth/) | Sessions, API keys, OAuth, passkeys, and authorization server | Published alpha; independent audit pending | | [`hayate-mcp`](https://github.com/hayatepy/hayate-mcp) | [`0.12.0`](https://pypi.org/project/hayate-mcp/) | MCP 2026-07-28 Streamable HTTP and discovery | Published alpha | | [`hayate-openapi`](https://github.com/hayatepy/hayate-openapi) | [`0.8.1`](https://pypi.org/project/hayate-openapi/) | OpenAPI 3.1, Scalar, and generated zero-runtime TypeScript clients | Published alpha | | [`hayate-sql`](https://github.com/hayatepy/hayate-sql) | [`0.1.1`](https://pypi.org/project/hayate-sql/) | Checked SQL contracts and typed query facades | Published alpha | @@ -19,7 +19,7 @@ This snapshot is dated **2026-07-30** and is pinned to [`hayatepy/.github` at `9 | [`hayate-htmx`](https://github.com/hayatepy/hayate-htmx) | `0.2.0` | Hypermedia, Jinja, htmx, fragments, and SSE | Pre-release source | | [`golden-app`](https://github.com/hayatepy/golden-app) | `0.1.0` | Executable production integration | Reference application | -[Open the tested compatibility snapshot](https://github.com/hayatepy/.github/blob/975173da2c1be720a4f902cde36ba3822b9b6e56/docs/COMPATIBILITY.md) +[Open the tested compatibility snapshot](https://github.com/hayatepy/.github/blob/9320a766fd5eab03298ae04a39ac39a68799dc48/docs/COMPATIBILITY.md) ## hayate @@ -33,7 +33,7 @@ Build one Fetch-style Python application for ASGI, Workers, or Lambda. Start with a tested API, Workers, MCP, auth, SQL, admin, or frontend composition and keep its generated client in sync. -**Current evidence:** Published alpha, version `0.13.0` · [source at `9fdb98e5642f`](https://github.com/hayatepy/create-hayate/tree/9fdb98e5642ff047e04e24251be1c2747fc7572a) +**Current evidence:** Published alpha, version `0.13.1` · [source at `97d19b59e125`](https://github.com/hayatepy/create-hayate/tree/97d19b59e1256ec6d50e9ecb725296442fda4b20) [Repository](https://github.com/hayatepy/create-hayate) · [PyPI](https://pypi.org/project/create-hayate/) @@ -41,7 +41,7 @@ Start with a tested API, Workers, MCP, auth, SQL, admin, or frontend composition Add first-party identity or protect an HTTP or MCP resource server. -**Current evidence:** Published alpha; independent audit pending, version `0.10.1` · [source at `eff705c335d0`](https://github.com/hayatepy/hayate-auth/tree/eff705c335d0903084988edea5b783be258f42e8) +**Current evidence:** Published alpha; independent audit pending, version `0.10.3` · [source at `20dd7ae08c12`](https://github.com/hayatepy/hayate-auth/tree/20dd7ae08c12051d23fdbe4b242578f800dacdfc) [Repository](https://github.com/hayatepy/hayate-auth) · [PyPI](https://pypi.org/project/hayate-auth/) @@ -97,6 +97,6 @@ Build progressively enhanced server-rendered applications without a JavaScript S Review tested trust boundaries and deployment choices before shipping. -**Current evidence:** Reference application, version `0.1.0` · [source at `842fd2486f3d`](https://github.com/hayatepy/golden-app/tree/842fd2486f3d8bd5c284e44e4f8528b0441a574a) +**Current evidence:** Reference application, version `0.1.0` · [source at `cfa07c1d129b`](https://github.com/hayatepy/golden-app/tree/cfa07c1d129b588b3c539a6d6c9fcdd443cefbf7) [Repository](https://github.com/hayatepy/golden-app) diff --git a/docs/get-started/first-app.md b/docs/get-started/first-app.md index 3f041a0..af12207 100644 --- a/docs/get-started/first-app.md +++ b/docs/get-started/first-app.md @@ -9,7 +9,7 @@ Cloudflare Access, checked SQL, request correlation, and production middleware. Install [uv](https://docs.astral.sh/uv/), then run: ```sh -uvx --refresh --from create-hayate==0.13.0 \ +uvx --refresh --from create-hayate==0.13.1 \ create-hayate my-app --template workers --preset production cd my-app ``` diff --git a/docs/index.md b/docs/index.md index 7eaaf9f..db88571 100644 --- a/docs/index.md +++ b/docs/index.md @@ -92,6 +92,6 @@ description: One Fetch-style Python application across ASGI, Cloudflare Workers,

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 ''}" + ) + if published_version != expected_version: + failures.append( + f"{expected_name}: manifest has {expected_version}, " + f"PyPI latest is {published_version or ''}" + ) + return failures + + +def validate_create_hayate_pins(root: Path, data: Mapping[str, Any]) -> list[str]: + versions = {str(package["name"]): str(package["version"]) for package in _public_packages(data)} + expected = versions.get("create-hayate") + if expected is None: + return ["ecosystem manifest has no public create-hayate package"] + + failures: list[str] = [] + references = 0 + for path in sorted((root / "docs").rglob("*.md")): + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + for match in CREATE_HAYATE_PIN.finditer(line): + references += 1 + if match.group(1) != expected: + relative = path.relative_to(root) + failures.append( + f"{relative}:{line_number}: create-hayate pin " + f"{match.group(1)} must match manifest {expected}" + ) + if references == 0: + failures.append("documentation contains no exact create-hayate release pin") + return failures + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.parse_args() + data = tomllib.loads(MANIFEST.read_text(encoding="utf-8")) + failures = validate_pypi_versions(data) + failures.extend(validate_create_hayate_pins(ROOT, data)) + if failures: + raise SystemExit("\n".join(failures)) + print("Validated public PyPI releases and documentation pins") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_release_freshness.py b/tests/test_release_freshness.py new file mode 100644 index 0000000..aa51e86 --- /dev/null +++ b/tests/test_release_freshness.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from pathlib import Path + +from scripts.check_release_freshness import ( + validate_create_hayate_pins, + validate_pypi_versions, +) + + +def manifest(version: str = "0.13.1") -> dict: + return { + "packages": [ + { + "name": "create-hayate", + "version": version, + "pypi": "https://pypi.org/project/create-hayate/", + }, + { + "name": "source-only", + "version": "0.1.0", + }, + ] + } + + +def test_public_release_matches_manifest() -> None: + failures = validate_pypi_versions( + manifest(), + lambda name: {"info": {"name": "create_hayate", "version": "0.13.1"}}, + ) + + assert failures == [] + + +def test_public_release_drift_is_reported() -> None: + failures = validate_pypi_versions( + manifest("0.13.0"), + lambda name: {"info": {"name": name, "version": "0.13.1"}}, + ) + + assert failures == ["create-hayate: manifest has 0.13.0, PyPI latest is 0.13.1"] + + +def test_documentation_pin_must_match_manifest(tmp_path: Path) -> None: + docs = tmp_path / "docs" + docs.mkdir() + (docs / "start.md").write_text( + "uvx --from create-hayate==0.13.0 create-hayate demo\n", + encoding="utf-8", + ) + + failures = validate_create_hayate_pins(tmp_path, manifest()) + + assert failures == ["docs/start.md:1: create-hayate pin 0.13.0 must match manifest 0.13.1"] + + +def test_documentation_requires_an_exact_release_pin(tmp_path: Path) -> None: + (tmp_path / "docs").mkdir() + + assert validate_create_hayate_pins(tmp_path, manifest()) == [ + "documentation contains no exact create-hayate release pin" + ]