Skip to content

Commit 82f3b11

Browse files
author
CometAPI
committed
fix: isolate release PR transport retries
1 parent ab19cf8 commit 82f3b11

8 files changed

Lines changed: 232 additions & 48 deletions

File tree

.github/workflows/publish.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,32 @@ jobs:
4343
contents: write
4444
pull-requests: write
4545
steps:
46-
- name: Open or update the release PR, or create its approved release
46+
- name: Create the approved immutable release
4747
id: release
4848
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
52+
skip-github-pull-request: "true"
53+
- name: Open or update the release PR
54+
id: release-pr
55+
if: steps.release.outputs.release_created != 'true'
56+
continue-on-error: true
57+
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0, node24
58+
with:
59+
config-file: release-please-config.json
60+
manifest-file: .release-please-manifest.json
61+
skip-github-release: "true"
62+
- name: Retry release PR maintenance once
63+
id: retry-release-pr
64+
if: >-
65+
steps.release.outputs.release_created != 'true' &&
66+
steps.release-pr.outcome == 'failure'
67+
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0, node24
68+
with:
69+
config-file: release-please-config.json
70+
manifest-file: .release-please-manifest.json
71+
skip-github-release: "true"
5272
- name: Verify the immutable release created by Release Please
5373
id: verify-release
5474
if: steps.release.outputs.release_created == 'true'

AGENTS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,18 @@ Post-stable invariants:
155155
9. Keep Release Please pinned to the reviewed `v5.0.0` commit
156156
`45996ed1f6d02564a971a2fa1b5860e934307cf7`, whose immutable action metadata
157157
uses `node24`. The workflow semantic checker must reject any different pin.
158+
Invoke that action release-only first with `skip-github-pull-request: true`;
159+
that immutable tag-and-GitHub-Release path must never continue on error or be
160+
retried. Only after it succeeds without creating a release may PR-only
161+
maintenance run with `skip-github-release: true`. Its first attempt is the
162+
sole Release Please step allowed to continue on error, and one identical
163+
second attempt may run only when that first PR attempt fails. Mutable branch
164+
and pull-request maintenance is idempotent and may use this bounded retry;
165+
immutable release creation may not. If the release-only invocation fails,
166+
immediately disable `RELEASE_PLEASE_ENABLED`, inspect tag and GitHub Release
167+
state read-only, and stop. Do not use another main push or recovery path
168+
until the exact external state is known and recovery is separately
169+
authorized.
158170
10. `README.md` is the distribution long description and must remain accurate
159171
before and after publication. Use `python -m pip install cometapi`,
160172
unversioned project links, and publication-neutral maintenance language.

ARCHITECTURE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,24 @@ uses `node24`. The workflow semantic contract fixes that SHA and runtime
140140
disposition so GitHub does not need to force a deprecated Node 20 action onto a
141141
newer runtime.
142142

