Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ At the Registry Alpha authorization gate:
authorizes that work, without reopening completed dispositions.
2. Keep `.github/CODEOWNERS` absent until a real multi-maintainer model exists.
3. Keep scheduled and manually dispatched live smoke fail-closed behind
`LIVE_SMOKE_ENABLED=true`, and keep `RELEASE_PLEASE_ENABLED` disabled through
the initial manual alpha.
`LIVE_SMOKE_ENABLED=true`, and keep `RELEASE_PLEASE_ENABLED` disabled until
a separate reviewed and tested `last-release-sha` bridge establishes the
recovery alpha as Release Please's previous-release boundary.
4. Treat the recorded public rules, security reporting, immutable releases, and
protected environments as readiness invariants. Any drift invalidates the
readiness claim until it is explicitly authorized, restored, and verified.
Expand Down Expand Up @@ -237,6 +238,15 @@ committed.
- Every distribution `Project-URL` must use HTTPS. The canonical Support URL
is `https://github.com/cometapi-dev/cometapi-python/blob/main/SUPPORT.md`;
`support@cometapi.com` remains the support and conduct contact.
- GitHub permanently reserved `v0.1.0-alpha.1` after its immutable release
reached OIDC publication but failed before any PyPI distribution was
accepted. The sole approved recovery tag is
`v0.1.0-alpha.1+recovery.1`, which maps to package version `0.1.0a1`.
Later releases must use their ordinary canonical tag spelling.
- Keep Release Please disabled after the recovery alpha. Its manifest cannot
infer the previous-release boundary from the recovery tag's build metadata;
enabling it requires a separate reviewed and tested `last-release-sha`
bridge.
- Keep third-party Actions pinned to full commit SHAs and grant
`id-token: write` only to the publishing job.
- Keep README, roadmap, compatibility matrix, examples, and changelog aligned
Expand Down
11 changes: 11 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ protected exact-release live job. OIDC permission is exposed only to the
protected publish job. Missing credentials, environments, approvals, or
remote configuration block publication.

The initial alpha has one release-identity exception. GitHub's immutable
release tombstone permanently reserves `v0.1.0-alpha.1`, so the reviewed
recovery release uses SemVer build metadata in
`v0.1.0-alpha.1+recovery.1`. The build suffix does not change the package
artifact identity: the PyPI version remains `0.1.0a1`.

Release Please remains disabled after this recovery release because its
manifest version does not include the recovery build metadata. A later,
separately reviewed change must establish the previous-release boundary with a
tested `last-release-sha` bridge before enabling automated release PRs.

## Rejected 0.1 approaches

- Hand-written HTTP, SSE, retry, timeout, or protocol model layers duplicate
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ No user-visible changes are currently recorded beyond the initial alpha scope.
live smoke defaults an unset or empty model setting to `gpt-5.4`.
- Distribution metadata now exposes Support as an HTTPS link to `SUPPORT.md`;
release checks reject non-HTTPS canonical project URLs.
- The initial GitHub prerelease uses the one-time recovery tag
`v0.1.0-alpha.1+recovery.1` because GitHub permanently reserved the failed
immutable release tag; the PyPI package version remains `0.1.0a1`.
- Release Please remains disabled until a separately reviewed and tested
previous-release boundary bridges the recovery tag's build metadata.

### Removed

Expand Down
30 changes: 19 additions & 11 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ completed private initialization and remain unnecessary while the project has
one active maintainer.

Before the historical first push, scheduled and manually dispatched live
execution was required to fail closed unless `LIVE_SMOKE_ENABLED=true`, and
`RELEASE_PLEASE_ENABLED` was kept disabled through the initial manual alpha. An
unset or non-true value prevents the corresponding gated job from executing.
execution was required to fail closed unless `LIVE_SMOKE_ENABLED=true`.
`RELEASE_PLEASE_ENABLED` was kept disabled and remains disabled until a
separately reviewed and tested `last-release-sha` bridge establishes the
recovery alpha as the previous-release boundary. An unset or non-true value
prevents the corresponding gated job from executing.
The release live-model configuration resolves an unset or empty
`COMETAPI_LIVE_MODEL` to `gpt-5.4`.

