From a5f5d8cf355d93a6e99e957facc7fbed82536c53 Mon Sep 17 00:00:00 2001 From: Dylan Bargatze Date: Thu, 16 Apr 2026 13:24:11 -0400 Subject: [PATCH] .github: initial Python build/publish support Adds GitHub workflow to build Python 3.12 wheels for Linux (x86_64/ARM64) and macOS (ARM64), perform a publish dry run, then actually publish them to PyPI for tagged releases. GitHub repository has Environments configured for PyPI ("pypi") and Test PyPI ("testpypi"), and both PyPI and Test PyPI projects for `tailscale-py` have Trusted Publishers set up to allow tokenless publication of wheels from the workflow. The GitHub environments require approval from a team member prior to running any jobs and will only allow jobs to run on tags. Additionally, the "publish" job in the workflow is gated on release tags or "workflow_dispatch" only. Signed-off-by: Dylan Bargatze --- .github/workflows/ci.yml | 2 + .github/workflows/python.yml | 126 +++++++++++++++++++++++++++++++++++ ts_python/pyproject.toml | 12 ++++ 3 files changed, 140 insertions(+) create mode 100644 .github/workflows/python.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b819bd2..fc3c3104 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,8 @@ on: push: branches: - main + tags: + - 'v*' pull_request: schedule: # Nightly run; execute daily at 06:37 AM UTC on main (default branch). diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 00000000..a96f97fa --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,126 @@ +name: python + +on: + push: + branches: + - main + tags: + - 'v*' + pull_request: + workflow_dispatch: + inputs: + python_environment: + default: 'pypi' + description: 'The GitHub environment to use for publishing, as well as the name + of the Python package index to publish to. Value must match both the name of a + GitHub environment and the name of a [[tool.uv.index]] entry in + ts_python/pyproject.toml.' + required: true + type: choice + options: + - 'pypi' + - 'testpypi' + +permissions: + contents: read + +env: + # Cache-busting key -- change it if the build changes in a way that invalidates old + # cached state. + cache_key: python-ci + # Is this a tagged release build? + is_tag_push: ${{ startsWith(github.ref, 'refs/tags/') }} + # The GitHub environment to use for the "publish" job. Use the workflow_dispatch input + # if present, 'pypi' if this is a tagged release build; otherwise, fall back to + # 'testpypi'. + python_environment: &python_environment ${{ case(inputs.python_environment != '', inputs.python_environment, startsWith(github.ref, 'refs/tags/'), 'pypi', 'testpypi') }} + # The Python package index to publish to. Identical to "python_environment", separated + # for clarity. + python_index: *python_environment + # The Python ABI to build wheels for. Serves as a "minimum supported CPython version". + python_version: 3.12 + # The Rust toolchain version to build the wheels with. Should be latest supported + # version (MSRV + 1). + rust_toolchain: 1.94.0 + +jobs: + build_test: + name: test (${{ matrix.platform.os }}, ${{ matrix.platform.target }}) + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - os: linux + runner: linux-arm64-16cpu + target: aarch64 + - os: linux + runner: linux-x86_64-16cpu + target: x86_64 + - os: macOS + runner: macos-26 + target: aarch64 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Rust cache + id: cache-cargo + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ~/.rustup/toolchains + target/ + key: ${{ matrix.platform.os }}-python-${{ env.cache_key }}-${{ env.rust_toolchain }}-${{ matrix.platform.target }}-${{ hashFiles('**/Cargo.lock') }} + - name: Install python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ env.python_version }} + - name: Build wheels + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 + with: + working-directory: ts_python + rust-toolchain: ${{ env.rust_toolchain }} + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !env.is_tag_push }} + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: wheels-${{ matrix.platform.os }}-${{ matrix.platform.target }} + path: ts_python/dist + + publish: + runs-on: ubuntu-latest + needs: build_test + environment: *python_environment + permissions: + # Use to sign the release artifacts + id-token: write + # Used to upload release artifacts + contents: write + # Used to generate artifact attestation + attestations: write + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Download built wheels + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: ts_python + - name: Generate artifact attestation + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-path: 'ts_python/wheels-*/*' + - name: Install uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + working-directory: ts_python + - name: (Dry Run) Publish to ${{ env.python_index }} + run: uv publish --dry-run --directory ts_python --index ${{ env.python_index }} 'wheels-*/*' + - name: Publish to ${{ env.python_index }} + if: ${{ env.is_tag_push || github.event_name == 'workflow_dispatch' }} + run: uv publish --directory ts_python --index ${{ env.python_index }} 'wheels-*/*' diff --git a/ts_python/pyproject.toml b/ts_python/pyproject.toml index 9f84d24f..1fa01790 100644 --- a/ts_python/pyproject.toml +++ b/ts_python/pyproject.toml @@ -36,3 +36,15 @@ build-backend = "maturin" [tool.maturin] python-source = "python" module-name = "tailscale._internal" + +[[tool.uv.index]] +name = "pypi" +url = "https://pypi.org/simple/" +publish-url = "https://pypi.org/legacy/" +explicit = true + +[[tool.uv.index]] +name = "testpypi" +url = "https://test.pypi.org/simple/" +publish-url = "https://test.pypi.org/legacy/" +explicit = true \ No newline at end of file