Skip to content

Commit 7ad4f02

Browse files
author
CometAPI
committed
fix: harden release metadata and runtime
1 parent e07b4e7 commit 7ad4f02

13 files changed

Lines changed: 363 additions & 84 deletions

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
steps:
4646
- name: Open or update the release PR, or create its approved release
4747
id: release
48-
uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
48+
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0, node24
4949
with:
5050
config-file: release-please-config.json
5151
manifest-file: .release-please-manifest.json

AGENTS.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,16 @@ Post-stable invariants:
152152
direct dependency's `result` to equal `success`. A skipped, cancelled,
153153
failed, or missing dependency must never make build, live smoke,
154154
publication, or registry verification eligible.
155+
9. Keep Release Please pinned to the reviewed `v5.0.0` commit
156+
`45996ed1f6d02564a971a2fa1b5860e934307cf7`, whose immutable action metadata
157+
uses `node24`. The workflow semantic checker must reject any different pin.
158+
10. `README.md` is the distribution long description and must remain accurate
159+
before and after publication. Use `python -m pip install cometapi`,
160+
unversioned project links, and publication-neutral maintenance language.
161+
Reject approval, unpublished, exact-version installation, and versioned
162+
release-link text in both source and built artifact metadata, and require
163+
each built long description to exactly match the source README. Post-release
164+
evidence changes must not rewrite README release state.
155165

156166
## Repository independence
157167

@@ -329,7 +339,9 @@ committed.
329339
evaluate skipped ancestry, reject cancellation and reruns, and require each
330340
direct dependency to succeed.
331341
- Keep README, roadmap, compatibility matrix, examples, and changelog aligned
332-
with shipped behavior. Use currently supported model IDs.
342+
with shipped behavior. README installation and availability guidance must be
343+
publication-neutral because it is embedded in immutable distribution
344+
metadata. Use currently supported model IDs.
333345
- All repository documentation is written in English.
334346

335347
The Public Preview readiness record requires

ARCHITECTURE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ local mocked/package evidence
128128
-> public registry digest, provenance, install, import, and mocked smoke
129129
```
130130

131+
The package metadata embeds `README.md` as its long description. Because wheel,
132+
sdist, and PyPI metadata are immutable, the README uses an unversioned install
133+
command and publication-neutral release language that remains accurate before
134+
and after a release. Source-document and artifact checks reject approval,
135+
unpublished, exact-version installation, and versioned release-link text; each
136+
artifact long description must also exactly match the source README.
137+
138+
Release Please is pinned to the immutable `v5.0.0` commit whose action metadata
139+
uses `node24`. The workflow semantic contract fixes that SHA and runtime
140+
disposition so GitHub does not need to force a deprecated Node 20 action onto a
141+
newer runtime.
142+
131143
This complete trust chain executed successfully in
132144
[release workflow run 30261746138](https://github.com/cometapi-dev/cometapi-python/actions/runs/30261746138)
133145
for release commit `31b68904141489ca04932edbf305ccf88af09372`, recovery tag

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ automation.
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- Pin Release Please to its reviewed Node 24 action and reject regressions to
12+
the deprecated Node 20 runtime.
13+
- Keep the README and built distribution long description publication-neutral,
14+
with an unpinned stable installation command and no transient approval state.
15+
916
### Documentation
1017

1118
- Record completed `0.1.1` publication, live, provenance, digest, and

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# CometAPI Python SDK
22

3-
> **Stable release:** `0.1.1` is available from PyPI.
3+
> **Stable release line:** `0.1.x` maintenance releases are available from PyPI.
44
55
`cometapi` is a thin Python adapter over the official OpenAI SDK for the
66
OpenAI-compatible CometAPI endpoint. It changes the default API key and base
@@ -29,16 +29,14 @@ Project links: [CometAPI](https://www.cometapi.com),
2929

3030
## Installation
3131

32-
Install the independently verified stable release from PyPI with:
32+
Install the latest stable release from PyPI with:
3333

3434
```bash
35-
python -m pip install 'cometapi==0.1.1'
35+
python -m pip install cometapi
3636
```
3737

38-
The immutable
39-
[GitHub release](https://github.com/cometapi-dev/cometapi-python/releases/tag/v0.1.1)
40-
and exact [PyPI release](https://pypi.org/project/cometapi/0.1.1/) record the
41-
published artifact.
38+
See the [CometAPI package on PyPI](https://pypi.org/project/cometapi/) for the
39+
current stable distribution and release history.
4240

4341
## Authentication and configuration
4442

RELEASING.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,26 @@ secret separation, and checkout-before-bundle-download ordering. Its git-backed
167167
tests exercise accepted and rejected release histories locally; they still do
168168
not emulate GitHub Actions.
169169

170+
The Release Please step is pinned to
171+
`googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7`
172+
(`v5.0.0`), whose immutable action metadata selects `node24`. The semantic
173+
checker rejects any other pin so the workflow cannot silently regress to the
174+
deprecated Node 20 runtime.
175+
170176
Release mode (`check_version.py --require-releasable-docs`) also fails closed
171177
until project authorship, the canonical GitHub repository URL, the copyright
172-
holder, security and support contacts, and the approved README/changelog
173-
release state are present. Public Preview validation reports all discovered
174-
violations in one run and still returns non-zero when any violation exists.
178+
holder, security and support contacts, a publication-neutral README, and a dated
179+
changelog release section are present. Public Preview validation reports all
180+
discovered violations in one run and still returns non-zero when any violation
181+
exists.
182+
183+
`pyproject.toml` embeds `README.md` as the immutable distribution long
184+
description. The README therefore uses the unpinned
185+
`python -m pip install cometapi` command and unversioned project links. Release
186+
PRs and post-release evidence changes must not introduce approval, unpublished,
187+
or exact-version availability statements. Artifact inspection applies the same
188+
policy to wheel `METADATA` and sdist `PKG-INFO`, so source and registry-facing
189+
descriptions cannot drift.
175190

176191
## Workflow responsibilities
177192

@@ -326,7 +341,7 @@ feature or fix pull request
326341
-> required offline CI
327342
-> merge to the default branch
328343
-> automated release pull request
329-
-> human finalization of stable docs, metadata, and one-time bridge cleanup
344+
-> human review of generated versions, changelog, and durable metadata
330345
-> required release-PR CI, review, and merge
331346
-> immutable tag and GitHub release
332347
-> bounded API verification of immutable tag and commit identity
@@ -340,15 +355,15 @@ feature or fix pull request
340355
-> roadmap milestone marked released
341356
```
342357

