Publish 2.0.0-rc.38@76bc2c15f70d6c4a814815fcf13d9aefd01ab6b2 from direct #154
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 }} | |
| 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 | |
| deploy-and-audit-api-reference: | |
| needs: [build, publish] | |
| if: >- | |
| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || | |
| (github.event_name == 'workflow_dispatch' && | |
| github.ref == 'refs/heads/main' && inputs.publish) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: write | |
| contents: read | |
| steps: | |
| - name: Dispatch the main-context release docs workflow | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_PARENT_SHA: ${{ needs.build.outputs.release_base_commit }} | |
| RELEASE_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| RELEASE_RUN_ID: ${{ github.run_id }} | |
| RELEASE_SOURCE_SHA: ${{ needs.build.outputs.release_commit }} | |
| RELEASE_VERSION: ${{ needs.build.outputs.release_tag }} | |
| run: | | |
| set -euo pipefail | |
| expected_title="Deploy Python release docs ${RELEASE_VERSION}@${RELEASE_SOURCE_SHA} from release run ${RELEASE_RUN_ID}.${RELEASE_RUN_ATTEMPT}" | |
| existing_runs="$(gh run list \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --workflow docs.yml \ | |
| --event workflow_dispatch \ | |
| --branch main \ | |
| --limit 100 \ | |
| --json databaseId,displayTitle,headBranch)" | |
| if jq -e --arg title "$expected_title" \ | |
| 'any(.[]; .displayTitle == $title and .headBranch == "main")' \ | |
| <<<"$existing_runs" >/dev/null; then | |
| printf 'a release docs run already exists for this release run identity\n' >&2 | |
| exit 1 | |
| fi | |
| gh workflow run docs.yml \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --ref main \ | |
| -f release_version="$RELEASE_VERSION" \ | |
| -f release_source_sha="$RELEASE_SOURCE_SHA" \ | |
| -f release_parent_sha="$RELEASE_PARENT_SHA" \ | |
| -f release_run_id="$RELEASE_RUN_ID" \ | |
| -f release_run_attempt="$RELEASE_RUN_ATTEMPT" | |
| printf 'expected_title=%s\n' "$expected_title" >> "$GITHUB_ENV" | |
| - name: Wait for the exact docs deployment and audit | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_SOURCE_SHA: ${{ needs.build.outputs.release_commit }} | |
| RELEASE_VERSION: ${{ needs.build.outputs.release_tag }} | |
| run: | | |
| set -euo pipefail | |
| docs_run_id='' | |
| for attempt in {1..60}; do | |
| runs="$(gh run list \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --workflow docs.yml \ | |
| --event workflow_dispatch \ | |
| --branch main \ | |
| --limit 100 \ | |
| --json databaseId,displayTitle,headBranch)" | |
| docs_run_id="$(jq -r --arg title "$expected_title" \ | |
| '[.[] | select(.displayTitle == $title and .headBranch == "main")] | max_by(.databaseId) | .databaseId // empty' \ | |
| <<<"$runs")" | |
| if [ -n "$docs_run_id" ]; then | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| if [ -z "$docs_run_id" ]; then | |
| printf 'the authenticated main-context docs run was not created\n' >&2 | |
| exit 1 | |
| fi | |
| gh run watch "$docs_run_id" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --exit-status \ | |
| --interval 10 | |
| details="$(gh run view "$docs_run_id" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --json conclusion,displayTitle,event,headBranch,url,workflowName)" | |
| jq -e --arg title "$expected_title" ' | |
| .conclusion == "success" and | |
| .displayTitle == $title and | |
| .event == "workflow_dispatch" and | |
| .headBranch == "main" and | |
| .workflowName == "Docs deployment" | |
| ' <<<"$details" >/dev/null | |
| docs_url="$(jq -r '.url' <<<"$details")" | |
| { | |
| printf '## Python API reference publication verified\n\n' | |
| printf -- '- SDK release: `%s`\n' "$RELEASE_VERSION" | |
| printf -- '- Source revision: `%s`\n' "$RELEASE_SOURCE_SHA" | |
| printf -- '- Main-context deployment and audit: %s\n' "$docs_url" | |
| printf -- '- Release evidence: https://python.durable-workflow.com/release-audit.json\n' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| 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 |