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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Run offline unit and contract tests
run: uv run pytest -m "not live"
- name: Check release version agreement
run: uv run python scripts/check_version.py --expected 0.1.0a1 --require-changelog
run: uv run python scripts/check_version.py --require-changelog
- name: Check canonical public content and identity
run: uv run python scripts/check_version.py --require-public-preview-docs
- name: Scan for credentials and scope mistakes
Expand Down
27 changes: 18 additions & 9 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
name: Publish immutable release

on:
release:
types:
- published
workflow_call:
inputs:
release-tag:
required: true
type: string
release-sha:
required: true
type: string
default-branch:
required: true
type: string

permissions:
contents: read
Expand All @@ -29,15 +37,16 @@ jobs:
- name: Check out the published release tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: refs/tags/${{ github.event.release.tag_name }}
ref: refs/tags/${{ inputs.release-tag }}
fetch-depth: 0
persist-credentials: false
- name: Reject an untrusted release target
id: trust
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
RELEASE_IMMUTABLE: ${{ github.event.release.immutable }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
DEFAULT_BRANCH: ${{ inputs.default-branch }}
EXPECTED_RELEASE_SHA: ${{ inputs.release-sha }}
RELEASE_IMMUTABLE: "true"
RELEASE_TAG: ${{ inputs.release-tag }}
run: bash scripts/verify_release_trust.sh
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
Expand All @@ -50,7 +59,7 @@ jobs:
- name: Verify project, manifest, changelog, release docs, and tag agreement
id: version
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_TAG: ${{ inputs.release-tag }}
run: |
version=$(uv run python scripts/check_version.py --tag "$RELEASE_TAG" --require-changelog --require-releasable-docs --print-version)
echo "version=$version" >> "$GITHUB_OUTPUT"
Expand All @@ -62,7 +71,7 @@ jobs:
run: uv build
- name: Verify artifact versions against the tag
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_TAG: ${{ inputs.release-tag }}
run: uv run python scripts/check_version.py --tag "$RELEASE_TAG" --require-changelog --require-releasable-docs dist/*
- name: Check package metadata rendering
run: uv run twine check dist/*
Expand Down
63 changes: 62 additions & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,74 @@ jobs:
name: Maintain the reviewed release PR and release
if: vars.RELEASE_PLEASE_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 15
outputs:
release-created: ${{ steps.release.outputs.release_created }}
release-sha: ${{ steps.verify-release.outputs.release-sha }}
release-tag: ${{ steps.verify-release.outputs.release-tag }}
release-verified: ${{ steps.verify-release.outputs.release-verified }}
permissions:
contents: write
pull-requests: write
steps:
- name: Open or update the release PR, or create its approved release
id: release
uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
- name: Verify the immutable release created by Release Please
id: verify-release
if: steps.release.outputs.release_created == 'true'
env:
EXPECTED_SHA: ${{ steps.release.outputs.sha }}
EXPECTED_TAG: ${{ steps.release.outputs.tag_name }}
GH_TOKEN: ${{ github.token }}
run: |
test -n "$EXPECTED_TAG"
test -n "$EXPECTED_SHA"
release=""
for attempt in $(seq 1 12); do
release=$(gh api "repos/${{ github.repository }}/releases/tags/$EXPECTED_TAG") || true
if test -n "$release" && test "$(jq -r .immutable <<<"$release")" = "true"; then
break
fi
if test "$attempt" -ge 12; then
echo "release did not become immutable" >&2
exit 1
fi
sleep 5
done
test "$(jq -r .tag_name <<<"$release")" = "$EXPECTED_TAG"
test "$(jq -r .draft <<<"$release")" = "false"
test "$(jq -r .prerelease <<<"$release")" = "false"
test "$(jq -r .immutable <<<"$release")" = "true"
ref=$(gh api "repos/${{ github.repository }}/git/ref/tags/$EXPECTED_TAG")
tag_type=$(jq -r .object.type <<<"$ref")
tag_sha=$(jq -r .object.sha <<<"$ref")
if test "$tag_type" = "tag"; then
tag_sha=$(gh api "repos/${{ github.repository }}/git/tags/$tag_sha" --jq .object.sha)
else
test "$tag_type" = "commit"
fi
test "$tag_sha" = "$EXPECTED_SHA"
{
echo "release-tag=$EXPECTED_TAG"
echo "release-sha=$EXPECTED_SHA"
echo "release-verified=true"
} >> "$GITHUB_OUTPUT"

publish-release:
name: Run the protected publication chain
needs: release-please
if: >-
needs.release-please.outputs.release-created == 'true' &&
needs.release-please.outputs.release-verified == 'true'
permissions:
contents: read
id-token: write
uses: ./.github/workflows/publish.yml
with:
release-tag: ${{ needs.release-please.outputs.release-tag }}
release-sha: ${{ needs.release-please.outputs.release-sha }}
default-branch: ${{ github.event.repository.default_branch }}
32 changes: 20 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,10 @@ Post-alpha invariants:
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 until
a separate reviewed and tested `last-release-sha` bridge establishes the
recovery alpha as Release Please's previous-release boundary.
`LIVE_SMOKE_ENABLED=true`, and keep `RELEASE_PLEASE_ENABLED` disabled outside
an explicitly authorized release sequence. The stable-readiness
configuration establishes the recovery alpha boundary with a reviewed and
tested `last-release-sha` bridge.
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 @@ -205,7 +206,7 @@ uv run ruff check src tests scripts
uv run ruff format --check src tests scripts
uv run pyright
uv run pytest -m "not live"
uv run python scripts/check_version.py --expected 0.1.0a1 --require-changelog
uv run python scripts/check_version.py --require-changelog
uv run python scripts/check_secrets.py
uv run python scripts/check_workflows.py
rm -rf dist
Expand Down Expand Up @@ -242,8 +243,11 @@ committed.
- Publication uses a reviewed immutable tag, a protected `pypi` environment,
and PyPI OIDC Trusted Publishing.
- The release commit must equal the tag target and belong to the protected
default branch. A protected live-smoke job must check out that exact commit
and succeed before the protected PyPI job can become eligible.
default branch. Release Please must independently confirm that the exact tag
and commit are immutable before directly calling the protected publication
workflow; do not rely on workflow-token release events to trigger it. A
protected live-smoke job must check out that exact commit and succeed before
the protected PyPI job can become eligible.
- Scheduled/default-branch live smoke is monitoring evidence only and cannot
satisfy the exact-release live gate.
- Missing identity, credentials, environments, reviewers, protection,
Expand All @@ -260,12 +264,16 @@ committed.
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 Release Please disabled outside an explicitly authorized release
sequence. Its stable-readiness configuration uses the reviewed and tested
`last-release-sha` bridge because the recovery tag's build metadata cannot be
inferred from the manifest. Remove the one-time bridge and prerelease
versioning controls in the human-finalized stable release PR before it is
merged.
- Keep third-party Actions pinned to full commit SHAs. Grant `id-token: write`
only to the reusable publication caller and the protected publishing job;
the caller passes this maximum permission and only the publishing job uses
the OIDC token.
- Keep README, roadmap, compatibility matrix, examples, and changelog aligned
with shipped behavior. Use currently supported model IDs.
- All repository documentation is written in English.
Expand Down
9 changes: 5 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Release evidence is intentionally ordered:
```text
local mocked/package evidence
-> immutable tag commit equals checkout and belongs to protected default branch
-> release API and tag ref confirm the exact immutable identity
-> protected live-smoke job checks that exact commit
-> protected PyPI OIDC job publishes the previously verified artifact
-> public registry digest, provenance, install, import, and mocked smoke
Expand All @@ -139,10 +140,10 @@ 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.
Release Please remains disabled outside an explicitly authorized release
sequence. The stable-readiness configuration establishes the recovery commit
as the previous-release boundary with a tested `last-release-sha` bridge, so
the one-time build-metadata recovery tag cannot replay earlier history.

## Rejected 0.1 approaches

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ uv run ruff check src tests scripts
uv run ruff format --check src tests scripts
uv run pyright
uv run pytest -m "not live"
uv run python scripts/check_version.py --expected 0.1.0a1 --require-changelog
uv run python scripts/check_version.py --require-changelog
uv run python scripts/check_secrets.py
uv run python scripts/check_workflows.py
rm -rf dist
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ variable, then the default:

### Chat Completions

<!-- cometapi-readme-example: sync-chat -->
```python
from cometapi import CometAPI

Expand All @@ -78,6 +79,7 @@ with CometAPI() as client:

Streaming uses the official OpenAI stream type:

<!-- cometapi-readme-example: sync-chat-stream -->
```python
from cometapi import CometAPI

Expand All @@ -93,6 +95,7 @@ with CometAPI() as client:

### Responses and Models

<!-- cometapi-readme-example: sync-responses-models -->
```python
from cometapi import CometAPI

Expand All @@ -109,6 +112,7 @@ print(models.data[0].id if models.data else "No models returned")

### Async client

<!-- cometapi-readme-example: async-response -->
```python
import asyncio

Expand Down Expand Up @@ -171,7 +175,7 @@ uv run ruff check src tests scripts
uv run ruff format --check src tests scripts
uv run pyright
uv run pytest -m "not live"
uv run python scripts/check_version.py --expected 0.1.0a1 --require-changelog
uv run python scripts/check_version.py --require-changelog
uv run python scripts/check_secrets.py
uv run python scripts/check_workflows.py
rm -rf dist
Expand Down
54 changes: 36 additions & 18 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ one active maintainer.

Before the historical first push, scheduled and manually dispatched live
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.
`RELEASE_PLEASE_ENABLED` was kept disabled. The reviewed stable-readiness
configuration now uses an explicit `last-release-sha` bridge to establish the
recovery alpha as the previous-release boundary; the repository variable stays
disabled until maintainers intentionally start the stable release sequence. 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 @@ -78,7 +79,7 @@ uv run ruff check src tests scripts
uv run ruff format --check src tests scripts
uv run pyright
uv run pytest -m "not live"
uv run python scripts/check_version.py --expected 0.1.0a1 --require-changelog
uv run python scripts/check_version.py --require-changelog
uv run python scripts/check_secrets.py
uv run python scripts/check_workflows.py
rm -rf dist
Expand Down Expand Up @@ -181,14 +182,18 @@ 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 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
`RELEASE_PLEASE_ENABLED` repository variable. The checked-in stable-readiness
configuration establishes the recovery release boundary with the reviewed
`last-release-sha` bridge. 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 invokes
the protected publication chain directly; workflow-token release events do
not trigger a second workflow run.
- `publish.yml` is called only with the independently verified immutable tag,
commit, and default branch. 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
`live-smoke` job then checks out that exact verified commit and must succeed
before the protected `pypi` job becomes eligible. The workflow publishes the
previously verified artifacts with OIDC, then checks the public package
Expand All @@ -197,7 +202,9 @@ violations in one run and still returns non-zero when any violation exists.
or empty live-model repository variable resolves to `gpt-5.4`.

Third-party Actions are pinned to full commit SHAs. Workflow permissions are
read-only by default; only the publishing job receives `id-token: write`.
read-only by default. The reusable publication caller and protected publishing
job declare `id-token: write`; the caller passes the maximum permission and
only the publishing job requests the OIDC token.
Publishing uses a protected `pypi` environment and concurrency control.
Arbitrary-branch and manual publication are forbidden.

Expand Down Expand Up @@ -272,8 +279,9 @@ changelog, GitHub release, wheel, and source distribution.
SHA256 `a6820347317943ca22f7632acbe354dd992f31a122a6172dfe45b57960e3a093`
and source-distribution SHA256
`98d86829ef14771e8b7ec180d452c6638289f49c14a39b7207be5c47cb64cde7`.
- `LIVE_SMOKE_ENABLED=false`. Release Please remains disabled until a separate
reviewed and tested `last-release-sha` bridge is merged.
- `LIVE_SMOKE_ENABLED=false`. Release Please remains disabled outside an
explicitly authorized release sequence; the reviewed `last-release-sha`
bridge is configured for the alpha-to-stable transition.

## Stable release sequence

Expand All @@ -282,8 +290,11 @@ feature or fix pull request
-> required offline CI
-> merge to the default branch
-> automated release pull request
-> human review and merge
-> human finalization of stable docs, metadata, and one-time bridge cleanup
-> required release-PR CI, review, and merge
-> immutable tag and GitHub release
-> bounded API verification of immutable tag and commit identity
-> direct call to the protected publication workflow
-> verify immutable tag commit and protected-default-branch ancestry
-> rebuild and verify exact artifacts
-> protected live smoke against that exact commit
Expand All @@ -295,4 +306,11 @@ feature or fix pull request

Stable `0.1.0` additionally requires the complete blocking Python matrix,
executed README examples against the built package, trusted live evidence, and
reviewed release-PR and changelog agreement.
reviewed release-PR and changelog agreement. Before the stable release PR is
merged, its finalization commit must state that `0.1.0` is approved for PyPI
publication, use the stable installation command and classifier, and remove the
one-time `last-release-sha` plus prerelease-versioning controls. The manifest,
project metadata, lock file, and changelog must remain at the exact generated
`0.1.0` version. If GitHub requires approval before checks run on the automated
pull request, approve only that reviewed workflow execution and wait for every
blocking check.
Loading