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
22 changes: 21 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,32 @@ jobs:
contents: write
pull-requests: write
steps:
- name: Open or update the release PR, or create its approved release
- name: Create the approved immutable release
id: release
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0, node24
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
skip-github-pull-request: "true"
- name: Open or update the release PR
id: release-pr
if: steps.release.outputs.release_created != 'true'
continue-on-error: true
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0, node24
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
skip-github-release: "true"
- name: Retry release PR maintenance once
id: retry-release-pr
if: >-
steps.release.outputs.release_created != 'true' &&
steps.release-pr.outcome == 'failure'
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0, node24
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
skip-github-release: "true"
- name: Verify the immutable release created by Release Please
id: verify-release
if: steps.release.outputs.release_created == 'true'
Expand Down
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ Post-stable invariants:
9. Keep Release Please pinned to the reviewed `v5.0.0` commit
`45996ed1f6d02564a971a2fa1b5860e934307cf7`, whose immutable action metadata
uses `node24`. The workflow semantic checker must reject any different pin.
Invoke that action release-only first with `skip-github-pull-request: true`;
that immutable tag-and-GitHub-Release path must never continue on error or be
retried. Only after it succeeds without creating a release may PR-only
maintenance run with `skip-github-release: true`. Its first attempt is the
sole Release Please step allowed to continue on error, and one identical
second attempt may run only when that first PR attempt fails. Mutable branch
and pull-request maintenance is idempotent and may use this bounded retry;
immutable release creation may not. If the release-only invocation fails,
immediately disable `RELEASE_PLEASE_ENABLED`, inspect tag and GitHub Release
state read-only, and stop. Do not use another main push or recovery path
until the exact external state is known and recovery is separately
authorized.
10. `README.md` is the distribution long description and must remain accurate
before and after publication. Use `python -m pip install cometapi`,
unversioned project links, and publication-neutral maintenance language.
Expand Down
18 changes: 18 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@ uses `node24`. The workflow semantic contract fixes that SHA and runtime
disposition so GitHub does not need to force a deprecated Node 20 action onto a
newer runtime.

Release Please execution is split at the mutability boundary. The first pinned
action invocation is release-only (`skip-github-pull-request: true`) and is
neither continued on error nor retried. Only when that invocation succeeds
without creating a release does PR-only maintenance run
(`skip-github-release: true`). The first PR-only attempt is allowed to continue
on error solely so one identical conditional retry can follow; the second
failure ends the job. Updating a branch or pull request is mutable and
idempotent, while retrying immutable tag or GitHub Release creation could leave
ambiguous external state and is forbidden.

[Release Please run 30509764960](https://github.com/cometapi-dev/cometapi-python/actions/runs/30509764960)
isolated the motivating failure to the action's Undici/global `fetch`: the PR
workflow reached its write boundary and then failed with `other side closed`
before any branch, pull-request, tag, GitHub Release, live, or registry write.
The existing release branch and repository pull-request permission were not the
cause. This is negative transport evidence, not evidence of a stale branch or
authorization drift.

This complete trust chain executed successfully in
[release workflow run 30261746138](https://github.com/cometapi-dev/cometapi-python/actions/runs/30261746138)
for release commit `31b68904141489ca04932edbf305ccf88af09372`, recovery tag
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ automation.

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

Expand Down
32 changes: 28 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,24 @@ The Release Please step is pinned to
checker rejects any other pin so the workflow cannot silently regress to the
deprecated Node 20 runtime.

The job invokes the pinned action release-only first with
`skip-github-pull-request: true`. That step cannot continue on error and is
never retried. If it succeeds without creating a release, PR-only maintenance
uses `skip-github-release: true`; its first attempt is the single Release Please
step allowed to continue on error, and a second identical attempt runs only
after that first PR attempt fails. A second failure ends the job. This permits
one bounded retry for mutable, idempotent branch and pull-request maintenance
without ever automatically retrying immutable tag or GitHub Release creation.

[Run 30509764960](https://github.com/cometapi-dev/cometapi-python/actions/runs/30509764960)
failed in the PR workflow with Undici/global `fetch` reporting
`other side closed`. It had built the candidate and reached the write boundary,
but created or updated no branch, pull request, tag, GitHub Release, live
request, or registry artifact. Read-only inspection ruled out a stale release
branch and missing pull-request permission. Treat that run as isolated
transport-failure evidence; do not rerun it or reinterpret it as an
authorization failure.

Release mode (`check_version.py --require-releasable-docs`) also fails closed
until project authorship, the canonical GitHub repository URL, the copyright
holder, security and support contacts, a publication-neutral README, and a dated
Expand Down Expand Up @@ -220,10 +238,16 @@ descriptions cannot drift.
`last-release-sha` bridge established the recovery release boundary and was
removed during human finalization of the stable release PR. Keep the variable
disabled except while executing an explicitly authorized release sequence.
When it creates an approved release with the GitHub workflow token, it polls
the GitHub API until that exact tag and commit are independently reported as
immutable, then selects it for the downstream chain in the same workflow;
workflow-token release events do not trigger a second workflow.
The job checks for and creates an approved release through its non-retryable
release-only invocation before it performs retryable PR-only maintenance. If
a release is created with the GitHub workflow token, it polls the GitHub API
until that exact tag and commit are independently reported as immutable, then
selects it for the downstream chain in the same workflow; workflow-token
release events do not trigger a second workflow. If the release-only
invocation fails, immediately set `RELEASE_PLEASE_ENABLED=false`, inspect tag
and GitHub Release state read-only, and stop. Do not make another main push or
use recovery until the exact external state is known and recovery is
separately authorized.
- The `verify-recovery` path in `publish.yml` is the only manual publication
path. It requires an exact immutable tag and commit, the protected default
branch, and the temporary `RELEASE_RECOVERY_TAG` and `RELEASE_RECOVERY_SHA`
Expand Down
33 changes: 29 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# CometAPI Python SDK Roadmap

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

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

## `0.1.2`: Release metadata and transport maintenance

[Release Please run 30509764960](https://github.com/cometapi-dev/cometapi-python/actions/runs/30509764960)
failed while maintaining the `0.1.2` release PR. The pinned v5 action had built
the candidate and reached its PR write boundary when Undici/global `fetch`
reported `other side closed`. It created or updated no branch, pull request,
tag, GitHub Release, live request, PyPI file, or other registry state. Read-only
inspection showed that neither the existing release branch nor repository
pull-request permission caused the failure.

Release Please now executes according to external-state reversibility. A
release-only invocation (`skip-github-pull-request: true`) runs first without
`continue-on-error` and without retry. Only if no release was created does
PR-only maintenance (`skip-github-release: true`) run; its first attempt may
continue on error solely to permit one identical conditional retry. Mutable,
idempotent branch and pull-request maintenance can therefore recover from one
isolated transport close, while immutable tag and GitHub Release creation can
never be automatically replayed. This remains 0.1.x maintenance and does not
authorize provider, resource, CLI, or 0.2 work.

## `0.2.0`: Provider-native text adapters

Planned scope:
Expand Down Expand Up @@ -596,8 +619,10 @@ The repository maintains three independently auditable workflows:
four requests, 16 output tokens per generation, a 30-second request timeout,
concurrency one, a ten-minute workflow timeout, and stop on first failure.
- `publish.yml`: the single top-level release and PyPI Trusted Publisher
identity. Its gated push path maintains the human-reviewed Release Please PR
and independently verifies a created release. Its explicitly enabled manual
identity. Its gated push path first attempts non-retryable release-only
creation, then, only when no release exists, permits one bounded retry for
PR-only maintenance. It independently verifies a created release. Its
explicitly enabled manual
path recovers an independently verified existing immutable release only from
the protected default branch. An exact selector feeds both paths into tag,
commit, and default-branch ancestry verification, artifact rebuild, protected
Expand Down
113 changes: 77 additions & 36 deletions scripts/check_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,26 @@
RELEASE_PLEASE_ACTION_SHA = "45996ed1f6d02564a971a2fa1b5860e934307cf7"
RELEASE_PLEASE_ACTION_VERSION = "5.0.0"
RELEASE_PLEASE_ACTION_RUNTIME = "node24"
RELEASE_PLEASE_ACTION = f"googleapis/release-please-action@{RELEASE_PLEASE_ACTION_SHA}"
RELEASE_PLEASE_BRIDGE_VERSION = "0.1.0-alpha.1"
RELEASE_PLEASE_STABLE_VERSION_PATTERN = re.compile(r"0\.1\.(?:0|[1-9][0-9]*)")
RELEASE_PR_CONDITION = "steps.release.outputs.release_created != 'true'"
RELEASE_PR_RETRY_CONDITION = (
"steps.release.outputs.release_created != 'true' && steps.release-pr.outcome == 'failure'"
)
RELEASE_VERIFY_CONDITION = "steps.release.outputs.release_created == 'true'"
REVIEWED_RELEASE_PR_FIRST_ATTEMPT: dict[str, object] = {
"name": "Open or update the release PR",
"id": "release-pr",
"if": RELEASE_PR_CONDITION,
"continue-on-error": "true",
"uses": RELEASE_PLEASE_ACTION,
"with": {
"config-file": "release-please-config.json",
"manifest-file": ".release-please-manifest.json",
"skip-github-release": "true",
},
}
RELEASE_PLEASE_VERIFY_COMMAND = """\
test -n "$EXPECTED_TAG"
test -n "$EXPECTED_SHA"
Expand Down Expand Up @@ -772,7 +790,9 @@ def check_release_please_workflow(text: str) -> None:
_require_step_names(
release_job,
[
"Open or update the release PR, or create its approved release",
"Create the approved immutable release",
"Open or update the release PR",
"Retry release PR maintenance once",
"Verify the immutable release created by Release Please",
],
"Release Please job",
Expand All @@ -797,45 +817,64 @@ def check_release_please_workflow(text: str) -> None:
"Release Please job",
)
_require_step_working_directories(release_job, {}, "Release Please job")
_, release_step = _named_action_step(
release_job,
"Open or update the release PR, or create its approved release",
"googleapis/release-please-action",
"Release Please job",
release_step, release_pr_step, retry_release_pr_step, verify_step = _workflow_steps(
release_job, "Release Please job"
)
_require_options(
release_step,
{
"config-file": "release-please-config.json",
"manifest-file": ".release-please-manifest.json",
},
"Release Please action",
)
if release_step.get("id") != "release":
raise CheckError("Release Please action must expose its reviewed release outputs")
if release_step.get("uses") != (
f"googleapis/release-please-action@{RELEASE_PLEASE_ACTION_SHA}"
):
raise CheckError(
"Release Please must retain the release-please "
f"{RELEASE_PLEASE_ACTION_VERSION} ({RELEASE_PLEASE_ACTION_RUNTIME}) action pin"
)
verify_matches = [
(index, step)
for index, step in enumerate(_workflow_steps(release_job, "Release Please job"))
if step.get("name") == "Verify the immutable release created by Release Please"
common_options = {
"config-file": "release-please-config.json",
"manifest-file": ".release-please-manifest.json",
}
reviewed_steps = [
(
release_step,
"release",
common_options | {"skip-github-pull-request": "true"},
{"name", "id", "uses", "with"},
),
(
release_pr_step,
"release-pr",
common_options | {"skip-github-release": "true"},
{"name", "id", "if", "continue-on-error", "uses", "with"},
),
(
retry_release_pr_step,
"retry-release-pr",
common_options | {"skip-github-release": "true"},
{"name", "id", "if", "uses", "with"},
),
]
if len(verify_matches) != 1:
raise CheckError("Release Please job must contain its immutable-release verification")
_, verify_step = verify_matches[0]
for step, expected_id, options, keys in reviewed_steps:
_require_exact_keys(step, keys, f"Release Please {expected_id} step")
if step.get("id") != expected_id:
raise CheckError(f"Release Please {expected_id} step must retain its reviewed id")
if step.get("uses") != RELEASE_PLEASE_ACTION:
raise CheckError(
"Release Please must retain the release-please "
f"{RELEASE_PLEASE_ACTION_VERSION} ({RELEASE_PLEASE_ACTION_RUNTIME}) action pin"
)
_require_options(step, options, f"Release Please {expected_id} action")
if release_pr_step.get("if") != RELEASE_PR_CONDITION:
raise CheckError("Release Please must run PR maintenance only when no release was created")
if release_pr_step.get("continue-on-error") != "true":
raise CheckError("Release Please must permit exactly one bounded PR-maintenance retry")
retry_condition = " ".join(
_scalar(retry_release_pr_step.get("if"), "Release Please PR retry condition").split()
)
if retry_condition != RELEASE_PR_RETRY_CONDITION:
raise CheckError("Release Please PR retry must require the first PR-only attempt to fail")
_require_exact_keys(
verify_step,
{"name", "id", "if", "env", "run"},
"Release Please immutable-release verification",
)
if verify_step.get("run") != RELEASE_PLEASE_VERIFY_COMMAND or "uses" in verify_step:
raise CheckError("Release Please immutable-release verification is not exact")
if verify_step.get("id") != "verify-release" or verify_step.get("if") != (
"steps.release.outputs.release_created == 'true'"
if (
verify_step.get("id") != "verify-release"
or verify_step.get("if") != RELEASE_VERIFY_CONDITION
):
raise CheckError("Release Please must verify only the release it just created")
if any(key in verify_step for key in ("continue-on-error", "shell", "working-directory")):
raise CheckError("Release Please immutable-release verification must fail closed")
if _secret_references(release_job):
raise CheckError("Release Please must not depend on repository credentials")

Expand Down Expand Up @@ -1084,13 +1123,15 @@ def check_publish_workflow(text: str, live_smoke_text: str) -> None:
for mapping in _walk_mappings(workflow, "publish workflow"):
if "if" in mapping:
conditions.append(" ".join(_scalar(mapping["if"], "release condition").split()))
if "continue-on-error" in mapping:
if "continue-on-error" in mapping and mapping != REVIEWED_RELEASE_PR_FIRST_ATTEMPT:
raise CheckError("release gates must not be allowed to continue on error")
if "defaults" in mapping or "shell" in mapping:
raise CheckError("release gates must not override command execution")
expected_conditions = [
RELEASE_PLEASE_JOB_CONDITION,
"steps.release.outputs.release_created == 'true'",
RELEASE_PR_CONDITION,
RELEASE_PR_RETRY_CONDITION,
RELEASE_VERIFY_CONDITION,
RECOVERY_JOB_CONDITION,
SELECT_RELEASE_CONDITION,
BUILD_JOB_CONDITION,
Expand Down
Loading