343-
Stable `0.1.0` additionally requires the complete blocking Python matrix,
358+
Stable `0.1.0` additionally required the complete blocking Python matrix,
344359
executed README examples against the built package, trusted live evidence, and
345-
reviewed release-PR and changelog agreement. Before the stable release PR is
346-
merged, its finalization commit must state that `0.1.0` is approved for PyPI
347-
publication, use the stable installation command and classifier, and remove the
348-
one-time `last-release-sha` plus prerelease-versioning controls. The manifest,
360+
reviewed release-PR and changelog agreement. Its one-time finalization removed
361+
the `last-release-sha` and prerelease-versioning controls. Later maintenance
362+
releases keep those controls absent and must retain publication-neutral README
363+
metadata throughout the release and post-release sequence. The manifest,
349364
project metadata, lock file, and changelog must remain at the exact generated
350-
`0.1.0` version. If GitHub requires approval before checks run on the automated
351-
pull request, approve only that reviewed workflow execution and wait for every
365+
version. If GitHub requires approval before checks run on the automated pull
366+
request, approve only that reviewed workflow execution and wait for every
352367
blocking check.
353368

354369
## Immutable release publication recovery

ROADMAP.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,9 @@ Public Preview remains ready only while:
346346
self-review.
347347
- Default-branch CI is rerun successfully after the public configuration.
348348
- The fail-closed content gate reports all violations together and then passes,
349-
repository self-containment and package gates pass, and the README accurately
350-
states prerelease and registry availability.
349+
repository self-containment and package gates pass, and the README uses
350+
publication-neutral stable installation guidance suitable for immutable
351+
distribution metadata.
351352
- An explicitly authorized protected live smoke passes within the four-request,
352353
16-output-token, 30-second-per-request, concurrency-one, stop-on-first-failure
353354
budget.

scripts/_checks.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99
from collections.abc import Iterable
1010
from email.message import Message
11+
from email.parser import Parser
1112
from pathlib import Path
1213
from typing import cast
1314

