Publish 2.0.0-beta.13@82f752135ab6f03c75d121721b7c1f8e8cb30d9c from direct #123
Workflow file for this run
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 to PyPI | |
| run-name: >- | |
| ${{ (github.event_name == 'push' || inputs.publish) && 'Publish' || 'Build' }} | |
| ${{ inputs.release_tag || github.ref_name }}@${{ inputs.release_commit || github.sha }} | |
| from ${{ inputs.release_plan || 'direct' }} | |
| on: | |
| push: | |
| tags: | |
| - '[0-9]+.[0-9]+.[0-9]+*' | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: 'Existing immutable SDK release tag; empty permits a build-only run' | |
| required: false | |
| type: string | |
| default: '' | |
| release_commit: | |
| description: 'Expected commit for a release-plan publication' | |
| required: false | |
| type: string | |
| default: '' | |
| release_plan: | |
| description: 'Immutable release-plan tag initiating this recovery run' | |
| required: false | |
| type: string | |
| default: 'direct' | |
| publish: | |
| description: 'Publish the exact release tag to PyPI' | |
| required: false | |
| type: boolean | |
| default: false | |
| dry_run: | |
| description: 'Legacy TestPyPI dry-run guard' | |
| required: false | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-${{ inputs.release_tag || github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| artifact-digest: ${{ steps.privileged-handoff.outputs.artifact-digest }} | |
| artifact-id: ${{ steps.privileged-handoff.outputs.artifact-id }} | |
| release_tag: ${{ steps.release_source.outputs.tag }} | |
| release_commit: ${{ steps.release_source.outputs.commit }} | |
| source-run-attempt: ${{ github.run_attempt }} | |
| source-run-id: ${{ github.run_id }} | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: >- | |
| ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag != '' | |
| && format('refs/tags/{0}', inputs.release_tag) || github.ref }} | |
| - name: Resolve exact release identity | |
| id: release_source | |
| env: | |
| REQUESTED_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }} | |
| REQUESTED_COMMIT: ${{ github.event_name == 'workflow_dispatch' && inputs.release_commit || '' }} | |
| PUBLISH_REQUESTED: ${{ github.event_name == 'push' || inputs.publish }} | |
| run: | | |
| set -euo pipefail | |
| package_version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')" | |
| head_commit="$(git rev-parse HEAD)" | |
| if [ "$PUBLISH_REQUESTED" = true ]; then | |
| if [[ ! "$REQUESTED_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]; then | |
| printf 'release tag must be an exact SDK SemVer: %s\n' "$REQUESTED_TAG" >&2 | |
| exit 1 | |
| fi | |
| if [ "$REQUESTED_TAG" != "$package_version" ]; then | |
| printf 'release tag %s does not match package version %s\n' "$REQUESTED_TAG" "$package_version" >&2 | |
| exit 1 | |
| fi | |
| tag_commit="$(git rev-list -n 1 "$REQUESTED_TAG")" | |
| if [ "$tag_commit" != "$head_commit" ]; then | |
| printf 'release tag %s points to %s, not checkout commit %s\n' \ | |
| "$REQUESTED_TAG" "$tag_commit" "$head_commit" >&2 | |
| exit 1 | |
| fi | |
| if [ -n "$REQUESTED_COMMIT" ]; then | |
| if [[ ! "$REQUESTED_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then | |
| printf 'release commit must be an exact Git object ID: %s\n' "$REQUESTED_COMMIT" >&2 | |
| exit 1 | |
| fi | |
| if [ "$tag_commit" != "$REQUESTED_COMMIT" ]; then | |
| printf 'release tag %s points to %s, not requested commit %s\n' \ | |
| "$REQUESTED_TAG" "$tag_commit" "$REQUESTED_COMMIT" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| else | |
| REQUESTED_TAG="$package_version" | |
| fi | |
| { | |
| printf 'tag=%s\n' "$REQUESTED_TAG" | |
| printf 'commit=%s\n' "$head_commit" | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Install build tools | |
| run: pip install build twine | |
| - name: Build package | |
| run: python -m build | |
| - name: Check package | |
| run: twine check dist/* | |
| - name: Smoke test built package | |
| run: python scripts/smoke-built-package.py | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Package the privileged PyPI handoff as one immutable file | |
| run: tar -cf dist-handoff.tar -C dist . | |
| - name: Bind the privileged PyPI handoff identity and digest | |
| id: privileged-handoff | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| archive: false | |
| if-no-files-found: error | |
| path: dist-handoff.tar | |
| publish: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| if: >- | |
| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || | |
| (github.event_name == 'workflow_dispatch' && | |
| github.ref == 'refs/heads/main' && inputs.publish) | |
| environment: pypi | |
| permissions: | |
| actions: read | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| ref: ${{ github.sha }} | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| artifact-ids: ${{ needs.build.outputs.artifact-id }} | |
| digest-mismatch: error | |
| github-token: ${{ github.token }} | |
| path: isolated-python-dist | |
| repository: ${{ github.repository }} | |
| run-id: ${{ needs.build.outputs.source-run-id }} | |
| - name: Validate the exact producer artifact before use | |
| env: | |
| ARTIFACT_DIRECTORY: isolated-python-dist | |
| EXPECTED_ARTIFACT_DIGEST: ${{ needs.build.outputs.artifact-digest }} | |
| EXPECTED_ARTIFACT_ID: ${{ needs.build.outputs.artifact-id }} | |
| EXPECTED_SOURCE_RUN_ATTEMPT: ${{ needs.build.outputs.source-run-attempt }} | |
| EXPECTED_SOURCE_RUN_ID: ${{ needs.build.outputs.source-run-id }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$EXPECTED_ARTIFACT_DIGEST" =~ ^[0-9a-f]{64}$ ]]; then | |
| printf 'producer artifact digest is not an exact SHA-256 digest\n' >&2 | |
| exit 1 | |
| fi | |
| for identity in "$EXPECTED_ARTIFACT_ID" "$EXPECTED_SOURCE_RUN_ID" "$EXPECTED_SOURCE_RUN_ATTEMPT"; do | |
| if [[ ! "$identity" =~ ^[1-9][0-9]*$ ]]; then | |
| printf 'producer artifact identity is invalid\n' >&2 | |
| exit 1 | |
| fi | |
| done | |
| if [[ ! "$ARTIFACT_DIRECTORY" =~ ^isolated-[a-z0-9][a-z0-9._-]*$ ]]; then | |
| printf 'artifact validation directory is unsafe\n' >&2 | |
| exit 1 | |
| fi | |
| mapfile -d '' entries < <( | |
| /usr/bin/find "$ARTIFACT_DIRECTORY" -mindepth 1 -maxdepth 1 -print0 | |
| ) | |
| if [ "${#entries[@]}" -ne 1 ] || [ ! -f "${entries[0]}" ] || [ -L "${entries[0]}" ]; then | |
| printf 'artifact handoff must contain exactly one regular file\n' >&2 | |
| exit 1 | |
| fi | |
| observed_digest="$(/usr/bin/sha256sum "${entries[0]}")" | |
| observed_digest="${observed_digest%% *}" | |
| if [ "$observed_digest" != "$EXPECTED_ARTIFACT_DIGEST" ]; then | |
| printf 'artifact digest mismatch: expected %s, got %s\n' \ | |
| "$EXPECTED_ARTIFACT_DIGEST" "$observed_digest" >&2 | |
| exit 1 | |
| fi | |
| printf 'validated artifact %s from run %s attempt %s\n' \ | |
| "$EXPECTED_ARTIFACT_ID" "$EXPECTED_SOURCE_RUN_ID" "$EXPECTED_SOURCE_RUN_ATTEMPT" | |
| - name: Extract the validated PyPI handoff | |
| run: | | |
| mkdir dist | |
| tar -xf isolated-python-dist/dist-handoff.tar -C dist | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1 | |
| with: | |
| password: ${{ secrets.PYPI_TOKEN }} | |
| print-hash: true | |
| skip-existing: true | |
| - name: Create the source GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ needs.build.outputs.release_tag }} | |
| run: | | |
| if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then | |
| arguments=(--verify-tag --generate-notes --title "$RELEASE_TAG") | |
| if [[ "$RELEASE_TAG" == *-* ]]; then | |
| arguments+=(--prerelease) | |
| fi | |
| gh release create "$RELEASE_TAG" "${arguments[@]}" | |
| fi | |
| verify-docs-release-audit: | |
| needs: [build, publish] | |
| runs-on: ubuntu-latest | |
| if: >- | |
| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || | |
| (github.event_name == 'workflow_dispatch' && inputs.publish) | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| ref: ${{ needs.build.outputs.release_tag }} | |
| - name: Verify live docs release audit after PyPI publish | |
| env: | |
| DOCS_RELEASE_AUDIT_ARTIFACT: sdk-python | |
| DOCS_RELEASE_AUDIT_VERSION: ${{ needs.build.outputs.release_tag }} | |
| DOCS_RELEASE_AUDIT_EVIDENCE: docs-release-audit-evidence.json | |
| DOCS_RELEASE_AUDIT_HANDOFF: docs-release-audit-handoff.json | |
| DOCS_RELEASE_AUDIT_ENFORCEMENT: advisory | |
| run: scripts/ci/check-docs-release-audit.sh | |
| - name: Upload docs release audit evidence | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: docs-release-audit-evidence | |
| path: | | |
| docs-release-audit-evidence.json | |
| docs-release-audit-handoff.json | |
| if-no-files-found: warn | |
| publish-test: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| if: >- | |
| github.event_name == 'workflow_dispatch' && | |
| github.ref == 'refs/heads/main' && | |
| !inputs.dry_run && !inputs.publish | |
| environment: test-pypi | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| ref: ${{ github.sha }} | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| artifact-ids: ${{ needs.build.outputs.artifact-id }} | |
| digest-mismatch: error | |
| github-token: ${{ github.token }} | |
| path: isolated-python-dist | |
| repository: ${{ github.repository }} | |
| run-id: ${{ needs.build.outputs.source-run-id }} | |
| - name: Validate the exact producer artifact before use | |
| env: | |
| ARTIFACT_DIRECTORY: isolated-python-dist | |
| EXPECTED_ARTIFACT_DIGEST: ${{ needs.build.outputs.artifact-digest }} | |
| EXPECTED_ARTIFACT_ID: ${{ needs.build.outputs.artifact-id }} | |
| EXPECTED_SOURCE_RUN_ATTEMPT: ${{ needs.build.outputs.source-run-attempt }} | |
| EXPECTED_SOURCE_RUN_ID: ${{ needs.build.outputs.source-run-id }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$EXPECTED_ARTIFACT_DIGEST" =~ ^[0-9a-f]{64}$ ]]; then | |
| printf 'producer artifact digest is not an exact SHA-256 digest\n' >&2 | |
| exit 1 | |
| fi | |
| for identity in "$EXPECTED_ARTIFACT_ID" "$EXPECTED_SOURCE_RUN_ID" "$EXPECTED_SOURCE_RUN_ATTEMPT"; do | |
| if [[ ! "$identity" =~ ^[1-9][0-9]*$ ]]; then | |
| printf 'producer artifact identity is invalid\n' >&2 | |
| exit 1 | |
| fi | |
| done | |
| if [[ ! "$ARTIFACT_DIRECTORY" =~ ^isolated-[a-z0-9][a-z0-9._-]*$ ]]; then | |
| printf 'artifact validation directory is unsafe\n' >&2 | |
| exit 1 | |
| fi | |
| mapfile -d '' entries < <( | |
| /usr/bin/find "$ARTIFACT_DIRECTORY" -mindepth 1 -maxdepth 1 -print0 | |
| ) | |
| if [ "${#entries[@]}" -ne 1 ] || [ ! -f "${entries[0]}" ] || [ -L "${entries[0]}" ]; then | |
| printf 'artifact handoff must contain exactly one regular file\n' >&2 | |
| exit 1 | |
| fi | |
| observed_digest="$(/usr/bin/sha256sum "${entries[0]}")" | |
| observed_digest="${observed_digest%% *}" | |
| if [ "$observed_digest" != "$EXPECTED_ARTIFACT_DIGEST" ]; then | |
| printf 'artifact digest mismatch: expected %s, got %s\n' \ | |
| "$EXPECTED_ARTIFACT_DIGEST" "$observed_digest" >&2 | |
| exit 1 | |
| fi | |
| printf 'validated artifact %s from run %s attempt %s\n' \ | |
| "$EXPECTED_ARTIFACT_ID" "$EXPECTED_SOURCE_RUN_ID" "$EXPECTED_SOURCE_RUN_ATTEMPT" | |
| - name: Extract the validated TestPyPI handoff | |
| run: | | |
| mkdir dist | |
| tar -xf isolated-python-dist/dist-handoff.tar -C dist | |
| - name: Publish to TestPyPI | |
| uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1 | |
| with: | |
| password: ${{ secrets.TEST_PYPI_TOKEN }} | |
| repository-url: https://test.pypi.org/legacy/ | |
| print-hash: true |