Expand Down Expand Up @@ -179,9 +181,11 @@ violations in one run and still returns non-zero when any violation exists.
`LIVE_SMOKE_ENABLED=true`.
- `release-please.yml` maintains a human-reviewed version and changelog pull
request from Conventional Commits after maintainers enable the
`RELEASE_PLEASE_ENABLED` repository variable. Keep it disabled until the
initial `v0.1.0-alpha.1` tag exists because the checked-in manifest seeds the
next release from the equivalent package version `0.1.0a1`.
`RELEASE_PLEASE_ENABLED` repository variable. Keep it disabled after the
initial `v0.1.0-alpha.1+recovery.1` release: the checked-in manifest version
lacks the recovery tag's build metadata and cannot safely infer the previous
release boundary. Enable it only after a separate reviewed change configures
and tests an explicit `last-release-sha` bridge.
- `publish.yml` runs only for a published immutable GitHub release. It resolves
the tag to the checked-out commit, fetches the protected default branch, and
rejects a commit that is not reachable from that branch. A protected
Expand Down Expand Up @@ -228,11 +232,15 @@ remaining authorized steps in order:
`CHANGELOG.md`, remove its candidate/unpublished wording, and rerun every
candidate verification gate, including
`uv run python scripts/check_version.py --expected 0.1.0a1 --require-changelog --require-releasable-docs`.
4. Review the exact candidate and create the immutable SemVer tag
`v0.1.0-alpha.1` and corresponding GitHub prerelease. The package and Python
metadata use the equivalent PEP 440 version `0.1.0a1`. This is the canonical
tag spelling; do not use `v0.1.0a1`. After this initial alpha exists, enable
Release Please for later reviewed release pull requests.
4. Review the exact candidate and create the immutable SemVer recovery tag
`v0.1.0-alpha.1+recovery.1` and corresponding GitHub prerelease. GitHub
permanently reserved `v0.1.0-alpha.1` after its immutable release reached
OIDC publication but failed before PyPI accepted any distribution. The
recovery build suffix preserves the equivalent PEP 440 package version
`0.1.0a1`; it is a one-time exception and must not be incremented or reused
for later releases. Keep Release Please disabled until a separate reviewed
and tested `last-release-sha` bridge establishes this recovery commit as its
previous-release boundary.
5. Allow the release workflow to prove `immutable=true`, resolve the tag to the
checked-out commit, verify that commit is reachable from the protected
default branch, and run the bounded protected live suite against that exact
Expand Down
18 changes: 14 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,8 @@ Public Preview readiness evidence on 2026-07-27:
`LIVE_SMOKE_ENABLED` opt-in was reset to `false` after the run.
- No tag, GitHub release, Trusted Publisher, PyPI OIDC publication, provenance,
or public-registry installation was created or claimed. Release Please remains
disabled through the initial manual alpha.
disabled until a separately reviewed and tested `last-release-sha` bridge
establishes the recovery alpha as its previous-release boundary.

Public Preview remains ready only while:

Expand Down Expand Up @@ -383,6 +384,13 @@ A mock never satisfies a live gate, static workflow validation never proves a
remote run, and a successful upload never proves registry installation.
PyPI publication is OIDC-only; Python has no token-bootstrap exception.

The first immutable release reached PyPI OIDC publication but Warehouse
rejected its non-HTTPS Support project URL before accepting any distribution.
GitHub permanently reserved the deleted `v0.1.0-alpha.1` release identity. The
approved recovery release therefore uses
`v0.1.0-alpha.1+recovery.1`, whose SemVer build suffix leaves the PyPI package
version at the required first public artifact `0.1.0a1`.

## `0.1.0`: OpenAI protocol foundation

Stable 0.1 retains the alpha surface. Its additional exit criteria are:
Expand Down Expand Up @@ -437,9 +445,11 @@ successfully in the canonical GitHub repository.
Scheduled and manually dispatched live smoke must require
`LIVE_SMOKE_ENABLED=true`; an unset or other value prevents live execution.
Release Please requires
`RELEASE_PLEASE_ENABLED=true` and remains disabled through the initial manual
alpha. Release jobs must resolve an unset or empty `COMETAPI_LIVE_MODEL` to
`gpt-5.4` rather than attempt a request with an empty model.
`RELEASE_PLEASE_ENABLED=true` and remains disabled after the recovery alpha
until a separate reviewed and tested `last-release-sha` bridge establishes its
previous-release boundary. Release jobs must resolve an unset or empty
`COMETAPI_LIVE_MODEL` to `gpt-5.4` rather than attempt a request with an empty
model.

