name: CI

on:
  push:
    branches:
      - main
    tags:
      - '**'
  pull_request: {}

env:
  COLUMNS: 150
  UV_FROZEN: true
  FORCE_COLOR: 1

permissions:
  contents: read

jobs:
  lint:
    runs-on: ubuntu-latest
    name: Lint ${{ matrix.python-version }}
    strategy:
      fail-fast: false
      matrix:
        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        # Installing pip is required for the pre-commit action:
        run: |
          uv sync --all-packages --group linting --all-extras
          uv pip install pip
      - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
        with:
          extra_args: --all-files --verbose
        env:
          SKIP: no-commit-to-branch
          UV_NO_PROGRESS: '1'

  core-build-sdist:
    runs-on: ubuntu-latest
    name: core / Build sdist
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: '3.13'
      - uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1
        with:
          working-directory: pydantic-core
          command: sdist
          args: --out dist
          rust-toolchain: stable
      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: pydantic_core_pypi_files_sdist
          path: pydantic-core/dist

  core-build:
    name: core / Build on ${{ matrix.os }} (${{ matrix.target }} - ${{ matrix.interpreter || 'all' }}${{ matrix.os == 'linux' && format(' - {0}', matrix.manylinux == 'auto' && 'manylinux' || matrix.manylinux) || '' }})
    # only run on push to main, if a full build was requested or on release:
    if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'full build')
    strategy:
      fail-fast: false
      matrix:
        os: [linux, macos, windows]
        target: [undefined]
        manylinux: [auto]
        include:
          # manylinux for various platforms
          # x86_64 and aarch64 are PGO optimized below
          - os: linux
            manylinux: auto
            target: i686
          - os: linux
            manylinux: auto
            target: armv7
            interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 3.14t
          - os: linux
            manylinux: auto
            target: ppc64le
            interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 3.14t
          - os: linux
            manylinux: auto
            target: riscv64gc-unknown-linux-gnu
            interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 3.14t
          - os: linux
            manylinux: auto
            target: s390x
            interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 3.14t
            # manylinux pypy and graalpy on major architectures:
          - os: linux
            manylinux: auto
            target: x86_64
            interpreter: pypy3.11 graalpy3.11 graalpy3.12
          - os: linux
            manylinux: auto
            target: aarch64
            interpreter: graalpy3.11 graalpy3.12

          # musllinux
          - os: linux
            manylinux: musllinux_1_1
            target: x86_64
          - os: linux
            manylinux: musllinux_1_1
            target: aarch64
          - os: linux
            manylinux: musllinux_1_1
            target: armv7

          # macos;
          # all versions x86_64
          # arm pypy and older pythons which can't be run on the arm hardware for PGO
          - os: macos
            target: x86_64
            interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 3.14t pypy3.11 graalpy3.11 graalpy3.12
          - os: macos
            target: aarch64
            interpreter: 3.9 pypy3.11 graalpy3.11 graalpy3.12

          # windows;
          # x86_64 pypy builds are not PGO optimized
          # i686 not supported by pypy
          # windows 11 arm supports 3.11 and up, not PGO optimized for now
          - os: windows
            target: x86_64
            interpreter: pypy3.11
          - os: windows
            target: i686
            python-architecture: x86
            interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 3.14t
          - os: windows
            target: aarch64
            runs-on: windows-11-arm
            interpreter: 3.11 3.12 3.13 3.14 3.14t

        exclude:
          # was just a dummy variable to set a default value for target
          - target: undefined

    runs-on: ${{ matrix.runs-on || format('{0}-latest', (matrix.os == 'linux' && 'ubuntu') || matrix.os) }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: '3.13'
          architecture: ${{ matrix.python-architecture || 'x64' }}

      - run: pip install -U twine 'ruff==0.5.0' typing_extensions

      - name: Build wheels
        uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1
        with:
          working-directory: pydantic-core
          target: ${{ matrix.target }}
          manylinux: ${{ matrix.manylinux }}
          args: --release --out dist --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 3.14 3.14t pypy3.11' }}
          rust-toolchain: stable
          docker-options: -e CI

      - run: ${{ (matrix.os == 'windows' && 'dir') || 'ls -lh' }} dist/
        working-directory: pydantic-core

      - run: twine check --strict dist/*
        working-directory: pydantic-core

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: pydantic_core_pypi_files_${{ matrix.os }}_${{ matrix.target }}_${{ matrix.interpreter || 'all' }}_${{ matrix.manylinux }}
          path: pydantic-core/dist

  core-build-pgo:
    name: core / Build PGO-optimized on ${{ matrix.platform.os }} / ${{ matrix.interpreter }}
    # only run on push to main, if a full build was requested or on release:
    if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'full build')
    strategy:
      fail-fast: false
      matrix:
        platform:
          - os: linux
            runs-on: ubuntu-latest
          - os: linux_aarch64
            runs-on: ubuntu-24.04-arm
          - os: windows
            runs-on: windows-latest
          - os: macos
            runs-on: macos-latest
        interpreter: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t']
        exclude:
          # macos arm only supported from 3.10 and up
          - platform:
              os: macos
            interpreter: '3.9'

    runs-on: ${{ matrix.platform.runs-on }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: ${{ matrix.interpreter }}
          enable-cache: false

      - name: Install rust stable
        id: rust-toolchain
        # no release versioning, see https://github.com/dtolnay/rust-toolchain/issues/180
        uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
        with:
          components: llvm-tools
          toolchain: stable

      - name: Build PGO wheel
        id: pgo-wheel
        uses: ./.github/actions/core-build-pgo-wheel
        with:
          working-directory: pydantic-core
          interpreter: ${{ matrix.interpreter }}
          rust-toolchain: ${{ steps.rust-toolchain.outputs.name }}

      - run: ${{ (startsWith(matrix.platform.os, 'windows') && 'dir') || 'ls -lh' }} dist/
        working-directory: pydantic-core

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: pydantic_core_pypi_files_${{ matrix.platform.os }}_${{ matrix.interpreter }}
          path: pydantic-core/dist

  core-build-wasm-emscripten:
    name: core / Build WASM Emscripten
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Install rust nightly
        # no release versioning, see https://github.com/dtolnay/rust-toolchain/issues/180
        uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
        with:
          components: rust-src
          targets: wasm32-unknown-emscripten
          toolchain: nightly

      - name: Cache rust
        uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
        with:
          lookup-only: true

      - uses: mymindstorm/setup-emsdk@6ab9eb1bda2574c4ddb79809fc9247783eaf9021 # v14
        with:
          # NOTE!: as per https://github.com/pydantic/pydantic-core/pull/149 this version needs to match the version
          # in pydantic-core/node_modules/pyodide/pyodide-lock.json, to get the version, run:
          # `cat pydantic-core/node_modules/pyodide/pyodide-lock.json | jq .info.platform`
          version: '4.0.9'
          actions-cache-folder: emsdk-cache

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: '3.13'
          enable-cache: false

      - name: Install dependencies
        run: uv sync --all-packages --group testing-extra --group wasm --all-extras

      - name: Build wheels
        run: make -C pydantic-core build-wasm

      - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version: '18'
          package-manager-cache: false

      - run: npm install
        working-directory: pydantic-core

      - run: npm run test
        working-directory: pydantic-core

      - run: |
          ls -lh dist/
        working-directory: pydantic-core

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: pydantic_core_wasm_wheels
          path: pydantic-core/dist

  core-bench:
    name: core / Run Rust benchmarks
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Install rust nightly
        # no release versioning, see https://github.com/dtolnay/rust-toolchain/issues/180
        uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
        with:
          toolchain: nightly

      - name: Cache rust
        uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
        with:
          lookup-only: false # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: '3.13'

      - run: pip install typing_extensions

      - run: cargo bench
        working-directory: pydantic-core

  core-test-builds-arch:
    name: core / Test build on ${{ matrix.target }}-${{ matrix.distro }}
    needs: [core-build]
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        target: [armv7, s390x, ppc64le]
        distro: ['ubuntu22.04']
        include:
          - target: aarch64
            distro: alpine_latest

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Get dist artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          pattern: pydantic_core_pypi_files_linux_*
          merge-multiple: true
          path: dist

      - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1
        name: Install and test
        with:
          arch: ${{ matrix.target }}
          distro: ${{ matrix.distro }}
          githubToken: ${{ github.token }}
          install: |
            set -x
            if command -v apt-get &> /dev/null; then
              echo "installing python & pip with apt-get..."
              apt-get update
              apt-get install -y --no-install-recommends python3 python3-pip python3-venv git curl
            else
              echo "installing python & pip with apk..."
              apk update
              apk add python3 py3-pip git curl
            fi
          env: |
            UV_NO_PROGRESS: '1'
          # Until we move all dev dependencies to the main `pydantic` package, use this workaround:
          # Sync the pydantic-core workspace to get the dev dependencies (without the actual `pydantic-core` package),
          # and then sync the parent workspace:
          run: |
            set -x
            curl -LsSf https://astral.sh/uv/install.sh | sh
            source $HOME/.local/bin/env
            uv sync --directory pydantic-core --group testing-extra --no-install-package pydantic-core
            uv sync --group testing-extra --no-install-package pydantic-core --no-install-package pytest-memray --no-install-package memray --no-install-package pytest-codspeed --no-install-package cffi --inexact
            uv pip install pydantic-core --no-index --no-deps --find-links dist --force-reinstall
            uv run --no-sync pytest tests/pydantic_core --ignore=tests/pydantic_core/test_docstrings.py
            uv run --no-sync python -c 'import pydantic_core._pydantic_core; print(pydantic_core._pydantic_core.__version__)'
            uv run --no-sync python -c 'from pydantic_core._pydantic_core import build_info; print(f"{build_info=}")'

  core-test-builds-os:
    name: core / Test build on ${{ matrix.platform.os }}
    needs: [core-build, core-build-pgo]

    strategy:
      fail-fast: false
      matrix:
        platform:
          - runs-on: ubuntu-latest
            os: linux
          - runs-on: ubuntu-24.04-arm
            os: linuw_aarch64
          - runs-on: windows-latest
            os: windows
          - runs-on: windows-11-arm
            os: windows_aarch64
          - runs-on: macos-latest
            os: macos

    runs-on: ${{ matrix.platform.runs-on }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: install uv
        uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: get dist artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          pattern: pydantic_core_pypi_files_*
          merge-multiple: true
          path: dist

      # Until we move all dev dependencies to the main `pydantic` package, use this workaround:
      # Sync the pydantic-core workspace to get the dev dependencies (without the actual `pydantic-core` package),
      # and then sync the parent workspace:
      - run: |
          uv sync --directory pydantic-core --group testing-extra --no-install-package pydantic-core
          uv sync --group testing-extra --no-install-package pydantic-core --inexact
          uv pip install pydantic-core --no-index --no-deps --find-links dist --force-reinstall
          uv run --no-sync pytest tests/pydantic_core --ignore=tests/pydantic_core/test_docstrings.py
          uv run --no-sync python -c 'import pydantic_core._pydantic_core; print(pydantic_core._pydantic_core.__version__)'
          uv run --no-sync python -c 'from pydantic_core._pydantic_core import build_info; print(f"{build_info=}")'

  core-test-os:
    name: core / Test on ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu, macos, windows]

    runs-on: ${{ matrix.os }}-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Install rust stable
        # no release versioning, see https://github.com/dtolnay/rust-toolchain/issues/180
        uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
        with:
          toolchain: stable

      - name: Cache rust
        uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
        with:
          lookup-only: false # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        run: uv sync --all-packages --group testing-extra
        env:
          RUST_BACKTRACE: 1

      - run: uv pip freeze
      - run: uv run pytest tests/pydantic_core
      - run: cargo test
        working-directory: pydantic-core

  core-test-msrv:
    name: core / Test MSRV

    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Resolve MSRV
        id: resolve-msrv
        run: echo MSRV=`cargo metadata --format-version 1 --no-deps --frozen --quiet --offline --manifest-path pydantic-core/Cargo.toml | jq -r '.packages[0].rust_version'` >> $GITHUB_OUTPUT

      - name: Install rust MSRV
        # no release versioning, see https://github.com/dtolnay/rust-toolchain/issues/180
        uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
        with:
          toolchain: ${{ steps.resolve-msrv.outputs.MSRV }}

      - name: Cache rust
        uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
        with:
          lookup-only: false # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        run: uv sync --all-packages --all-extras --group testing-extra
        env:
          RUST_BACKTRACE: 1

      - run: uv pip freeze
      - run: uv run pytest tests/pydantic_core
      - run: cargo test
        working-directory: pydantic-core

  # test with a debug build as it picks up errors which optimised release builds do not
  core-test-debug:
    name: core / Test with debug build (${{ matrix.python-version }})
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        python-version:
          - '3.13'
          # Segmentation fault on pypy3.11.15 (latest), see https://github.com/pypy/pypy/issues/5400
          - 'pypy3.11.13'

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install rust stable
        # no release versioning, see https://github.com/dtolnay/rust-toolchain/issues/180
        uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
        with:
          toolchain: stable

      - name: Cache rust
        uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
        with:
          lookup-only: false # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        run: uv sync --all-packages --all-extras --group testing-extra

      - run: uv run --no-sync maturin develop --uv
        working-directory: pydantic-core

      - run: uv pip freeze
      - run: uv run pytest

  build-pydantic:
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    outputs:
      pydantic-version: ${{ steps.check-tag.outputs.VERSION }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          enable-cache: false

      - run: uv sync --only-group build

      - name: Check pydantic version
        id: check-tag
        uses: samuelcolvin/check-python-version@ee87cddb8049d2694cc03badc8569765a05cef00 # v5
        with:
          version_file_path: pydantic/version.py

      - name: Build pydantic library
        run: uv run --no-sync python -m build --installer uv

      - run: ls -lh dist/

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: pydantic_pypi_files
          path: dist

  docs-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: '3.12'
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        # Unlike the docs build, we don't use mkdocs_material-insiders
        # Because the secret for accessing the library is not accessible from forks, but we still want to run
        # this job on public CI runs.
        run: uv sync --all-packages --group docs

      - run: uv run python -c 'import docs.plugins.main'

      # Taken from docs-build.sh
      - name: prepare shortcuts for extra modules
        run: |
          ln -s pydantic-core/python/pydantic_core pydantic_core
          ln -s .venv/lib/python*/site-packages/pydantic_settings pydantic_settings
          ln -s .venv/lib/python*/site-packages/pydantic_extra_types pydantic_extra_types

      - run: PYTHONPATH="$PWD${PYTHONPATH:+:${PYTHONPATH}}" uv run mkdocs build

  test-memray:
    name: Test memray
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: '3.12'
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: install deps
        run: uv sync --all-packages --group testing-extra

      - name: Run tests
        run: uv run pytest --ignore=tests/mypy/ --ignore=tests/test_docs.py --memray

  test:
    name: Test ${{ matrix.os }} / ${{ matrix.python-version }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        # Segmentation fault on pypy3.11.15 (latest), see https://github.com/pypy/pypy/issues/5400
        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t', 'pypy3.11.13']
        include:
          # test macos intel just with latest python version
          - os: macos-15-intel
            python-version: '3.14'
          - os: macos-15-intel
            python-version: '3.14t'

    env:
      OS: ${{ matrix.os }}
      DEPS: yes
      UV_PYTHON_PREFERENCE: only-managed

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        run: uv sync --all-packages --group testing-extra --all-extras

      - run: 'uv run python -c "import pydantic.version; print(pydantic.version.version_info())"'

      - run: mkdir coverage

      - name: Run pytest
        run: make test NUM_THREADS=${{ endsWith(matrix.python-version, 't') && '4' || '1' }}
        env:
          COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
          CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-without-deps

      - name: Store coverage files
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: coverage-${{ matrix.os }}-${{ matrix.python-version }}
          path: coverage
          include-hidden-files: true

  test-plugin:
    name: Test Pydantic plugin
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: '3.12'
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        run: uv sync

      - name: Install example plugin
        run: uv pip install ./tests/plugin

      - run: uv run pytest tests/plugin
        env:
          TEST_PLUGIN: 1

  test-mypy:
    name: Mypy typechecking tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: '3.13'
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        run: uv sync --group typechecking --all-extras

      - run: mkdir coverage

      - name: Run mypy tests
        run: uv run coverage run -m pytest tests/mypy --test-mypy
        env:
          COVERAGE_FILE: coverage/.coverage.linux-mypy
          CONTEXT: linux-mypy

      - name: Store coverage files
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: coverage-mypy
          path: coverage
          include-hidden-files: true

  test-typechecking-integration:
    name: Typechecking integration tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: '3.12'
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        run: uv sync --group typechecking

      - name: Run typechecking integration tests (Pyright)
        run: make test-typechecking-pyright

      - name: Run typechecking integration tests (Mypy)
        run: make test-typechecking-mypy

      - name: Run typechecking integration tests (Pyrefly)
        run: make test-typechecking-pyrefly

  coverage-combine:
    needs: [test, test-mypy]
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: '3.12'

      - name: Get coverage files
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          merge-multiple: true
          pattern: coverage-*
          path: coverage

      - run: pip install coverage[toml]

      - run: ls -la coverage
      - run: coverage combine coverage
      - run: coverage report
      - run: coverage html --show-contexts --title "pydantic coverage for ${{ github.sha }}"

      - name: Store coverage data
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: coverage-data
          path: .coverage
          include-hidden-files: true

      - name: Store coverage HTML
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: coverage-html
          path: htmlcov

  coverage-pr-comment:
    needs: coverage-combine
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    permissions:
      pull-requests: write
      contents: write

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Download coverage data
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: coverage-data

      - name: Generate coverage comment
        id: coverage-comment
        uses: py-cov-action/python-coverage-comment-action@7188638f871f721a365d644f505d1ff3df20d683 # v3.40
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Store coverage comment
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        if: steps.coverage-comment.outputs.COMMENT_FILE_WRITTEN == 'true'
        with:
          name: python-coverage-comment-action
          path: python-coverage-comment-action.txt

  test-typing-extensions:
    name: Test typing-extensions (`main` branch) on Python ${{ matrix.python-version }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions

      - name: Install dependencies
        run: uv sync --all-packages --group testing-extra --all-extras

      - name: Install typing-extensions
        run: uv pip install 'typing-extensions @ git+https://github.com/python/typing_extensions.git'

      - name: Run tests
        run: make test

  # https://github.com/marketplace/actions/alls-green
  check: # This job does nothing and is only used for the branch protection
    if: always()
    outputs:
      result: ${{ steps.all-green.outputs.result }}
    needs: [lint, docs-build, test, test-memray, test-mypy, test-plugin, core-test-os, core-test-debug, core-build-wasm-emscripten]
    runs-on: ubuntu-latest

    steps:
      - name: Decide whether the needed jobs succeeded or failed
        uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
        id: all-green
        with:
          jobs: ${{ toJSON(needs) }}

  release-pydantic-core:
    needs: [core-test-builds-arch, core-test-builds-os, core-build-sdist, check]
    if: needs.check.outputs.result == 'success' && startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    environment: release

    permissions:
      id-token: write

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          enable-cache: false

      - run: uv sync --only-group build

      - name: Extract pydantic-core version
        id: core-version
        run: |
          VERSION=$(cargo metadata --format-version 1 --no-deps --frozen --quiet --offline --manifest-path pydantic-core/Cargo.toml | jq -r '.packages[0].version')
          echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
          echo "pydantic-core version: $VERSION"

      - name: Check if pydantic-core version is already on PyPI
        id: check-pypi
        run: |
          STATUS=$(curl -s -o /dev/null -I -w "%{http_code}" https://pypi.org/pypi/pydantic-core/${STEPS_CORE_VERSION_OUTPUTS_VERSION}/json)
          if [ "$STATUS" = "200" ]; then
            echo "already_published=true" >> "$GITHUB_OUTPUT"
            echo "pydantic-core ${STEPS_CORE_VERSION_OUTPUTS_VERSION} is already on PyPI, skipping publish"
          else
            echo "already_published=false" >> "$GITHUB_OUTPUT"
            echo "pydantic-core ${STEPS_CORE_VERSION_OUTPUTS_VERSION} not found on PyPI, will publish"
          fi
        env:
          STEPS_CORE_VERSION_OUTPUTS_VERSION: ${{ steps.core-version.outputs.VERSION }}

      - name: Get pydantic-core dist artifacts
        if: steps.check-pypi.outputs.already_published != 'true'
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          pattern: pydantic_core_pypi_files_*
          merge-multiple: true
          path: pydantic-core-dist

      - name: Test pydantic-core wheels integrity
        if: steps.check-pypi.outputs.already_published != 'true'
        run: |
          for whl in pydantic-core-dist/*.whl; do unzip -qt "$whl"; done
          uv run --no-sync twine check --strict pydantic-core-dist/*

      - name: Publish pydantic-core to PyPI
        if: steps.check-pypi.outputs.already_published != 'true'
        run: uv publish --trusted-publishing always pydantic-core-dist/*

  release-pydantic:
    needs: [build-pydantic, release-pydantic-core]
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    environment: release

    permissions:
      id-token: write
      contents: write

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
        with:
          enable-cache: false

      - run: uv sync --only-group build

      - name: Get pydantic dist artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: pydantic_pypi_files
          path: pydantic-dist

      - name: Test pydantic artifacts integrity
        run: |
          for whl in pydantic-dist/*.whl; do unzip -qt "$whl"; done
          uv run --no-sync twine check --strict pydantic-dist/*

      - name: Upload package to PyPI
        uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
        with:
          packages-dir: pydantic-dist/

  send-tweet:
    name: Send tweet
    needs: [build-pydantic, release-pydantic]
    if: needs.release-pydantic.result == 'success'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: '3.12'
      - name: Install dependencies
        run: pip install tweepy==4.14.0
      - name: Send tweet
        shell: python
        run: |
          import os
          import tweepy

          client = tweepy.Client(
              access_token=os.getenv("TWITTER_ACCESS_TOKEN"),
              access_token_secret=os.getenv("TWITTER_ACCESS_TOKEN_SECRET"),
              consumer_key=os.getenv("TWITTER_CONSUMER_KEY"),
              consumer_secret=os.getenv("TWITTER_CONSUMER_SECRET"),
          )
          version = os.getenv("VERSION").strip('"')

          if "b" in version:
              official_version = version[:version.index("b")]
              tweet = os.getenv("BETA_TWEET").format(version=version, official_version=official_version)
          else:
              tweet = os.getenv("TWEET").format(version=version)

          client.create_tweet(text=tweet)
        env:
          VERSION: ${{ needs.build-pydantic.outputs.pydantic-version }}
          TWEET: |
            Pydantic version {version} is out! 🎉

            https://github.com/pydantic/pydantic/releases/tag/v{version}
          BETA_TWEET: |
            Pydantic beta version {version} is out! 🚀

            Please try v{version} in the next week before we release v{official_version},
            and let us know if you encounter any issues!

            https://github.com/pydantic/pydantic/releases/tag/v{version}
          TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
          TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
          TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
          TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
