diff --git a/.github/workflows/speakeasy_sdk_generation.yml b/.github/workflows/speakeasy_sdk_generation.yml index 5803f1d1..e2c8dc3b 100644 --- a/.github/workflows/speakeasy_sdk_generation.yml +++ b/.github/workflows/speakeasy_sdk_generation.yml @@ -22,5 +22,4 @@ jobs: speakeasy_version: latest secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} - pypi_token: ${{ secrets.PYPI_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} diff --git a/.github/workflows/speakeasy_sdk_publish.yaml b/.github/workflows/speakeasy_sdk_publish.yaml index cdfbfa8e..6591fc0e 100644 --- a/.github/workflows/speakeasy_sdk_publish.yaml +++ b/.github/workflows/speakeasy_sdk_publish.yaml @@ -1,20 +1,78 @@ -name: Publish +name: PyPI Release + +on: + release: + types: + - published + permissions: - checks: write - contents: write - pull-requests: write - statuses: write -"on": - push: - branches: - - main - paths: - - RELEASES.md - - '*/RELEASES.md' + contents: read + +concurrency: + group: release + cancel-in-progress: false + +env: + PYTHON_VERSION: "3.13" + jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v5 + + - uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + env: + UV_LOCKED: "1" + UV_PYTHON: ${{ env.PYTHON_VERSION }} + run: make install + + - name: Validate version matches release tag + env: + TAG: ${{ github.event.release.tag_name }} + run: | + PKG_VERSION=$(PYTHONPATH=src uv run python - <<'PYEOF' + from unstructured_client._version import __version__ + print(__version__) + PYEOF + ) + if [[ "$TAG" != "$PKG_VERSION" && "$TAG" != "v$PKG_VERSION" ]]; then + echo "Tag '$TAG' does not match package version '$PKG_VERSION'" + exit 1 + fi + + - name: Build artifacts + run: uv build --out-dir dist --clear + + - name: Upload dist artifacts + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + publish: - uses: speakeasy-api/sdk-generation-action/.github/workflows/sdk-publish.yaml@v15 - secrets: - github_access_token: ${{ secrets.GITHUB_TOKEN }} - pypi_token: ${{ secrets.PYPI_TOKEN }} - speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Download dist artifacts + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish package + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index d9cd3c31..6b5f3d76 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -32,9 +32,6 @@ workflow: unstructured-python: target: python source: my-source - publish: - pypi: - token: $PYPI_TOKEN codeSamples: output: codeSamples.yaml registry: diff --git a/.speakeasy/workflow.yaml b/.speakeasy/workflow.yaml index c3873f58..9f359913 100644 --- a/.speakeasy/workflow.yaml +++ b/.speakeasy/workflow.yaml @@ -13,9 +13,6 @@ targets: unstructured-python: target: python source: my-source - publish: - pypi: - token: $PYPI_TOKEN codeSamples: output: codeSamples.yaml registry: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b9e40dc..e2ed8167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.43.2 + +### Enhancements +* Switch PyPI publishing to GitHub trusted publishing so releases can publish via OIDC without a long-lived `PYPI_TOKEN` secret. + +### Features + +### Fixes +* Align release automation, package metadata, and generator config on `0.43.2` for the trusted-publishing release flow. + ## 0.43.1 ### Enhancements diff --git a/_test_unstructured_client/unit/test_regeneration_guards.py b/_test_unstructured_client/unit/test_regeneration_guards.py index 58dede6c..1286057b 100644 --- a/_test_unstructured_client/unit/test_regeneration_guards.py +++ b/_test_unstructured_client/unit/test_regeneration_guards.py @@ -1,3 +1,4 @@ +import re from pathlib import Path import tomllib @@ -37,7 +38,34 @@ def test_publish_script_is_hardened(): assert "set -euo pipefail" in publish_script assert "sys.version_info < (3, 11)" in publish_script - assert 'uv publish --token "${PYPI_TOKEN}" --check-url https://pypi.org/simple' in publish_script + assert "uv build --out-dir dist --clear" in publish_script + + +def test_release_workflow_uses_trusted_publishing(): + workflow = (REPO_ROOT / ".github" / "workflows" / "speakeasy_sdk_publish.yaml").read_text() + + assert "release:" in workflow + assert "pypa/gh-action-pypi-publish" in workflow + assert "PYPI_TOKEN" not in workflow + assert "upload-artifact" in workflow + assert "download-artifact" in workflow + assert re.search(r"publish:\n\s+needs: build", workflow) + assert re.search(r"publish:\n(?:.*\n)*?\s+permissions:\n\s+contents: read\n\s+id-token: write", workflow) + + +def test_release_workflow_keeps_oidc_out_of_build_job(): + workflow = (REPO_ROOT / ".github" / "workflows" / "speakeasy_sdk_publish.yaml").read_text() + + build_job = workflow.split("\n publish:\n", maxsplit=1)[0] + + assert "id-token: write" not in build_job + + +def test_speakeasy_workflow_does_not_manage_pypi_publishing(): + workflow = (REPO_ROOT / ".speakeasy" / "workflow.yaml").read_text() + + assert "publish:" not in workflow + assert "PYPI_TOKEN" not in workflow def test_makefile_installs_with_locked_uv_sync(): diff --git a/gen.yaml b/gen.yaml index 2f03dd05..966a3a8d 100644 --- a/gen.yaml +++ b/gen.yaml @@ -27,7 +27,7 @@ generation: generateNewTests: false skipResponseBodyAssertions: false python: - version: 0.43.1 + version: 0.43.2 additionalDependencies: dev: deepdiff: '>=9.0.0' diff --git a/src/unstructured_client/_version.py b/src/unstructured_client/_version.py index 92998fc8..5bfb2929 100644 --- a/src/unstructured_client/_version.py +++ b/src/unstructured_client/_version.py @@ -3,10 +3,10 @@ import importlib.metadata __title__: str = "unstructured-client" -__version__: str = "0.43.1" +__version__: str = "0.43.2" __openapi_doc_version__: str = "1.2.31" __gen_version__: str = "2.680.0" -__user_agent__: str = "speakeasy-sdk/python 0.43.1 2.680.0 1.2.31 unstructured-client" +__user_agent__: str = "speakeasy-sdk/python 0.43.2 2.680.0 1.2.31 unstructured-client" try: if __package__ is not None: