{% set cache_key = "${{ runner.os }}-pip-${{ matrix.python-version }}" %}
###
# Initially copied from
# https://github.com/actions/starter-workflows/blob/main/ci/python-package.yml
# And later based on the version jamadden updated at
# gevent/gevent, and then at zodb/relstorage and zodb/perfmetrics
#
# Original comment follows.
###
###
# This workflow will install Python dependencies, run tests with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
###

###
# Important notes on GitHub actions:
#
# - We only get 2,000 free minutes a month (private repos)
# - We only get 500MB of artifact storage
# - Cache storage is limited to 7 days and 5GB.
# - macOS minutes are 10x as expensive as Linux minutes
# - windows minutes are twice as expensive.
#
# So keep those workflows light. Note: Currently, they seem to be free
# and unlimited for open source projects. But for how long...
#
# In December 2020, github only supports x86/64. If we wanted to test
# on other architectures, we can use docker emulation, but there's no
# native support. It works, but is slow.
#
# Another major downside: You can't just re-run the job for one part
# of the matrix. So if there's a transient test failure that hit, say, 3.11,
# to get a clean run every version of Python runs again. That's bad.
# https://github.community/t/ability-to-rerun-just-a-single-job-in-a-workflow/17234/65

name: tests


# Triggers the workflow on push or pull request events and periodically
on:
  push:
  pull_request:
  schedule:
    - cron: '0 12 * * 0'  # run once a week on Sunday
  # Allow to run this workflow manually from the Actions tab
  workflow_dispatch:

env:
  # Weirdly, this has to be a top-level key, not ``defaults.env``
  PYTHONHASHSEED: 8675309
  PYTHONUNBUFFERED: 1
  PYTHONDONTWRITEBYTECODE: 1
  PYTHONDEVMODE: 1
  PYTHONFAULTHANDLER: 1
  ZOPE_INTERFACE_STRICT_IRO: 1

  PIP_UPGRADE_STRATEGY: eager

  CFLAGS: -O3 -pipe
  CXXFLAGS: -O3 -pipe
  # Uploading built wheels for releases.
  # TWINE_PASSWORD is encrypted and stored directly in the
  # github repo settings.
  TWINE_USERNAME: __token__

  ###
  # caching
  # This is where we'd set up ccache, but this compiles so fast its not worth it.
  ###


jobs:
  # Because sharing code/steps is so hard, and because it can be
  # extremely valuable to be able to get binary wheels without
  # uploading to PyPI and even if there is some failure, (e.g., for
  # other people to test/debug), the strategy is to divide the process
  # into several different jobs. The first builds and saves the binary
  # wheels. It has dependent jobs that download and install the wheel
  # to run tests, and build docs. Building the
  # manylinux wheels is an independent set of jobs.
  #
  # This division is time-saving for projects that take awhile to
  # build, but somewhat less of a clear-cut win given how quick this
  # is to compile (at least at this writing).
  build-package:
    # Sigh. Note that the matrix must be kept in sync
    # with `test`, and `docs` must use a subset.
    runs-on: ${{ matrix.os }}
    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
{% include 'tests-strategy.j2' %}

    steps:
{% include 'tests-cache.j2' %}

{% if with_future_python %}
      - name: Install Build Dependencies (%(future_python_version)s)
        if: matrix.python-version == '%(future_python_version)s'
        run: |
          pip install -U pip
          pip install -U "setuptools %(setuptools_version_spec)s" wheel twine
{% endif %}
      - name: Install Build Dependencies
{% if with_future_python %}
        if: matrix.python-version != '%(future_python_version)s'
{% endif %}
        run: |
          pip install -U pip
          pip install -U "setuptools %(setuptools_version_spec)s" wheel twine

{% for kind in ('macOS x86_64', 'macOS arm64', 'all other versions') %}
      - name: Build %(package_name)s (%(kind)s)
        if: >
  {% if kind == 'macOS x86_64' %}
          startsWith(runner.os, 'Mac')
          && !startsWith(matrix.python-version, 'pypy')
        env:
          MACOSX_DEPLOYMENT_TARGET: 10.9
          _PYTHON_HOST_PLATFORM: macosx-10.9-x86_64
          ARCHFLAGS: -arch x86_64
  {% elif kind == 'macOS arm64' %}
          startsWith(runner.os, 'Mac')
          && !startsWith(matrix.python-version, 'pypy')
        env:
          MACOSX_DEPLOYMENT_TARGET: 11.0
          _PYTHON_HOST_PLATFORM: macosx-11.0-arm64
          ARCHFLAGS: -arch arm64
  {% elif kind == 'macOS universal2' %}
          startsWith(runner.os, 'Mac')
          && !startsWith(matrix.python-version, 'pypy')
        env:
          MACOSX_DEPLOYMENT_TARGET: 10.9
          _PYTHON_HOST_PLATFORM: macosx-10.9-universal2
          ARCHFLAGS: -arch arm64 -arch x86_64
  {% else %}
          !startsWith(runner.os, 'Mac')
          || startsWith(matrix.python-version, 'pypy')
  {% endif %}
        run: |
          # Next, build the wheel *in place*. This helps ccache, and also lets us cache the configure
          # output (pip install uses a random temporary directory, making this difficult).
          python setup.py build_ext -i
          python setup.py bdist_wheel
{% endfor %}

{% if with_future_python %}
      - name: Install %(package_name)s and dependencies (%(future_python_version)s)
        if: matrix.python-version == '%(future_python_version)s'
        run: |
          # Install to collect dependencies into the (pip) cache.
          # Use "--pre" here because dependencies with support for this future
          # Python release may only be available as pre-releases
          pip install --pre .[test]
{% endif %}
      - name: Install %(package_name)s and dependencies
{% if with_future_python %}
        if: matrix.python-version != '%(future_python_version)s'
{% endif %}
        run: |
          # Install to collect dependencies into the (pip) cache.
          pip install -U pip "setuptools %(setuptools_version_spec)s"
          pip install .[test]

      - name: Check %(package_name)s build
        run: |
          ls -l dist
          twine check dist/*
{% for kind in ('macOS x86_64', 'macOS arm64', 'all other platforms') %}
      - name: Upload %(package_name)s wheel (%(kind)s)
        if: >
  {% if kind == 'macOS x86_64' %}
          startsWith(runner.os, 'Mac')
  {% elif kind == 'macOS arm64' or kind == 'macOS universal2' %}
          startsWith(runner.os, 'Mac')
          && !startsWith(matrix.python-version, 'pypy')
  {% else %}
          !startsWith(runner.os, 'Mac')
  {% endif %}
        uses: actions/upload-artifact@v4
        with:
  {% if kind == 'macOS x86_64' %}
          # The x86_64 wheel is uploaded with a different name just so it can be
          # manually downloaded when desired. The wheel itself *cannot* be tested
          # on the GHA runner, which uses arm64 architecture.
          name: %(package_name)s-${{ runner.os }}-${{ matrix.python-version }}-x86_64.whl
  {% elif kind == 'macOS universal2' %}
          # The universal2 wheel is uploaded with a different name just so it
          # can be manually downloaded when desired. The GHA runner does not
          # test this wheel, it uses the arm64 version.
          name: %(package_name)s-${{ runner.os }}-${{ matrix.python-version }}-universal2.whl
  {% else %}
          name: %(package_name)s-${{ runner.os }}-${{ matrix.python-version }}.whl
  {% endif %}
  {% if kind == 'macOS x86_64' %}
          path: dist/*x86_64.whl
  {% elif kind == 'macOS arm64' %}
          path: dist/*arm64.whl
  {% elif kind == 'macOS universal2' %}
          path: dist/*universal2.whl
  {% else %}
          path: dist/*whl
  {% endif %}
{% endfor %}
      - name: Publish package to PyPI (Non-Linux)
        # We cannot use pypa/gh-action-pypi-publish because that
        # is a container action, and those don't run on macOS
        # or Windows GHA runners.
        if: >
          github.event_name == 'push'
          && startsWith(github.ref, 'refs/tags')
          && !startsWith(runner.os, 'Linux')
          && !startsWith(matrix.python-version, 'pypy')
{% if with_future_python %}
          && !startsWith(matrix.python-version, '%(future_python_version)s')
{% endif %}
        env:
          TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
        run: |
          twine upload --skip-existing dist/*

  test:
    needs: build-package
    runs-on: ${{ matrix.os }}
{% include 'tests-strategy.j2' %}

    steps:
{% include 'tests-cache.j2' %}
{% include 'tests-download.j2' %}
{% if gha_additional_install %}
  {% for line in gha_additional_install %}
      %(line)s
  {% endfor %}
{% else %}
{% if with_future_python %}
      - name: Install %(package_name)s ${{ matrix.python-version }}
        if: matrix.python-version == '%(future_python_version)s'
        run: |
          pip install -U wheel "setuptools %(setuptools_version_spec)s"
          # coverage might have a wheel on PyPI for a future python version which is
          # not ABI compatible with the current one, so build it from sdist:
          pip install -U --no-binary :all: coverage[toml]
          # Unzip into src/ so that testrunner can find the .so files
          # when we ask it to load tests from that directory. This
          # might also save some build time?
          ls -l dist/
          unzip -n dist/*.whl -d src
          # Use "--pre" here because dependencies with support for this future
          # Python release may only be available as pre-releases
          pip install --pre -e .[test]
{% endif %}
      - name: Install %(package_name)s
{% if with_future_python %}
        if: matrix.python-version != '%(future_python_version)s'
{% endif %}
        run: |
          pip install -U wheel "setuptools %(setuptools_version_spec)s"
          pip install -U coverage[toml]
          pip install -U 'cffi; platform_python_implementation == "CPython"'
          # Unzip into src/ so that testrunner can find the .so files
          # when we ask it to load tests from that directory. This
          # might also save some build time?
          ls -l dist/
          unzip -n dist/*.whl -d src
          pip install -e .[test]
      - name: Run tests with C extensions
        if: ${{ !startsWith(matrix.python-version, 'pypy') }}
        run: |
        {% if gha_test_commands %}
        {% for line in gha_test_commands %}
          %(line)s
        {% endfor %}
        {% else %}
          python -m coverage run -p -m zope.testrunner --test-path=src --auto-color --auto-progress
        {% endif %}
      - name: Run tests without C extensions
        run:
        {% if gha_test_commands %}
        {% for line in gha_test_commands %}
          %(line)s
        {% endfor %}
        {% else %}
          # coverage makes PyPy run about 3x slower!
          python -m coverage run -p -m zope.testrunner --test-path=src --auto-color --auto-progress
        {% endif %}
        env:
          PURE_PYTHON: 1
      - name: Report Coverage
        run: |
          coverage combine
          coverage report -i
{% endif %}
      - name: Submit to Coveralls
        # This is a container action, which only runs on Linux.
        if: ${{ startsWith(runner.os, 'Linux') }}
        uses: AndreMiras/coveralls-python-action@develop
        with:
          parallel: true

  coveralls_finish:
    needs: test
    runs-on: ubuntu-latest
    steps:
    - name: Coveralls Finished
      uses: AndreMiras/coveralls-python-action@develop
      with:
        parallel-finished: true
{% if with_docs %}

  docs:
    needs: build-package
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        python-version: ["%(manylinux_python_version)s"]
        os: [ubuntu-latest]

    steps:
{% include 'tests-cache.j2' %}
{% include 'tests-download.j2' %}
      - name: Install %(package_name)s
        run: |
          pip install -U wheel
          pip install -U coverage[toml]
          pip install -U  "`ls dist/*.whl`[docs]"
      - name: Build docs
        env:
          ZOPE_INTERFACE_STRICT_IRO: 1
        run: |
          sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
  {% if with_sphinx_doctests %}
          sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest
  {% endif %}
{% endif %}

  release-check:
    needs: build-package
    runs-on: "ubuntu-latest"
    strategy:
      matrix:
        python-version: ["%(manylinux_python_version)s"]
        os: [ubuntu-latest]

    steps:
{% include 'tests-cache.j2' %}
{% include 'tests-download.j2' %}
      - name: Install %(package_name)s
        run: |
          pip install -U wheel
          pip install -U tox
          pip install -U  "`ls dist/*.whl`[docs]"
      - name: Run release check
        env:
          ZOPE_INTERFACE_STRICT_IRO: 1
        run: |
          tox -e release-check

  manylinux:
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
    # We use a regular Python matrix entry to share as much code as possible.
    strategy:
      matrix:
        python-version: ["%(manylinux_python_version)s"]
        image: [%(manylinux_x86_64)s, %(manylinux_i686)s, %(manylinux_aarch64)s]

    steps:
{% set cache_key = "${{ runner.os }}-pip_manylinux-${{ matrix.image }}-${{ matrix.python-version }}" %}
{% include 'tests-cache.j2' %}

      - name: Update pip
        run: pip install -U pip
      - name: Build %(package_name)s (x86_64)
        if: matrix.image == '%(manylinux_x86_64)s'
        # An alternate way to do this is to run the container directly with a uses:
        # and then the script runs inside it. That may work better with caching.
        # See https://github.com/pyca/bcrypt/blob/f6b5ee2eda76d077c531362ac65e16f045cf1f29/.github/workflows/wheel-builder.yml
        env:
          DOCKER_IMAGE: quay.io/pypa/${{ matrix.image }}
        run: |
          bash .manylinux.sh
      - name: Build %(package_name)s (i686)
        if: matrix.image == '%(manylinux_i686)s'
        env:
          DOCKER_IMAGE: quay.io/pypa/${{ matrix.image }}
          PRE_CMD: linux32
        run: |
          bash .manylinux.sh
      - name: Build %(package_name)s (aarch64)
        if: matrix.image == '%(manylinux_aarch64)s'
        env:
          DOCKER_IMAGE: quay.io/pypa/${{ matrix.image }}
        run: |
          # First we must enable emulation
          docker run --rm --privileged hypriot/qemu-register
          bash .manylinux.sh

      - name: Upload %(package_name)s wheels
        uses: actions/upload-artifact@v4
        with:
          path: wheelhouse/*whl
          name: manylinux_${{ matrix.image }}_wheels.zip
      - name: Restore pip cache permissions
        run: sudo chown -R $(whoami) ${{ steps.pip-cache-default.outputs.dir }}
{% if with_future_python %}
      - name: Prevent publishing wheels for unreleased Python versions
        run: VER=$(echo '%(future_python_version)s' | tr -d .) && ls -al wheelhouse && sudo rm -f wheelhouse/*-cp${VER}*.whl && ls -al wheelhouse
{% endif %}
      - name: Publish package to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        if: >
          github.event_name == 'push'
          && startsWith(github.ref, 'refs/tags')
        with:
          user: __token__
          password: ${{ secrets.TWINE_PASSWORD }}
          skip-existing: true
          packages-dir: wheelhouse/