@@ -31,6 +32,26 @@
3132
"Support": f"{CANONICAL_REPOSITORY}/blob/main/SUPPORT.md",
3233
"Security": CANONICAL_SECURITY,
3334
}
35+
PUBLIC_README_INSTALL_COMMAND = "python -m pip install cometapi"
36+
PUBLIC_README_FORBIDDEN_PATTERNS = (
37+
(r"(?i)\bpending[\s-]+owner(?:ship|s)?\b", "pending owner identity"),
38+
(r"(?i)\bapproved\s+for\s+pypi\s+publication\b", "publication approval state"),
39+
(r"(?i)\b(?:candidate|unreleased|unpublished)\b", "unpublished release state"),
40+
(r"(?i)\bno\s+pypi\s+publication\b", "missing PyPI publication"),
41+
(r"(?i)\b(?:has\s+)?not\s+been\s+published\b", "unpublished release state"),
42+
(
43+
r"(?i)\bdo\s+not\s+treat\b[^\n]*\bcurrently\s+available\b",
44+
"temporary availability warning",
45+
),
46+
(r"(?i)\blocal\s+candidate\b", "local-candidate narrative"),
47+
(r"(?i)\b0\.1\.\d+(?:a\d+)?\s+is\s+(?:available|approved)\b", "versioned release state"),
48+
(r"(?i)cometapi==\d+\.\d+\.\d+(?:a\d+)?", "version-pinned installation command"),
49+
(r"https://pypi\.org/project/cometapi/\d", "versioned PyPI release link"),
50+
(
51+
r"https://github\.com/cometapi-dev/cometapi-python/releases/tag/v\d",
52+
"versioned GitHub release link",
53+
),
54+
)
3455

3556

3657
class CheckError(RuntimeError):
@@ -96,25 +117,37 @@ def sha256_file(path: Path) -> str:
96117

97118
def parse_metadata(raw: bytes, source: str) -> Message:
98119
"""Parse package core metadata and require its identity fields."""
99-
message = Message()
100-
text = raw.decode("utf-8")
101-
for line in text.splitlines():
102-
if not line.strip():
103-
break
104-
if line[0].isspace():
105-
current = message.get_payload()
106-
message.set_payload(f"{current}\n{line}" if current else line)
107-
continue
108-
name, separator, value = line.partition(":")
109-
if separator:
110-
message[name] = value.strip()
120+
try:
121+
message = Parser().parsestr(raw.decode("utf-8"))
122+
except (UnicodeDecodeError, ValueError) as exc:
123+
raise CheckError(f"{source}: invalid metadata: {exc}") from exc
111124
if message.get("Name") != DIST_NAME:
112125
raise CheckError(f"{source}: expected Name: {DIST_NAME!r}")
113126
if message.get("Version") is None:
114127
raise CheckError(f"{source}: missing Version metadata")
115128
return message
116129

117130

131+
def metadata_description(message: Message, source: str) -> str:
132+
"""Return the rendered long description from a distribution metadata message."""
133+
payload = message.get_payload()
134+
if not isinstance(payload, str) or not payload.strip():
135+
raise CheckError(f"{source}: missing long description metadata")
136+
return payload
137+
138+
139+
def public_readme_release_violations(text: str) -> list[str]:
140+
"""Return transient or version-specific release statements in public README text."""
141+
return [
142+
label for pattern, label in PUBLIC_README_FORBIDDEN_PATTERNS if re.search(pattern, text)
143+
]
144+
145+
146+
def public_readme_has_install_command(text: str) -> bool:
147+
"""Return whether the public README contains the exact unpinned install command."""
148+
return any(line.strip() == PUBLIC_README_INSTALL_COMMAND for line in text.splitlines())
149+
150+
118151
def require_equal_versions(items: Iterable[tuple[str, str]]) -> str:
119152
"""Require all named version values to normalize to the same version."""
120153
normalized = [(name, normalize_version(value)) for name, value in items]

scripts/check_artifacts.py

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,36 @@
99
import zipfile
1010
from pathlib import Path, PurePosixPath
1111

12-
from _checks import (
13-
CANONICAL_AUTHOR,
14-
CANONICAL_PROJECT_URLS,
15-
CheckError,
16-
normalize_version,
17-
parse_metadata,
18-
read_project_version,
19-
sha256_file,
20-
)
12+
try:
13+
from ._checks import (
14+
CANONICAL_AUTHOR,
15+
CANONICAL_PROJECT_URLS,
16+
PROJECT_ROOT,
17+
PUBLIC_README_INSTALL_COMMAND,
18+
CheckError,
19+
metadata_description,
20+
normalize_version,
21+
parse_metadata,
22+
public_readme_has_install_command,
23+
public_readme_release_violations,
24+
read_project_version,
25+
sha256_file,
26+
)
27+
except ImportError: # Direct execution from the repository root.
28+
from _checks import (
29+
CANONICAL_AUTHOR,
30+
CANONICAL_PROJECT_URLS,
31+
PROJECT_ROOT,
32+
PUBLIC_README_INSTALL_COMMAND,
33+
CheckError,
34+
metadata_description,
35+
normalize_version,
36+
parse_metadata,
37+
public_readme_has_install_command,
38+
public_readme_release_violations,
39+
read_project_version,
40+
sha256_file,
41+
)
2142

