From 7ad4f02b37c3c8ab8c20bcb7109989d6e5079e57 Mon Sep 17 00:00:00 2001 From: CometAPI Date: Thu, 30 Jul 2026 10:34:08 +0800 Subject: [PATCH 1/2] fix: harden release metadata and runtime --- .github/workflows/publish.yml | 2 +- AGENTS.md | 14 ++- ARCHITECTURE.md | 12 +++ CHANGELOG.md | 7 ++ README.md | 12 ++- RELEASING.md | 37 +++++--- ROADMAP.md | 5 +- scripts/_checks.py | 57 ++++++++++--- scripts/check_artifacts.py | 86 +++++++++++++++---- scripts/check_version.py | 43 +++------- scripts/check_workflows.py | 9 +- tests/test_release_documents.py | 145 +++++++++++++++++++++++++++++++- tests/test_release_workflow.py | 18 ++++ 13 files changed, 363 insertions(+), 84 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0c18e79..e59bb25 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Open or update the release PR, or create its approved release id: release - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0, node24 with: config-file: release-please-config.json manifest-file: .release-please-manifest.json diff --git a/AGENTS.md b/AGENTS.md index 896b528..c0433c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,6 +152,16 @@ Post-stable invariants: direct dependency's `result` to equal `success`. A skipped, cancelled, failed, or missing dependency must never make build, live smoke, publication, or registry verification eligible. +9. Keep Release Please pinned to the reviewed `v5.0.0` commit + `45996ed1f6d02564a971a2fa1b5860e934307cf7`, whose immutable action metadata + uses `node24`. The workflow semantic checker must reject any different pin. +10. `README.md` is the distribution long description and must remain accurate + before and after publication. Use `python -m pip install cometapi`, + unversioned project links, and publication-neutral maintenance language. + Reject approval, unpublished, exact-version installation, and versioned + release-link text in both source and built artifact metadata, and require + each built long description to exactly match the source README. Post-release + evidence changes must not rewrite README release state. ## Repository independence @@ -329,7 +339,9 @@ committed. evaluate skipped ancestry, reject cancellation and reruns, and require each direct dependency to succeed. - Keep README, roadmap, compatibility matrix, examples, and changelog aligned - with shipped behavior. Use currently supported model IDs. + with shipped behavior. README installation and availability guidance must be + publication-neutral because it is embedded in immutable distribution + metadata. Use currently supported model IDs. - All repository documentation is written in English. The Public Preview readiness record requires diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c834ac5..74636d2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -128,6 +128,18 @@ local mocked/package evidence -> public registry digest, provenance, install, import, and mocked smoke ``` +The package metadata embeds `README.md` as its long description. Because wheel, +sdist, and PyPI metadata are immutable, the README uses an unversioned install +command and publication-neutral release language that remains accurate before +and after a release. Source-document and artifact checks reject approval, +unpublished, exact-version installation, and versioned release-link text; each +artifact long description must also exactly match the source README. + +Release Please is pinned to the immutable `v5.0.0` commit whose action metadata +uses `node24`. The workflow semantic contract fixes that SHA and runtime +disposition so GitHub does not need to force a deprecated Node 20 action onto a +newer runtime. + This complete trust chain executed successfully in [release workflow run 30261746138](https://github.com/cometapi-dev/cometapi-python/actions/runs/30261746138) for release commit `31b68904141489ca04932edbf305ccf88af09372`, recovery tag diff --git a/CHANGELOG.md b/CHANGELOG.md index 89457a2..718caa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ automation. ## [Unreleased] +### Fixed + +- Pin Release Please to its reviewed Node 24 action and reject regressions to + the deprecated Node 20 runtime. +- Keep the README and built distribution long description publication-neutral, + with an unpinned stable installation command and no transient approval state. + ### Documentation - Record completed `0.1.1` publication, live, provenance, digest, and diff --git a/README.md b/README.md index d5f387b..836cb00 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CometAPI Python SDK -> **Stable release:** `0.1.1` is available from PyPI. +> **Stable release line:** `0.1.x` maintenance releases are available from PyPI. `cometapi` is a thin Python adapter over the official OpenAI SDK for the OpenAI-compatible CometAPI endpoint. It changes the default API key and base @@ -29,16 +29,14 @@ Project links: [CometAPI](https://www.cometapi.com), ## Installation -Install the independently verified stable release from PyPI with: +Install the latest stable release from PyPI with: ```bash -python -m pip install 'cometapi==0.1.1' +python -m pip install cometapi ``` -The immutable -[GitHub release](https://github.com/cometapi-dev/cometapi-python/releases/tag/v0.1.1) -and exact [PyPI release](https://pypi.org/project/cometapi/0.1.1/) record the -published artifact. +See the [CometAPI package on PyPI](https://pypi.org/project/cometapi/) for the +current stable distribution and release history. ## Authentication and configuration diff --git a/RELEASING.md b/RELEASING.md index a98d0ad..ecf5201 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -167,11 +167,26 @@ secret separation, and checkout-before-bundle-download ordering. Its git-backed tests exercise accepted and rejected release histories locally; they still do not emulate GitHub Actions. +The Release Please step is pinned to +`googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7` +(`v5.0.0`), whose immutable action metadata selects `node24`. The semantic +checker rejects any other pin so the workflow cannot silently regress to the +deprecated Node 20 runtime. + Release mode (`check_version.py --require-releasable-docs`) also fails closed until project authorship, the canonical GitHub repository URL, the copyright -holder, security and support contacts, and the approved README/changelog -release state are present. Public Preview validation reports all discovered -violations in one run and still returns non-zero when any violation exists. +holder, security and support contacts, a publication-neutral README, and a dated +changelog release section are present. Public Preview validation reports all +discovered violations in one run and still returns non-zero when any violation +exists. + +`pyproject.toml` embeds `README.md` as the immutable distribution long +description. The README therefore uses the unpinned +`python -m pip install cometapi` command and unversioned project links. Release +PRs and post-release evidence changes must not introduce approval, unpublished, +or exact-version availability statements. Artifact inspection applies the same +policy to wheel `METADATA` and sdist `PKG-INFO`, so source and registry-facing +descriptions cannot drift. ## Workflow responsibilities @@ -326,7 +341,7 @@ feature or fix pull request -> required offline CI -> merge to the default branch -> automated release pull request - -> human finalization of stable docs, metadata, and one-time bridge cleanup + -> human review of generated versions, changelog, and durable metadata -> required release-PR CI, review, and merge -> immutable tag and GitHub release -> bounded API verification of immutable tag and commit identity @@ -340,15 +355,15 @@ feature or fix pull request -> roadmap milestone marked released ``` -Stable `0.1.0` additionally requires the complete blocking Python matrix, +Stable `0.1.0` additionally required the complete blocking Python matrix, executed README examples against the built package, trusted live evidence, and -reviewed release-PR and changelog agreement. Before the stable release PR is -merged, its finalization commit must state that `0.1.0` is approved for PyPI -publication, use the stable installation command and classifier, and remove the -one-time `last-release-sha` plus prerelease-versioning controls. The manifest, +reviewed release-PR and changelog agreement. Its one-time finalization removed +the `last-release-sha` and prerelease-versioning controls. Later maintenance +releases keep those controls absent and must retain publication-neutral README +metadata throughout the release and post-release sequence. The manifest, project metadata, lock file, and changelog must remain at the exact generated -`0.1.0` version. If GitHub requires approval before checks run on the automated -pull request, approve only that reviewed workflow execution and wait for every +version. If GitHub requires approval before checks run on the automated pull +request, approve only that reviewed workflow execution and wait for every blocking check. ## Immutable release publication recovery diff --git a/ROADMAP.md b/ROADMAP.md index a2bf1f3..0d27a34 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -346,8 +346,9 @@ Public Preview remains ready only while: self-review. - Default-branch CI is rerun successfully after the public configuration. - The fail-closed content gate reports all violations together and then passes, - repository self-containment and package gates pass, and the README accurately - states prerelease and registry availability. + repository self-containment and package gates pass, and the README uses + publication-neutral stable installation guidance suitable for immutable + distribution metadata. - An explicitly authorized protected live smoke passes within the four-request, 16-output-token, 30-second-per-request, concurrency-one, stop-on-first-failure budget. diff --git a/scripts/_checks.py b/scripts/_checks.py index f3ac86a..777eb6f 100644 --- a/scripts/_checks.py +++ b/scripts/_checks.py @@ -8,6 +8,7 @@ import sys from collections.abc import Iterable from email.message import Message +from email.parser import Parser from pathlib import Path from typing import cast @@ -31,6 +32,26 @@ "Support": f"{CANONICAL_REPOSITORY}/blob/main/SUPPORT.md", "Security": CANONICAL_SECURITY, } +PUBLIC_README_INSTALL_COMMAND = "python -m pip install cometapi" +PUBLIC_README_FORBIDDEN_PATTERNS = ( + (r"(?i)\bpending[\s-]+owner(?:ship|s)?\b", "pending owner identity"), + (r"(?i)\bapproved\s+for\s+pypi\s+publication\b", "publication approval state"), + (r"(?i)\b(?:candidate|unreleased|unpublished)\b", "unpublished release state"), + (r"(?i)\bno\s+pypi\s+publication\b", "missing PyPI publication"), + (r"(?i)\b(?:has\s+)?not\s+been\s+published\b", "unpublished release state"), + ( + r"(?i)\bdo\s+not\s+treat\b[^\n]*\bcurrently\s+available\b", + "temporary availability warning", + ), + (r"(?i)\blocal\s+candidate\b", "local-candidate narrative"), + (r"(?i)\b0\.1\.\d+(?:a\d+)?\s+is\s+(?:available|approved)\b", "versioned release state"), + (r"(?i)cometapi==\d+\.\d+\.\d+(?:a\d+)?", "version-pinned installation command"), + (r"https://pypi\.org/project/cometapi/\d", "versioned PyPI release link"), + ( + r"https://github\.com/cometapi-dev/cometapi-python/releases/tag/v\d", + "versioned GitHub release link", + ), +) class CheckError(RuntimeError): @@ -96,18 +117,10 @@ def sha256_file(path: Path) -> str: def parse_metadata(raw: bytes, source: str) -> Message: """Parse package core metadata and require its identity fields.""" - message = Message() - text = raw.decode("utf-8") - for line in text.splitlines(): - if not line.strip(): - break - if line[0].isspace(): - current = message.get_payload() - message.set_payload(f"{current}\n{line}" if current else line) - continue - name, separator, value = line.partition(":") - if separator: - message[name] = value.strip() + try: + message = Parser().parsestr(raw.decode("utf-8")) + except (UnicodeDecodeError, ValueError) as exc: + raise CheckError(f"{source}: invalid metadata: {exc}") from exc if message.get("Name") != DIST_NAME: raise CheckError(f"{source}: expected Name: {DIST_NAME!r}") if message.get("Version") is None: @@ -115,6 +128,26 @@ def parse_metadata(raw: bytes, source: str) -> Message: return message +def metadata_description(message: Message, source: str) -> str: + """Return the rendered long description from a distribution metadata message.""" + payload = message.get_payload() + if not isinstance(payload, str) or not payload.strip(): + raise CheckError(f"{source}: missing long description metadata") + return payload + + +def public_readme_release_violations(text: str) -> list[str]: + """Return transient or version-specific release statements in public README text.""" + return [ + label for pattern, label in PUBLIC_README_FORBIDDEN_PATTERNS if re.search(pattern, text) + ] + + +def public_readme_has_install_command(text: str) -> bool: + """Return whether the public README contains the exact unpinned install command.""" + return any(line.strip() == PUBLIC_README_INSTALL_COMMAND for line in text.splitlines()) + + def require_equal_versions(items: Iterable[tuple[str, str]]) -> str: """Require all named version values to normalize to the same version.""" normalized = [(name, normalize_version(value)) for name, value in items] diff --git a/scripts/check_artifacts.py b/scripts/check_artifacts.py index 23624b6..8ab4e5f 100644 --- a/scripts/check_artifacts.py +++ b/scripts/check_artifacts.py @@ -9,15 +9,36 @@ import zipfile from pathlib import Path, PurePosixPath -from _checks import ( - CANONICAL_AUTHOR, - CANONICAL_PROJECT_URLS, - CheckError, - normalize_version, - parse_metadata, - read_project_version, - sha256_file, -) +try: + from ._checks import ( + CANONICAL_AUTHOR, + CANONICAL_PROJECT_URLS, + PROJECT_ROOT, + PUBLIC_README_INSTALL_COMMAND, + CheckError, + metadata_description, + normalize_version, + parse_metadata, + public_readme_has_install_command, + public_readme_release_violations, + read_project_version, + sha256_file, + ) +except ImportError: # Direct execution from the repository root. + from _checks import ( + CANONICAL_AUTHOR, + CANONICAL_PROJECT_URLS, + PROJECT_ROOT, + PUBLIC_README_INSTALL_COMMAND, + CheckError, + metadata_description, + normalize_version, + parse_metadata, + public_readme_has_install_command, + public_readme_release_violations, + read_project_version, + sha256_file, + ) REQUIRED_PACKAGE_FILES = { "cometapi/__init__.py", @@ -88,7 +109,12 @@ def _safe_path(name: str, source: Path) -> PurePosixPath: return path -def _check_metadata(raw: bytes, source: str, expected_version: str) -> None: +def check_metadata( + raw: bytes, + source: str, + expected_version: str, + expected_description: str, +) -> None: metadata = parse_metadata(raw, source) actual_version = normalize_version(str(metadata["Version"])) if actual_version != expected_version: @@ -114,9 +140,23 @@ def _check_metadata(raw: bytes, source: str, expected_version: str) -> None: for label, expected in CANONICAL_PROJECT_URLS.items(): if project_urls.get(label) != expected: raise CheckError(f"{source}: expected Project-URL {label}, {expected}") + description = metadata_description(metadata, source) + if description != expected_description: + raise CheckError(f"{source}: long description does not exactly match source README.md") + violations = public_readme_release_violations(description) + if violations: + raise CheckError( + f"{source}: long description contains publication-specific release text: " + + ", ".join(sorted(set(violations))) + ) + if not public_readme_has_install_command(description): + raise CheckError( + f"{source}: long description must contain the unpinned stable install command " + f"{PUBLIC_README_INSTALL_COMMAND!r}" + ) -def _check_wheel(path: Path, expected_version: str) -> None: +def _check_wheel(path: Path, expected_version: str, expected_description: str) -> None: expected_fragment = f"cometapi-{expected_version}-" if expected_fragment not in path.name: raise CheckError(f"{path.name}: filename does not contain {expected_fragment!r}") @@ -142,8 +182,11 @@ def _check_wheel(path: Path, expected_version: str) -> None: metadata_names = [name for name in names if name.endswith(".dist-info/METADATA")] if len(metadata_names) != 1: raise CheckError(f"{path.name}: expected exactly one .dist-info/METADATA") - _check_metadata( - archive.read(metadata_names[0]), f"{path.name}:{metadata_names[0]}", expected_version + check_metadata( + archive.read(metadata_names[0]), + f"{path.name}:{metadata_names[0]}", + expected_version, + expected_description, ) for source_name in ("cometapi/__init__.py", "cometapi/client.py"): text = archive.read(source_name).decode("utf-8") @@ -151,7 +194,7 @@ def _check_wheel(path: Path, expected_version: str) -> None: raise CheckError(f"{path.name}:{source_name}: legacy public client name remains") -def _check_sdist(path: Path, expected_version: str) -> None: +def _check_sdist(path: Path, expected_version: str, expected_description: str) -> None: expected_root = f"cometapi-{expected_version}" if path.name != f"{expected_root}.tar.gz": raise CheckError(f"{path.name}: expected sdist filename {expected_root}.tar.gz") @@ -182,7 +225,12 @@ def _check_sdist(path: Path, expected_version: str) -> None: stream = archive.extractfile(metadata_members[0]) if stream is None: raise CheckError(f"{path.name}: cannot read PKG-INFO") - _check_metadata(stream.read(), f"{path.name}:PKG-INFO", expected_version) + check_metadata( + stream.read(), + f"{path.name}:PKG-INFO", + expected_version, + expected_description, + ) def _artifacts(arguments: list[str]) -> list[Path]: @@ -206,12 +254,16 @@ def main() -> int: parser.add_argument("--expected-version", default=read_project_version()) args = parser.parse_args() expected_version = normalize_version(args.expected_version) + try: + expected_description = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8") + except (OSError, UnicodeError) as exc: + raise CheckError(f"cannot read source README.md: {exc}") from exc paths = _artifacts(args.artifacts) for path in paths: if path.suffix == ".whl": - _check_wheel(path, expected_version) + _check_wheel(path, expected_version, expected_description) else: - _check_sdist(path, expected_version) + _check_sdist(path, expected_version, expected_description) print(f"{sha256_file(path)} {path}") print(f"artifact checks passed for cometapi {expected_version}") return 0 diff --git a/scripts/check_version.py b/scripts/check_version.py index a5288b4..cd82438 100644 --- a/scripts/check_version.py +++ b/scripts/check_version.py @@ -18,8 +18,11 @@ CANONICAL_SECURITY, CANONICAL_SUPPORT, DIST_NAME, + PUBLIC_README_INSTALL_COMMAND, CheckError, normalize_version, + public_readme_has_install_command, + public_readme_release_violations, read_project_metadata, read_project_version, read_release_manifest, @@ -34,8 +37,11 @@ CANONICAL_SECURITY, CANONICAL_SUPPORT, DIST_NAME, + PUBLIC_README_INSTALL_COMMAND, CheckError, normalize_version, + public_readme_has_install_command, + public_readme_release_violations, read_project_metadata, read_project_version, read_release_manifest, @@ -230,39 +236,16 @@ def require_releasable_docs(project_version: str) -> None: require_public_preview_docs() readme = Path("README.md").read_text(encoding="utf-8") - if "pending owner" in readme.casefold(): - raise CheckError("README.md still contains pending owner identity or contact metadata") - readme_status = " ".join(re.sub(r"[^\w.]+", " ", readme).split()) - unpublished_patterns = ( - r"\bcandidate\b", - r"\bno\s+pypi\s+publication\b", - r"\b(?:has\s+)?not\s+been\s+published\b", - r"\bdo\s+not\s+treat\b[^\n]*\bcurrently\s+available\b", - r"\blocal\s+0\.1\.0a1\s+registry\s+alpha\s+candidate\b", - r"\blocal\s+candidate\b", - ) - present = [ - pattern - for pattern in unpublished_patterns - if re.search(pattern, readme_status, flags=re.IGNORECASE) - ] - if present: + violations = public_readme_release_violations(readme) + if violations: raise CheckError( - "README.md still describes the release as local or unpublished; " - "remove every stale status statement before tagging" + "README.md must use publication-neutral release guidance; found: " + + ", ".join(sorted(set(violations))) ) - if ( - re.search( - rf"\b{re.escape(normalize_version(project_version))}\s+is\s+approved\s+for\s+" - r"pypi\s+publication\b", - readme_status, - flags=re.IGNORECASE, - ) - is None - ): + if not public_readme_has_install_command(readme): raise CheckError( - "README.md must explicitly state ' is approved for PyPI publication' " - "before tagging" + "README.md must contain the unpinned stable install command " + f"{PUBLIC_README_INSTALL_COMMAND!r}" ) changelog = Path("CHANGELOG.md").read_text(encoding="utf-8") diff --git a/scripts/check_workflows.py b/scripts/check_workflows.py index 81a70f1..67e3a49 100644 --- a/scripts/check_workflows.py +++ b/scripts/check_workflows.py @@ -19,7 +19,9 @@ RELEASE_PLEASE_BASELINE_SHA = "31b68904141489ca04932edbf305ccf88af09372" RELEASE_PLEASE_LOCK_JSONPATH = "$.package[?(@.name.value == 'cometapi')].version" -RELEASE_PLEASE_ACTION_SHA = "5c625bfb5d1ff62eadeeb3772007f7f66fdcf071" +RELEASE_PLEASE_ACTION_SHA = "45996ed1f6d02564a971a2fa1b5860e934307cf7" +RELEASE_PLEASE_ACTION_VERSION = "5.0.0" +RELEASE_PLEASE_ACTION_RUNTIME = "node24" RELEASE_PLEASE_BRIDGE_VERSION = "0.1.0-alpha.1" RELEASE_PLEASE_STABLE_VERSION_PATTERN = re.compile(r"0\.1\.(?:0|[1-9][0-9]*)") RELEASE_PLEASE_VERIFY_COMMAND = """\ @@ -814,7 +816,10 @@ def check_release_please_workflow(text: str) -> None: if release_step.get("uses") != ( f"googleapis/release-please-action@{RELEASE_PLEASE_ACTION_SHA}" ): - raise CheckError("Release Please must retain the release-please 17.3.0 action pin") + raise CheckError( + "Release Please must retain the release-please " + f"{RELEASE_PLEASE_ACTION_VERSION} ({RELEASE_PLEASE_ACTION_RUNTIME}) action pin" + ) verify_matches = [ (index, step) for index, step in enumerate(_workflow_steps(release_job, "Release Please job")) diff --git a/tests/test_release_documents.py b/tests/test_release_documents.py index fdbe935..27ef82b 100644 --- a/tests/test_release_documents.py +++ b/tests/test_release_documents.py @@ -14,9 +14,11 @@ CANONICAL_PROJECT_URLS, CANONICAL_SECURITY, CANONICAL_SUPPORT, + PUBLIC_README_INSTALL_COMMAND, CheckError, read_project_version, ) +from scripts.check_artifacts import check_metadata from scripts.check_version import require_public_preview_docs, require_releasable_docs PROJECT_ROOT = Path(__file__).resolve().parents[1] @@ -50,7 +52,8 @@ def _write_release_documents(root: Path) -> None: ".release-please-manifest.json": '{".": "0.1.0-alpha.1"}\n', "LICENSE": f"{CANONICAL_COPYRIGHT}\n", "README.md": ( - "0.1.0a1 is approved for PyPI publication.\n" + f"Stable 0.1.x maintenance releases are available from PyPI.\n" + f"{PUBLIC_README_INSTALL_COMMAND}\n" + "\n".join(CANONICAL_PROJECT_URLS.values()) + f"\n{CANONICAL_SUPPORT}\n" ), @@ -113,6 +116,19 @@ def _change_conduct_contact(root: Path) -> None: _replace(root, "CODE_OF_CONDUCT.md", CANONICAL_SUPPORT, "conduct@example.invalid") +def _artifact_metadata(description: str) -> bytes: + headers = [ + "Metadata-Version: 2.4", + "Name: cometapi", + "Version: 0.1.0a1", + f"Author: {CANONICAL_AUTHOR}", + "Requires-Dist: openai<3.0.0,>=2.45.0", + "Description-Content-Type: text/markdown", + ] + headers.extend(f"Project-URL: {label}, {url}" for label, url in CANONICAL_PROJECT_URLS.items()) + return ("\n".join(headers) + "\n\n" + description + "\n").encode() + + @pytest.fixture def releasable_documents(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: _write_release_documents(tmp_path) @@ -131,6 +147,133 @@ def test_public_preview_documents_accept_durable_public_content( require_public_preview_docs() +@pytest.mark.parametrize( + "replacement", + [ + "Pending owner identity must be filled.", + "Pending ownership confirmation must be filled.", + "Pending-owner identity must be filled.", + "0.1.0a1 is approved for PyPI publication.", + "python -m pip install 'cometapi==0.1.0a1'", + "https://pypi.org/project/cometapi/0.1.0a1/", + "https://github.com/cometapi-dev/cometapi-python/releases/tag/v0.1.0", + ], + ids=[ + "pending-owner", + "pending-ownership", + "pending-owner-hyphen", + "approval-state", + "version-pin", + "pypi-version-link", + "github-version-link", + ], +) +def test_releasable_documents_reject_publication_specific_readme_state( + releasable_documents: Path, + replacement: str, +) -> None: + readme = releasable_documents / "README.md" + if replacement.startswith("https://"): + readme.write_text( + readme.read_text(encoding="utf-8") + replacement + "\n", + encoding="utf-8", + ) + else: + _replace( + releasable_documents, + "README.md", + "Stable 0.1.x maintenance releases are available from PyPI.\n" + f"{PUBLIC_README_INSTALL_COMMAND}", + replacement, + ) + + with pytest.raises(CheckError, match=r"publication-neutral release guidance|unpinned stable"): + require_releasable_docs("0.1.0a1") + + +def test_releasable_documents_require_unpinned_install_command( + releasable_documents: Path, +) -> None: + _replace( + releasable_documents, + "README.md", + PUBLIC_README_INSTALL_COMMAND, + "python -m pip install cometapi-sdk", + ) + + with pytest.raises(CheckError, match="unpinned stable install command"): + require_releasable_docs("0.1.0a1") + + +def test_artifact_metadata_accepts_publication_neutral_long_description() -> None: + description = ( + "Stable 0.1.x maintenance releases are available from PyPI.\n" + f"{PUBLIC_README_INSTALL_COMMAND}" + ) + check_metadata( + _artifact_metadata(description), + "fixture:METADATA", + "0.1.0a1", + f"{description}\n", + ) + + +@pytest.mark.parametrize( + "description", + [ + "Pending ownership confirmation.\npython -m pip install cometapi", + "Pending-owner confirmation.\npython -m pip install cometapi", + "0.1.0a1 is approved for PyPI publication.\npython -m pip install cometapi", + "python -m pip install 'cometapi==0.1.0a1'", + "python -m pip install cometapi\nhttps://pypi.org/project/cometapi/0.1.0a1/", + ], + ids=[ + "pending-ownership", + "pending-owner-hyphen", + "approval-state", + "version-pin", + "versioned-release-link", + ], +) +def test_artifact_metadata_rejects_publication_specific_long_description( + description: str, +) -> None: + with pytest.raises(CheckError, match="long description contains publication-specific"): + check_metadata( + _artifact_metadata(description), + "fixture:METADATA", + "0.1.0a1", + f"{description}\n", + ) + + +def test_artifact_metadata_requires_unpinned_install_command() -> None: + description = "Stable 0.1.x maintenance releases are available from PyPI." + with pytest.raises(CheckError, match="unpinned stable install command"): + check_metadata( + _artifact_metadata(description), + "fixture:METADATA", + "0.1.0a1", + f"{description}\n", + ) + + +def test_artifact_metadata_must_match_source_readme_exactly() -> None: + expected = ( + "Stable 0.1.x maintenance releases are available from PyPI.\n" + f"{PUBLIC_README_INSTALL_COMMAND}" + ) + changed = expected.replace("Stable 0.1.x", "The stable 0.1.x") + + with pytest.raises(CheckError, match="exactly match source README"): + check_metadata( + _artifact_metadata(changed), + "fixture:METADATA", + "0.1.0a1", + f"{expected}\n", + ) + + def test_public_preview_documents_reject_non_https_canonical_project_url( releasable_documents: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_release_workflow.py b/tests/test_release_workflow.py index e6c2027..f34a35e 100644 --- a/tests/test_release_workflow.py +++ b/tests/test_release_workflow.py @@ -400,6 +400,24 @@ def test_current_publish_workflow_satisfies_semantic_contract() -> None: ) +def test_release_please_uses_node24_v5_pin() -> None: + text = RELEASE_PLEASE_WORKFLOW.read_text(encoding="utf-8") + assert ( + "googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0, node24" + ) in text + check_release_please_workflow(text) + + +def test_release_please_rejects_legacy_node20_pin() -> None: + text = RELEASE_PLEASE_WORKFLOW.read_text(encoding="utf-8").replace( + "45996ed1f6d02564a971a2fa1b5860e934307cf7", + "5c625bfb5d1ff62eadeeb3772007f7f66fdcf071", + 1, + ) + with pytest.raises(RuntimeError, match=r"5\.0\.0.*node24"): + check_release_please_workflow(text) + + @pytest.mark.parametrize("configured", [None, ""]) def test_live_model_defaults_when_unset_or_empty(configured: str | None) -> None: assert resolve_live_model(configured) == "gpt-5.4" From 917da4bd8a402893812ef97d7941e3c3c504faf6 Mon Sep 17 00:00:00 2001 From: CometAPI Date: Thu, 30 Jul 2026 10:39:31 +0800 Subject: [PATCH 2/2] docs: record release please replacement --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 0d27a34..4b2272c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -143,7 +143,7 @@ Recorded pre-visibility dependency dispositions: | Dependabot [PR #2](https://github.com/cometapi-dev/cometapi-python/pull/2): `actions/checkout` 4.2.2 to 7.0.1 | Closed unmerged; superseded by merged private [PR #9](https://github.com/cometapi-dev/cometapi-python/pull/9) | PR #2's [CI run 29796719306](https://github.com/cometapi-dev/cometapi-python/actions/runs/29796719306) failed because its regression test hard-coded the previous checkout SHA. PR #9 instead validates parsed action references independently of version, passed final CI run 29916685839, and squash-merged as `72b212dd72e66bbde9c6714329f72071cc1ca129`; PR #2 was closed without merging, and its failed run remains negative evidence only. | | Dependabot [PR #3](https://github.com/cometapi-dev/cometapi-python/pull/3): `pypa/gh-action-pypi-publish` 1.14.0 to 1.14.1 | Deferred; keep out of `main` | Pull-request CI does not execute the release-triggered OIDC publish action or prove PyPI publication, provenance, or registry installation. Revisit with an authorized release-path review and the separately required protected release evidence; credential-free CI success alone is insufficient. | | Dependabot [PR #4](https://github.com/cometapi-dev/cometapi-python/pull/4): `actions/upload-artifact` 4.6.2 to 7.0.1 | Closed unmerged; superseded by merged private [PR #9](https://github.com/cometapi-dev/cometapi-python/pull/9) | PR #9 applies the reviewed SHA pin in CI and release builds, requires missing artifacts to fail, retains digest evidence, passed final CI run 29916685839, and squash-merged as `72b212dd72e66bbde9c6714329f72071cc1ca129`; PR #4 was closed without merging. | -| Dependabot [PR #5](https://github.com/cometapi-dev/cometapi-python/pull/5): `googleapis/release-please-action` 4.4.1 to 5.0.0 | Deferred; keep out of `main` | `RELEASE_PLEASE_ENABLED` remains disabled, and pull-request CI does not execute the gated write-capable Release Please action. Revisit only after its real config, manifest, permissions, and release behavior can be reviewed without treating a skipped action as execution evidence. | +| Dependabot [PR #5](https://github.com/cometapi-dev/cometapi-python/pull/5): `googleapis/release-please-action` 4.4.1 to 5.0.0 | Superseded by authorized replacement [PR #29](https://github.com/cometapi-dev/cometapi-python/pull/29); keep PR #5 out of `main` and close it after PR #29 merges | PR #29 pins the reviewed upstream `v5.0.0` commit, verifies its Node 24 runtime through the workflow semantic contract, and carries the release-metadata hardening required for `0.1.2`. PR #5's failed CI is negative evidence from the former semantic check that required the old action pin; it must not be merged independently or treated as runtime evidence. | | Dependabot [PR #6](https://github.com/cometapi-dev/cometapi-python/pull/6): `actions/setup-python` 5.6.0 to 7.0.0 | Closed unmerged; superseded by merged private [PR #9](https://github.com/cometapi-dev/cometapi-python/pull/9) | PR #9 applies the reviewed SHA pin across CI, monitoring, and release workflows, passed final CI run 29916685839 on every blocking lane, and squash-merged as `72b212dd72e66bbde9c6714329f72071cc1ca129`; PR #6 was closed without merging. | Recorded pre-visibility replacement evidence on 2026-07-22: