Python application

# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python application

on:
  push:
    branches: [ "develop" ]
  pull_request:
    branches: [ "develop" ]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python
      uses: actions/setup-python@v3
      with:
        python-version: "3.11"
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install pytest poetry coverage
        poetry install
    #- name: Lint with flake8
    #  run: |
    #    pip install flake8
    #    # stop the build if there are Python syntax errors or undefined names
    #    flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
    #    # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
    #    flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
    - name: Test with pytest
      run: |
        poetry run pytest --cov=browse --cov-fail-under=80 tests


ci-build

name: ci-build

on:
  pull_request:
    branches:
      - master

jobs:
  # lint is handled by pre-commit ci conf
  generate-api:
    runs-on: ubuntu-latest
    env:
      FLIT_ROOT_INSTALL: 1
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python 3.9
        uses: actions/setup-python@v2
        with:
          python-version: 3.9
      - name: Install build tools
        run: pip install --upgrade pip flit
      - name: Install icclim and dev requirements (dev, test and doc)
        run: flit install --deps develop
      - name: call fun extractor
        run: python ./tools/extract_icclim_funs.py
      - name: run ruff format on generated module
        run: |
          ruff format src/icclim/_generated/
          ruff check src/icclim/_generated/ --fix
      - name: create local ref
        run: |
          git fetch origin ${{ github.head_ref }}
          git checkout ${{ github.head_ref }}
      - name: setup git config
        run: |
          # setup the username and email.
          git config user.name "GitHub Actions Bot - API updater"
          git config user.email "<>"
      - name: commit
        run: |
          # Stage the file, commit and push
          git add src/icclim/_generated
          # Ignore error cases
          git commit -m "MAINT: Update generated API" || true
          git push origin ${{ github.head_ref }}

  test:
    runs-on: ubuntu-latest
    env:
      FLIT_ROOT_INSTALL: 1
    steps:
       # actions/checkout@v2 checkouts to a new merge commit
      - uses: actions/checkout@v3
      - name: Set up Python 3.9
        uses: actions/setup-python@v2
        with:
          python-version: 3.9
      - name: Install build tools
        run: pip install --upgrade pip flit
      - name: Install icclim and dev requirements (dev, test and doc)
        run: flit install --deps develop
      - name: Build coverage file
        run: pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=icclim tests/ | tee pytest-coverage.txt; ( exit ${PIPESTATUS[0]} )
      - name: Pytest coverage comment
        id: coverageComment
        uses: MishaKav/pytest-coverage-comment@main
        with:
          pytest-coverage-path: ./pytest-coverage.txt
          title: Report
          badge-title: Coverage
          hide-badge: false
          hide-report: false
          create-new-comment: false
          hide-comment: false
          report-only-changed-files: false
          junitxml-path: ./pytest.xml
          junitxml-title: Test results
      - name: create local ref
        run: |
          git fetch origin ${{ github.head_ref }}
          git checkout ${{ github.head_ref }}
      - name: Update Readme with Coverage Html
        run: |
          sed -i '/  Pytest Coverage Comment:Begin/,/  Pytest Coverage Comment:End/c\  Pytest Coverage Comment:Begin\n\n.. |coverage| image:: https://img.shields.io/badge/Coverage-${{ steps.coverageComment.outputs.coverage }}25-${{ steps.coverageComment.outputs.color }}.svg\n        :target: https://github.com/cerfacs-globc/icclim/blob/master/README.rst#code-coverage\n        :alt: Code coverage\n\n..\n  Pytest Coverage Comment:End' ./README.rst
      - name: setup git config
        run: |
          # setup the username and email.
          git config user.name "GitHub Actions Bot"
          git config user.email "<>"
      - name: commit
        run: |
          # Stage the file, commit and push
          git add README.rst
          # Ignore error cases with `|| true`
          git commit -m "[skip ci] DOC: Update coverage" || true
          git push origin ${{ github.head_ref }}

  logo-update:
    runs-on: ubuntu-latest
    env:
      FLIT_ROOT_INSTALL: 1
    steps:
      - uses: actions/checkout@v3
        with:
          lfs: true
      - name: Set up Python 3.9
        uses: actions/setup-python@v2
        with:
          python-version: 3.9
      - name: Install build tools
        run: pip install --upgrade pip flit
      - name: Install icclim and dev requirements (dev, test and doc)
        run: flit install --deps develop
      - name: Generate logos
        run: |
          python ./tools/update_logo_version.py ./doc/source/_static/logo_icclim_colored__base.svg ./doc/source/_static/logo_icclim_colored__displayed.svg
          python ./tools/update_logo_version.py ./doc/source/_static/logo_icclim_grey__base.svg ./doc/source/_static/logo_icclim_grey__displayed.svg
          python ./tools/update_logo_version.py ./doc/source/_static/logo_icclim_white__base.svg ./doc/source/_static/logo_icclim_white__displayed.svg
      - name: create local ref
        run: |
          git fetch origin ${{ github.head_ref }}
          git checkout ${{ github.head_ref }}
      - name: setup git config
        run: |
          # setup the username and email.
          git config user.name "GitHub Actions Bot - logo updater"
          git config user.email "<>"
      - name: commit
        run: |
          # Stage the file, commit and push
          git add doc/source/_static/*__displayed.svg
          # Ignore error cases with `|| true`
          git commit -m "MAINT: Update logos" || true
          git push origin ${{ github.head_ref }}


Publish icclim to pypi

name: Publish icclim to pypi

on:
  release:
    types:
      - published

jobs:
  deployment:
    runs-on: ubuntu-latest
    environment: release
    permissions:
        id-token: write # needed for trusted publishing
    steps:
      - uses: actions/checkout@v3
      - name: Setup python 3.9
        uses: actions/setup-python@v2
        with:
          python-version: 3.9
      - name: Install tools
        run: python -m pip install --upgrade pip flit
      - name: Install icclim for prod
        run: python -m flit install --deps production
      - name: build package
        run: python -m flit build
      - name: Publish to pypi
        uses: pypa/gh-action-pypi-publish@v1.8.11


build

name: build
on:
  push:
  pull_request:
  # build weekly at 4:00 AM UTC
  schedule:
    - cron: '0 4 * * 1'

jobs:
  pylint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: '3.x'
      - run: python -m pip install -U tox
      - run: tox -e pylint

  mypy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: '3.11'
      - run: python -m pip install -U tox
      - run: tox -e mypy,mypy-test

  test:
    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
        # we do not want a large number of windows and macos builds, so
        # enumerate them explicitly
        include:
          - os: windows-latest
            python-version: "3.11"
          - os: macos-latest
            python-version: "3.11"
    name: "test py${{ matrix.python-version }} on ${{ matrix.os }} "
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: ${{ matrix.python-version }}
          allow-prereleases: true
      - name: install tox
        run: python -m pip install -U tox
      - name: run tests
        run: python -m tox -e py,coverage_report

  test-lazy-imports:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: '3.x'
      - run: python -m pip install -U tox
      - run: tox -e test-lazy-imports

  test-mindeps:
    runs-on: ubuntu-latest
    name: "mindeps"
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: "3.7"
      - name: install tox
        run: python -m pip install -U tox
      - name: test
        run: tox -e py-mindeps

  test-package-metadata:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: "3.11"
      - name: install tox
        run: python -m pip install -U tox
      - name: check package metadata
        run: python -m tox -e twine-check,poetry-check
      - name: check min version is tested in CI
        run: python -m tox r -e check-min-python-is-tested


changelog

name: changelog
on:
  workflow_call:
  pull_request:
    types:
      - labeled
      - unlabeled
      - opened
      - reopened
      - synchronize

jobs:
  check_has_news_in_changelog_dir:
    if: |
      ! (
        contains(github.event.pull_request.labels.*.name, 'no-news-is-good-news') ||
        github.event.pull_request.user.login == 'pre-commit-ci[bot]' ||
        github.event.pull_request.user.login == 'dependabot[bot]'
      )
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
        with:  # do a deep fetch to allow merge-base and diff
          fetch-depth: 0
      - name: check PR adds a news file
        run: |
          news_files="$(git diff --name-only "$(git merge-base origin/main "$GITHUB_SHA")" "$GITHUB_SHA" -- changelog.d/*.rst)"
          if [ -n "$news_files" ]; then
            echo "Saw new files. changelog.d:"
            echo "$news_files"
          else
            echo "No news files seen"
            exit 1
          fi


Publish PyPI Release

name: Publish PyPI Release

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: publish-pypi
    permissions:
      id-token: write

    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: "3.11"

      - run: python -m pip install build
      - run: python -m build .

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14


Publish Test PyPI Release

name: Publish Test PyPI Release

on:
  push:
    tags: ["*"]

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: publish-test-pypi
    permissions:
      id-token: write

    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: "3.11"

      - run: python -m pip install build
      - run: python -m build .

      - name: Publish to TestPyPI
        uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14
        with:
          repository-url: https://test.pypi.org/legacy/


Read the Docs Pull Request Preview

name: Read the Docs Pull Request Preview
on:
  pull_request_target:
    types:
      - opened

permissions:
  pull-requests: write

jobs:
  documentation-links:
    runs-on: ubuntu-latest
    steps:
      - uses: readthedocs/actions/preview@cc0920454cf03ca8a3fbd3cbaa2ce2e509e70636 # v1.2
        with:
          project-slug: "globus-sdk-python"


update-pr-references

name: update-pr-references
on:
  push:
    branches:
      - main

jobs:
  update_pr_numbers_in_change_fragments:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: '3.x'
      - name: update any PR numbers in change fragments
        run: |
          python ./changelog.d/update-pr-refs.py
          if [ "$(git status --porcelain)" = "" ]; then
            echo "no changes"
          else
            git add changelog.d/
            git \
              -c user.name="GitHub Actions" \
              -c user.email="actions@github.com" \
              commit -m '(actions) update PR references'
            git push origin
          fi


CI

name: CI

on:
  push:
    branches:
      - master
    tags:
      # Version tags.
      #
      # Tags matching this pattern will cause the "release" job below to run,
      # so edit it carefully!  It should not match arbitrary tags.
      - "[0-9]+.[0-9]+.[0-9]+*"
  pull_request:
  workflow_dispatch:

  # Routinely check that we continue to work in the face of external changes.
  schedule:
    # Every day at 17:42 UTC / 9:42 Seattle (winter) / 10:42 Seattle (summer)
    - cron: "42 17 * * *"

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: nextstrain/.github/actions/shellcheck@master

  test-source:
    name: test-source (python=${{ matrix.python }} os=${{ matrix.os }})
    strategy:
      fail-fast: false
      matrix:
        os:
          - ubuntu-latest
          - macos-latest
          - windows-latest
        python:
          - '3.8'
          - '3.9'
          - '3.10'
          - '3.11'
          - '3.12'

    runs-on: ${{ matrix.os }}
    defaults:
      run:
        shell: bash
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}

      - name: Install Nextstrain CLI
        run: python3 -m pip install --upgrade '.[dev]'

      - run: ./devel/pytest -v

  build-dist:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.10"

      # Install up-to-date packaging toolchain.
      - run: python3 -m pip install --upgrade pip setuptools wheel
      - run: python3 -m pip install --upgrade build

      # Update version to include local git rev if we're not building a release tag.
      - if: github.ref_type != 'tag'
        run: |
          version="$(./devel/read-version)"
          git_rev="$(git rev-parse --short @)"
          ./devel/update-version "${version%%+*}+git.${git_rev}"

      # Build dists.
      - run: python3 -m build

      # Upload dists as workflow artifacts.
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  build-standalone:
    needs: build-dist
    strategy:
      fail-fast: false
      matrix:
        # Generally we want to build on the oldest supported OS to maximize the
        # final binary's compatibility.  See pyoxidizer's docs for more
        # considerations affecting the choice of build machine OS¹ and future
        # plans for robust, turnkey build environments².
        #
        # On Linux, we build inside a container to avoid portability issues, so
        # the container's host OS version here doesn't have an impact (other
        # than CI stability).
        #
        # ¹ https://pyoxidizer.readthedocs.io/en/stable/pyoxidizer_distributing_binary_portability.html
        # ² https://pyoxidizer.readthedocs.io/en/stable/pyoxidizer_status.html#an-official-build-environment
        include:
          - os: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
            exe: nextstrain

          - os: macos-11
            target: x86_64-apple-darwin
            exe: nextstrain

          - os: macos-14
            target: aarch64-apple-darwin
            exe: nextstrain

          - os: windows-2019
            target: x86_64-pc-windows-msvc
            exe: nextstrain.exe

    runs-on: ${{ matrix.os }}
    defaults:
      run:
        shell: bash
    steps:
      # Note that this Python version doesn't impact the actual build.
      - uses: actions/setup-python@v5
        with:
          python-version: "3.10"

      # Build the executable + necessary external files from the dists.
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      - name: Set DIST, DIST_VERSION, and INSTALLATION_ARCHIVE_STEM
        run: |
          # shellcheck disable=SC2034
          DISTS=(dist/nextstrain_cli-*-py3-none-any.whl)
          DIST="${DISTS[0]}"
          DIST_VERSION="$DIST"
          DIST_VERSION="${DIST_VERSION#dist/nextstrain_cli-}"
          DIST_VERSION="${DIST_VERSION%-py3-none-any.whl}"
          INSTALLATION_ARCHIVE_STEM="nextstrain-cli-${DIST_VERSION}-standalone-${{ matrix.target }}"

          for var in DIST DIST_VERSION INSTALLATION_ARCHIVE_STEM; do
            echo "${var}=${!var}" | tee -a "$GITHUB_ENV"
          done

      - run: |
          ./devel/pyoxidizer build \
            --release \
            --target-triple ${{ matrix.target }} \
            --var NEXTSTRAIN_CLI_DIST "$DIST"

      # Analyze the executable for potential portability issues.
      #
      # This is for informational purposes only in build logs, so we don't care
      # if it fails.  Currently it only works on Linux, though it's supposed to
      # eventually work on all platforms supported by pyoxidizer.
      - if: runner.os == 'Linux'
        run: ./devel/pyoxidizer analyze build/${{ matrix.target }}/release/installation/${{ matrix.exe }}
        continue-on-error: true

      # XXX TODO: Review and report on licensing of all the stuff built into
      # the binary, as bundling things statically can trigger different license
      # terms than "normal" installs (e.g. via pip).  See also pyoxidizer's
      # docs about this and the tooling it includes to support license review.¹
      #   -trs, 1 June 2022
      #
      # ¹ https://pyoxidizer.readthedocs.io/en/stable/pyoxidizer_packaging_licensing.html#licensing-considerations

      # Create installation archive.
      #
      # Use tar on Unix to preserve file modes (e.g. the executable bit), thus
      # avoiding having to restore them manually after archive extraction.  Use
      # zip on Windows because it's a native format which requires no extra
      # tooling.
      - if: runner.os != 'Windows'
        run: tar czvpf "$INSTALLATION_ARCHIVE_STEM.tar.gz" -C build/${{ matrix.target }}/release/installation/ .

      - if: runner.os == 'Windows'
        run: Compress-Archive -DestinationPath "$Env:INSTALLATION_ARCHIVE_STEM.zip" -Path build/${{ matrix.target }}/release/installation/*
        shell: pwsh

      # Upload installation archive as a workflow artifact.
      #
      # At least one path needs to match, or this errors.
      - uses: actions/upload-artifact@v4
        with:
          name: standalone-${{ matrix.target }}
          path: |
            ${{ env.INSTALLATION_ARCHIVE_STEM }}.tar.gz
            ${{ env.INSTALLATION_ARCHIVE_STEM }}.zip
          if-no-files-found: error

      # Quick smoke test that the executable at least runs!  Useful before
      # launching the more extensive tests below.
      - run: ./build/${{ matrix.target }}/release/installation/${{ matrix.exe }} --help

  write-install-commands:
    needs: build-standalone
    runs-on: ubuntu-latest
    steps:
      - name: Write installation commands to job summary
        run: |
          cat >"$GITHUB_STEP_SUMMARY" <<'~~'
          Build complete. Commands to install locally:

          Linux:

          ```bash
          curl -fsSL --proto '=https' https://nextstrain.org/cli/installer/linux | bash -s ci-build/${{ github.run_id }}
          ```

          macOS:

          ```bash
          curl -fsSL --proto '=https' https://nextstrain.org/cli/installer/mac | bash -s ci-build/${{ github.run_id }}
          ```

          Windows:

          ```powershell
          Invoke-Expression "& { $(Invoke-RestMethod https://nextstrain.org/cli/installer/windows) } ci-build/${{ github.run_id }}"
          ```
          ~~

  test-dist:
    needs: build-dist
    name: test-dist (python=${{ matrix.python }} os=${{ matrix.os }})
    strategy:
      fail-fast: false
      matrix:
        os:
          - ubuntu-latest
          - macos-latest
          - windows-latest
        python:
          - '3.8'
          - '3.9'
          - '3.10'
          # XXX TODO: Add 3.11 here once supported by bioconda: https://github.com/nextstrain/augur/issues/1334
          # XXX TODO: Add 3.12 here once supported by bioconda: https://github.com/nextstrain/augur/issues/1334
    runs-on: ${{ matrix.os }}
    defaults:
      run:
        # Add -l for setup-integration-tests → setup-miniconda → automatic
        # activation of "test" environment.
        shell: bash -l -eo pipefail {0}
    steps:
      - uses: actions/checkout@v4
        with:
          path: src/

      - uses: ./src/.github/actions/setup-integration-tests
        with:
          python-version: ${{ matrix.python }}

      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      - name: Install Nextstrain CLI
        run: python3 -m pip install --upgrade dist/nextstrain_cli-*-py3-none-any.whl

      - uses: ./src/.github/actions/run-integration-tests

  test-standalone:
    needs: build-standalone
    name: test-standalone (os=${{ matrix.os}}, target=${{ matrix.target }})
    strategy:
      fail-fast: false
      matrix:
        # Test on all the platforms available via GitHub Actions.
        #
        # Ideally we'd test on machines with ~fresh OS installs.  The kitchen
        # sink of development/build software pre-installed into GitHub Action's
        # virtual-environments has a decent risk of making this CI blind to
        # end-user runtime issues with our binaries (e.g. missing DLLs).  Such
        # fresh CI machines are not readily available, however, since
        # pre-installation is convenient for builds.
        include:
          - { os: ubuntu-20.04, target: x86_64-unknown-linux-gnu }
          - { os: ubuntu-22.04, target: x86_64-unknown-linux-gnu }
          - { os: macos-11,     target: x86_64-apple-darwin }
          - { os: macos-12,     target: x86_64-apple-darwin }
          - { os: macos-14,     target: aarch64-apple-darwin }
          - { os: windows-2019, target: x86_64-pc-windows-msvc }
          - { os: windows-2022, target: x86_64-pc-windows-msvc }

    runs-on: ${{matrix.os}}
    defaults:
      run:
        # Add -l for setup-integration-tests → setup-miniconda → automatic
        # activation of "test" environment.
        shell: bash -l -eo pipefail {0}
    steps:
      - uses: actions/checkout@v4
        with:
          path: src/

      - uses: ./src/.github/actions/setup-integration-tests
        with:
          python-version: '3.9'

      # Download and extract the installation archive.
      - uses: actions/download-artifact@v4
        with:
          name: standalone-${{ matrix.target }}

      - if: runner.os != 'Windows'
        run: tar xzvpf nextstrain-cli-*-standalone-${{ matrix.target }}.tar.gz

      - if: runner.os == 'Windows'
        run: Expand-Archive -Path nextstrain-cli-*-standalone-${{ matrix.target }}.zip -DestinationPath .
        shell: pwsh

      - run: echo "$PWD" >> "$GITHUB_PATH"

      - uses: ./src/.github/actions/run-integration-tests

  doc:
    uses: nextstrain/.github/.github/workflows/docs-ci.yaml@master
    with:
      docs-directory: doc/
      pip-install-target: .[dev]
      make-target: dirhtml

  release:
    # Restricted to version tags by the "on: push: tags: …" config at the top.
    if: |2
         github.event_name == 'push'
      && github.ref_type == 'tag'
    needs:
      - build-dist
      - build-standalone
      - test-source
      - test-dist
      - test-standalone
      - lint
      - doc
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # In actions/checkout@v4 above, annotated tags are intentionally
      # **overwritten** and converted to a lightweight tag.  Forcibly restore
      # the annotated tag object from the remote so we can verify/use it later.
      - run: git fetch --force origin tag "$GITHUB_REF_NAME"

      - uses: actions/setup-python@v5
        with:
          python-version: "3.10"
      - run: python3 -m pip install --upgrade twine

      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      - uses: actions/download-artifact@v4
        with:
          name: standalone-x86_64-unknown-linux-gnu

      - uses: actions/download-artifact@v4
        with:
          name: standalone-x86_64-apple-darwin

      - uses: actions/download-artifact@v4
        with:
          name: standalone-aarch64-apple-darwin

      - uses: actions/download-artifact@v4
        with:
          name: standalone-x86_64-pc-windows-msvc

      - run: twine upload dist/*
        env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
          TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/

      - run: ./devel/create-github-release "${{github.ref_name}}" dist/* nextstrain-cli-*-standalone-*
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


Standalone installers

name: Standalone installers

on:
  push:
    branches:
      - master
    paths:
      - bin/standalone-installer-unix
      - bin/standalone-installer-windows

  pull_request:
    paths:
      - bin/standalone-installer-unix
      - bin/standalone-installer-windows

  # Routinely check that the external resources the installers rely on keep
  # functioning as expected.
  schedule:
    # Every day at 17:42 UTC / 9:42 Seattle (winter) / 10:42 Seattle (summer)
    - cron: "42 17 * * *"

  workflow_dispatch:

jobs:
  # The goal here is to make sure the installers run successfully on a variety
  # of OS versions.  We're _not_ testing unreleased standalone builds here—the
  # installation archives are downloaded from the latest release on GitHub via
  # nextstrain.org endpoints—which is why this isn't part of CI.  That is, this
  # is akin to testing `pip install nextstrain-cli` if we wanted to make sure
  # `pip` worked.
  #   -trs, 29 August 2022
  test:
    name: test (os=${{ matrix.os }})
    strategy:
      fail-fast: false
      matrix:
        os:
          - ubuntu-20.04
          - ubuntu-22.04
          - macos-11
          - macos-12
          - macos-14      # (aarch64)
          - windows-2019
          - windows-2022

    runs-on: ${{matrix.os}}
    steps:
      - uses: actions/checkout@v4

      # Use pipes like are used in the real instructions when the installer is
      # fetched from nextstrain.org.  This tests that the shell programs work
      # when they're specifically executed this way.
      - if: runner.os != 'Windows'
        run: |
          # shellcheck disable=SC2002
          cat ./bin/standalone-installer-unix | bash
        shell: bash

      - if: runner.os == 'Windows'
        run: Get-Content -Raw ./bin/standalone-installer-windows | Invoke-Expression
        shell: pwsh


Sync RTD redirects

name: Sync RTD redirects

on:
  push:
    branches:
      - master
    paths:
      - doc/redirects.yaml
      - .github/workflows/sync-rtd-redirects.yaml

  # The reusable workflow will only actually make changes when running on the
  # default branch (e.g. refs/heads/main); every other run will be a dry run.
  pull_request:

  # Manually triggered using GitHub's UI
  workflow_dispatch:

jobs:
  sync:
    name: rtd redirects
    uses: nextstrain/.github/.github/workflows/sync-rtd-redirects.yaml@master
    with:
      project: nextstrain-cli
      file: doc/redirects.yaml
    secrets:
      RTD_TOKEN: ${{ secrets.RTD_TOKEN }}


pyecharts CI

name: pyecharts CI

on: [push, pull_request]

jobs:
  build:
    strategy:
      # Allows for matrix sub-jobs to fail without canceling the rest
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
        exclude: # Python < v3.8 does not support Apple Silicon ARM64.
          - python-version: "3.7"
            os: macos-latest
        include: # So run those legacy versions on Intel CPUs.
          - python-version: "3.7"
            os: macos-13
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip setuptools
        pip install -r requirements.txt
        pip install -r test/requirements.txt
    - name: Run unit test
      run: |
        python setup.py install
        python test.py
    - name: Codecov
      run: |
        codecov


CodeQL

# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ master, gh-pages ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ master, gh-pages ]
  schedule:
    - cron: '17 11 * * 3'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'python' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
        # Learn more about CodeQL language support at https://git.io/codeql-language-support

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v2
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v2

    # ℹ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v2


Coconut Test Suite

name: Coconut Test Suite
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version:
          - '2.7'
          - '3.6'
          - '3.7'
          - '3.8'
          - '3.9'
          - '3.10'
          - '3.11'
          - '3.12'
          - 'pypy-2.7'
          - 'pypy-3.6'
          - 'pypy-3.7'
          - 'pypy-3.8'
          - 'pypy-3.9'
          - 'pypy-3.10'
      fail-fast: false
    name: Python ${{ matrix.python-version }}
    steps:
      - uses: actions/checkout@v4
      - name: Setup python
        uses: MatteoH2O1999/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
      - run: make install
      - run: make test-all
      - run: make build
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          user: __token__
          password: ${{ secrets.PYPI_API_TOKEN }}
        continue-on-error: true


Update Downloads Badges

name: Update Downloads Badges

on:
  schedule:
    - cron: '15 17 * * *'  # Every day at 5:15pm UTC

jobs:
  update:
    name: Update Downloads Badges
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
          architecture: 'x64'
      - name: Install Requirements
        run: python -m pip install requests
      - name: Run Python Script
        run: python etc/downloads_badges.py
        env:
          ZAPIER_SHA: ${{ secrets.ZAPIER_SHA }}


Mac and Windows Builds

name: Mac and Windows Builds

on:
  push:
    branches:
      - 'master'
    tags:
      - '*'

  pull_request:
    branches:
      - '*'

# Cancel older runs of the same workflow on the same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-and-deploy:
    strategy:
      matrix:
        python3-minor-version: [7, 8, 9, 10, 11, 12]
        os: [macos-latest, ubuntu-latest, windows-latest]
    defaults:
      run:
        shell: bash
    env:
      PYTHON_VERSION: ${{ format('3.{0}', matrix.python3-minor-version) }}
      PYTHON_EXECUTABLE: ${{ format('cp3{0}', matrix.python3-minor-version) }}

    runs-on: ${{ matrix.os }}
    name: Build and Deploy (${{ matrix.os }}, 3.${{ matrix.python3-minor-version }})

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      # Python interpreter used by cibuildwheel, but it uses a different one internally
      - name: Setting up Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ env.PYTHON_VERSION }}

      - name: Set up QEMU
        if: runner.os == 'Linux'
        uses: docker/setup-qemu-action@v2
        with:
          platforms: all

      # We build the source archive separately because of this: https://github.com/alkaline-ml/pmdarima/pull/136#discussion_r279781731
      # We build it first because of this: https://github.com/alkaline-ml/pmdarima/issues/448
      - name: Building source distribution
        run: make version sdist

      - name: Install cibuildwheel
        run: python -m pip install cibuildwheel

      - name: Building and testing wheels
        run: python -m cibuildwheel --output-dir dist
        env:
          # TODO: Move Linux x86_64 builds to GHA?
          CIBW_ARCHS_LINUX: "aarch64"
          CIBW_ARCHS_MACOS: "x86_64 arm64"
          CIBW_ARCHS_WINDOWS: "AMD64"
          CIBW_BEFORE_ALL: make version
          CIBW_BEFORE_BUILD: >
            pip install -r build_tools/build_requirements.txt &&
            pip install -r requirements.txt
          # Tests are run in a separate virtual env, so we need to re-install deps
          CIBW_BEFORE_TEST: >
            pip install -r build_tools/build_requirements.txt &&
            pip install -r requirements.txt
          CIBW_BUILD: "${{ env.PYTHON_EXECUTABLE }}-*"
          CIBW_ENVIRONMENT: CC=gcc
          CIBW_ENVIRONMENT_MACOS: PMD_MPL_BACKEND=TkAgg PYTHON_CROSSENV=true
          CIBW_ENVIRONMENT_PASS_LINUX: GITHUB_REF
          # No support for pypy or musl
          CIBW_SKIP: "pp* *-musllinux_*"
          # Runs tests and checks for a numpy regression by upgrading numpy and running tests again
          CIBW_TEST_COMMAND: >
            pytest --showlocals --durations=20 --pyargs pmdarima &&
            pip install --upgrade scipy numpy &&
            pytest --showlocals --durations=20 --pyargs pmdarima
          # Avoid testing on emulated architectures
          CIBW_TEST_SKIP: "*-*linux_{aarch64,ppc64le,s390x} *-macosx_arm64"

      - name: Checking README compatibility
        run: |
          python -m pip install "twine>=1.13.0" readme_renderer
          if python -c "from twine.commands.check import check; check(['dist/*'])" | grep  "warning"; then
              echo "README will not render properly on PyPI"
              exit 1
          else
              echo "README rendered appropriately"
          fi

      # See https://github.com/alkaline-ml/pmdarima/issues/448
      - name: Ensuring sdist SOURCES.txt has no absolute paths
        run: |
          cd dist
          tar xzf $(find . -name "*.tar.gz")
          OUTPUT_DIRECTORY=$(find . -type d -name "pmdarima-*")
          if grep -q "^/" ${OUTPUT_DIRECTORY}/pmdarima.egg-info/SOURCES.txt; then
            echo "SOURCES.txt contains absolute paths in sdist"
            exit 1
          else
            echo "SOURCES.txt looks good"
            rm -rf "$OUTPUT_DIRECTORY"
          fi

      - name: Ensuring sdist can be installed
        run: pip install dist/$(ls dist | grep tar)

      - name: Ensuring VERSION file exists
        id: version_check  # Need this to refer to output below
        run: |
          if [ -f "${GITHUB_WORKSPACE}/pmdarima/VERSION" ]; then
            echo "VERSION file exists"
            echo "version_exists=true" >> $GITHUB_OUTPUT
          else
            echo "VERSION file does not exist"
            echo "version_exists=false" >> $GITHUB_OUTPUT
          fi

      - name: Deploying to PyPI
        # Only deploy on tags and when VERSION file created
        if: startsWith(github.ref, 'refs/tags') && success() && steps.version_check.outputs.version_exists == 'true'
        run: |
          chmod +x build_tools/github/deploy.sh
          ./build_tools/github/deploy.sh
        env:
          TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
          TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}


Test against latest dependencies

name: Test against latest dependencies

# Run if we change any of these paths and every night at 1 (or 2) AM Central time
on:
  push:
    branches-ignore:
      - 'master'
    paths:
    - 'requirements.txt'
    - '.github/workflows/nightly_cron.yml'

  schedule:
    - cron: '0 7 * * *'  # Every day at 07:00 UTC (1AM CST or 2AM CDT)

  # Allows us to run manually
  workflow_dispatch:

# Cancel older runs of the same workflow on the same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    name: Build against latest dependencies
    strategy:
      fail-fast: false
      matrix:
        os: [windows-latest, macos-latest, ubuntu-latest]
        python-version: ['3.12']
        python-executable: ['cp312']
    defaults:
      run:
        shell: bash

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

    outputs:
      dependency-table: ${{ steps.dependency-table.outputs.table }}

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setting up Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}

      - name: Set Run ID
        id: definition  # Needed to retrieve the output of this step later
        run: echo "run_id=$GITHUB_RUN_ID" >> $GITHUB_OUTPUT

      - name: Collecting naked dependencies
        id: dependencies  # Needed to retrieve the output of this step later
        run: |
          dependencies=$(python build_tools/github/get_latest_dependencies.py)
          echo "latest_dependencies=$dependencies" >> $GITHUB_OUTPUT
        shell: bash

      - name: Install cibuildwheel
        run: python -m pip install cibuildwheel

      - name: Generating dependency table
        id: dependency-table  # Needed to set output of job
        run: |
          pip install requests tabulate
          table=$(python .github/utils/get_dependency_releases.py $DEPENDENCIES)

          # This is used in the next job (if necessary) rather than re-running the above
          echo "table=$table" >> $GITHUB_OUTPUT
        env:
          DEPENDENCIES: ${{ steps.dependencies.outputs.latest_dependencies }}

      - name: Building and testing wheel
        run: python -m cibuildwheel --output-dir dist
        env:
          CIBW_ARCHS_LINUX: "x86_64"
          CIBW_ARCHS_MACOS: "x86_64"
          CIBW_ARCHS_WINDOWS: "AMD64"
          CIBW_BEFORE_ALL: make version
          CIBW_BEFORE_BUILD: >
            for dependency in ${{ steps.dependencies.outputs.latest_dependencies }}; do
              pip install $dependency
            done
          # Windows runs a batch script that I couldn't get to work, so we just force it to run bash
          CIBW_BEFORE_BUILD_WINDOWS: bash -c 'for dependency in ${{ steps.dependencies.outputs.latest_dependencies }}; do pip install $dependency; done'
          # Tests are run in a separate virtual env, so we need to re-install deps
          CIBW_BEFORE_TEST: >
            for dependency in ${{ steps.dependencies.outputs.latest_dependencies }}; do
              pip install $dependency
            done
          CIBW_BEFORE_TEST_WINDOWS: bash -c 'for dependency in ${{ steps.dependencies.outputs.latest_dependencies }}; do pip install $dependency; done'
          CIBW_BUILD: "${{ matrix.python-executable }}-*"
          CIBW_ENVIRONMENT_MACOS: PMD_MPL_BACKEND=TkAGG
          # No support for pypy or musl
          CIBW_SKIP: "pp* *-musllinux_*"
          CIBW_TEST_COMMAND: pytest --showlocals --durations=20 --pyargs pmdarima

      # https://github.com/marketplace/actions/action-slack#custom-notification
      - name: Posting to Slack
        uses: 8398a7/action-slack@v3
        if: failure()
        with:
          status: custom
          custom_payload: |
            {
              text: ":no_entry: Nightly Build Failed",
              attachments: [{
                "author_name": "Nightly Build", // This one needs to be in quotes for some reason
                fallback: 'Nightly Build Failed!',
                color: 'danger',
                title: 'CI Result',
                text: 'Failed',
                fields: [
                  {
                    title: 'OS',
                    value: '${{ matrix.os }}',
                    short: false
                  },
                  {
                    title: 'Python Version',
                    value: '${{ matrix.python-version }}',
                    short: false
                  },
                  {
                    title: 'Link to Run',
                    value: '<https://github.com/alkaline-ml/pmdarima/actions/runs/${{ env.RUN_ID }}|HERE>',
                    short: false
                  }
                ],
                actions: [{}]  // Don't know if we actually need this, but it is shown in the docs as empty
              }]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.NIGHTLY_SLACK_CHANNEL }}
          RUN_ID: ${{ steps.definition.outputs.run_id }}

  # We only run this job if the previous one fails (any of the 4 jobs that it spins up)
  send_latest_releases:
    needs: [build]
    if: failure()
    name: Send latest dependency releases to Slack
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@master

      - name: Setting up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.8'

      - name: Posting to Slack
        uses: 8398a7/action-slack@v3
        with:
          status: custom
          custom_payload: |
            {
              text: "Latest Dependency Releases (most recent first):\n${{ env.TABLE }}",
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.NIGHTLY_SLACK_CHANNEL }}
          TABLE: ${{ needs.build.outputs.dependency-table }}


Test VERSION tagging

name: Test VERSION tagging

on:
  push:
    branches:
      - 'master'

  pull_request:
    branches:
      - '*'

# Cancel older runs of the same workflow on the same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test-tagging:
    name: Test VERSION tagging

    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setting up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
          architecture: 'x64'

      - name: Ensure VERSION tagging works
        run: |
          chmod +x build_tools/github/test_version_tagging.sh
          ./build_tools/github/test_version_tagging.sh
        shell: bash


test

name: test

on:
  push:
    branches:
      - master
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  mypy-self-check:
    timeout-minutes: 10
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          pip install -U pip setuptools wheel
          SETUPTOOLS_ENABLE_FEATURES=legacy-editable pip install -r ./requirements.txt

      - name: Run mypy on plugin code
        run: mypy --strict mypy_django_plugin

      - name: Run mypy on ext code
        run: mypy --strict ext
      - name: Run mypy on scripts and utils
        run: mypy --strict scripts
      - name: Run mypy on stubs
        run: mypy --cache-dir=/dev/null --no-incremental django-stubs

  test:
    timeout-minutes: 15
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
        shard: [0, 1, 2, 3]
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - name: Setup system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install binutils libproj-dev gdal-bin
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          pip install -U pip setuptools wheel
          SETUPTOOLS_ENABLE_FEATURES=legacy-editable pip install -r ./requirements.txt

      # Must match `shard` definition in the test matrix:
      - name: Run pytest tests
        run: PYTHONPATH='.' pytest --num-shards=4 --shard-id=${{ matrix.shard }} tests
      - name: Run mypy on the test cases
        run: mypy tests/assert_type

  stubtest:
    timeout-minutes: 10
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.12']
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - name: Setup system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install binutils libproj-dev gdal-bin
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          pip install -U pip setuptools wheel
          SETUPTOOLS_ENABLE_FEATURES=legacy-editable pip install -r ./requirements.txt

      - name: Run stubtest
        run: bash ./scripts/stubtest.sh

  run-pyright:
    timeout-minutes: 10
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.12']  # TODO: enable on other versions
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          pip install -U pip setuptools wheel
          SETUPTOOLS_ENABLE_FEATURES=legacy-editable pip install -r ./requirements.txt
      - name: Run pyright on the stubs
        uses: jakebailey/pyright-action@v2
        with:
          version: PATH
          annotate: false
        continue-on-error: true  # TODO: remove this part
      - name: Run pyright on the test cases
        uses: jakebailey/pyright-action@v2
        with:
          version: PATH
          project: ./pyrightconfig.testcases.json

  matrix-test:
    timeout-minutes: 10
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ['3.11', '3.12']
        django-version: ['4.2', '5.0']
        include:
          - python-version: '3.8'
            django-version: '4.2'
          - python-version: '3.9'
            django-version: '4.2'
          - python-version: '3.10'
            django-version: '4.2'
    steps:
      - uses: actions/checkout@v4
      - name: Setup system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install binutils libproj-dev gdal-bin
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          pip install -U pip setuptools wheel
          SETUPTOOLS_ENABLE_FEATURES=legacy-editable pip install -r ./requirements.txt
          pip install "Django==${{ matrix.django-version }}"
          pip check

      - name: Run django-stubs-ext tests
        run: PYTHONPATH='.' pytest ext

  build-and-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-tags: true
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install dependencies
        run: python3 -m pip install --upgrade build twine
      - name: Build
        run: |
          python3 -m build --sdist --wheel .
          python3 -m build --sdist --wheel ext/
      - name: Check package metadata
        run: |
          twine check --strict dist/*
          twine check --strict ext/dist/*


Release

name: Release

on:
  release:
    types: [published]

concurrency:
  group: release-${{ github.event.release.tag_name }}

jobs:
  build-and-publish-ext:
    runs-on: ubuntu-latest
    environment:
      name: release-ext
      url: https://pypi.org/p/django-stubs-ext
    permissions:
      id-token: write
    steps:
      - name: Setup python to build package
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install build
        run: python -m pip install build
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Build ext package
        run: python -m build ext/
      - name: Publish ext to PyPI
        uses: pypa/gh-action-pypi-publish@v1.9.0
        with:
          packages-dir: ext/dist/

  build-and-publish:
    runs-on: ubuntu-latest
    needs: [build-and-publish-ext]
    environment:
      name: release
      url: https://pypi.org/p/django-stubs
    permissions:
      id-token: write
    steps:
      - name: Setup python to build package
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install build
        run: python -m pip install build
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Build package
        run: python -m build
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@v1.9.0


Build wheels

# Build on every branch push, tag push, and pull request change:
# From: https://github.com/pypa/cibuildwheel/blob/main/examples/github-deploy.yml
# Also:
# https://github.com/airspeed-velocity/asv/blob/main/.github/workflows/wheels.yml
# include [wheel build] in the commit to trigger wheel builds
name: Build wheels
on: [push, pull_request, workflow_dispatch]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read # to fetch code (actions/checkout)

jobs:
  get_commit_message:
    name: Get commit message
    runs-on: ubuntu-latest
    if: "github.repository == 'airspeed-velocity/asv'"
    outputs:
      message: ${{ steps.commit_message.outputs.message }}
    steps:
      - name: Checkout asv
        uses: actions/checkout@v4
        # Gets the correct commit message for pull request
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - name: Get commit message
        id: commit_message
        run: |
          set -xe
          COMMIT_MSG=$(git log --no-merges -1 --oneline)
          echo "message=$COMMIT_MSG" >> $GITHUB_OUTPUT
          echo github.ref ${{ github.ref }}
  build_wheels:
    name: Build wheels
    needs: get_commit_message
    if: >-
      contains(needs.get_commit_message.outputs.message, '[wheel build]') ||
      github.event_name == 'schedule' ||
      github.event_name == 'workflow_dispatch' ||
      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && ( ! endsWith(github.ref, 'dev0')))
    runs-on: ${{ matrix.buildplat[0] }}
    strategy:
      # Ensure that a wheel builder finishes even if another fails
      fail-fast: false
      matrix:
        # From NumPy
        # Github Actions doesn't support pairing matrix values together, let's improvise
        # https://github.com/github/feedback/discussions/7835#discussioncomment-1769026
        buildplat:
          - [ubuntu-20.04, manylinux_x86_64]
          - [ubuntu-20.04, musllinux_x86_64]
          - [macos-12, macosx_x86_64]
          - [macos-12, macosx_arm64]
          - [windows-2019, win_amd64]
        python-version: ['3.8', '3.9', '3.10', '3.11', 'pypy3.8', 'pypy3.9']

    steps:
      - uses: actions/checkout@v4
      - name: Echo Python version and build platform
        run: echo Building wheel for ${{ matrix.python-version }}-${{ matrix.buildplat[1] }}
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Build wheels
        uses: pypa/cibuildwheel@v2.16.5
      - uses: actions/upload-artifact@v3
        with:
          path: ./wheelhouse/*.whl

  build_sdist:
    name: Build source distribution
    needs: get_commit_message
    if: >-
      contains(needs.get_commit_message.outputs.message, '[wheel build]') ||
      github.event_name == 'schedule' ||
      github.event_name == 'workflow_dispatch' ||
      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && ( ! endsWith(github.ref, 'dev0')))
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build sdist
        shell: bash -l {0}
        run: pipx run build --sdist

      - uses: actions/upload-artifact@v3
        with:
          path: dist/*.tar.gz

  upload_pypi:
    needs: [build_wheels, build_sdist]
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/asv_runner
    permissions:
      id-token: write  # for trusted publishing
    # upload to PyPI on every tag starting with 'v'
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.repository == 'airspeed-velocity/asv'
    # alternatively, to publish when a GitHub Release is created, use the following rule:
    # if: github.event_name == 'release' && github.event.action == 'published'
    steps:
      - uses: actions/download-artifact@v3
        with:
          # unpacks default artifact into dist/
          # if `name: artifact` is omitted, the action will create extra parent dir
          name: artifact
          path: dist
      - uses: pypa/gh-action-pypi-publish@release/v1


Linux and MacOS CI

name: Linux and MacOS CI

on: [push, pull_request]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    name: test
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: ["ubuntu-latest"]
        python-version: ["3.7", "3.12", "pypy-3.8", "pypy-3.9"]
        r-version: ['release']
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # We need Python 3.7 to always be installed, so tests with
      # multiple environments can run.
      - name: Set up Python 3.7
        uses: actions/setup-python@v4
        with:
          python-version: 3.7

      - name: Set up Python version ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
          cache-dependency-path: pyproject.toml

      - name: Setup a browser for more tests
        uses: browser-actions/setup-chrome@latest

      - name: Set up R version ${{ matrix.r-version }}
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: ${{ matrix.r-version }}

      - name: Install dependencies (standard)
        if: matrix.python-version != '3.12.0-rc.2'
        run: python -m pip install ".[test,hg]"

      - name: Install dependencies (with --pre)
        if: matrix.python-version == '3.12.0-rc.2'
        run: python -m pip install ".[test,hg]" --pre

      - name: Install asv
        run: pip install .

      - name: Run tests
        run: python -m pytest -v --timeout=300 --webdriver=ChromeHeadless --durations=100 test

  test_env:
    name: test_environments
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: ["ubuntu-latest", "macos-latest"]
      fail-fast: false
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: mamba-org/setup-micromamba@v1
        with:
          init-shell: >-
            bash
          environment-name: test-env
          cache-environment: true
          create-args: >-
            python
            pip
            libmambapy
            conda-build

      - name: Install dependencies
        run: python -m pip install ".[test,hg]" --pre
        shell: micromamba-shell {0}

      - name: Install asv
        run: pip install .
        shell: micromamba-shell {0}

      - name: Run tests
        run: pytest -k environment_bench -vvvvv
        shell: micromamba-shell {0}

  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v4
        with:
          python-version: '3.x'
          cache: pip
          cache-dependency-path: pyproject.toml

      - name: Install asv
        run: pip install ".[doc]"

      - name: Build docs
        run: sphinx-build -W docs/source html


Windows CI

name: Windows CI

on: [push, pull_request]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    name: test
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: ["windows-latest"]
        python-version: ["3.7"]
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Python version ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
          cache-dependency-path: pyproject.toml

      - name: Install and test
        shell: pwsh
        run: |
          python.exe -m pip install .[test]
          python.exe -m pip install packaging virtualenv
          python.exe -m pytest -v -l -x --timeout=300 --durations=100 test --environment-type=virtualenv

  test_env:
    name: test_environments
    runs-on: "ubuntu-latest"
    strategy:
      fail-fast: false
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: mamba-org/setup-micromamba@v1
        with:
          init-shell: >-
            powershell
          environment-name: test-env
          cache-environment: true
          create-args: >-
            python
            pip
            libmambapy
            conda-build

      - name: Install dependencies
        run: python -m pip install ".[test,hg]" --pre
        shell: micromamba-shell {0}

      - name: Install asv
        run: pip install .
        shell: micromamba-shell {0}

      - name: Run tests
        run: pytest -k environment_bench -vvvvv
        shell: micromamba-shell {0}


pre-commit

name: pre-commit

on:
  pull_request:
  push:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v4
      with:
        python-version: '3.9'
    - uses: pre-commit/action@v2.0.3


Triggered

name: Triggered

on:
  workflow_dispatch:
    inputs:
      pr_number:
        description: 'PR Number'
        required: true

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  ASVRunner:
    name: test with specific asv_runner
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: ["ubuntu-latest"]
        python-version: ["3.7", "3.10", "pypy-3.8", "pypy-3.9"]
        r-version: ['release']
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # We need Python 3.7 to always be installed, so tests with
      # multiple environments can run.
      - name: Set up Python 3.7
        uses: actions/setup-python@v4
        with:
          python-version: 3.7

      - name: Set up Python version ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
          cache-dependency-path: pyproject.toml

      - name: Setup a browser for more tests
        uses: browser-actions/setup-chrome@latest

      - name: Set up R version ${{ matrix.r-version }}
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: ${{ matrix.r-version }}

      - name: Install dependencies
        run: python -m pip install ".[test,hg]"

      - name: Get asv_runner to be tested
        uses: actions/checkout@v4
        with:
          repository: airspeed-velocity/asv_runner
          ref: refs/pull/${{ github.event.inputs.pr_number }}/head
          path: latest_asv_runner

      - name: Install asv
        run: pip install .

      - name: Run tests with the current asv_runner
        run: ASV_RUNNER="$(pwd)/latest_asv_runner" python -m pytest -v --timeout=300 --webdriver=ChromeHeadless --durations=100 test


Tests

# Run the project's test suite
name: Tests

on:
  push:
    branches:
    - master
    - main
    - '*.x'
  pull_request:
    branches:
    - master
    - main
    - '*.x'

concurrency:
  group: test-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    name: Test ${{ matrix.os }} Python ${{ matrix.python-version }} conda=${{ matrix.use-conda }}
    runs-on: ${{ matrix.os }}
    timeout-minutes: 20
    defaults:
      run:
        shell: ${{ matrix.special-invocation }}bash -l {0}
    env:
      CI: 'True'
      PYTHON_VERSION: ${{ matrix.python-version }}
      USE_CONDA: ${{ matrix.use-conda }}
      PYQT5_VERSION: ${{ matrix.pyqt5-version || matrix.qt5-version-default }}
      PYQT5_QT_VERSION: ${{ matrix.pyqt5-qt-version || matrix.pyqt5-version || matrix.qt5-version-default }}
      PYQT6_VERSION: ${{ matrix.pyqt6-version || matrix.qt6-version-default }}
      PYQT6_QT_VERSION: ${{ matrix.pyqt6-qt-version || matrix.pyqt6-version || matrix.qt6-version-default }}
      PYSIDE2_VERSION: ${{ matrix.pyside2-version || matrix.qt5-version-default }}
      PYSIDE2_QT_VERSION: ${{ matrix.pyside2-qt-version || matrix.pyside2-version || matrix.qt5-version-default }}
      PYSIDE6_VERSION: ${{ matrix.pyside6-version || matrix.qt6-version-default }}
      PYSIDE6_QT_VERSION: ${{ matrix.pyside6-qt-version || matrix.pyside6-version || matrix.qt6-version-default }}
      QSCINTILLA_VERSION: ${{ matrix.qscintilla-version || matrix.qscintilla-version-default }}
      PYQT_EXTRAS: ${{ matrix.pyqt-extras || matrix.pyqt-extras-default }}
      SKIP_PIP_CHECK: ${{ matrix.skip-pip-check }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-13]
        python-version: ['3.7', '3.11']
        use-conda: ['Yes', 'No']
        qt5-version-default: ['5.12']
        qt6-version-default: ['6.3']
        qscintilla-version-default: ['2.13']
        pyqt-extras-default: ['No']
        include:
        - os: ubuntu-latest
          special-invocation: 'xvfb-run --auto-servernum '  # Needed for GUI tests to work
        - python-version: '3.11'
          pyqt5-version: '5.15'  # Python 3.11 needs 5.15+
          pyside2-version: '5.15'  # Python 3.11 needs 5.15+
          pyside6-version: '6.5'  # Python 3.11 needs 6.4+. Test upper bound
        - use-conda: 'Yes'
          skip-pyqt6: true  # No PyQt6 conda packages yet
        - use-conda: 'No'
          pyqt5-version: '5.15'  # Test with latest optional packages
        - python-version: '3.7'
          use-conda: 'Yes'
          pyside2-version: '5.13'  # Conda needs 5.13+ to work reliably
          pyside2-qt-version: '5.12'  # Conda only has 5.12 and 5.15, not 5.13
          pyside6-version: '6.4'  # Conda only has 6.4 for Python <3.8
        - python-version: '3.11'
          use-conda: 'No'
          pyqt-extras: 'Yes'  # Check PyQt extras
          skip-pyside2: true  # Pyside2 wheels don't support Python 3.11+
          pyqt6-version: '6.5'  # Test upper bound
          pyside6-version: '6.5'  # Test upper bound
        - os: windows-latest
          python-version: '3.7'
          use-conda: 'Yes'
          pyqt5-version: '5.9'  # Test lower bound
          skip-pyside6: true  # Test hangs with 6.4. 6.5 is not available for Python 3.7
        - os: windows-latest
          python-version: '3.7'
          use-conda: 'No'
          pyqt6-version: 6.2  # Test lower bound
          pyside6-version: 6.2  # Test lower bound
        - os: windows-latest
          python-version: '3.11'
          use-conda: 'Yes'
          pyside6-version: 6.5  # Test upper bound
        - os: macos-13
          python-version: '3.11'
          use-conda: 'No'
          pyqt6-version: 6.5  # Test upper bound
          pyside2-version: 5.15  # Test upper bound
    steps:
      - name: Check job values
        run: |
          echo "---- General setup"
          echo "OS:" ${{ matrix.os }}
          echo "PYTHON_VERSION:" ${{ env.PYTHON_VERSION }}
          echo "USE_CONDA:" ${{ env.USE_CONDA }}
          echo "---- PyQt"
          echo "PYQT_EXTRAS:" ${{ env.PYQT_EXTRAS }}
          echo "---- PyQt5"
          echo "SKIP_PYQT5:" ${{ matrix.skip-pyqt5 }}
          echo "PYQT5_VERSION:" ${{ env.PYQT5_VERSION }}
          echo "PYQT5_QT_VERSION:" ${{ env.PYQT5_QT_VERSION }}
          echo "QSCINTILLA_VERSION:" ${{ env.QSCINTILLA_VERSION }}
          echo "---- PyQt6"
          echo "SKIP_PYQT6:" ${{ matrix.skip-pyqt6 }}
          echo "PYQT6_VERSION:" ${{ env.PYQT6_VERSION }}
          echo "PYQT6_QT_VERSION:" ${{ env.PYQT6_QT_VERSION }}
          echo "---- PySide2"
          echo "SKIP_PYSIDE2:" ${{ matrix.skip-pyside2 }}
          echo "PYSIDE2_VERSION:" ${{ env.PYSIDE2_VERSION }}
          echo "PYSIDE2_QT_VERSION:" ${{ env.PYSIDE2_QT_VERSION }}
          echo "---- PySide6"
          echo "SKIP_PYSIDE6:" ${{ matrix.skip-pyside6 }}
          echo "PYSIDE6_VERSION:" ${{ env.PYSIDE6_VERSION }}
          echo "PYSIDE6_QT_VERSION:" ${{ env.PYSIDE6_QT_VERSION }}
          echo "---- Other"
          echo "SKIP_PIP_CHECK:" ${{ env.SKIP_PIP_CHECK }}
      - name: Checkout branch
        uses: actions/checkout@v3
      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.x'
      - name: Install Linux system packages
        if: contains(matrix.os, 'ubuntu')
        shell: bash
        run: |
          sudo apt update
          sudo apt install libpulse-dev libegl1-mesa libopengl0 gstreamer1.0-gl
      - uses: tlambert03/setup-qt-libs@v1
      - name: Install Conda
        uses: conda-incubator/setup-miniconda@v2
        with:
           activate-environment: ''
           auto-activate-base: true
           auto-update-conda: true
           channels: conda-forge
           channel-priority: strict
           miniforge-variant: Mambaforge
           use-mamba: true
      - name: Print Conda info
        shell: bash -l {0}
        run: |
          mamba info
          mamba list
      - name: Test PyQt5
        if: (! matrix.skip-pyqt5)
        run: ./.github/workflows/test.sh pyqt5
      - name: Test PyQt6
        if: always() && (! (matrix.skip-pyqt6))
        run: ./.github/workflows/test.sh pyqt6
      - name: Test PySide2
        if: always() && (! (matrix.skip-pyside2))
        run: ./.github/workflows/test.sh pyside2
      - name: Test PySide6
        if: always() && (! (matrix.skip-pyside6))
        run: ./.github/workflows/test.sh pyside6
      - name: Upload coverage data to coveralls.io
        uses: coverallsapp/github-action@v2
        with:
          parallel: true
          flag-name: ${{ matrix.os }} Python ${{ matrix.python-version }} conda=${{ matrix.use-conda }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
          debug: true
  finish:
    name: Finish Coveralls
    needs: test
    if: ${{ always() }}
    runs-on: ubuntu-latest
    steps:
    - name: Mark Coveralls finished
      uses: coverallsapp/github-action@v2
      with:
        parallel-finished: true


CodeQL

name: CodeQL

on:
  merge_group:
  pull_request:
    types:
      - opened
      - synchronize
  push:
    branches:
      - master
  schedule:
    - cron: "56 12 * * 1"

permissions:
  actions: read
  contents: read
  security-events: write

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

jobs:
  analyze:
    name: Check for Vulnerabilities
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        language: [python]

    steps:
      - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group'
        run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection.

      - name: Checkout
        uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          queries: +security-and-quality

      - name: Autobuild
        uses: github/codeql-action/autobuild@v3

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:${{ matrix.language }}"


Build Documentation

name: Build Documentation

on:
  push:
    branches:
      - master

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "documentation"
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Pages
        uses: actions/configure-pages@v4

      - name: Configure Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.8"

      - name: Configure dependencies
        run: |
          pip install --user --upgrade pip
          pip install --user pipx
          pipx ensurepath
          pipx install sphinx==5.3.0
          pipx inject sphinx pyjwt cryptography sphinx-mdinclude sphinx-rtd-theme sphinx-autodoc-typehints

      - name: Build documentation
        run: |
          sphinx-build ./docs/source ./docs/build --keep-going -n -a -b html

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v2
        with:
          path: "./docs/build"

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: "github-pages"
      url: ${{ steps.deployment.outputs.page_url }}

    steps:
      - id: deployment
        name: Deploy to GitHub Pages
        uses: actions/deploy-pages@v3


Publish Release

name: Publish Release

on:
  workflow_dispatch:

### TODO: Replace instances of './.github/actions/' with reference to the `dx-sdk-actions` repo is made public and this file is transferred over
### TODO: Also remove `get-prerelease`, `get-version`, `release-create`, `tag-create` and `tag-exists` actions from this repo's .github/actions folder once the repo is public.

permissions:
  contents: write
  id-token: write # Required for trusted publishing to PyPI

jobs:
  publish-pypi:
    if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/'))
    name: "PyPI"
    runs-on: ubuntu-latest
    environment: release

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          fetch-tags: true

      # Get the version from the branch name
      - id: get_version
        uses: ./.github/actions/get-version

      # Get the prerelease flag from the branch name
      - id: get_prerelease
        uses: ./.github/actions/get-prerelease
        with:
          version: ${{ steps.get_version.outputs.version }}

      # Get the release notes
      # This will expose the release notes as env.RELEASE_NOTES
      - id: get_release_notes
        uses: ./.github/actions/get-release-notes
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          version: ${{ steps.get_version.outputs.version }}
          repo_owner: ${{ github.repository_owner }}
          repo_name: ${{ github.event.repository.name }}

      # Create a release for the tag
      - uses: ./.github/actions/release-create
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          name: ${{ steps.get_version.outputs.version }}
          body: ${{ steps.get_release_notes.outputs.release-notes }}
          tag: ${{ steps.get_version.outputs.version }}
          commit: ${{ github.sha }}
          prerelease: ${{ steps.get_prerelease.outputs.prerelease }}

      - name: Configure Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.8"

      - name: Configure dependencies
        run: |
          pip install --user --upgrade pip
          pip install --user pipx
          pipx ensurepath
          pipx install poetry==1.4.2
          poetry config virtualenvs.in-project true
          poetry install --with dev
          poetry self add "poetry-dynamic-versioning[plugin]==1.1.1"

      - name: Build release
        run: |
          poetry build

      - name: Publish release
        uses: pypa/gh-action-pypi-publish@release/v1


Semgrep

name: Semgrep

on:
  merge_group:
  pull_request_target:
    types:
      - opened
      - synchronize
  push:
    branches:
      - master
  schedule:
    - cron: "30 0 1,15 * *"

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

jobs:
  authorize:
    name: Authorize
    environment: ${{ github.actor != 'dependabot[bot]' && github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && 'external' || 'internal' }}
    runs-on: ubuntu-latest
    steps:
      - run: true

  run:
    needs: authorize # Require approval before running on forked pull requests

    name: Check for Vulnerabilities
    runs-on: ubuntu-latest

    container:
      image: returntocorp/semgrep

    steps:
      - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group'
        run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection.

      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.ref }}

      - run: semgrep ci
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}


Snyk

name: Snyk

on:
  merge_group:
  pull_request_target:
    types:
      - opened
      - synchronize
  push:
    branches:
      - master
  schedule:
    - cron: "30 0 1,15 * *"

permissions:
  security-events: write
  actions: read
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

jobs:
  authorize:
    name: Authorize
    environment: ${{ github.actor != 'dependabot[bot]' && github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && 'external' || 'internal' }}
    runs-on: ubuntu-latest
    steps:
      - run: true

  check:
    needs: authorize

    name: Check for Vulnerabilities
    runs-on: ubuntu-latest

    steps:
      - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group'
        run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection.

      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.ref }}

      - uses: snyk/actions/python-3.7@b98d498629f1c368650224d6d212bf7dfa89e4bf # pin@0.4.0
        continue-on-error: true # Make sure the SARIF upload is called
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --sarif-file-output=snyk.sarif

      - name: Upload result to GitHub Code Scanning
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: snyk.sarif


Build and Test

name: Build and Test

on:
  merge_group:
  pull_request_target:
    types:
      - opened
      - synchronize
  push:
    branches:
      - master

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

jobs:
  authorize:
    name: Authorize
    environment: ${{ github.actor != 'dependabot[bot]' && github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && 'external' || 'internal' }}
    runs-on: ubuntu-latest
    steps:
      - run: true

  run:
    needs: authorize # Require approval before running on forked pull requests

    name: Run
    runs-on: ubuntu-latest

    env:
      BUBBLEWRAP_ARGUMENTS: |
        --unshare-all \
        --clearenv \
        --ro-bind / / \
        --bind ${{ github.workspace }} ${{ github.workspace }} \
        --tmpfs $HOME \
        --tmpfs /tmp \
        --tmpfs /var \
        --dev /dev \
        --proc /proc \
        --die-with-parent \
        --new-session \

    strategy:
      matrix:
        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.ref }}

      - name: Configure Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: "${{ matrix.python-version }}"

      - name: Configure dependencies
        run: |
          sudo apt install bubblewrap
          pip install --user --upgrade pip
          pip install --user pipx
          pip install --user setuptools
          pipx ensurepath
          pipx install poetry
          poetry config virtualenvs.in-project true
          poetry install --with dev
          poetry self add "poetry-dynamic-versioning[plugin]==1.1.1"

      - name: Run tests
        run: |
          poetry run pytest --cov=auth0 --cov-report=term-missing:skip-covered --cov-report=xml

      # - name: Run lint
      #   run: |
      #     pipx install black==23.3.0
      #     pipx install flake8==5.0.4
      #     pipx install isort==5.11.5
      #     pipx install pyupgrade==3.3.2
      #     black . --check
      #     flake8 . --count --show-source --statistics
      #     isort .  --diff --profile black
      #     pyupgrade .  --py37-plus --keep-runtime-typing

      - if: ${{ matrix.python-version == '3.10' }}
        name: Upload coverage
        uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # pin@3.1.5


Python package

name: Python package
on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version:
          - "3.7"
          - "3.8"
          - "3.9"
          - "3.10"
          - "3.11"
    services:
      redis:
        image: redis
        options:
          --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v3
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          make develop
      - name: Test with pytest
        run: |
          make testcoverage
      - name: Coveralls
        uses: coverallsapp/github-action@v1
        with:
          path-to-lcov: coverage.lcov


CI

name: CI

on: [push, pull_request]

jobs:
  industrial_ci:
    strategy:
      matrix:
        env:
          - {ROS_DISTRO: noetic}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: 'ros-industrial/industrial_ci@master'
        env: ${{matrix.env}}


name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  # yamllint disable-line rule:line-length
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  ci:
    name: ${{ matrix.name }} py ${{ matrix.python-version }} on ${{ matrix.os }} (${{ matrix.extension }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        python-version:
          - "3.9"
          - "3.10"
          - "3.11"
          - "3.12"
        os:
          - ubuntu-latest
        extension:
          - "skip_cython"
          - "use_cython"
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        id: python
        with:
          python-version: ${{ matrix.python-version }}

      - name: Get pip cache dir
        id: pip-cache
        run: |
          echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
      - name: Restore PIP cache
        uses: actions/cache@v4
        with:
          path: ${{ steps.pip-cache.outputs.dir }}
          key: pip-${{ steps.python.outputs.python-version }}-${{ matrix.extension }}-${{ hashFiles('requirements.txt', 'requirements_test.txt') }}
          restore-keys: |
            pip-${{ steps.python.outputs.python-version }}-${{ matrix.extension }}-
      - name: Set up Python environment (no cython)
        if: ${{ matrix.extension == 'skip_cython' }}
        env:
          SKIP_CYTHON: 1
        run: |
          pip3 install -r requirements.txt -r requirements_test.txt
          pip3 install -e .
      - name: Set up Python environment (cython)
        if: ${{ matrix.extension == 'use_cython' }}
        env:
          REQUIRE_CYTHON: 1
        run: |
          pip3 install -r requirements.txt -r requirements_test.txt
          pip3 install -e .
      - name: Register problem matchers
        run: |
          echo "::add-matcher::.github/workflows/matchers/flake8.json"
          echo "::add-matcher::.github/workflows/matchers/pylint.json"
          echo "::add-matcher::.github/workflows/matchers/isort.json"
          echo "::add-matcher::.github/workflows/matchers/mypy.json"
          echo "::add-matcher::.github/workflows/matchers/pytest.json"

      - run: flake8 aioesphomeapi
        name: Lint with flake8
        if: ${{ matrix.python-version == '3.11' && matrix.extension == 'skip_cython' }}
      - run: pylint aioesphomeapi
        name: Lint with pylint
        if: ${{ matrix.python-version == '3.11' && matrix.extension == 'skip_cython' }}
      - run: black --check --diff --color aioesphomeapi tests
        name: Check formatting with black
        if: ${{ matrix.python-version == '3.11' && matrix.extension == 'skip_cython' }}
      - run: isort --check --diff aioesphomeapi tests
        name: Check import order with isort
        if: ${{ matrix.python-version == '3.11' && matrix.extension == 'skip_cython' }}
      - run: mypy aioesphomeapi
        name: Check typing with mypy
        if: ${{ matrix.python-version == '3.11' && matrix.extension == 'skip_cython' }}
      - run: pytest -vv --cov=aioesphomeapi --cov-report=xml --tb=native tests
        name: Run tests with pytest
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
      - run: |
          docker run \
            -v "$PWD":/aioesphomeapi \
            -u "$(id -u):$(id -g)" \
            ghcr.io/esphome/aioesphomeapi-proto-builder:latest
          if ! git diff --quiet; then
            echo "You have altered the generated proto files but they do not match what is expected."
            echo "Please run the following to update the generated files:"
            echo 'docker run -v "$PWD":/aioesphomeapi ghcr.io/esphome/aioesphomeapi-proto-builder:latest'
            exit 1
          fi
        name: Check protobuf files match
        if: ${{ matrix.python-version == '3.11' && matrix.extension == 'skip_cython' }}


Build docker image on changes

name: Build docker image on changes

on:
  push:
    branches:
      - main
    paths:
      - Dockerfile
      - requirements_test.txt
      - .github/workflows/docker.yml

permissions:
  contents: read
  packages: write

concurrency:
  # yamllint disable-line rule:line-length
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-image:
    runs-on: ubuntu-latest
    name: Build and push Docker image
    steps:
      -
        name: Checkout source code
        uses: actions/checkout@v4
      -
        name: Log in to docker hub
        uses: docker/login-action@v3.2.0
        with:
          username: ${{ secrets.DOCKER_USER }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      -
        name: Log in to the GitHub container registry
        uses: docker/login-action@v3.2.0
        with:
            registry: ghcr.io
            username: ${{ github.actor }}
            password: ${{ secrets.GITHUB_TOKEN }}
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v3.0.0
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3.3.0
      -
        name: Build and Push
        uses: docker/build-push-action@v6.1.0
        with:
          context: .
          tags: |
            ghcr.io/esphome/aioesphomeapi-proto-builder:latest
            esphome/aioesphomeapi-proto-builder:latest
          push: true
          pull: true
          cache-to: type=inline
          cache-from: ghcr.io/esphome/aioesphomeapi-proto-builder:latest
          platforms: linux/amd64,linux/arm64


Release Drafter

name: Release Drafter

on:
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: read

jobs:
  update_release_draft:
    runs-on: ubuntu-latest
    steps:
      - uses: release-drafter/release-drafter@v6
        id: release-draft
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      # Update version number in setup.py
      - uses: actions/checkout@v4
      - name: Calculate version
        id: version
        run: |
          tag="${{ steps.release-draft.outputs.tag_name }}"
          echo "version=${tag:1}" >> $GITHUB_OUTPUT

      - name: Set setup.py version
        run: |
          sed -i "s/VERSION = .*/VERSION = \"${{ steps.version.outputs.version }}\"/g" setup.py
      # github actions email from here: https://github.community/t/github-actions-bot-email-address/17204
      - name: Commit changes
        run: |
          if ! git diff --quiet; then
            git config --global user.name "github-actions[bot]"
            git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
            git commit -am "Bump version to ${{ steps.version.outputs.version }}"
            git push
          fi


Publish Release

name: Publish Release

on:
  release:
    types: [published]

permissions:
  contents: read

jobs:
  build_wheels:
    name: Build wheels on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-22.04]

    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        if: runner.os == 'Linux'
        uses: docker/setup-qemu-action@v3
        with:
          platforms: arm64

      - name: Build wheels
        uses: pypa/cibuildwheel@v2.19.1
        env:
          CIBW_SKIP: cp36-* cp37-* cp38-* cp39-* cp310-* pp* *musllinux*
          CIBW_BEFORE_ALL_LINUX: apt-get install -y gcc || yum install -y gcc || apk add gcc
          CIBW_BUILD_VERBOSITY: 3
          CIBW_ARCHS_LINUX: auto aarch64
          REQUIRE_CYTHON: 1

      - uses: actions/upload-artifact@v3
        with:
          path: ./wheelhouse/*.whl

  build_sdist:
    name: Build source distribution
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build sdist
        run: pipx run build --sdist

      - uses: actions/upload-artifact@v3
        with:
          path: dist/*.tar.gz

  upload_pypi:
    needs: [build_wheels, build_sdist]
    runs-on: ubuntu-latest
    environment: pypi
    permissions:
      id-token: write
    if: github.event_name == 'release' && github.event.action == 'published'
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v3
        with:
          name: artifact
          path: dist

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@v1.9.0


notify

# This GitHub workflow will send a notification to Slack when a scheduled test
# workflow completes.

# There needs to be a GitHub secret named "SLACK_HOOK" that is set to the
# incoming webhook URL for Slack. That webhook is tied to a Slack channel, so
# this workflow does not select the channel and relies on that default.
# Instructions to set up Slack for incoming webhooks are here:
# https://api.slack.com/messaging/webhooks.

name: notify

on:
  workflow_run:
    workflows:
      - test
    types:
      - completed

jobs:

  notify:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.event == 'schedule' }}
    steps:

    - name: Post status to Slack
      run: |
        run_id=$(echo "${{ github.event.workflow_run.url }}" | rev | cut -d '/' -f 1 | rev)
        run_url="https://github.com/${{ github.repository }}/actions/runs/$run_id"
        branch="${{ github.event.workflow_run.head_branch }}"
        branch_url="https://github.com/${{ github.repository }}/commits/$branch"
        repo_url="https://github.com/${{ github.repository }}/actions/workflows/test.yml"
        emoji=$(if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then echo ":white_check_mark:"; else echo ":x:"; fi)
        json="{ \"text\": \"$emoji <$repo_url|${{ github.repository }}>: Scheduled run <$run_url|$run_id> on branch <$branch_url|$branch>: ${{ github.event.workflow_run.conclusion }}\" }"
        curl -s -d "payload=$json" ${{ secrets.SLACK_HOOK }}