2243
REQUIRED_PACKAGE_FILES = {
2344
"cometapi/__init__.py",
@@ -88,7 +109,12 @@ def _safe_path(name: str, source: Path) -> PurePosixPath:
88109
return path
89110

90111

91-
def _check_metadata(raw: bytes, source: str, expected_version: str) -> None:
112+
def check_metadata(
113+
raw: bytes,
114+
source: str,
115+
expected_version: str,
116+
expected_description: str,
117+
) -> None:
92118
metadata = parse_metadata(raw, source)
93119
actual_version = normalize_version(str(metadata["Version"]))
94120
if actual_version != expected_version:
@@ -114,9 +140,23 @@ def _check_metadata(raw: bytes, source: str, expected_version: str) -> None:
114140
for label, expected in CANONICAL_PROJECT_URLS.items():
115141
if project_urls.get(label) != expected:
116142
raise CheckError(f"{source}: expected Project-URL {label}, {expected}")
143+
description = metadata_description(metadata, source)
144+
if description != expected_description:
145+
raise CheckError(f"{source}: long description does not exactly match source README.md")
146+
violations = public_readme_release_violations(description)
147+
if violations:
148+
raise CheckError(
149+
f"{source}: long description contains publication-specific release text: "
150+
+ ", ".join(sorted(set(violations)))
151+
)
152+
if not public_readme_has_install_command(description):
153+
raise CheckError(
154+
f"{source}: long description must contain the unpinned stable install command "
155+
f"{PUBLIC_README_INSTALL_COMMAND!r}"
156+
)
117157

118158

119-
def _check_wheel(path: Path, expected_version: str) -> None:
159+
def _check_wheel(path: Path, expected_version: str, expected_description: str) -> None:
120160
expected_fragment = f"cometapi-{expected_version}-"
121161
if expected_fragment not in path.name:
122162
raise CheckError(f"{path.name}: filename does not contain {expected_fragment!r}")
@@ -142,16 +182,19 @@ def _check_wheel(path: Path, expected_version: str) -> None:
142182
metadata_names = [name for name in names if name.endswith(".dist-info/METADATA")]
143183
if len(metadata_names) != 1:
144184
raise CheckError(f"{path.name}: expected exactly one .dist-info/METADATA")
145-
_check_metadata(
146-
archive.read(metadata_names[0]), f"{path.name}:{metadata_names[0]}", expected_version
185+
check_metadata(
186+
archive.read(metadata_names[0]),
187+
f"{path.name}:{metadata_names[0]}",
188+
expected_version,
189+
expected_description,
147190
)
148191
for source_name in ("cometapi/__init__.py", "cometapi/client.py"):
149192
text = archive.read(source_name).decode("utf-8")
150193
if re.search(r"\b(?:CometClient|AsyncCometClient)\b", text):
151194
raise CheckError(f"{path.name}:{source_name}: legacy public client name remains")
152195

153196

154-
def _check_sdist(path: Path, expected_version: str) -> None:
197+
def _check_sdist(path: Path, expected_version: str, expected_description: str) -> None:
155198
expected_root = f"cometapi-{expected_version}"
156199
if path.name != f"{expected_root}.tar.gz":
157200
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:
182225
stream = archive.extractfile(metadata_members[0])
183226
if stream is None:
184227
raise CheckError(f"{path.name}: cannot read PKG-INFO")
185-
_check_metadata(stream.read(), f"{path.name}:PKG-INFO", expected_version)
228+
check_metadata(
229+
stream.read(),
230+
f"{path.name}:PKG-INFO",
231+
expected_version,
232+
expected_description,
233+
)
186234

187235

188236
def _artifacts(arguments: list[str]) -> list[Path]:
@@ -206,12 +254,16 @@ def main() -> int:
206254
parser.add_argument("--expected-version", default=read_project_version())
207255
args = parser.parse_args()
208256
expected_version = normalize_version(args.expected_version)
257+
try:
258+
expected_description = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8")
259+
except (OSError, UnicodeError) as exc:
260+
raise CheckError(f"cannot read source README.md: {exc}") from exc
209261
paths = _artifacts(args.artifacts)
210262
for path in paths:
211263
if path.suffix == ".whl":
212-
_check_wheel(path, expected_version)
264+
_check_wheel(path, expected_version, expected_description)
213265
else:
214-
_check_sdist(path, expected_version)
266+
_check_sdist(path, expected_version, expected_description)
215267
print(f"{sha256_file(path)} {path}")
216268
print(f"artifact checks passed for cometapi {expected_version}")
217269
return 0

0 commit comments

Comments
 (0)