rustwright-core 0.1.1

Rust CDP core for a Python Playwright-compatible automation API
name: Release Python package

on:
  push:
    tags:
      - "v*"
  workflow_dispatch:
    inputs:
      dry_run:
        description: "Build and upload artifacts without publishing"
        required: true
        type: boolean
        default: true

permissions:
  contents: read

concurrency:
  group: release-pypi-${{ github.ref }}
  cancel-in-progress: false

jobs:
  metadata:
    name: validate package versions
    runs-on: ubuntu-latest

    steps:
      - name: Check out repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

      - name: Set up Python
        uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
        with:
          python-version: "3.12"

      - name: Validate release metadata
        shell: bash
        run: |
          set -euo pipefail
          python - <<'PY'
          import json
          import os
          import pathlib
          import tomllib

          root = pathlib.Path(".")
          cargo_lock = tomllib.loads((root / "Cargo.lock").read_text())
          cargo_versions = {
              package["name"]: package["version"]
              for package in cargo_lock["package"]
              if package["name"] in {"rustwright-core", "rustwright-node"}
          }
          node_lock = json.loads((root / "node/package-lock.json").read_text())
          versions = {
              "pyproject.toml": tomllib.loads((root / "pyproject.toml").read_text())["project"]["version"],
              "Cargo.toml": tomllib.loads((root / "Cargo.toml").read_text())["package"]["version"],
              "node/Cargo.toml": tomllib.loads((root / "node/Cargo.toml").read_text())["package"]["version"],
              "node/package.json": json.loads((root / "node/package.json").read_text())["version"],
              "Cargo.lock rustwright-core": cargo_versions["rustwright-core"],
              "Cargo.lock rustwright-node": cargo_versions["rustwright-node"],
              "node/package-lock.json": node_lock["version"],
              'node/package-lock.json packages[""]': node_lock["packages"][""]["version"],
          }

          expected = next(iter(versions.values()))
          if any(version != expected for version in versions.values()):
              raise SystemExit(f"Package versions do not match: {versions}")

          # Match the npm workflow, which rejects stable versions: a single tag
          # must publish to both registries or neither. This alpha project ships
          # prereleases only (e.g. 0.2.0-alpha.1), so reject a stable version here
          # too rather than let PyPI publish something npm would refuse.
          if "-" not in expected:
              raise SystemExit(
                  f"Refusing to publish stable version {expected!r}: this project "
                  f"publishes prereleases only (e.g. 0.2.0-alpha.1), matching release-npm.yml."
              )

          if os.environ["GITHUB_REF_TYPE"] == "tag":
              tag_version = os.environ["GITHUB_REF_NAME"].removeprefix("v")
              if tag_version != expected:
                  raise SystemExit(
                      f"Tag version {tag_version!r} does not match package version {expected!r}"
                  )
          print(f"Validated package version {expected}")
          PY

  wheels:
    name: wheel - ${{ matrix.platform.target }}
    needs: metadata
    runs-on: ${{ matrix.platform.runner }}
    strategy:
      fail-fast: false
      matrix:
        platform:
          - runner: macos-latest
            target: aarch64-apple-darwin
            manylinux: auto
            extra_args: ""
          - runner: macos-latest
            target: x86_64-apple-darwin
            manylinux: auto
            extra_args: ""
          - runner: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            manylinux: "2_17"
            extra_args: ""
          - runner: ubuntu-latest
            target: aarch64-unknown-linux-gnu
            manylinux: "2_17"
            # ring's pregenerated aarch64 assembly fails under the manylinux
            # cross-gcc ("ARM assembler must define __ARM_ARCH"); build with zig.
            extra_args: "--zig"
          - runner: windows-latest
            target: x86_64-pc-windows-msvc
            manylinux: auto
            extra_args: ""

    steps:
      - name: Check out repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

      - name: Set up Python
        uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
        with:
          python-version: "3.12"

      - name: Build abi3 wheel
        uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
        with:
          command: build
          maturin-version: v1.11.5
          target: ${{ matrix.platform.target }}
          manylinux: ${{ matrix.platform.manylinux }}
          rust-toolchain: stable
          args: --release --locked --compatibility pypi --out dist ${{ matrix.platform.extra_args }}

      - name: Upload wheel
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: pypi-wheel-${{ matrix.platform.target }}
          path: dist/*.whl
          if-no-files-found: error

  sdist:
    name: source distribution
    needs: metadata
    runs-on: ubuntu-latest

    steps:
      - name: Check out repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

      - name: Build source distribution
        uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
        with:
          command: sdist
          maturin-version: v1.11.5
          rust-toolchain: stable
          args: --out dist

      - name: Upload source distribution
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: pypi-sdist
          path: dist/*.tar.gz
          if-no-files-found: error

  publish:
    name: publish to PyPI
    needs:
      - wheels
      - sdist
    if: >-
      ${{
        github.repository == 'Skyvern-AI/rustwright' &&
        github.ref_type == 'tag' &&
        (
          github.event_name == 'push' ||
          (github.event_name == 'workflow_dispatch' && !inputs.dry_run)
        )
      }}
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/project/rustwright/
    permissions:
      contents: read
      id-token: write

    steps:
      - name: Download distributions
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          pattern: pypi-*
          path: dist
          merge-multiple: true

      - name: Publish with PyPI Trusted Publishing
        uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1