## Maintenance cadence

Expand Down
17 changes: 12 additions & 5 deletions scripts/check_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,26 @@ def _artifact_version(path: Path) -> str:
return match.group(1)


def _require_canonical_tag(tag: str, project_version: str) -> None:
APPROVED_RECOVERY_TAGS = {
"0.1.0a1": "v0.1.0-alpha.1+recovery.1",
}


def _require_canonical_tag(tag: str, project_version: str) -> str:
normalized = normalize_version(project_version)
alpha = re.fullmatch(r"(?P<base>\d+\.\d+\.\d+)a(?P<number>\d+)", normalized)
expected = (
f"v{alpha.group('base')}-alpha.{alpha.group('number')}"
if alpha is not None
else f"v{normalized}"
)
if tag != expected:
recovery = APPROVED_RECOVERY_TAGS.get(normalized)
allowed = {recovery} if recovery is not None else {expected}
if tag not in allowed:
raise CheckError(
f"release tag must use canonical SemVer spelling {expected!r}, got {tag!r}"
f"release tag must use an approved spelling {sorted(allowed)!r}, got {tag!r}"
)
return normalized


PUBLIC_DOCUMENTS = (
Expand Down Expand Up @@ -302,8 +310,7 @@ def main() -> int:
if args.expected:
versions.append(("expected", args.expected))
if args.tag:
_require_canonical_tag(args.tag, project)
versions.append(("tag", args.tag))
versions.append(("tag", _require_canonical_tag(args.tag, project)))
for value in args.artifacts:
path = Path(value)
if not path.is_file():
Expand Down
63 changes: 63 additions & 0 deletions tests/test_release_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,66 @@ def test_public_preview_cli_reports_aggregated_violations_and_fails(
assert result.returncode != 0
assert "[project].authors" in result.stderr
assert "SECURITY.md: missing canonical public value" in result.stderr


def test_release_version_cli_accepts_approved_initial_alpha_recovery_tag(
releasable_documents: Path,
) -> None:
result = subprocess.run(
[
sys.executable,
str(VERSION_SCRIPT),
"--tag",
"v0.1.0-alpha.1+recovery.1",
"--require-changelog",
],
cwd=releasable_documents,
text=True,
check=False,
capture_output=True,
)

assert result.returncode == 0, result.stderr
assert "version agreement passed: 0.1.0a1" in result.stdout


def test_release_version_cli_rejects_tombstoned_initial_alpha_tag(
releasable_documents: Path,
) -> None:
result = subprocess.run(
[
sys.executable,
str(VERSION_SCRIPT),
"--tag",
"v0.1.0-alpha.1",
"--require-changelog",
],
cwd=releasable_documents,
text=True,
check=False,
capture_output=True,
)

assert result.returncode != 0
assert "release tag must use an approved spelling" in result.stderr


def test_release_version_cli_rejects_unapproved_recovery_tag(
releasable_documents: Path,
) -> None:
result = subprocess.run(
[
sys.executable,
str(VERSION_SCRIPT),
"--tag",
"v0.1.0-alpha.1+recovery.2",
"--require-changelog",
],
cwd=releasable_documents,
text=True,
check=False,
capture_output=True,
)

assert result.returncode != 0
assert "release tag must use an approved spelling" in result.stderr
15 changes: 15 additions & 0 deletions tests/test_release_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,21 @@ def test_release_trust_accepts_exact_immutable_default_branch_commit(
)


def test_release_trust_accepts_approved_recovery_tag(
release_repository: tuple[Path, str], tmp_path: Path
) -> None:
repository, release_commit = release_repository
recovery_tag = "v0.1.0-alpha.1+recovery.1"
_git(repository, "tag", recovery_tag, release_commit)
_git(repository, "checkout", "--detach", recovery_tag)
result = _verify_trust(repository, tmp_path, tag=recovery_tag)

assert result.returncode == 0, result.stderr
assert (tmp_path / "github-output.txt").read_text(encoding="utf-8") == (
f"release-commit={release_commit}\n"
)


def test_release_trust_rejects_non_immutable_release(
release_repository: tuple[Path, str], tmp_path: Path
) -> None:
Expand Down