Skip to content

Commit 31b6890

Browse files
authored
fix: add a one-time alpha recovery tag (#17)
1 parent 6344c2d commit 31b6890

8 files changed

Lines changed: 151 additions & 22 deletions

File tree

AGENTS.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ At the Registry Alpha authorization gate:
8282
authorizes that work, without reopening completed dispositions.
8383
2. Keep `.github/CODEOWNERS` absent until a real multi-maintainer model exists.
8484
3. Keep scheduled and manually dispatched live smoke fail-closed behind
85-
`LIVE_SMOKE_ENABLED=true`, and keep `RELEASE_PLEASE_ENABLED` disabled through
86-
the initial manual alpha.
85+
`LIVE_SMOKE_ENABLED=true`, and keep `RELEASE_PLEASE_ENABLED` disabled until
86+
a separate reviewed and tested `last-release-sha` bridge establishes the
87+
recovery alpha as Release Please's previous-release boundary.
8788
4. Treat the recorded public rules, security reporting, immutable releases, and
8889
protected environments as readiness invariants. Any drift invalidates the
8990
readiness claim until it is explicitly authorized, restored, and verified.
@@ -237,6 +238,15 @@ committed.
237238
- Every distribution `Project-URL` must use HTTPS. The canonical Support URL
238239
is `https://github.com/cometapi-dev/cometapi-python/blob/main/SUPPORT.md`;
239240
`support@cometapi.com` remains the support and conduct contact.
241+
- GitHub permanently reserved `v0.1.0-alpha.1` after its immutable release
242+
reached OIDC publication but failed before any PyPI distribution was
243+
accepted. The sole approved recovery tag is
244+
`v0.1.0-alpha.1+recovery.1`, which maps to package version `0.1.0a1`.
245+
Later releases must use their ordinary canonical tag spelling.
246+
- Keep Release Please disabled after the recovery alpha. Its manifest cannot
247+
infer the previous-release boundary from the recovery tag's build metadata;
248+
enabling it requires a separate reviewed and tested `last-release-sha`
249+
bridge.
240250
- Keep third-party Actions pinned to full commit SHAs and grant
241251
`id-token: write` only to the publishing job.
242252
- Keep README, roadmap, compatibility matrix, examples, and changelog aligned

ARCHITECTURE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,17 @@ protected exact-release live job. OIDC permission is exposed only to the
125125
protected publish job. Missing credentials, environments, approvals, or
126126
remote configuration block publication.
127127

128+
The initial alpha has one release-identity exception. GitHub's immutable
129+
release tombstone permanently reserves `v0.1.0-alpha.1`, so the reviewed
130+
recovery release uses SemVer build metadata in
131+
`v0.1.0-alpha.1+recovery.1`. The build suffix does not change the package
132+
artifact identity: the PyPI version remains `0.1.0a1`.
133+
134+
Release Please remains disabled after this recovery release because its
135+
manifest version does not include the recovery build metadata. A later,
136+
separately reviewed change must establish the previous-release boundary with a
137+
tested `last-release-sha` bridge before enabling automated release PRs.
138+
128139
## Rejected 0.1 approaches
129140

130141
- Hand-written HTTP, SSE, retry, timeout, or protocol model layers duplicate

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ No user-visible changes are currently recorded beyond the initial alpha scope.
3939
live smoke defaults an unset or empty model setting to `gpt-5.4`.
4040
- Distribution metadata now exposes Support as an HTTPS link to `SUPPORT.md`;
4141
release checks reject non-HTTPS canonical project URLs.
42+
- The initial GitHub prerelease uses the one-time recovery tag
43+
`v0.1.0-alpha.1+recovery.1` because GitHub permanently reserved the failed
44+
immutable release tag; the PyPI package version remains `0.1.0a1`.
45+
- Release Please remains disabled until a separately reviewed and tested
46+
previous-release boundary bridges the recovery tag's build metadata.
4247

4348
### Removed
4449

RELEASING.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ completed private initialization and remain unnecessary while the project has
4444
one active maintainer.
4545

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

@@ -179,9 +181,11 @@ violations in one run and still returns non-zero when any violation exists.
179181
`LIVE_SMOKE_ENABLED=true`.
180182
- `release-please.yml` maintains a human-reviewed version and changelog pull
181183
request from Conventional Commits after maintainers enable the
182-
`RELEASE_PLEASE_ENABLED` repository variable. Keep it disabled until the
183-
initial `v0.1.0-alpha.1` tag exists because the checked-in manifest seeds the
184-
next release from the equivalent package version `0.1.0a1`.
184+
`RELEASE_PLEASE_ENABLED` repository variable. Keep it disabled after the
185+
initial `v0.1.0-alpha.1+recovery.1` release: the checked-in manifest version
186+
lacks the recovery tag's build metadata and cannot safely infer the previous
187+
release boundary. Enable it only after a separate reviewed change configures
188+
and tests an explicit `last-release-sha` bridge.
185189
- `publish.yml` runs only for a published immutable GitHub release. It resolves
186190
the tag to the checked-out commit, fetches the protected default branch, and
187191
rejects a commit that is not reachable from that branch. A protected
@@ -228,11 +232,15 @@ remaining authorized steps in order:
228232
`CHANGELOG.md`, remove its candidate/unpublished wording, and rerun every
229233
candidate verification gate, including
230234
`uv run python scripts/check_version.py --expected 0.1.0a1 --require-changelog --require-releasable-docs`.
231-
4. Review the exact candidate and create the immutable SemVer tag
232-
`v0.1.0-alpha.1` and corresponding GitHub prerelease. The package and Python
233-
metadata use the equivalent PEP 440 version `0.1.0a1`. This is the canonical
234-
tag spelling; do not use `v0.1.0a1`. After this initial alpha exists, enable
235-
Release Please for later reviewed release pull requests.
235+
4. Review the exact candidate and create the immutable SemVer recovery tag
236+
`v0.1.0-alpha.1+recovery.1` and corresponding GitHub prerelease. GitHub
237+
permanently reserved `v0.1.0-alpha.1` after its immutable release reached
238+
OIDC publication but failed before PyPI accepted any distribution. The
239+
recovery build suffix preserves the equivalent PEP 440 package version
240+
`0.1.0a1`; it is a one-time exception and must not be incremented or reused
241+
for later releases. Keep Release Please disabled until a separate reviewed
242+
and tested `last-release-sha` bridge establishes this recovery commit as its
243+
previous-release boundary.
236244
5. Allow the release workflow to prove `immutable=true`, resolve the tag to the
237245
checked-out commit, verify that commit is reachable from the protected
238246
default branch, and run the bounded protected live suite against that exact

ROADMAP.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,8 @@ Public Preview readiness evidence on 2026-07-27:
315315
`LIVE_SMOKE_ENABLED` opt-in was reset to `false` after the run.
316316
- No tag, GitHub release, Trusted Publisher, PyPI OIDC publication, provenance,
317317
or public-registry installation was created or claimed. Release Please remains
318-
disabled through the initial manual alpha.
318+
disabled until a separately reviewed and tested `last-release-sha` bridge
319+
establishes the recovery alpha as its previous-release boundary.
319320

320321
Public Preview remains ready only while:
321322

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

387+
The first immutable release reached PyPI OIDC publication but Warehouse
388+
rejected its non-HTTPS Support project URL before accepting any distribution.
389+
GitHub permanently reserved the deleted `v0.1.0-alpha.1` release identity. The
390+
approved recovery release therefore uses
391+
`v0.1.0-alpha.1+recovery.1`, whose SemVer build suffix leaves the PyPI package
392+
version at the required first public artifact `0.1.0a1`.
393+
386394
## `0.1.0`: OpenAI protocol foundation
387395

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

444454
## Maintenance cadence
445455

scripts/check_version.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,26 @@ def _artifact_version(path: Path) -> str:
6262
return match.group(1)
6363

6464

65-
def _require_canonical_tag(tag: str, project_version: str) -> None:
65+
APPROVED_RECOVERY_TAGS = {
66+
"0.1.0a1": "v0.1.0-alpha.1+recovery.1",
67+
}
68+
69+
70+
def _require_canonical_tag(tag: str, project_version: str) -> str:
6671
normalized = normalize_version(project_version)
6772
alpha = re.fullmatch(r"(?P<base>\d+\.\d+\.\d+)a(?P<number>\d+)", normalized)
6873
expected = (
6974
f"v{alpha.group('base')}-alpha.{alpha.group('number')}"
7075
if alpha is not None
7176
else f"v{normalized}"
7277
)
73-
if tag != expected:
78+
recovery = APPROVED_RECOVERY_TAGS.get(normalized)
79+
allowed = {recovery} if recovery is not None else {expected}
80+
if tag not in allowed:
7481
raise CheckError(
75-
f"release tag must use canonical SemVer spelling {expected!r}, got {tag!r}"
82+
f"release tag must use an approved spelling {sorted(allowed)!r}, got {tag!r}"
7683
)
84+
return normalized
7785

7886

7987
PUBLIC_DOCUMENTS = (
@@ -302,8 +310,7 @@ def main() -> int:
302310
if args.expected:
303311
versions.append(("expected", args.expected))
304312
if args.tag:
305-
_require_canonical_tag(args.tag, project)
306-
versions.append(("tag", args.tag))
313+
versions.append(("tag", _require_canonical_tag(args.tag, project)))
307314
for value in args.artifacts:
308315
path = Path(value)
309316
if not path.is_file():

tests/test_release_documents.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,66 @@ def test_public_preview_cli_reports_aggregated_violations_and_fails(
231231
assert result.returncode != 0
232232
assert "[project].authors" in result.stderr
233233
assert "SECURITY.md: missing canonical public value" in result.stderr
234+
235+
236+
def test_release_version_cli_accepts_approved_initial_alpha_recovery_tag(
237+
releasable_documents: Path,
238+
) -> None:
239+
result = subprocess.run(
240+
[
241+
sys.executable,
242+
str(VERSION_SCRIPT),
243+
"--tag",
244+
"v0.1.0-alpha.1+recovery.1",
245+
"--require-changelog",
246+
],
247+
cwd=releasable_documents,
248+
text=True,
249+
check=False,
250+
capture_output=True,
251+
)
252+
253+
assert result.returncode == 0, result.stderr
254+
assert "version agreement passed: 0.1.0a1" in result.stdout
255+
256+
257+
def test_release_version_cli_rejects_tombstoned_initial_alpha_tag(
258+
releasable_documents: Path,
259+
) -> None:
260+
result = subprocess.run(
261+
[
262+
sys.executable,
263+
str(VERSION_SCRIPT),
264+
"--tag",
265+
"v0.1.0-alpha.1",
266+
"--require-changelog",
267+
],
268+
cwd=releasable_documents,
269+
text=True,
270+
check=False,
271+
capture_output=True,
272+
)
273+
274+
assert result.returncode != 0
275+
assert "release tag must use an approved spelling" in result.stderr
276+
277+
278+
def test_release_version_cli_rejects_unapproved_recovery_tag(
279+
releasable_documents: Path,
280+
) -> None:
281+
result = subprocess.run(
282+
[
283+
sys.executable,
284+
str(VERSION_SCRIPT),
285+
"--tag",
286+
"v0.1.0-alpha.1+recovery.2",
287+
"--require-changelog",
288+
],
289+
cwd=releasable_documents,
290+
text=True,
291+
check=False,
292+
capture_output=True,
293+
)
294+
295+
assert result.returncode != 0
296+
assert "release tag must use an approved spelling" in result.stderr

tests/test_release_workflow.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,21 @@ def test_release_trust_accepts_exact_immutable_default_branch_commit(
12501250
)
12511251

12521252

1253+
def test_release_trust_accepts_approved_recovery_tag(
1254+
release_repository: tuple[Path, str], tmp_path: Path
1255+
) -> None:
1256+
repository, release_commit = release_repository
1257+
recovery_tag = "v0.1.0-alpha.1+recovery.1"
1258+
_git(repository, "tag", recovery_tag, release_commit)
1259+
_git(repository, "checkout", "--detach", recovery_tag)
1260+
result = _verify_trust(repository, tmp_path, tag=recovery_tag)
1261+
1262+
assert result.returncode == 0, result.stderr
1263+
assert (tmp_path / "github-output.txt").read_text(encoding="utf-8") == (
1264+
f"release-commit={release_commit}\n"
1265+
)
1266+
1267+
12531268
def test_release_trust_rejects_non_immutable_release(
12541269
release_repository: tuple[Path, str], tmp_path: Path
12551270
) -> None:

0 commit comments

Comments
 (0)