From 86e927d60b5d7e9a6ee6e5313e676f1c46f41ce1 Mon Sep 17 00:00:00 2001 From: CometAPI Date: Thu, 30 Jul 2026 21:39:20 +0800 Subject: [PATCH] fix: prevent stale mutable release claims --- .github/workflows/ci.yml | 6 +- AGENTS.md | 19 +- CONTRIBUTING.md | 2 +- README.md | 2 +- RELEASING.md | 2 +- ROADMAP.md | 16 +- scripts/_checks.py | 501 ++++++++++++++++++++++- scripts/check_artifacts.py | 33 +- scripts/check_repository_independence.py | 23 +- scripts/check_version.py | 31 +- scripts/check_workflows.py | 7 +- tests/test_release_documents.py | 359 +++++++++++++++- tests/test_release_workflow.py | 31 +- 13 files changed, 970 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbd9285..454432c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,10 +44,8 @@ jobs: run: uv run pyright - name: Run offline unit and contract tests run: uv run pytest -m "not live" - - name: Check release version agreement - run: uv run python scripts/check_version.py --require-changelog - - name: Check canonical public content and identity - run: uv run python scripts/check_version.py --require-public-preview-docs + - name: Check version agreement and durable public content + run: uv run python scripts/check_version.py --require-changelog --require-public-preview-docs - name: Scan for credentials and scope mistakes run: uv run python scripts/check_secrets.py - name: Validate workflow syntax with checksum-pinned actionlint diff --git a/AGENTS.md b/AGENTS.md index a18c021..a3bdee8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,10 +50,13 @@ proves only its own evidence layer. Never invent or mock missing evidence. ## Current milestone: Stable 0.1 -Stable 0.1 is complete: `cometapi==0.1.1` is the latest publicly available -maintenance release, and its release and registry trust chain has been -verified. Do not begin 0.2 provider adapters without a separate maintainer -request that authorizes that milestone. +Verified stable `0.1.x` maintenance releases are available from PyPI. The +public PyPI registry is authoritative for the latest published version. +`pyproject.toml` and `.release-please-manifest.json` are authoritative for the +current repository candidate and must agree; a candidate must not be described +as published. `ROADMAP.md`, `RELEASING.md`, and `CHANGELOG.md` retain exact +versions only as dated immutable historical evidence. Do not begin 0.2 provider +adapters without a separate maintainer request that authorizes that milestone. Private Remote Validation, the sanitized first history, private initialization, pre-visibility closeout, public visibility configuration, Public Preview, @@ -174,6 +177,12 @@ Post-stable invariants: 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. +11. Never encode a mutable "latest/current published patch version" in + persistent guidance or current-state documentation. Query public PyPI when + current registry state is required. Keep candidate version truth in + `pyproject.toml` and `.release-please-manifest.json`, and keep exact released + versions only in immutable historical evidence. The document/version + checker must fail before merge or release when this boundary is violated. ## Repository independence @@ -270,7 +279,7 @@ uv run ruff check src tests scripts uv run ruff format --check src tests scripts uv run pyright uv run pytest -m "not live" -uv run python scripts/check_version.py --require-changelog +uv run python scripts/check_version.py --require-changelog --require-public-preview-docs uv run python scripts/check_secrets.py uv run python scripts/check_workflows.py rm -rf dist diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ca3b04..55fb740 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ uv run ruff check src tests scripts uv run ruff format --check src tests scripts uv run pyright uv run pytest -m "not live" -uv run python scripts/check_version.py --require-changelog +uv run python scripts/check_version.py --require-changelog --require-public-preview-docs uv run python scripts/check_secrets.py uv run python scripts/check_workflows.py rm -rf dist diff --git a/README.md b/README.md index 836cb00..e640d55 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ uv run ruff check src tests scripts uv run ruff format --check src tests scripts uv run pyright uv run pytest -m "not live" -uv run python scripts/check_version.py --require-changelog +uv run python scripts/check_version.py --require-changelog --require-public-preview-docs uv run python scripts/check_secrets.py uv run python scripts/check_workflows.py rm -rf dist diff --git a/RELEASING.md b/RELEASING.md index 9321597..0660356 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -83,7 +83,7 @@ uv run ruff check src tests scripts uv run ruff format --check src tests scripts uv run pyright uv run pytest -m "not live" -uv run python scripts/check_version.py --require-changelog +uv run python scripts/check_version.py --require-changelog --require-public-preview-docs uv run python scripts/check_secrets.py uv run python scripts/check_workflows.py rm -rf dist diff --git a/ROADMAP.md b/ROADMAP.md index b0abb78..3df43e5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # CometAPI Python SDK Roadmap -Status: `0.1.2` stable maintenance released +Status: Stable `0.1.x` maintenance active Last updated: 2026-07-30 Repository contract: this roadmap is self-contained. Current gate: maintain the verified stable 0.1 surface. Begin 0.2 only after a @@ -12,13 +12,13 @@ The SDK provides the shortest reliable path from an OpenAI Python integration to CometAPI while preserving official request, response, error, retry, timeout, sync, async, and streaming behavior. -Private Remote Validation, Public Preview, the functional `0.1.0a1` Registry -Alpha, stable `0.1.0`, and maintenance releases `0.1.1` and `0.1.2` are complete -for the sanitized public repository. Protected repository configuration, -public default-branch CI, exact-release live smoke, PyPI OIDC publication, -provenance, digest comparison, and public-registry smoke provide separate -evidence layers. Only 0.1.x maintenance is active; `0.1.2` does not activate -0.2 scope. +Private Remote Validation, Public Preview, the functional Registry Alpha, the +first stable release, and verified `0.1.x` maintenance releases are complete +for the sanitized public repository and recorded below as immutable evidence. +Protected repository configuration, public default-branch CI, exact-release +live smoke, PyPI OIDC publication, provenance, digest comparison, and +public-registry smoke provide separate evidence layers. Only 0.1.x maintenance +is active; no maintenance release activates 0.2 scope. Support and release claims remain limited to the evidence defined in this roadmap and `COMPATIBILITY.md`. diff --git a/scripts/_checks.py b/scripts/_checks.py index 777eb6f..e5e1f30 100644 --- a/scripts/_checks.py +++ b/scripts/_checks.py @@ -3,14 +3,18 @@ from __future__ import annotations import hashlib +import html import json import re import sys +import unicodedata from collections.abc import Iterable +from datetime import date from email.message import Message from email.parser import Parser +from itertools import pairwise from pathlib import Path -from typing import cast +from typing import NamedTuple, cast if sys.version_info >= (3, 11): import tomllib @@ -33,6 +37,180 @@ "Security": CANONICAL_SECURITY, } PUBLIC_README_INSTALL_COMMAND = "python -m pip install cometapi" +PERSISTENT_DOCUMENTS = ( + "README.md", + "ROADMAP.md", + "CHANGELOG.md", + "AGENTS.md", + "RELEASING.md", + "COMPATIBILITY.md", + "ARCHITECTURE.md", + "CONTRIBUTING.md", + "SECURITY.md", + "SUPPORT.md", + "CODE_OF_CONDUCT.md", + "CLAUDE.md", + ".github/PULL_REQUEST_TEMPLATE.md", + ".github/ISSUE_TEMPLATE/bug_report.yml", + ".github/ISSUE_TEMPLATE/config.yml", + ".github/ISSUE_TEMPLATE/feature_request.yml", +) +SDIST_PUBLIC_DOCUMENTS = tuple( + name for name in PERSISTENT_DOCUMENTS if not name.startswith(".github/") and name != "CLAUDE.md" +) +MUTABLE_PUBLISHED_VERSION_FIX = ( + "replace the exact patch with version-neutral 0.1.x guidance, query PyPI for current " + "registry state, and keep exact released versions only in immutable historical evidence" +) +MUTABLE_PUBLISHED_VERSION_CATEGORY = "mutable latest/current published patch version" +_BARE_PATCH_VERSION = r"v?\d+\.\d+\.\d+(?:[-_.]?(?:a|alpha)[-_.]?\d+)?(?:\+[0-9A-Za-z.-]+)?" +_EXACT_PATCH_VERSION = rf"(?)\]]+", flags=re.IGNORECASE) +_HTML = re.compile(r"|<[^>]*>", flags=re.DOTALL) +_MARKDOWN_LINK = re.compile(r"\[([^\]]+)\]\((?:[^()]|\([^()]*\))*\)") +_VERSIONED_RELEASE_URL = re.compile( + rf"https?://(?:" + rf"pypi\.org/project/cometapi/(?P{_BARE_PATCH_VERSION})" + rf"|github\.com/cometapi-dev/cometapi-python/releases/tag/(?P{_BARE_PATCH_VERSION})" + rf")(?=$|[/#?\s<>\"')\]])", + flags=re.IGNORECASE, +) +_CLAIM_TOKEN = re.compile( + rf"(?P{_EXACT_PATCH_VERSION})" + r"|(?P[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*)" + r"|(?P[.!?;](?=\s|$))" + r"|(?P[,:|\u2013\u2014])", + flags=re.IGNORECASE, +) +_CURRENT_WORDS = {"current", "currently", "latest", "newest", "now"} +_RELEASE_IDENTITY_WORDS = {"build", "distribution", "patch", "release", "version"} +_PUBLICATION_STATE_WORDS = { + "available", + "hosts", + "lists", + "offers", + "published", + "publishes", + "released", + "serves", +} +_HISTORICAL_ACTION_WORDS = { + "accepted", + "completed", + "created", + "executed", + "failed", + "passed", + "published", + "reached", + "reserved", + "verified", +} +_PRESENT_WORDS = {"are", "is", "now", "currently", "remains"} +_PAST_WORDS = {"had", "was", "were"} +_ATTRIBUTION_SKIP_WORDS = { + "a", + "an", + "accepted", + "active", + "are", + "as", + "at", + "available", + "build", + "candidate", + "client", + "cometapi", + "completed", + "current", + "currently", + "distribution", + "exact", + "for", + "from", + "has", + "hosts", + "immutable", + "install", + "installed", + "is", + "its", + "latest", + "library", + "lists", + "maintenance", + "most", + "newest", + "now", + "of", + "offers", + "on", + "our", + "package", + "patch", + "project", + "public", + "publicly", + "published", + "publishes", + "recent", + "registry", + "release", + "released", + "repository", + "sdk", + "serves", + "stable", + "status", + "supported", + "that", + "the", + "these", + "this", + "those", + "verified", + "version", + "was", + "were", +} +_IMMUTABLE_CONTEXT_WORDS = { + "commit", + "completed", + "digest", + "evidence", + "executed", + "immutable", + "recovery", + "run", + "tag", + "workflow", +} +_CANONICAL_ATTRIBUTIONS = { + "client", + "cometapi", + "cometapi-python", + "library", + "package", + "project", + "repository", + "sdk", + "this", +} +_KNOWN_THIRD_PARTY_ATTRIBUTIONS = { + "httpx", + "openai", + "pyright", + "pytest", + "ruff", + "twine", + "uv", +} +_MUTABLE_LABEL_WORDS = { + "current", + "latest", + "newest", + "pypi", +} 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"), @@ -58,6 +236,244 @@ class CheckError(RuntimeError): """Raised when release-candidate evidence does not satisfy a local gate.""" +class _ClaimToken(NamedTuple): + kind: str + value: str + start: int + end: int + + +def _visible_release_url_versions(value: str) -> str: + """Expose release URL versions while preserving offsets and line numbers.""" + replacement = ["\n" if character == "\n" else " " for character in value] + for match in _VERSIONED_RELEASE_URL.finditer(value): + group = "pypi" if match.group("pypi") is not None else "github" + start, end = match.span(group) + marker = "immutableurl" + marker_start = max(match.start(), start - len(marker) - 1) + replacement[marker_start : marker_start + len(marker)] = marker + replacement[start:end] = value[start:end] + return "".join(replacement) + + +def _replace_html(match: re.Match[str]) -> str: + """Discard markup without shifting line numbers used in diagnostics.""" + value = match.group(0) + if value.startswith("