From 8aa8da7f73615f202116184d9a8c02a1efedec60 Mon Sep 17 00:00:00 2001 From: CometAPI Date: Mon, 27 Jul 2026 19:15:46 +0800 Subject: [PATCH] fix: add a one-time alpha recovery tag --- AGENTS.md | 14 ++++++-- ARCHITECTURE.md | 11 ++++++ CHANGELOG.md | 5 +++ RELEASING.md | 30 ++++++++++------ ROADMAP.md | 18 +++++++--- scripts/check_version.py | 17 ++++++--- tests/test_release_documents.py | 63 +++++++++++++++++++++++++++++++++ tests/test_release_workflow.py | 15 ++++++++ 8 files changed, 151 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 85af3c5..573a0ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. @@ -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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d39df4d..f6cb7e8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b1d37f..eebb482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/RELEASING.md b/RELEASING.md index 8fa75e6..b5a2796 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -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`. @@ -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 @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index 2dd392b..6afe2ec 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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: @@ -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: @@ -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 diff --git a/scripts/check_version.py b/scripts/check_version.py index 5c7a986..a5288b4 100644 --- a/scripts/check_version.py +++ b/scripts/check_version.py @@ -62,7 +62,12 @@ 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\d+\.\d+\.\d+)a(?P\d+)", normalized) expected = ( @@ -70,10 +75,13 @@ def _require_canonical_tag(tag: str, project_version: str) -> None: 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 = ( @@ -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(): diff --git a/tests/test_release_documents.py b/tests/test_release_documents.py index 5a1d2b0..9472dcd 100644 --- a/tests/test_release_documents.py +++ b/tests/test_release_documents.py @@ -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 diff --git a/tests/test_release_workflow.py b/tests/test_release_workflow.py index ad9a7e1..a1147c1 100644 --- a/tests/test_release_workflow.py +++ b/tests/test_release_workflow.py @@ -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: