docs: record stable release evidence (#24) #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish immutable release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| release-tag: | |
| description: Exact immutable GitHub release tag | |
| required: true | |
| type: string | |
| release-sha: | |
| description: Exact commit resolved by the release tag | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: pypi-publish | |
| cancel-in-progress: false | |
| env: | |
| UV_VERSION: 0.11.8 | |
| jobs: | |
| release-please: | |
| name: Maintain the reviewed release PR and release | |
| if: >- | |
| github.run_attempt == 1 && | |
| github.event_name == 'push' && | |
| vars.RELEASE_PLEASE_ENABLED == 'true' | |
| runs-on: ubuntu-latest | |
| 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" | |
| verify-recovery: | |
| name: Verify the authorized immutable release recovery | |
| if: >- | |
| github.run_attempt == 1 && | |
| github.event_name == 'workflow_dispatch' && | |
| github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && | |
| vars.RELEASE_RECOVERY_TAG == inputs.release-tag && | |
| vars.RELEASE_RECOVERY_SHA == inputs.release-sha | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| release-sha: ${{ steps.verify-release.outputs.release-sha }} | |
| release-tag: ${{ steps.verify-release.outputs.release-tag }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Verify the immutable release selected for recovery | |
| id: verify-release | |
| env: | |
| EXPECTED_SHA: ${{ inputs.release-sha }} | |
| EXPECTED_TAG: ${{ inputs.release-tag }} | |
| 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" | |
| select-release: | |
| name: Select one independently verified release identity | |
| needs: | |
| - release-please | |
| - verify-recovery | |
| if: >- | |
| always() && | |
| !cancelled() && | |
| github.run_attempt == 1 && | |
| ( | |
| ( | |
| github.event_name == 'push' && | |
| needs.release-please.result == 'success' && | |
| needs.release-please.outputs.release-created == 'true' && | |
| needs.release-please.outputs.release-verified == 'true' | |
| ) || | |
| ( | |
| github.event_name == 'workflow_dispatch' && | |
| needs.verify-recovery.result == 'success' | |
| ) | |
| ) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| release-sha: ${{ steps.select.outputs.release-sha }} | |
| release-tag: ${{ steps.select.outputs.release-tag }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Select the independently verified release identity | |
| id: select | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| RECOVERY_SHA: ${{ needs.verify-recovery.outputs.release-sha }} | |
| RECOVERY_TAG: ${{ needs.verify-recovery.outputs.release-tag }} | |
| RELEASE_PLEASE_SHA: ${{ needs.release-please.outputs.release-sha }} | |
| RELEASE_PLEASE_TAG: ${{ needs.release-please.outputs.release-tag }} | |
| run: | | |
| case "$EVENT_NAME" in | |
| push) | |
| release_sha=$RELEASE_PLEASE_SHA | |
| release_tag=$RELEASE_PLEASE_TAG | |
| ;; | |
| workflow_dispatch) | |
| release_sha=$RECOVERY_SHA | |
| release_tag=$RECOVERY_TAG | |
| ;; | |
| *) | |
| exit 1 | |
| ;; | |
| esac | |
| test -n "$release_sha" | |
| test -n "$release_tag" | |
| { | |
| echo "release-sha=$release_sha" | |
| echo "release-tag=$release_tag" | |
| } >> "$GITHUB_OUTPUT" | |
| build: | |
| name: Verify and build the immutable default-branch release | |
| if: >- | |
| always() && | |
| !cancelled() && | |
| github.run_attempt == 1 && | |
| needs.select-release.result == 'success' | |
| needs: | |
| - select-release | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| outputs: | |
| release-commit: ${{ steps.trust.outputs.release-commit }} | |
| version: ${{ steps.version.outputs.version }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out the published release tag | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: refs/tags/${{ needs.select-release.outputs.release-tag }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Reject an untrusted release target | |
| id: trust | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| EXPECTED_RELEASE_SHA: ${{ needs.select-release.outputs.release-sha }} | |
| RELEASE_IMMUTABLE: "true" | |
| RELEASE_TAG: ${{ needs.select-release.outputs.release-tag }} | |
| run: bash scripts/verify_release_trust.sh | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.14" | |
| - name: Install the pinned uv frontend | |
| run: python -m pip install --disable-pip-version-check "uv==$UV_VERSION" | |
| - name: Reproduce the locked environment | |
| run: uv sync --locked | |
| - name: Verify project, manifest, changelog, release docs, and tag agreement | |
| id: version | |
| env: | |
| RELEASE_TAG: ${{ needs.select-release.outputs.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" | |
| - name: Scan the immutable source for credentials and scope mistakes | |
| run: uv run python scripts/check_secrets.py | |
| - name: Verify release-workflow trust semantics | |
| run: uv run python scripts/check_workflows.py | |
| - name: Build wheel and source distribution from the tag | |
| run: uv build | |
| - name: Verify artifact versions against the tag | |
| env: | |
| RELEASE_TAG: ${{ needs.select-release.outputs.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/* | |
| - name: Inspect artifact identity and shape | |
| run: uv run python scripts/check_artifacts.py dist/* | |
| - name: Install and smoke-test each exact artifact | |
| run: uv run python scripts/check_clean_install.py dist/* | |
| - name: Record immutable artifact digests | |
| run: sha256sum dist/* > artifact-sha256.txt | |
| - name: Retain only the verified release bundle | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-${{ steps.version.outputs.version }} | |
| path: | | |
| dist/* | |
| artifact-sha256.txt | |
| if-no-files-found: error | |
| retention-days: 30 | |
| release-live-smoke: | |
| name: Verify the exact release commit against CometAPI | |
| if: >- | |
| always() && | |
| !cancelled() && | |
| github.run_attempt == 1 && | |
| needs.build.result == 'success' | |
| needs: | |
| - build | |
| concurrency: | |
| group: trusted-live-smoke | |
| cancel-in-progress: false | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| # Required repository configuration: protect this environment without required reviewers, | |
| # and configure COMETAPI_KEY plus the approved COMETAPI_LIVE_MODEL variable. | |
| environment: live-smoke | |
| env: | |
| COMETAPI_LIVE_CONCURRENCY: "1" | |
| COMETAPI_LIVE_MAX_OUTPUT_TOKENS: "16" | |
| COMETAPI_LIVE_MAX_REQUESTS: "4" | |
| COMETAPI_LIVE_MODEL: ${{ vars.COMETAPI_LIVE_MODEL || 'gpt-5.4' }} | |
| COMETAPI_LIVE_REQUEST_TIMEOUT_SECONDS: "30" | |
| COMETAPI_LIVE_RUN: "1" | |
| COMETAPI_LIVE_STOP_ON_FAILURE: "1" | |
| steps: | |
| - name: Require the protected live credential | |
| env: | |
| COMETAPI_KEY: ${{ secrets.COMETAPI_KEY }} | |
| run: test -n "$COMETAPI_KEY" | |
| - name: Check out the verified release commit | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.build.outputs.release-commit }} | |
| persist-credentials: false | |
| - name: Require the exact verified release commit | |
| env: | |
| RELEASE_COMMIT: ${{ needs.build.outputs.release-commit }} | |
| run: test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT" | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.14" | |
| - name: Install the pinned uv frontend | |
| run: python -m pip install --disable-pip-version-check "uv==$UV_VERSION" | |
| - name: Reproduce the locked release environment | |
| run: uv sync --locked | |
| - name: Run the bounded exact-release live suite | |
| env: | |
| COMETAPI_KEY: ${{ secrets.COMETAPI_KEY }} | |
| run: uv run pytest -m live --maxfail=1 -q | |
| publish: | |
| name: Publish verified artifacts with PyPI OIDC | |
| if: >- | |
| always() && | |
| !cancelled() && | |
| github.run_attempt == 1 && | |
| needs.build.result == 'success' && | |
| needs.release-live-smoke.result == 'success' | |
| needs: | |
| - build | |
| - release-live-smoke | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/project/cometapi/${{ needs.build.outputs.version }}/ | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Download the verified release bundle | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-${{ needs.build.outputs.version }} | |
| path: release-bundle | |
| - name: Recheck immutable artifact digests | |
| working-directory: release-bundle | |
| run: sha256sum --check artifact-sha256.txt | |
| - name: Publish through the configured PyPI Trusted Publisher | |
| uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 | |
| with: | |
| packages-dir: release-bundle/dist/ | |
| print-hash: true | |
| attestations: true | |
| verify-registry: | |
| name: Verify the public registry artifact | |
| if: >- | |
| always() && | |
| !cancelled() && | |
| github.run_attempt == 1 && | |
| needs.build.result == 'success' && | |
| needs.publish.result == 'success' | |
| needs: | |
| - build | |
| - publish | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out the registry verification source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.build.outputs.release-commit }} | |
| persist-credentials: false | |
| - name: Download the verified release bundle after checkout | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-${{ needs.build.outputs.version }} | |
| path: release-bundle | |
| - name: Require retained pre-publication digest evidence | |
| run: test -f release-bundle/artifact-sha256.txt | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.14" | |
| - name: Install the pinned provenance verifier | |
| env: | |
| PIP_BUILD_CONSTRAINT: "" | |
| PIP_CONFIG_FILE: /dev/null | |
| PIP_CONSTRAINT: "" | |
| PIP_EXTRA_INDEX_URL: "" | |
| PIP_FIND_LINKS: "" | |
| PIP_REQUIREMENT: "" | |
| run: >- | |
| python -m pip --isolated install | |
| --disable-pip-version-check | |
| --index-url https://pypi.org/simple/ | |
| --no-cache-dir | |
| "pypi-attestations==0.0.29" | |
| - name: Verify public artifact identity, digests, and provenance | |
| env: | |
| RELEASE_VERSION: ${{ needs.build.outputs.version }} | |
| run: >- | |
| python scripts/check_registry_release.py | |
| --version "$RELEASE_VERSION" | |
| --repository "https://github.com/${{ github.repository }}" | |
| --digest-file release-bundle/artifact-sha256.txt | |
| --download-directory registry-artifacts | |
| --attempts 12 | |
| --retry-delay 10 | |
| - name: Install from public PyPI and run the isolated mocked-call smoke | |
| env: | |
| RELEASE_VERSION: ${{ needs.build.outputs.version }} | |
| run: >- | |
| python scripts/check_clean_install.py | |
| --expected-version "$RELEASE_VERSION" | |
| --requirement "cometapi==$RELEASE_VERSION" | |
| --index-url https://pypi.org/simple/ | |
| --attempts 12 | |
| --retry-delay 10 |