143+
Release Please execution is split at the mutability boundary. The first pinned
144+
action invocation is release-only (`skip-github-pull-request: true`) and is
145+
neither continued on error nor retried. Only when that invocation succeeds
146+
without creating a release does PR-only maintenance run
147+
(`skip-github-release: true`). The first PR-only attempt is allowed to continue
148+
on error solely so one identical conditional retry can follow; the second
149+
failure ends the job. Updating a branch or pull request is mutable and
150+
idempotent, while retrying immutable tag or GitHub Release creation could leave
151+
ambiguous external state and is forbidden.
152+
153+
[Release Please run 30509764960](https://github.com/cometapi-dev/cometapi-python/actions/runs/30509764960)
154+
isolated the motivating failure to the action's Undici/global `fetch`: the PR
155+
workflow reached its write boundary and then failed with `other side closed`
156+
before any branch, pull-request, tag, GitHub Release, live, or registry write.
157+
The existing release branch and repository pull-request permission were not the
158+
cause. This is negative transport evidence, not evidence of a stale branch or
159+
authorization drift.
160+
143161
This complete trust chain executed successfully in
144162
[release workflow run 30261746138](https://github.com/cometapi-dev/cometapi-python/actions/runs/30261746138)
145163
for release commit `31b68904141489ca04932edbf305ccf88af09372`, recovery tag

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ automation.
1010

1111
- Pin Release Please to its reviewed Node 24 action and reject regressions to
1212
the deprecated Node 20 runtime.
13+
- Separate non-retryable immutable release creation from PR-only maintenance,
14+
with exactly one bounded retry for an isolated pull-request transport failure.
1315
- Keep the README and built distribution long description publication-neutral,
1416
with an unpinned stable installation command and no transient approval state.
1517

RELEASING.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,24 @@ The Release Please step is pinned to
173173
checker rejects any other pin so the workflow cannot silently regress to the
174174
deprecated Node 20 runtime.
175175

176+
The job invokes the pinned action release-only first with
177+
`skip-github-pull-request: true`. That step cannot continue on error and is
178+
never retried. If it succeeds without creating a release, PR-only maintenance
179+
uses `skip-github-release: true`; its first attempt is the single Release Please
180+
step allowed to continue on error, and a second identical attempt runs only
181+
after that first PR attempt fails. A second failure ends the job. This permits
182+
one bounded retry for mutable, idempotent branch and pull-request maintenance
183+
without ever automatically retrying immutable tag or GitHub Release creation.
184+
185+
[Run 30509764960](https://github.com/cometapi-dev/cometapi-python/actions/runs/30509764960)
186+
failed in the PR workflow with Undici/global `fetch` reporting
187+
`other side closed`. It had built the candidate and reached the write boundary,
188+
but created or updated no branch, pull request, tag, GitHub Release, live
189+
request, or registry artifact. Read-only inspection ruled out a stale release
190+
branch and missing pull-request permission. Treat that run as isolated
191+
transport-failure evidence; do not rerun it or reinterpret it as an
192+
authorization failure.
193+
176194
Release mode (`check_version.py --require-releasable-docs`) also fails closed
177195
until project authorship, the canonical GitHub repository URL, the copyright
178196
holder, security and support contacts, a publication-neutral README, and a dated
@@ -220,10 +238,16 @@ descriptions cannot drift.
220238
`last-release-sha` bridge established the recovery release boundary and was
221239
removed during human finalization of the stable release PR. Keep the variable
222240
disabled except while executing an explicitly authorized release sequence.
223-
When it creates an approved release with the GitHub workflow token, it polls
224-
the GitHub API until that exact tag and commit are independently reported as
225-
immutable, then selects it for the downstream chain in the same workflow;
226-
workflow-token release events do not trigger a second workflow.
241+
The job checks for and creates an approved release through its non-retryable
242+
release-only invocation before it performs retryable PR-only maintenance. If
243+
a release is created with the GitHub workflow token, it polls the GitHub API
244+
until that exact tag and commit are independently reported as immutable, then
245+
selects it for the downstream chain in the same workflow; workflow-token
246+
release events do not trigger a second workflow. If the release-only
247+
invocation fails, immediately set `RELEASE_PLEASE_ENABLED=false`, inspect tag
248+
and GitHub Release state read-only, and stop. Do not make another main push or
249+
use recovery until the exact external state is known and recovery is
250+
separately authorized.
227251
- The `verify-recovery` path in `publish.yml` is the only manual publication
228252
path. It requires an exact immutable tag and commit, the protected default
229253
branch, and the temporary `RELEASE_RECOVERY_TAG` and `RELEASE_RECOVERY_SHA`

ROADMAP.md

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

3-
Status: `0.1.1` stable maintenance released
4-
Last updated: 2026-07-29
3+
Status: `0.1.2` stable maintenance in progress
4+
Last updated: 2026-07-30
55
Repository contract: this roadmap is self-contained.
66
Current gate: maintain the verified stable 0.1 surface. Begin 0.2 only after a
77
separate maintainer request authorizes its provider schemas and live contracts.
@@ -17,6 +17,8 @@ Alpha, stable `0.1.0`, and maintenance release `0.1.1` are complete for the
1717
sanitized public repository. Protected repository configuration, public
1818
default-branch CI, exact-release live smoke, PyPI OIDC publication, provenance,
1919
digest comparison, and public-registry smoke provide separate evidence layers.
20+
Maintenance `0.1.2` is active only to harden release automation and immutable
21+
package metadata; it does not activate 0.2 scope.
2022
Support and release claims remain limited to the evidence defined in this
2123
roadmap and `COMPATIBILITY.md`.
2224

@@ -30,6 +32,7 @@ roadmap and `COMPATIBILITY.md`.
3032
| `0.1.0a1` Registry Alpha | Complete | Early adopters can install the functional prerelease from PyPI; every release and registry gate passed. |
3133
| `0.1.0` stable | Complete | Complete runtime, release-PR, example, provenance, and registry gates passed. |
3234
| `0.1.1` maintenance | Complete | Configuration validation and every stable release, live, provenance, and registry gate passed. |
35+
| `0.1.2` maintenance | In progress | Publication-neutral metadata and release-transport boundaries must pass every normal release gate before publication. |
3336
| `0.2.0` provider-native text | Planned | Optional official Anthropic and Gemini adapters. |
3437
| `0.3.0` CometAPI resources | Planned | First schema-backed typed CometAPI-specific resource. |
3538
| Media and task APIs | Planned | Coherent task lifecycle precedes individual media helpers. |
@@ -567,6 +570,26 @@ imports, and passed every supported mocked call and README example.
567570
`RELEASE_PLEASE_ENABLED=false` and `LIVE_SMOKE_ENABLED=false`. Recovery
568571
variables are absent. No recovery tag or recovery workflow was used.
569572

573+
## `0.1.2`: Release metadata and transport maintenance
574+
575+
[Release Please run 30509764960](https://github.com/cometapi-dev/cometapi-python/actions/runs/30509764960)
576+
failed while maintaining the `0.1.2` release PR. The pinned v5 action had built
577+
the candidate and reached its PR write boundary when Undici/global `fetch`
578+
reported `other side closed`. It created or updated no branch, pull request,
579+
tag, GitHub Release, live request, PyPI file, or other registry state. Read-only
580+
inspection showed that neither the existing release branch nor repository
581+
pull-request permission caused the failure.
582+
583+
Release Please now executes according to external-state reversibility. A
584+
release-only invocation (`skip-github-pull-request: true`) runs first without
585+
`continue-on-error` and without retry. Only if no release was created does
586+
PR-only maintenance (`skip-github-release: true`) run; its first attempt may
587+
continue on error solely to permit one identical conditional retry. Mutable,
588+
idempotent branch and pull-request maintenance can therefore recover from one
589+
isolated transport close, while immutable tag and GitHub Release creation can
590+
never be automatically replayed. This remains 0.1.x maintenance and does not
591+
authorize provider, resource, CLI, or 0.2 work.
592+
570593
## `0.2.0`: Provider-native text adapters
571594

572595
Planned scope:
@@ -596,8 +619,10 @@ The repository maintains three independently auditable workflows:
596619
four requests, 16 output tokens per generation, a 30-second request timeout,
597620
concurrency one, a ten-minute workflow timeout, and stop on first failure.
598621
- `publish.yml`: the single top-level release and PyPI Trusted Publisher
599-
identity. Its gated push path maintains the human-reviewed Release Please PR
600-
and independently verifies a created release. Its explicitly enabled manual
622+
identity. Its gated push path first attempts non-retryable release-only
623+
creation, then, only when no release exists, permits one bounded retry for
624+
PR-only maintenance. It independently verifies a created release. Its
625+
explicitly enabled manual
601626
path recovers an independently verified existing immutable release only from
602627
the protected default branch. An exact selector feeds both paths into tag,
603628
commit, and default-branch ancestry verification, artifact rebuild, protected

scripts/check_workflows.py

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,26 @@
2222
RELEASE_PLEASE_ACTION_SHA = "45996ed1f6d02564a971a2fa1b5860e934307cf7"
2323
RELEASE_PLEASE_ACTION_VERSION = "5.0.0"
2424
RELEASE_PLEASE_ACTION_RUNTIME = "node24"
25+
RELEASE_PLEASE_ACTION = f"googleapis/release-please-action@{RELEASE_PLEASE_ACTION_SHA}"
2526
RELEASE_PLEASE_BRIDGE_VERSION = "0.1.0-alpha.1"
2627
RELEASE_PLEASE_STABLE_VERSION_PATTERN = re.compile(r"0\.1\.(?:0|[1-9][0-9]*)")
28+
RELEASE_PR_CONDITION = "steps.release.outputs.release_created != 'true'"
29+
RELEASE_PR_RETRY_CONDITION = (
30+
"steps.release.outputs.release_created != 'true' && steps.release-pr.outcome == 'failure'"
31+
)
32+
RELEASE_VERIFY_CONDITION = "steps.release.outputs.release_created == 'true'"
33+
REVIEWED_RELEASE_PR_FIRST_ATTEMPT: dict[str, object] = {
34+
"name": "Open or update the release PR",
35+
"id": "release-pr",
36+
"if": RELEASE_PR_CONDITION,
37+
"continue-on-error": "true",
38+
"uses": RELEASE_PLEASE_ACTION,
39+
"with": {
40+
"config-file": "release-please-config.json",
41+
"manifest-file": ".release-please-manifest.json",
42+
"skip-github-release": "true",
43+
},
44+
}
2745
RELEASE_PLEASE_VERIFY_COMMAND = """\
2846
test -n "$EXPECTED_TAG"
2947
test -n "$EXPECTED_SHA"
@@ -772,7 +790,9 @@ def check_release_please_workflow(text: str) -> None:
772790
_require_step_names(
773791
release_job,
774792
[
775-
"Open or update the release PR, or create its approved release",
793+
"Create the approved immutable release",
794+
"Open or update the release PR",
795+
"Retry release PR maintenance once",
776796
"Verify the immutable release created by Release Please",
777797
],
778798
"Release Please job",
@@ -797,45 +817,64 @@ def check_release_please_workflow(text: str) -> None:
797817
"Release Please job",
798818
)
799819
_require_step_working_directories(release_job, {}, "Release Please job")
800-
_, release_step = _named_action_step(
801-
release_job,
802-
"Open or update the release PR, or create its approved release",
803-
"googleapis/release-please-action",
804-
"Release Please job",
820+
release_step, release_pr_step, retry_release_pr_step, verify_step = _workflow_steps(
821+
release_job, "Release Please job"
805822
)
806-
_require_options(
807-
release_step,
808-
{
809-
"config-file": "release-please-config.json",
810-
"manifest-file": ".release-please-manifest.json",
811-
},
812-
"Release Please action",
813-
)
814-
if release_step.get("id") != "release":
815-
raise CheckError("Release Please action must expose its reviewed release outputs")
816-
if release_step.get("uses") != (
817-
f"googleapis/release-please-action@{RELEASE_PLEASE_ACTION_SHA}"
818-
):
819-
raise CheckError(
820-
"Release Please must retain the release-please "
821-
f"{RELEASE_PLEASE_ACTION_VERSION} ({RELEASE_PLEASE_ACTION_RUNTIME}) action pin"
822-
)
823-
verify_matches = [
824-
(index, step)
825-
for index, step in enumerate(_workflow_steps(release_job, "Release Please job"))
826-
if step.get("name") == "Verify the immutable release created by Release Please"
823+
common_options = {
824+
"config-file": "release-please-config.json",
825+
"manifest-file": ".release-please-manifest.json",
826+
}
827+
reviewed_steps = [
828+
(
829+
release_step,
830+
"release",
831+
common_options | {"skip-github-pull-request": "true"},
832+
{"name", "id", "uses", "with"},
833+
),
834+
(
835+
release_pr_step,
836+
"release-pr",
837+
common_options | {"skip-github-release": "true"},
838+
{"name", "id", "if", "continue-on-error", "uses", "with"},
839+
),
840+
(
841+
retry_release_pr_step,
842+
"retry-release-pr",
843+
common_options | {"skip-github-release": "true"},
844+
{"name", "id", "if", "uses", "with"},
845+
),
827846
]
828-
if len(verify_matches) != 1:
829-
raise CheckError("Release Please job must contain its immutable-release verification")
830-
_, verify_step = verify_matches[0]
847+
for step, expected_id, options, keys in reviewed_steps:
848+
_require_exact_keys(step, keys, f"Release Please {expected_id} step")
849+
if step.get("id") != expected_id:
850+
raise CheckError(f"Release Please {expected_id} step must retain its reviewed id")
851+
if step.get("uses") != RELEASE_PLEASE_ACTION:
852+
raise CheckError(
853+
"Release Please must retain the release-please "
854+
f"{RELEASE_PLEASE_ACTION_VERSION} ({RELEASE_PLEASE_ACTION_RUNTIME}) action pin"
855+
)
856+
_require_options(step, options, f"Release Please {expected_id} action")
857+
if release_pr_step.get("if") != RELEASE_PR_CONDITION:
858+
raise CheckError("Release Please must run PR maintenance only when no release was created")
859+
if release_pr_step.get("continue-on-error") != "true":
860+
raise CheckError("Release Please must permit exactly one bounded PR-maintenance retry")
861+
retry_condition = " ".join(
862+
_scalar(retry_release_pr_step.get("if"), "Release Please PR retry condition").split()
863+
)
864+
if retry_condition != RELEASE_PR_RETRY_CONDITION:
865+
raise CheckError("Release Please PR retry must require the first PR-only attempt to fail")
866+
_require_exact_keys(
867+
verify_step,
868+
{"name", "id", "if", "env", "run"},
869+
"Release Please immutable-release verification",
870+
)
831871
if verify_step.get("run") != RELEASE_PLEASE_VERIFY_COMMAND or "uses" in verify_step:
832872
raise CheckError("Release Please immutable-release verification is not exact")
833-
if verify_step.get("id") != "verify-release" or verify_step.get("if") != (
834-
"steps.release.outputs.release_created == 'true'"
873+
if (
874+
verify_step.get("id") != "verify-release"
875+
or verify_step.get("if") != RELEASE_VERIFY_CONDITION
835876
):
836877
raise CheckError("Release Please must verify only the release it just created")
837-
if any(key in verify_step for key in ("continue-on-error", "shell", "working-directory")):
838-
raise CheckError("Release Please immutable-release verification must fail closed")
839878
if _secret_references(release_job):
840879
raise CheckError("Release Please must not depend on repository credentials")
841880

@@ -1084,13 +1123,15 @@ def check_publish_workflow(text: str, live_smoke_text: str) -> None:
10841123
for mapping in _walk_mappings(workflow, "publish workflow"):
10851124
if "if" in mapping:
10861125
conditions.append(" ".join(_scalar(mapping["if"], "release condition").split()))
1087-
if "continue-on-error" in mapping:
1126+
if "continue-on-error" in mapping and mapping != REVIEWED_RELEASE_PR_FIRST_ATTEMPT:
10881127
raise CheckError("release gates must not be allowed to continue on error")
10891128
if "defaults" in mapping or "shell" in mapping:
10901129
raise CheckError("release gates must not override command execution")
10911130
expected_conditions = [
10921131
RELEASE_PLEASE_JOB_CONDITION,
1093-
"steps.release.outputs.release_created == 'true'",
1132+
RELEASE_PR_CONDITION,
1133+
RELEASE_PR_RETRY_CONDITION,
1134+
RELEASE_VERIFY_CONDITION,
10941135
RECOVERY_JOB_CONDITION,
10951136
SELECT_RELEASE_CONDITION,
10961137
BUILD_JOB_CONDITION,

0 commit comments

Comments
 (0)