name: CI

permissions: {}

on:
  push:
    branches-ignore:
      # these branches always have another event associated
      - gh-readonly-queue/**        # GitHub's merge queue uses `merge_group`
      - autodeps/**                 # autodeps always makes a PR
      - pre-commit-ci-update-config # pre-commit.ci's updates always have a PR
  pull_request:
  merge_group:

concurrency:
  group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && format('-{0}', github.sha) || '' }}
  cancel-in-progress: true

env:
  dists-artifact-name: python-package-distributions
  dist-name: trio

jobs:
  build:
    name: 👷 dists

    runs-on: ubuntu-latest

    outputs:
      dist-version: ${{ steps.dist-version.outputs.version }}
      sdist-artifact-name: ${{ steps.artifact-name.outputs.sdist }}
      wheel-artifact-name: ${{ steps.artifact-name.outputs.wheel }}

    steps:
      - name: Switch to using Python 3.11
        uses: actions/setup-python@v6
        with:
          python-version: 3.11

      - name: Grab the source from Git
        uses: actions/checkout@v5
        with:
          persist-credentials: false

      - name: Get the dist version
        id: dist-version
        run: >-
          echo "version=$(
          grep ^__version__ src/trio/_version.py
          | sed 's#__version__ = "\([^"]\+\)"#\1#'
          )"
          >> "${GITHUB_OUTPUT}"

      - name: Set the expected dist artifact names
        id: artifact-name
        run: |
          echo "sdist=${DIST_NAME}-${VERSION}.tar.gz" >> "${GITHUB_OUTPUT}"
          echo "wheel=${DIST_NAME}-${VERSION}-py3-none-any.whl" >> "${GITHUB_OUTPUT}"
        env:
          DIST_NAME: ${{ env.dist-name }}
          VERSION: ${{ steps.dist-version.outputs.version }}

      - name: Install build
        run: python -Im pip install build

      - name: Build dists
        run: python -Im build
      - name: Verify that the artifacts with expected names got created
        run: >-
          ls -1
          dist/${SDIST}
          dist/${WHEEL}
        env:
          SDIST: ${{ steps.artifact-name.outputs.sdist }}
          WHEEL: ${{ steps.artifact-name.outputs.wheel }}
      - name: Store the distribution packages
        uses: actions/upload-artifact@v5
        with:
          name: ${{ env.dists-artifact-name }}
          # NOTE: Exact expected file names are specified here
          # NOTE: as a safety measure — if anything weird ends
          # NOTE: up being in this dir or not all dists will be
          # NOTE: produced, this will fail the workflow.
          path: |
            dist/${{ steps.artifact-name.outputs.sdist }}
            dist/${{ steps.artifact-name.outputs.wheel }}
          retention-days: 5

      - name: >-
          Smoke-test:
          retrieve the project source from an sdist inside the GHA artifact
        uses: re-actors/checkout-python-sdist@release/v2
        with:
          source-tarball-name: ${{ steps.artifact-name.outputs.sdist }}
          workflow-artifact-name: ${{ env.dists-artifact-name }}

      - name: >-
          Smoke-test: move the sdist-retrieved dir into sdist-src
        run: |
          mv -v '${{ github.workspace }}' '${{ runner.temp }}/sdist-src'
          mkdir -pv '${{ github.workspace }}'
          mv -v '${{ runner.temp }}/sdist-src' '${{ github.workspace }}/sdist-src'
        shell: bash -eEuo pipefail {0}

      - name: >-
          Smoke-test: grab the source from Git into git-src
        uses: actions/checkout@v5
        with:
          path: git-src
          persist-credentials: false

      - name: >-
          Smoke-test: install test requirements from the Git repo
        run: >-
          python -Im
          pip install -c test-requirements.txt -r test-requirements.txt
        shell: bash -eEuo pipefail {0}
        working-directory: git-src

      - name: >-
          Smoke-test: collect tests from the Git repo
        env:
          PYTHONPATH: src/
        run: >-
          pytest --collect-only -qq .
          | sort
          | tee collected-tests
        shell: bash -eEuo pipefail {0}
        working-directory: git-src

      - name: >-
          Smoke-test: collect tests from the sdist tarball
        env:
          PYTHONPATH: src/
        run: >-
          pytest --collect-only -qq .
          | sort
          | tee collected-tests
        shell: bash -eEuo pipefail {0}
        working-directory: sdist-src

      - name: >-
          Smoke-test:
          verify that all the tests from Git are included in the sdist
        run: diff --unified sdist-src/collected-tests git-src/collected-tests
        shell: bash -eEuo pipefail {0}

  Windows:
    name: 'Windows (${{ matrix.python }}, ${{ matrix.arch }}${{ matrix.extra_name }})'
    needs:
      - build

    timeout-minutes: 20
    runs-on: 'windows-latest'
    strategy:
      fail-fast: false
      matrix:
        python: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.14t']
        arch: ['x86', 'x64']
        lsp: ['']
        lsp_extract_file: ['']
        extra_name: ['']
        include:
          - python: '3.10'
            arch: 'x64'
            lsp: 'https://raw.githubusercontent.com/python-trio/trio-ci-assets/master/komodia-based-vpn-setup.zip'
            lsp_extract_file: 'komodia-based-vpn-setup.exe'
            extra_name: ', with Komodia LSP'
          - python: '3.10'
            arch: 'x64'
            lsp: 'https://www.proxifier.com/download/legacy/ProxifierSetup342.exe'
            lsp_extract_file: ''
            extra_name: ', with IFS LSP'
          - python: 'pypy-3.11'
            arch: 'x64'
            lsp: ''
            lsp_extract_file: ''
            extra_name: ''
          #- python: '3.10'
          #  arch: 'x64'
          #  lsp: 'http://download.pctools.com/mirror/updates/9.0.0.2308-SDavfree-lite_en.exe'
          #  lsp_extract_file: ''
          #  extra_name: ', with non-IFS LSP'

    continue-on-error: >-
      ${{
        (
          endsWith(matrix.python, '-dev')
          || endsWith(matrix.python, '-nightly')
        )
        && true
        || false
      }}
    steps:
      - name: Retrieve the project source from an sdist inside the GHA artifact
        uses: re-actors/checkout-python-sdist@release/v2
        with:
          source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }}
          workflow-artifact-name: ${{ env.dists-artifact-name }}
      - name: Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: '${{ matrix.python }}'
          architecture: '${{ matrix.arch }}'
          allow-prereleases: true
      - name: Run tests
        run: ./ci.sh
        shell: bash
        env:
          LSP: '${{ matrix.lsp }}'
          LSP_EXTRACT_FILE: '${{ matrix.lsp_extract_file }}'
      - if: always()
        uses: codecov/codecov-action@v5
        with:
          name: Windows (${{ matrix.python }}, ${{ matrix.arch }}${{ matrix.extra_name }})
          # multiple flags is marked as an error in codecov UI, but is actually fine
          # https://github.com/codecov/feedback/issues/567
          flags: Windows,${{ matrix.python }}
          fail_ci_if_error: true

  Ubuntu:
    name: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})'
    needs:
      - build

    timeout-minutes: 10
    runs-on: 'ubuntu-latest'
    strategy:
      fail-fast: false
      matrix:
        python: ['pypy-3.11', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t']
        check_formatting: ['0']
        no_test_requirements: ['0']
        extra_name: ['']
        include:
          - python: '3.13'
            check_formatting: '1'
            extra_name: ', check formatting'
          # separate test run that doesn't install test-requirements.txt
          - python: '3.10'
            no_test_requirements: '1'
            extra_name: ', no test-requirements'
    continue-on-error: >-
      ${{
        (
          endsWith(matrix.python, '-dev')
          || endsWith(matrix.python, '-nightly')
        )
        && true
        || false
      }}
    steps:
      - name: Retrieve the project source from an sdist inside the GHA artifact
        if: matrix.check_formatting != '1'
        uses: re-actors/checkout-python-sdist@release/v2
        with:
          source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }}
          workflow-artifact-name: ${{ env.dists-artifact-name }}
      - name: Grab the source from Git
        if: matrix.check_formatting == '1'
        uses: actions/checkout@v4
        with:
          persist-credentials: false
      - name: Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: '${{ matrix.python }}'
          cache: pip
          cache-dependency-path: test-requirements.txt
          allow-prereleases: true
      - name: Check Formatting
        if: matrix.check_formatting == '1'
        run:
          python -m pip install tox &&
          tox -m check
      - name: Install python3-apport
        run: |
          sudo apt update
          sudo apt install -q python3-apport
      - name: Run tests
        if: matrix.check_formatting == '0'
        run: ./ci.sh
        env:
          NO_TEST_REQUIREMENTS: '${{ matrix.no_test_requirements }}'
      - if: >-
          always()
          && matrix.check_formatting != '1'
        uses: codecov/codecov-action@v5
        with:
          name: Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})
          flags: Ubuntu,${{ matrix.python }}
          fail_ci_if_error: true

  macOS:
    name: 'macOS (${{ matrix.python }})'
    needs:
      - build

    timeout-minutes: 15
    runs-on: 'macos-latest'
    strategy:
      fail-fast: false
      matrix:
        python: ['pypy-3.11', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t']
    continue-on-error: >-
      ${{
        (
          endsWith(matrix.python, '-dev')
          || endsWith(matrix.python, '-nightly')
        )
        && true
        || false
      }}
    steps:
      - name: Retrieve the project source from an sdist inside the GHA artifact
        uses: re-actors/checkout-python-sdist@release/v2
        with:
          source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }}
          workflow-artifact-name: ${{ env.dists-artifact-name }}
      - name: Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: '${{ matrix.python }}'
          cache: pip
          cache-dependency-path: test-requirements.txt
          allow-prereleases: true
      - name: Run tests
        run: ./ci.sh
      - if: always()
        uses: codecov/codecov-action@v5
        with:
          name: macOS (${{ matrix.python }})
          flags: macOS,${{ matrix.python }}
          fail_ci_if_error: true

  # run CI on a musl linux
  Alpine:
    name: "Alpine"
    needs:
      - build

    runs-on: ubuntu-latest
    container: alpine
    steps:
      - name: Install necessary packages
        # can't use setup-python because that python doesn't seem to work;
        # `python3-dev` (rather than `python:alpine`) for some ctypes reason,
        # `curl`, `gpg`, `git` for codecov-action v4/v5 to work (https://github.com/codecov/codecov-action/issues/1320).
        # `nodejs` for pyright (`node-env` pulls in nodejs but that takes a while and can time out the test).
        # `perl` for a platform independent `sed -i` alternative
        run: apk update && apk add python3-dev bash curl gpg git nodejs perl

      - name: Retrieve the project source from an sdist inside the GHA artifact
        # must be after `apk add` because it relies on `bash` existing
        uses: re-actors/checkout-python-sdist@release/v2
        with:
          source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }}
          workflow-artifact-name: ${{ env.dists-artifact-name }}

      - name: Enter virtual environment
        run: python -m venv .venv

      - name: Run tests
        run: source .venv/bin/activate && ./ci.sh

      - name: Get Python version for codecov flag
        id: get-version
        shell: python
        run: |
          import sys, os
          with open(os.environ["GITHUB_OUTPUT"], "a") as f:
            f.write("version=" + ".".join(map(str, sys.version_info[:2])))
            f.write("\n")

      - if: always()
        uses: codecov/codecov-action@v5
        with:
          name: Alpine
          flags: Alpine,${{ steps.get-version.outputs.version }}
          fail_ci_if_error: true

  Cython:
    name: "Cython"
    needs:
      - build

    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          # Cython 2 supports 3.10 and 3.11 and Cython 3 supports all versions we do,
          # so test both the lowest and higher version for both
          - python: '3.10'
            cython: '<3'
          - python: '3.11'
            cython: '<3'
          # TODO: technically we should pin cython versions
          - python: '3.10'
            cython: '>=3'
          - python: '3.14'
            cython: '>=3'
    steps:
      - name: Retrieve the project source from an sdist inside the GHA artifact
        uses: re-actors/checkout-python-sdist@release/v2
        with:
          source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }}
          workflow-artifact-name: ${{ env.dists-artifact-name }}
      - name: Setup Python
        uses: actions/setup-python@v6
        with:
            python-version: '${{ matrix.python }}'
            cache: pip
        # setuptools is needed to get distutils on 3.12, which cythonize requires
      - name: install trio and setuptools
        run: python -m pip install --upgrade pip . setuptools 'coverage[toml]'

      - name: add cython plugin to the coveragepy config
        run: >-
          sed -i 's#plugins\s=\s\[\]#plugins = ["Cython.Coverage"]#'
          pyproject.toml

      - name: install cython & compile pyx file
        env:
          CFLAGS: ${{ env.CFLAGS }} -DCYTHON_TRACE_NOGIL=1
        run: |
          python -m pip install "cython${{ matrix.cython }}"
          cythonize --inplace -X linetrace=True tests/cython/test_cython.pyx

      - name: import & run module
        run: coverage run -m tests.cython.run_test_cython

      - name: Get Python version for codecov flag
        id: get-version
        shell: python
        run: |
          import sys, os
          with open(os.environ["GITHUB_OUTPUT"], "a") as f:
            f.write("version=" + ".".join(map(str, sys.version_info[:2])))
            f.write("\n")

      - run: |
          coverage combine
          coverage report

      - if: always()
        uses: codecov/codecov-action@v5
        with:
          name: Cython
          flags: Cython,${{ steps.get-version.outputs.version }}
          fail_ci_if_error: true

  # https://github.com/marketplace/actions/alls-green#why
  check:  # This job does nothing and is only used for the branch protection

    if: always()

    needs:
      - Windows
      - Ubuntu
      - macOS
      - Alpine
      - Cython

    runs-on: ubuntu-latest

    steps:
      - name: Decide whether the needed jobs succeeded or failed
        uses: re-actors/alls-green@release/v1
        with:
          jobs: ${{ toJSON(needs) }}
