From a49e0980a3f72861b1bc2373eb64efc74db12a65 Mon Sep 17 00:00:00 2001 From: CometAPI Date: Thu, 22 Oct 2026 15:24:26 +0800 Subject: [PATCH] ci: pin Python build backend and harden dependency updates --- .github/dependabot.yml | 17 +++++++++++++++++ CONTRIBUTING.md | 10 ++++++---- pyproject.toml | 2 +- scripts/_checks.py | 29 +++++++++++++++++++++++++++++ scripts/check_workflows.py | 15 +++++++++++++-- tests/test_release_documents.py | 20 ++++++++++++++++++++ 6 files changed, 86 insertions(+), 7 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fdae464..f522ec1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,23 @@ updates: time: "04:00" timezone: Etc/UTC open-pull-requests-limit: 10 + allow: + - dependency-type: production + # Toolchain majors require a coordinated compatibility review. + # This filter affects version updates, not security updates. + - dependency-type: development + update-types: + - version-update:semver-minor + - version-update:semver-patch + groups: + production-dependencies: + dependency-type: production + development-dependencies: + dependency-type: development + ignore: + - dependency-name: openai + versions: + - ">=3" labels: - dependencies - python diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c1c62f..f38b57c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,11 +43,13 @@ when it is not cached. A passing static workflow check does not mean the workflow has run successfully on GitHub. Both Hatch build targets explicitly emit Core Metadata 2.4, which the locked -Twine toolchain can validate. Build isolation resolves Hatchling separately from -`uv.lock`, so relying on its default metadata version can break an unchanged -checkout. The build regression test checks both the sdist-to-wheel path and a +Twine toolchain can validate. The build backend is pinned to +`hatchling==1.32.4` because build isolation resolves it separately from +`uv.lock`; the workflow semantic check rejects a floating or alternate backend +constraint. The build regression test checks both the sdist-to-wheel path and a direct wheel build with the locked Twine validator. Change the metadata format -only together with verified support in the validation and publication tools. +or backend pin only together with verified support in the validation and +publication tools. If a check is unavailable, say so in the pull request rather than describing it as passing. diff --git a/pyproject.toml b/pyproject.toml index 4359dff..f1ae220 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dev = [ ] [build-system] -requires = ["hatchling>=1.27.0,<2.0.0"] +requires = ["hatchling==1.32.4"] build-backend = "hatchling.build" [tool.hatch.build.targets.sdist] diff --git a/scripts/_checks.py b/scripts/_checks.py index 1bf98f1..2fdaec2 100644 --- a/scripts/_checks.py +++ b/scripts/_checks.py @@ -21,6 +21,8 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1] DIST_NAME = "cometapi" +BUILD_SYSTEM_BACKEND = "hatchling.build" +BUILD_SYSTEM_REQUIREMENT = "hatchling==1.32.4" CANONICAL_ACTIVE_MODEL = "gpt-5.6-sol" CANONICAL_AUTHOR = "CometAPI" CANONICAL_COPYRIGHT = "Copyright (c) 2026 CometAPI" @@ -281,6 +283,33 @@ def read_project_metadata(root: Path = PROJECT_ROOT) -> dict[str, object]: return cast(dict[str, object], project) +def require_exact_build_system(root: Path = PROJECT_ROOT) -> None: + """Require the reviewed isolated build backend and exact version constraint.""" + path = root / "pyproject.toml" + try: + if sys.version_info >= (3, 11): + import tomllib + else: + import tomli as tomllib + except ImportError as exc: + raise CheckError("TOML parsing requires tomli on Python 3.10") from exc + try: + document = cast(dict[str, object], tomllib.loads(path.read_text(encoding="utf-8"))) + except (OSError, UnicodeError, tomllib.TOMLDecodeError) as exc: + raise CheckError(f"cannot read {path.name}: {exc}") from exc + build_system = document.get("build-system") + if not isinstance(build_system, dict): + raise CheckError("pyproject.toml has no [build-system] table") + build_system = cast(dict[str, object], build_system) + backend = build_system.get("build-backend") + requires = build_system.get("requires") + if backend != BUILD_SYSTEM_BACKEND or requires != [BUILD_SYSTEM_REQUIREMENT]: + raise CheckError( + "[build-system] must use exactly " + f"{BUILD_SYSTEM_BACKEND!r} with requires=[{BUILD_SYSTEM_REQUIREMENT!r}]" + ) + + def read_project_version(root: Path = PROJECT_ROOT) -> str: """Return the static PEP 621 project version.""" version = read_project_metadata(root).get("version") diff --git a/scripts/check_workflows.py b/scripts/check_workflows.py index e596015..4a62f22 100644 --- a/scripts/check_workflows.py +++ b/scripts/check_workflows.py @@ -13,9 +13,19 @@ import yaml try: - from ._checks import CANONICAL_ACTIVE_MODEL, PROJECT_ROOT, CheckError + from ._checks import ( + CANONICAL_ACTIVE_MODEL, + PROJECT_ROOT, + CheckError, + require_exact_build_system, + ) except ImportError: # Direct execution from the repository root. - from _checks import CANONICAL_ACTIVE_MODEL, PROJECT_ROOT, CheckError + from _checks import ( + CANONICAL_ACTIVE_MODEL, + PROJECT_ROOT, + CheckError, + require_exact_build_system, + ) RELEASE_PLEASE_BASELINE_SHA = "31b68904141489ca04932edbf305ccf88af09372" RELEASE_PLEASE_LOCK_JSONPATH = "$.package[?(@.name.value == 'cometapi')].version" @@ -1864,6 +1874,7 @@ def main() -> int: default=PROJECT_ROOT / ".release-please-manifest.json", ) args = parser.parse_args() + require_exact_build_system(PROJECT_ROOT) paths = check_workflow_inventory(args.ci_workflow.parent) publish_text = args.publish_workflow.read_text(encoding="utf-8") check_publish_workflow( diff --git a/tests/test_release_documents.py b/tests/test_release_documents.py index b5d54ae..f063eb6 100644 --- a/tests/test_release_documents.py +++ b/tests/test_release_documents.py @@ -23,6 +23,7 @@ CheckError, exact_release_version_violations, read_project_version, + require_exact_build_system, ) from scripts.check_artifacts import check_metadata, check_sdist, check_wheel from scripts.check_version import require_public_preview_docs, require_releasable_docs @@ -31,6 +32,25 @@ VERSION_SCRIPT = PROJECT_ROOT / "scripts" / "check_version.py" +def test_build_system_requires_exact_hatchling_constraint(tmp_path: Path) -> None: + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + '[build-system]\nrequires = ["hatchling==1.32.4"]\nbuild-backend = "hatchling.build"\n', + encoding="utf-8", + ) + require_exact_build_system(tmp_path) + + for invalid_build_system in ( + '[build-system]\nrequires = ["hatchling>=1.27.0,<2.0.0"]\n' + 'build-backend = "hatchling.build"\n', + '[build-system]\nrequires = ["setuptools==80.0.0"]\n' + 'build-backend = "setuptools.build_meta"\n', + ): + pyproject.write_text(invalid_build_system, encoding="utf-8") + with pytest.raises(CheckError, match=r"\[build-system\].*exactly"): + require_exact_build_system(tmp_path) + + def _copy_version_checker(root: Path) -> Path: scripts = root / "scripts" scripts.mkdir(exist_ok=True)