Repository navigation
Publish 2.2.0@fa91ca80880b30d4f8cf5b59ec73bf6b87cf0931 #170
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 }} | |
| 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 publication' | |
| required: false | |
| type: string | |
| default: '' | |
| 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 }} | |
| release_base_commit: ${{ steps.release_source.outputs.base_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)" | |
| base_commit="$(git rev-parse "$head_commit^")" | |
| 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" | |
| printf 'base_commit=%s\n' "$base_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: Compare built release metadata with the exact source commit | |
| env: | |
| RELEASE_SOURCE_SHA: ${{ steps.release_source.outputs.commit }} | |
| run: python scripts/check_release_metadata.py --source-ref "$RELEASE_SOURCE_SHA" --dist dist | |
| - 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: Verify exact PyPI JSON with an advisory rendered-page audit | |
| env: | |
| RELEASE_SOURCE_SHA: ${{ needs.build.outputs.release_commit }} | |
| RELEASE_VERSION: ${{ needs.build.outputs.release_tag }} | |
| run: >- | |
| python scripts/check_release_metadata.py | |
| --source-ref "$RELEASE_SOURCE_SHA" | |
| --pypi-version "$RELEASE_VERSION" | |
| --attempts 30 | |
| --interval-seconds 10 | |
| - name: Verify canonical PyPI project metadata and supported installs | |
| env: | |
| RELEASE_SOURCE_SHA: ${{ needs.build.outputs.release_commit }} | |
| run: >- | |
| python scripts/check_pypi_project_surface.py | |
| --source-ref "$RELEASE_SOURCE_SHA" | |
| --attempts 30 | |
| --interval-seconds 10 | |
| - 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 |