Bump version

name: Bump version

on:
  workflow_dispatch:
    inputs:
      part:
        description: 'Semver type of new version (major | minor | patch)'
        required: true
        type: choice
        options:
        - patch
        - minor
        - major
      release_candidate:
        type: boolean
        description: 'Release candidate'
        default: false
      new_version:
        description: 'New version to bump to'
        required: true
        type: string
      target_branch:
        description: 'Branch to push tag to'
        default: 'main'
        required: true
        type: string
      force:
        type: boolean
        description: 'Force override check'
        default: false
      dry_run:
        type: boolean
        description: 'Perform a dry run to check'
        default: true

permissions:
  contents: read

jobs:
  bump-version:
    permissions:
      contents: write  # for Git to git push
    runs-on: ubuntu-latest
    if: github.repository == 'scikit-hep/pyhf'

    steps:
    # Use GitHub PAT to authenticate so other workflows trigger
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        ref: ${{ github.event.inputs.target_branch }}
        fetch-depth: 0
        token: ${{ secrets.ACCESS_TOKEN }}

    - name: Check target branch is intended for release
      if: github.event.inputs.force == 'false'
      shell: bash
      run: |
        git rev-parse --abbrev-ref HEAD | grep 'main\|release/'
        if [ $? -eq 1 ]; then
            echo "ERROR: Branch $(git rev-parse --abbrev-ref HEAD) is not intended for release."
            echo "       Releases are made only from main or release branches."
            exit 1
        fi

    - name: Verify new version bump step is valid
      if: github.event.inputs.force == 'false'
      id: script
      shell: bash
      run: |
        current_tag="$(git describe --tags --abbrev=0)"
        current_tag="${current_tag:1}"

        latest_stable_tag="$(git tag | grep --invert-match 'rc' | tail -n 1)"
        latest_stable_tag="${latest_stable_tag:1}"

        echo "* Current version: ${current_tag}"
        echo "* Latest stable version: ${latest_stable_tag}"

        if [ ${{ github.event.inputs.release_candidate }} == 'true' ]; then
            echo "* Attempting a ${{ github.event.inputs.part }} version release candidate bump from ${current_tag} to: ${{ github.event.inputs.new_version }}"
        else
            # For ease of use, set current tag to latest stable
            current_tag="${latest_stable_tag}"

            echo "* Attempting a ${{ github.event.inputs.part }} version bump from ${current_tag} to: ${{ github.event.inputs.new_version }}"
        fi

        echo "* Validating bump target version matches SemVer..."

        # IFS is single charecter, so split on the 'r' in "rc"
        IFS='r' read current_tag_read current_rc <<EOF
        ${current_tag}
        EOF
        current_rc="${current_rc:1}"

        IFS='.' read current_major current_minor current_patch <<EOF
        ${current_tag_read}
        EOF
        # current_tag_read is read only and no longer used
        unset current_tag_read

        # IFS is single charecter, so split on the 'r' in "rc"
        IFS='r' read bump_version bump_rc <<EOF
        ${{ github.event.inputs.new_version }}
        EOF
        bump_rc="${bump_rc:1}"

        # bump_version is a read only variable
        IFS='.' read bump_major bump_minor bump_patch <<EOF
        ${bump_version}
        EOF
        unset bump_version

        # Check release candidates are valid before proceeding
        if [ ${{ github.event.inputs.release_candidate }} == 'true' ]; then
            if [ -z "${current_rc}" ]; then
                current_rc=0
            fi
            if [ "${bump_rc}" != "$((${current_rc} + 1))" ]; then
                echo "ERROR: ${{ github.event.inputs.new_version }} is more than 1 release candidate version greater then ${current_tag}"
                exit 1
            fi
        else
            if [ ! -z "${bump_rc}" ]; then
                echo "ERROR: ${{ github.event.inputs.new_version }} contains a release candidate signature rc${bump_rc} but was marked as stable release."
                exit 1
            fi
        fi

        if [ ${{ github.event.inputs.part }} == "major" ]; then
            # Minor version should be zero
            if [ "${bump_minor}" != "0" ]; then
                echo "ERROR: ${{ github.event.inputs.part }} release attempted, ${{ github.event.inputs.new_version }} minor version should equal 0."
                exit 1
            fi
            # Patch version should be zero
            if [ "${bump_patch}" != "0" ]; then
                echo "ERROR: ${{ github.event.inputs.part }} release attempted, ${{ github.event.inputs.new_version }} patch version should equal 0."
                exit 1
            fi
            if [ "${bump_major}" != "$((${current_major} + 1))" ]; then
                if ! ([ "${bump_major}" == "${current_major}" ] && [ ${{ github.event.inputs.release_candidate }} == 'true' ]); then
                    echo "ERROR: ${{ github.event.inputs.part }} release candidate release attempted, but ${{ github.event.inputs.new_version }} is more than 1 ${{ github.event.inputs.part }} version greater then ${current_tag}."
                    exit 1
                fi
            fi
        fi

        if [ ${{ github.event.inputs.part }} == "minor" ]; then
            # Major versions should be equal
            if [ "${bump_major}" != "${current_major}" ]; then
                echo "ERROR: ${{ github.event.inputs.part }} release attempted, but ${{ github.event.inputs.new_version }} major version not equal to ${current_tag}."
                exit 1
            fi
            # Patch version should be zero
            if [ "${bump_patch}" != "0" ]; then
                echo "ERROR: ${{ github.event.inputs.part }} release attempted, ${{ github.event.inputs.new_version }} patch version should equal 0."
                exit 1
            fi
            if [ "${bump_minor}" != "$((${current_minor} + 1))" ]; then
                if ! ([ "${bump_minor}" == "${current_minor}" ] && [ ${{ github.event.inputs.release_candidate }} == 'true' ]); then
                    echo "ERROR: ${{ github.event.inputs.part }} release candidate release attempted, but ${{ github.event.inputs.new_version }} is more than 1 ${{ github.event.inputs.part }} version greater then ${current_tag}."
                    exit 1
                fi
            fi
        fi

        if [ ${{ github.event.inputs.part }} == "patch" ]; then
            # Major versions should be equal
            if [ "${bump_major}" != "${current_major}" ]; then
                echo "ERROR: ${{ github.event.inputs.part }} release attempted, but ${{ github.event.inputs.new_version }} major version not equal to ${current_tag}."
                exit 1
            fi
            # Minor versions should be equal
            if [ "${bump_minor}" != "${current_minor}" ]; then
                echo "ERROR: ${{ github.event.inputs.part }} release attempted, but ${{ github.event.inputs.new_version }} minor version not equal to ${current_tag}."
                exit 1
            fi
            if [ "${bump_patch}" != "$((${current_patch} + 1))" ]; then
                if ! ([ "${bump_patch}" == "${current_patch}" ] && [ ${{ github.event.inputs.release_candidate }} == 'true' ]); then
                    echo "ERROR: ${{ github.event.inputs.part }} release candidate release attempted, but ${{ github.event.inputs.new_version }} is more than 1 ${{ github.event.inputs.part }} version greater then ${current_tag}."
                    exit 1
                fi
            fi
        fi

        echo "  ...version bump validated!"
        if [ ${{ github.event.inputs.release_candidate }} == 'true' ]; then
            echo "* Bumping version ${current_tag} to ${{ github.event.inputs.part }} version release candidate ${{ github.event.inputs.new_version }}"
        else
            echo "* Bumping version ${current_tag} to ${{ github.event.inputs.part }} version ${{ github.event.inputs.new_version }}"
        fi

        echo "steps.script.outputs.old_tag=v${current_tag}"
        echo "old_tag=v${current_tag}" >> $GITHUB_OUTPUT

    - name: Set up Python 3.12
      if: success()
      uses: actions/setup-python@v5
      with:
        python-version: '3.12'

    - name: Install Python dependencies
      run: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip install --system tbump
        python -m pip list

    - name: Setup Git user to push new tag
      run: |
        git config --local user.email "action@github.com"
        git config --local user.name "GitHub Action"

    - name: Bump version and push to GitHub
      if: >-
        github.event_name == 'workflow_dispatch'
        && (
          github.event.sender.login == 'lukasheinrich' ||
          github.event.sender.login == 'matthewfeickert' ||
          github.event.sender.login == 'kratsg'
        )
      shell: bash
      run: |
        tbump --non-interactive --no-push ${{ github.event.inputs.new_version }}

    - name: Update the Git tag annotation
      if: ${{ github.event.inputs.dry_run }} == 'false'
      shell: bash
      run: |
        OLD_TAG=${{ steps.script.outputs.old_tag }}
        git tag -n99 --list "${OLD_TAG}"

        NEW_TAG=v${{ github.event.inputs.new_version }}
        git tag -n99 --list "${NEW_TAG}"

        CHANGES=$(git log --pretty=format:'%s' "${OLD_TAG}"..HEAD --regexp-ignore-case --extended-regexp --grep='^([a-z]*?):')
        # Also include any backported changes
        BACKPORTED_CHANGES=$(git log --pretty=format:'%s' "${OLD_TAG}"..HEAD --grep='(backport):')
        if [ ! -z "${BACKPORTED_CHANGES}" ]; then
            CHANGES=$(printf "${CHANGES}\n${BACKPORTED_CHANGES}")
        fi

        CHANGES_NEWLINE="$(echo "${CHANGES}" | sed -e 's/^/  - /')"
        SANITIZED_CHANGES=$(echo "${CHANGES}" | sed -e 's/^/<li>/' -e 's|$|</li>|' -e 's/(#[0-9]\+)//' -e 's/"/'"'"'/g')
        NUM_CHANGES=$(echo -n "${CHANGES}" | grep -c '^')

        if [ ${{ github.event.inputs.release_candidate }} == 'true' ]; then
            git tag "${NEW_TAG}" "${NEW_TAG}"^{} -f -m "$(printf "This is a ${{ github.event.inputs.part }} release candidate from ${OLD_TAG} → ${NEW_TAG}.\n\nChanges:\n${CHANGES_NEWLINE}")"
        else
            git tag "${NEW_TAG}" "${NEW_TAG}"^{} -f -m "$(printf "This is a ${{ github.event.inputs.part }} release from ${OLD_TAG} → ${NEW_TAG}.\n\nChanges:\n${CHANGES_NEWLINE}")"
        fi

        git tag -n99 --list "${NEW_TAG}"

    - name: Show annotated Git tag
      shell: bash
      run: |
        git show v${{ github.event.inputs.new_version }}

    - name: Push new tag back to GitHub
      shell: bash
      run: |
        if [ ${{ github.event.inputs.dry_run }} == 'true' ]; then
            echo "# DRY RUN"
        else
            git push origin ${{ github.event.inputs.target_branch }} --tags
        fi


CI on Windows

name: CI on Windows

on:
  # Run daily at 1:23 UTC
  schedule:
  - cron:  '23 1 * * *'
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  test:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [windows-latest]
        python-version: ['3.9', '3.10', '3.11', '3.12']

    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: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip install --system --upgrade '.[all,test]'

    - name: List installed Python packages
      run: python -m pip list

    - name: Test with pytest and coverage
      # FIXME: ignore tests/test_scripts.py as runner breaks on Windows currently
      run: |
        coverage run --module pytest --ignore tests/test_scripts.py --ignore tests/contrib --ignore tests/benchmarks --ignore tests/test_notebooks.py

    - name: Coverage report for core project
      run: |
        coverage report
        coverage xml


CI/CD

name: CI/CD

on:
  push:
  pull_request:
    branches:
    - main
    - release/v*
  # Run daily at 0:01 UTC
  schedule:
  - cron:  '1 0 * * *'
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  test:

    runs-on: ${{ matrix.os }}
    # On push events run the CI only on main by default, but run on any branch if the commit message contains '[ci all]'
    if: >-
      github.event_name != 'push'
      || (github.event_name == 'push' && github.ref == 'refs/heads/main')
      || (github.event_name == 'push' && github.ref != 'refs/heads/main' && contains(github.event.head_commit.message, '[ci all]'))
    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
        include:
          - os: macos-latest
            python-version: '3.12'
          # Intel runner
          - os: macos-13
            python-version: '3.12'

    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
      if: matrix.python-version != '3.8'
      run: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip install --system --upgrade ".[all,test]"

      # c.f. https://github.com/astral-sh/uv/issues/2062
    - name: Install dependencies (Python 3.8)
      if: matrix.python-version == '3.8'
      run: |
        python -m pip install --upgrade pip wheel
        python -m pip install --upgrade ".[all,test]"

    - name: List installed Python packages
      run: python -m pip list

    - name: Test with pytest and coverage
      run: |
        coverage run --module pytest --ignore tests/contrib --ignore tests/benchmarks --ignore tests/test_notebooks.py

    - name: Launch a tmate session if tests fail
      if: failure() && github.event_name == 'workflow_dispatch'
      uses: mxschmitt/action-tmate@v3

    - name: Coverage report for core project
      run: |
        coverage report
        coverage xml

      # Report coverage for oldest and newest Python tested to deal with version differences
    - name: Report core project coverage with Codecov
      if: >-
        github.event_name != 'schedule' &&
        matrix.os == 'ubuntu-latest'
      uses: codecov/codecov-action@v4
      with:
        fail_ci_if_error: true
        files: ./coverage.xml
        flags: unittests-${{ matrix.python-version }}
        token: ${{ secrets.CODECOV_TOKEN }}

    - name: Test Contrib module with pytest
      run: |
        coverage run --append --module pytest tests/contrib --mpl --mpl-baseline-path tests/contrib/baseline

    - name: Coverage report with contrib
      run: |
        coverage report
        coverage xml

    - name: Report contrib coverage with Codecov
      if: github.event_name != 'schedule' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
      uses: codecov/codecov-action@v4
      with:
        fail_ci_if_error: true
        files: ./coverage.xml
        flags: contrib
        token: ${{ secrets.CODECOV_TOKEN }}

    - name: Test docstring examples with doctest
      # TODO: Don't currently try to match amd64 and arm64 floating point for docs, but will in the future.
      if: matrix.python-version == '3.12' && matrix.os != 'macos-latest'
      run: coverage run --data-file=.coverage-doctest --module pytest src/ README.rst

    - name: Coverage report for doctest only
      if: matrix.python-version == '3.12' && matrix.os != 'macos-latest'
      run: |
        coverage report --data-file=.coverage-doctest
        coverage xml --data-file=.coverage-doctest -o doctest-coverage.xml

    - name: Report doctest coverage with Codecov
      if: github.event_name != 'schedule' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
      uses: codecov/codecov-action@v4
      with:
        fail_ci_if_error: true
        files: doctest-coverage.xml
        flags: doctest
        token: ${{ secrets.CODECOV_TOKEN }}

    - name: Run benchmarks
      if: github.event_name == 'schedule' && matrix.python-version == '3.12'
      run: |
        pytest --benchmark-sort=mean tests/benchmarks/test_benchmark.py


CodeQL

name: "CodeQL"

on:
  push:
    branches: [main]
  pull_request:
    # branches must be a subset of push branches
    branches: [main]
  # Run weekly on Sundays at 0:01 UTC
  schedule:
    - cron: '1 0 * * 0'
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  analyze:
    permissions:
      actions: read  # for github/codeql-action/init to get workflow details
      contents: read  # for actions/checkout to fetch code
      security-events: write  # for github/codeql-action/analyze to upload SARIF results
    name: Analyze
    runs-on: ubuntu-latest

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

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: python
        # 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

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


HEAD of dependencies

name: HEAD of dependencies

on:
  # Run daily at 0:01 UTC
  schedule:
  - cron:  '1 0 * * *'
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  release-candidates:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, macos-13]
        python-version: ['3.12']

    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: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip --no-cache --quiet install --system --upgrade --pre ".[all,test]"
        python -m pip list

    - name: List release candidates, alpha, and beta releases
      run: |
        python -m pip list | egrep '[0-9](rc|[ab])[0-9]'

    - name: Test with pytest
      run: |
        pytest --ignore tests/contrib --ignore tests/benchmarks --ignore tests/test_notebooks.py

  scipy:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ['3.12']

    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: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip --no-cache --quiet install --system --upgrade ".[all,test]"
        uv pip uninstall --system scipy
        # uv wants to upgrade dependencies (numpy) to a dev release too, so don't --upgrade
        uv pip install --system --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple scipy
        python -m pip list

    - name: Test with pytest
      run: |
        pytest --ignore tests/contrib --ignore tests/benchmarks --ignore tests/test_notebooks.py

  iminuit:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ['3.12']

    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: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip --no-cache --quiet install --system --upgrade ".[all,test]"
        uv pip uninstall --system iminuit
        uv pip install --system --upgrade cython
        uv pip install --system --upgrade git+https://github.com/scikit-hep/iminuit.git
        python -m pip list
    - name: Test with pytest
      run: |
        pytest --ignore tests/contrib --ignore tests/benchmarks --ignore tests/test_notebooks.py

  uproot5:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ['3.12']

    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: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip --no-cache --quiet install --system --upgrade ".[all,test]"
        uv pip uninstall --system uproot
        uv pip install --system --upgrade git+https://github.com/scikit-hep/uproot5.git
        python -m pip list
    - name: Test with pytest
      run: |
        pytest --ignore tests/contrib --ignore tests/benchmarks --ignore tests/test_notebooks.py

  matplotlib:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ['3.12']

    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: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip --no-cache --quiet install --system --upgrade ".[all,test]"
        uv pip uninstall --system matplotlib
        # Need to use --extra-index-url as all dependencies aren't on scientific-python-nightly-wheels package index.
        # Need to use --pre as dev releases will need priority over stable releases.
        # Note that uv and pip differ on --extra-index-url priority
        # c.f. https://github.com/scientific-python/upload-nightly-action/issues/76
        uv pip install --system \
          --pre \
          --index-url https://pypi.org/simple/ \
          --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \
          matplotlib

    - name: List installed Python packages
      run: python -m pip list

    - name: Test contrib module API with pytest
      run: |
        pytest tests/contrib

  pytest:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ['3.12']

    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: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip --no-cache --quiet install --system --upgrade ".[all,test]"
        uv pip uninstall --system pytest
        uv pip install --system --upgrade git+https://github.com/pytest-dev/pytest.git
        python -m pip list
    - name: Test with pytest
      run: |
        pytest --ignore tests/contrib --ignore tests/benchmarks --ignore tests/test_notebooks.py


Docker Images

name: Docker Images

on:
  push:
    branches:
    - main
    tags:
    - v*
  pull_request:
    branches:
    - main
  schedule:
    - cron:  '1 0 * * *'
  release:
    types: [published]
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  docker:
    name: Build, test, and publish Docker images to Docker Hub
    permissions:
      contents: read
      packages: write  # for docker to push to registry
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Prepare
        id: prep
        run: |
          DOCKER_IMAGE=pyhf/pyhf
          VERSION=latest
          if [[ $GITHUB_REF == refs/tags/* ]]; then
            VERSION=${GITHUB_REF_NAME}
          elif [[ $GITHUB_REF == refs/pull/* ]]; then
            VERSION=pr-${{ github.event.number }}
          fi
          TAGS="${DOCKER_IMAGE}:${VERSION}"
          TAGS="$TAGS,${DOCKER_IMAGE}:latest,${DOCKER_IMAGE}:sha-${GITHUB_SHA::8}"
          # Releases also have GITHUB_REFs that are tags, so reuse VERSION
          if [ "${{ github.event_name }}" = "release" ]; then
            TAGS="$TAGS,${DOCKER_IMAGE}:latest-stable,ghcr.io/${{github.repository}}:latest-stable,ghcr.io/${{github.repository}}:${VERSION}"
          fi
          echo "steps.prep.outputs.version=${VERSION}"
          echo "version=${VERSION}" >> $GITHUB_OUTPUT
          echo "steps.prep.outputs.tags=${TAGS}"
          echo "tags=${TAGS}" >> $GITHUB_OUTPUT
          echo "steps.prep.outputs.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
          echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to DockerHub
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Test build
        id: docker_build_test
        uses: docker/build-push-action@v6
        with:
          context: .
          file: docker/Dockerfile
          tags: ${{ steps.prep.outputs.tags }}
          labels: |
            org.opencontainers.image.source=${{ github.event.repository.html_url }}
            org.opencontainers.image.created=${{ steps.prep.outputs.created }}
            org.opencontainers.image.revision=${{ github.sha }}
          load: true
          push: false

      - name: Image digest
        run: echo ${{ steps.docker_build_test.outputs.digest }}

      - name: List built images
        run: docker images

      - name: Run CLI API check
        run: |
          printf "\npyhf\n"
          docker run --rm pyhf/pyhf:sha-${GITHUB_SHA::8}
          printf "\npyhf --version\n"
          docker run --rm pyhf/pyhf:sha-${GITHUB_SHA::8} --version
          printf "\npyhf --help\n"
          docker run --rm pyhf/pyhf:sha-${GITHUB_SHA::8} --help

      - name: Check for curl and tar
        run: >-
          docker run --rm
          --entrypoint /bin/bash
          pyhf/pyhf:sha-${GITHUB_SHA::8}
          -c "which curl; which tar"

      - name: Build and publish to registry
        # every PR will trigger a push event on main, so check the push event is actually coming from main
        if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'scikit-hep/pyhf'
        id: docker_build_latest
        uses: docker/build-push-action@v6
        with:
          context: .
          file: docker/Dockerfile
          tags: |
            pyhf/pyhf:latest
            ghcr.io/${{ github.repository }}:latest
          labels: |
            org.opencontainers.image.source=${{ github.event.repository.html_url }}
            org.opencontainers.image.created=${{ steps.prep.outputs.created }}
            org.opencontainers.image.revision=${{ github.sha }}
          push: true
          platforms: linux/amd64,linux/arm64

      - name: Build and publish to registry with release tag
        if: github.event_name == 'release' && github.event.action == 'published' && github.repository == 'scikit-hep/pyhf'
        id: docker_build_release
        uses: docker/build-push-action@v6
        with:
          context: .
          file: docker/Dockerfile
          tags: ${{ steps.prep.outputs.tags }}
          labels: |
            org.opencontainers.image.source=${{ github.event.repository.html_url }}
            org.opencontainers.image.created=${{ steps.prep.outputs.created }}
            org.opencontainers.image.revision=${{ github.sha }}
          push: true
          platforms: linux/amd64,linux/arm64


Docs

name: Docs

on:
  push:
    branches:
    - main
  pull_request:
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  build:
    name: Build docs
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.12'

    - name: Install Python dependencies
      run: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip --quiet install --system --upgrade ".[docs,test]"
        uv pip install --system yq
        python -m pip list

    - name: Install apt-get dependencies
      run: |
        sudo apt-get update
        # Ubuntu 22.04's pandoc is too old (2.9.2.x), so install manually
        # until the ubuntu-latest updates.
        curl --silent --location --remote-name https://github.com/jgm/pandoc/releases/download/3.1.6.2/pandoc-3.1.6.2-1-amd64.deb
        sudo apt-get install ./pandoc-*amd64.deb

    - name: Check docstrings
      run: |
        # Group 1 is related to docstrings
        pydocstyle --select D1 src/pyhf/pdf.py \
                               src/pyhf/workspace.py \
                               src/pyhf/probability.py \
                               src/pyhf/patchset.py \
                               src/pyhf/compat.py \
                               src/pyhf/interpolators \
                               src/pyhf/infer \
                               src/pyhf/optimize \
                               src/pyhf/contrib \
                               src/pyhf/cli

    - name: Verify CITATION.cff schema
      run: |
        jsonschema <(curl -sL "https://citation-file-format.github.io/1.2.0/schema.json") --instance <(cat CITATION.cff | yq)

    - name: Check for broken links
      run: |
        pushd docs
        make linkcheck
        # Don't ship the linkcheck
        rm -r _build/linkcheck
        popd

    - name: Test and build docs
      run: |
        python -m doctest README.rst
        pushd docs
        make html

    - name: Check schemas are copied over
      run: |
        # is a directory
        [ -d "docs/_build/html/schemas" ]
        # is not a symlink
        [ ! -L "docs/_build/html/schemas" ]
        # is not empty
        [ "$(ls -A docs/_build/html/schemas)" ]
        # is not empty
        [ "$(ls -A docs/_build/html/lite)" ]

    - name: Fix permissions if needed
      run: |
        chmod -c -R +rX "docs/_build/html/" | while read line; do
          echo "::warning title=Invalid file permissions automatically fixed::$line"
        done

    - name: Upload artifact
      uses: actions/upload-pages-artifact@v3
      with:
        path: 'docs/_build/html'

  deploy:
    name: Deploy docs to GitHub Pages
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: build
    # Set permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
    permissions:
      contents: read
      pages: write
      id-token: write

    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}

    runs-on: ubuntu-latest

    steps:
    - name: Setup Pages
      uses: actions/configure-pages@v5

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


Lint

name: Lint

on:
  pull_request:
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  lint:

    name: Lint Codebase
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Lint Dockerfile
      uses: hadolint/hadolint-action@v3.1.0
      with:
        dockerfile: docker/Dockerfile


Minimum supported dependencies

name: Minimum supported dependencies

on:
  # Run daily at 0:01 UTC
  schedule:
  - cron:  '1 0 * * *'
  workflow_dispatch:

permissions:
  contents: read

jobs:
  test:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
        # minimum supported Python
        python-version: ['3.8']

    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 and force lowest bound
      run: |
        python -m pip install uv
        uv pip install --system --upgrade pip 'setuptools<70.0.0' wheel
        uv pip --no-cache install --system --constraint tests/constraints.txt ".[all,test]"

    - name: List installed Python packages
      run: python -m pip list

    - name: Test with pytest
      run: |
        # Override the ini option for filterwarnings with an empty list to disable error on filterwarnings
        # as testing for oldest releases that work with latest API, not the oldest releases that are warning
        # free. Though still show warnings by setting warning control to 'default'.
        export PYTHONWARNINGS='default'
        # Run on tests/ to skip doctests of src given examples are for latest APIs
        pytest --override-ini filterwarnings= --ignore tests/contrib --ignore tests/benchmarks --ignore tests/test_notebooks.py tests/


Merged PR

name: Merged PR

on:
  pull_request:
    types: [closed]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  binder:
    name: Trigger Binder build
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged
    steps:
    - uses: actions/checkout@v4
    - name: Trigger Binder build
      run: |
        # Use Binder build API to trigger repo2docker to build image on Google Cloud and Turing Institute Binder Federation clusters
        bash binder/trigger_binder.sh https://turing.mybinder.org/build/gh/scikit-hep/pyhf/main


Notebooks

name: Notebooks

on:
  # Run daily at 0:01 UTC
  schedule:
  - cron:  '1 0 * * *'
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  test:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.12']

    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 uv
        uv pip install --system --upgrade pip wheel
        # FIXME: c.f. https://github.com/scikit-hep/pyhf/issues/2104
        uv pip install --system --upgrade ".[all,test]" 'jupyter-client<8.0.0'

    - name: List installed Python packages
      run: python -m pip list

    - name: Test example notebooks
      run: |
        # Override the ini option for filterwarnings with an empty list to disable error
        # on filterwarnings as testing for notebooks to run with the latest API, not if
        # Jupyter infrastructure is warning free.
        pytest --verbose --override-ini filterwarnings= tests/test_notebooks.py


publish distributions

name: publish distributions
on:
  push:
    branches:
    - main
    tags:
    - v*
  pull_request:
    branches:
    - main
    - release/v*
  release:
    types: [published]
  # Run weekly at 1:23 UTC
  schedule:
  - cron:  '23 1 * * 0'
  workflow_dispatch:
    inputs:
      publish:
        type: boolean
        description: 'Publish to TestPyPI'
        default: false

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

permissions:
  contents: read

jobs:
  build:
    name: Build Python distribution
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      attestations: write

    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Set up Python 3.12
      uses: actions/setup-python@v5
      with:
        python-version: '3.12'

    - name: Install python-build and twine
      run: |
        python -m pip install uv
        uv pip install --system --upgrade pip
        uv pip install --system build twine
        python -m pip list

    - name: Build a sdist and wheel
      if: github.event_name != 'schedule'
      run: |
        python -m build --installer uv .

    - name: Build a sdist and wheel and check for warnings
      if: github.event_name == 'schedule'
      run: |
        PYTHONWARNINGS=error,default::DeprecationWarning python -m build --installer uv .

    - name: Verify untagged commits have dev versions
      if: "!startsWith(github.ref, 'refs/tags/')"
      run: |
        latest_tag=$(git describe --tags)
        latest_tag_revlist_SHA=$(git rev-list -n 1 ${latest_tag})
        main_SHA="$(git rev-parse --verify origin/main)"
        wheel_name=$(find dist/ -iname "*.whl" -printf "%f\n")
        if [[ "${latest_tag_revlist_SHA}" != "${main_SHA}" ]]; then # don't check main push events coming from tags
          if [[ "${wheel_name}" == *"pyhf-0.1.dev"* || "${wheel_name}" != *"dev"* ]]; then
            echo "python-build incorrectly named built distribution: ${wheel_name}"
            echo "python-build is lacking the history and tags required to determine version number"
            echo "intentionally erroring with 'return 1' now"
            return 1
          fi
        else
          echo "Push event to origin/main was triggered by push of tag ${latest_tag}"
        fi
        echo "python-build named built distribution: ${wheel_name}"

    - name: Verify tagged commits don't have dev versions
      if: startsWith(github.ref, 'refs/tags')
      run: |
        wheel_name=$(find dist/ -iname "*.whl" -printf "%f\n")
        if [[ "${wheel_name}" == *"dev"* ]]; then
          echo "python-build incorrectly named built distribution: ${wheel_name}"
          echo "this is incorrrectly being treated as a dev release"
          echo "intentionally erroring with 'return 1' now"
          return 1
        fi
        echo "python-build named built distribution: ${wheel_name}"

    - name: Verify the distribution
      run: twine check --strict dist/*

    - name: List contents of sdist
      run: python -m tarfile --list dist/pyhf-*.tar.gz

    - name: List contents of wheel
      run: python -m zipfile --list dist/pyhf-*.whl

    - name: Generate artifact attestation for sdist and wheel
      # If publishing to TestPyPI or PyPI
      if: >-
        (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.repository == 'scikit-hep/pyhf')
        || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true' && github.repository == 'scikit-hep/pyhf')
        || (github.event_name == 'release' && github.event.action == 'published' && github.repository == 'scikit-hep/pyhf')
      uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2
      with:
        subject-path: "dist/pyhf-*"

    - name: Upload distribution artifact
      uses: actions/upload-artifact@v4
      with:
        name: dist-artifact
        path: dist

  publish:
    name: Publish Python distribution to (Test)PyPI
    if: github.event_name != 'pull_request'
    needs: build
    runs-on: ubuntu-latest
    # Mandatory for publishing with a trusted publisher
    # c.f. https://docs.pypi.org/trusted-publishers/using-a-publisher/
    permissions:
      id-token: write
    # Restrict to the environment set for the trusted publisher
    environment:
      name: publish-package

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

    - name: List all files
      run: ls -lh dist

    - name: Verify sdist artifact attestation
      # If publishing to TestPyPI or PyPI
      if: >-
        (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.repository == 'scikit-hep/pyhf')
        || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true' && github.repository == 'scikit-hep/pyhf')
        || (github.event_name == 'release' && github.event.action == 'published' && github.repository == 'scikit-hep/pyhf')
      env:
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: gh attestation verify dist/pyhf-*.tar.gz --repo ${{ github.repository }}

    - name: Verify wheel artifact attestation
      # If publishing to TestPyPI or PyPI
      if: >-
        (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.repository == 'scikit-hep/pyhf')
        || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true' && github.repository == 'scikit-hep/pyhf')
        || (github.event_name == 'release' && github.event.action == 'published' && github.repository == 'scikit-hep/pyhf')
      env:
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: gh attestation verify dist/pyhf-*.whl --repo ${{ github.repository }}

    - name: Publish distribution 📦 to Test PyPI
      # Publish to TestPyPI on tag events of if manually triggered
      # Compare to 'true' string as booleans get turned into strings in the console
      if: >-
        (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.repository == 'scikit-hep/pyhf')
        || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true' && github.repository == 'scikit-hep/pyhf')
      uses: pypa/gh-action-pypi-publish@v1.9.0
      with:
        repository-url: https://test.pypi.org/legacy/
        print-hash: true

    - name: Publish distribution 📦 to PyPI
      if: github.event_name == 'release' && github.event.action == 'published' && github.repository == 'scikit-hep/pyhf'
      uses: pypa/gh-action-pypi-publish@v1.9.0
      with:
        print-hash: true


Current Release

name: Current Release

on:
  # Run daily at 0:01 UTC
  schedule:
  - cron:  '1 0 * * *'
  workflow_dispatch:

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

permissions:
  contents: read

jobs:

  pypi_release:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
        include:
          - os: macos-latest
            python-version: '3.12'
          # Intel runner
          - os: macos-13
            python-version: '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 from PyPI
      run: |
        python -m pip install uv
        uv pip install --system --upgrade pip wheel
        uv pip install --system --pre 'pyhf[backends,xmlio]'
        uv pip install --system pytest
        python -m pip list

    - name: Canary test public API
      run: |
        # Override the ini option for filterwarnings with an empty list to disable error on filterwarnings
        # as testing the latest release API still works, not the release is warning free.
        # Though still show warnings by setting warning control to 'default'.
        export PYTHONWARNINGS='default'
        pytest tests/test_public_api.py


Scorecard supply-chain security

# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.

name: Scorecard supply-chain security
on:
  # For Branch-Protection check. Only the default branch is supported. See
  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
  branch_protection_rule:
  # To guarantee Maintained check is occasionally updated. See
  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
  schedule:
    - cron: '23 3 * * 0'
  push:
    branches: [ "main" ]

# Declare default permissions as read only.
permissions: read-all

jobs:
  analysis:
    name: Scorecard analysis
    runs-on: ubuntu-latest
    permissions:
      # Needed to upload the results to code-scanning dashboard.
      security-events: write
      # Needed to publish results and get a badge (see publish_results below).
      id-token: write
      # Uncomment the permissions below if installing in a private repository.
      # contents: read
      # actions: read

    steps:
      - name: "Checkout code"
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          persist-credentials: false

      - name: "Run analysis"
        uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
        with:
          results_file: results.sarif
          results_format: sarif
          # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
          # - you want to enable the Branch-Protection check on a *public* repository, or
          # - you are installing Scorecard on a *private* repository
          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
          # repo_token: ${{ secrets.SCORECARD_TOKEN }}

          # Public repositories:
          #   - Publish results to OpenSSF REST API for easy access by consumers
          #   - Allows the repository to include the Scorecard badge.
          #   - See https://github.com/ossf/scorecard-action#publishing-results.
          # For private repositories:
          #   - `publish_results` will always be set to `false`, regardless
          #     of the value entered here.
          publish_results: true

      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
      # format to the repository Actions tab.
      - name: "Upload artifact"
        uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20
        with:
          name: SARIF file
          path: results.sarif
          retention-days: 5

      # Upload the results to GitHub's code scanning dashboard (optional).
      # Commenting out will disable upload of results to your repo's Code Scanning dashboard
      - name: "Upload to code-scanning"
        uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
        with:
          sarif_file: results.sarif


Semantic Pull Request

name: Semantic Pull Request

on:
  pull_request_target:
    types:
      - opened
      - edited
      - synchronize

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

permissions:
  contents: read

jobs:
  main:

    permissions:
      pull-requests: read  # for amannn/action-semantic-pull-request to analyze PRs
      statuses: write  # for amannn/action-semantic-pull-request to mark status of analyzed PR
    name: Validate PR title
    runs-on: ubuntu-latest

    steps:
      - name: Check PR title matches Conventional Commits spec
        uses: amannn/action-semantic-pull-request@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


CI

name: CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - "**" # run on all branches

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"
          cache-dependency-path: |
            requirements/production.txt
            requirements/testing.txt
            .pre-commit-config.yaml
      - name: Install dependencies
        run: |
          pip install -r requirements/testing.txt
      - name: CI
        run: |
          tox
      - name: Upload coverage artifact
        uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ matrix.python-version }}
          path: .coverage

  coverage:
    name: Process test coverage
    runs-on: ubuntu-latest
    needs: ["build"]
    strategy:
      matrix:
        python-version: ["3.12"]
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"
          cache-dependency-path: |
            requirements/production.txt
            requirements/testing.txt
            .pre-commit-config.yaml
      - name: Install dependencies
        run: |
          pip install -r requirements/testing.txt
      - name: Download all coverage artifacts
        uses: actions/download-artifact@v4
      - name: Create coverage report
        run: |
          coverage combine coverage-*/.coverage
          coverage report --fail-under=94
          coverage xml
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        env:
          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}


Pre-commit auto-update

name: Pre-commit auto-update

on:
  schedule:
    - cron: "0 8 15 * *"
  # on demand
  workflow_dispatch:

jobs:
  auto-update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.x"
          cache: "pip"
          cache-dependency-path: |
            requirements/production.txt
            requirements/testing.txt
            .pre-commit-config.yaml
      - name: Install dependencies
        # needed for the mypy hook
        run: |
          pip install -r requirements/testing.txt
      - name: Pre-commit auto-update
        env:
          # new branch is created in create-pull-request step
          SKIP: no-commit-to-branch
        run: |
          pre-commit autoupdate
      - uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          branch: update/pre-commit-hooks
          title: Update pre-commit hooks
          commit-message: "update pre-commit hooks"
          labels: |
            dependencies
          body: |
            Update versions of pre-commit hooks to latest version.
            Run tests locally to check for conflicts since PRs from GitHub Actions don't trigger workflows.


Upload Python Package

# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: Upload Python Package

on:
  release:
    types: [published]

jobs:
  deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.x'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        python -m pip install build twine
    - name: Build and publish
      env:
        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
      run: |
        python -m build
        twine upload dist/*


Stale

name: Stale

# yamllint disable-line rule:truthy
on:
  schedule:
    - cron: "0 9 * * 5"
  workflow_dispatch:

jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
      # The 90 day stale policy
      # Used for:
      # - Issues & PRs
      # - No PRs marked as no-stale
      # - No issues marked as no-stale or help-wanted
      - name: 90 days stale issues & PRs policy
        uses: actions/stale@v9
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          days-before-stale: 90
          days-before-close: 7
          operations-per-run: 150
          remove-stale-when-updated: true
          stale-issue-label: "stale"
          exempt-issue-labels: "no-stale,💡 feature request,🙋♂ help wanted,🏗 in progress,👩💻 needs testing,🐜 bug"
          stale-issue-message: >
            This issue has been automatically marked as stale because it has not had
            recent activity. It will be closed if no further activity occurs.
            Please make sure to update to the latest version of xknx (or Home Assistant) and
            check if that solves the issue. Let us know if that works for you by
            adding a comment 👍
            This issue has now been marked as stale and will be closed if no
            further activity occurs. Thank you for your contributions.
          stale-pr-label: "stale"
          exempt-pr-labels: "no-stale,🙋♂ help wanted,🏗 in progress,👩💻 needs testing,🐜 bug"
          stale-pr-message: >
            There hasn't been any activity on this pull request recently. This
            pull request has been automatically marked as stale because of that
            and will be closed if no further activity occurs within 7 days.
            Thank you for your contributions.


Publish distributions to PyPI and TestPyPI


CI

name: CI

on:
  push:
    branches:
      - dev
      - master
  pull_request: ~

jobs:
  shared-ci:
    uses: zigpy/workflows/.github/workflows/ci.yml@main
    with:
      CODE_FOLDER: zhaquirks
      CACHE_VERSION: 3
      PYTHON_VERSION_DEFAULT: 3.12
      PRE_COMMIT_CACHE_PATH:  ~/.cache/pre-commit
      MINIMUM_COVERAGE_PERCENTAGE: 80
      PYTHON_MATRIX: "3.12"
    secrets:
      CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}


Publish distributions to PyPI

name: Publish distributions to PyPI

on:
  release:
    types:
      - published

jobs:
  shared-build-and-publish:
    uses: zigpy/workflows/.github/workflows/publish-to-pypi.yml@main
    secrets:
      PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}


Stale

name: Stale

# yamllint disable-line rule:truthy
on:
  schedule:
    - cron: "0 * * * *"
  workflow_dispatch:

jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
      # The 180 day stale policy
      # Used for:
      # - Issues & PRs
      # - No PRs marked as no-stale
      # - No issues marked as no-stale or help-wanted
      - name: 180 days stale issues & PRs policy
        uses: actions/stale@v8
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          days-before-stale: 180
          days-before-close: 7
          operations-per-run: 150
          remove-stale-when-updated: true
          stale-issue-label: "stale"
          exempt-issue-labels: "no stale,help wanted"
          stale-issue-message: >
            There hasn't been any activity on this issue recently. Due to the
            high number of incoming GitHub notifications, we have to clean some
            of the old issues, as many of them have already been resolved with
            the latest updates.
            Please make sure to update to the latest version and check if that
            solves the issue. Let us know if that works for you by adding a
            comment 👍
            This issue has now been marked as stale and will be closed if no
            further activity occurs. Thank you for your contributions.
          stale-pr-label: "stale"
          exempt-pr-labels: "no stale"
          stale-pr-message: >
            There hasn't been any activity on this pull request recently. This
            pull request has been automatically marked as stale because of that
            and will be closed if no further activity occurs within 7 days.
            Thank you for your contributions.

      # The 60 day stale policy for issues
      # Used for:
      # - Issues that are pending more information (incomplete issues)
      # - No Issues marked as no-stale or help-wanted
      # - No PRs (-1)
      - name: Needs more information stale issues policy
        uses: actions/stale@v8
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          only-labels: "needs more information"
          days-before-stale: 60
          days-before-close: 7
          days-before-pr-close: -1
          operations-per-run: 50
          remove-stale-when-updated: true
          stale-issue-label: "stale"
          exempt-issue-labels: "no stale,help wanted"
          stale-issue-message: >
            There hasn't been any activity on this issue recently. Due to the
            high number of incoming GitHub notifications, we have to clean some
            of the old issues, as many of them have already been resolved with
            the latest updates.
            Please make sure to update to the latest version and check if that
            solves the issue. Let us know if that works for you by adding a
            comment 👍
            This issue has now been marked as stale and will be closed if no
            further activity occurs. Thank you for your contributions.


Coverage

name: Coverage

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

  # Allow job to be triggered manually.
  workflow_dispatch:

# Cancel in-progress jobs when pushing to the same branch.
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}

jobs:
  run:
    runs-on: ubuntu-latest
    name: Check code coverage
    env:
      OS: "ubuntu-latest"
      PYTHON: "3.12"
    steps:
      - name: Acquire sources
        uses: actions/checkout@v4

      - name: Install Poetry
        run: pipx install poetry

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          architecture: x64
          cache: poetry
          cache-dependency-path: |
            pyproject.toml
            poetry.lock

      - name: Install project
        run: .github/workflows/install.sh testing

      - name: Run tests, with coverage
        run: |
          poetry run pytest --cov=wetterdienst tests/
          poetry run coverage xml

      - name: Upload coverage report to Codecov
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage.xml
          flags: unittests
          env_vars: OS,PYTHON
          name: codecov-umbrella
          fail_ci_if_error: false


Tests

name: Tests

on:
  pull_request: ~
  push:
    branches: [ main ]

  # Allow job to be triggered manually.
  workflow_dispatch:

# Cancel in-progress jobs when pushing to the same branch.
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}

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

    strategy:
      matrix:
        os: ["ubuntu-latest"]
        python-version: ["3.9", "3.10", "3.11", "3.12"]
        include:
          # arm64 (M1)
          - os: "macos-14"
            python-version: "3.12"
          # x86 (Intel)
          - os: "macos-latest"
            python-version: "3.12"
          - os: "windows-latest"
            python-version: "3.12"

    env:
      OS: ${{ matrix.os }}
      PYTHON: ${{ matrix.python-version }}

    defaults:
      run:
        shell: bash

    name: Python ${{ matrix.python-version }} on OS ${{ matrix.os }}
    steps:
      - name: Acquire sources
        uses: actions/checkout@v4

      # Workaround until pipx is available on M1 machines
      # - https://github.com/actions/runner-images/issues/9256
      # We install poetry using brew instead of pipx
      - name: Install Poetry (Mac M1 only)
        if: ${{ startsWith(matrix.os, 'macos-14') }}
        run: brew install poetry

      - name: Install Poetry
        if: ${{ !startsWith(matrix.os, 'macos-14') }}
        run: pipx install poetry

      - name: Setup Python
        if: ${{ !startsWith(matrix.os, 'macos-14') }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          architecture: x64
          cache: poetry
          cache-dependency-path: |
            pyproject.toml
            poetry.lock

      - name: Setup Python (Mac M1 only)
        if: ${{ startsWith(matrix.os, 'macos-14') }}
        run: |
          brew install pyenv
          pyenv install ${{ matrix.python-version }}
          pyenv global ${{ matrix.python-version }}

          export PYENV_ROOT="$HOME/.pyenv"
          [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
          eval "$(pyenv init -)"

      - name: Install eccodes (Mac only)
        if: ${{ startsWith(matrix.os, 'macos-') }}
        run: |
          brew install eccodes
          export WD_ECCODES_DIR=$(brew --prefix eccodes)

      - name: Install project
        run: .github/workflows/install.sh testing

      - name: Install Jupyter kernel
        run: poetry run python -m ipykernel install --name "python3" --user

      - name: Run tests
        run: |
          poetry run pytest -vvv --numprocesses=auto -m "not (explorer or cflake)" tests
          poetry run pytest -vvv -m cflake tests


Release

name: Release
on:
  release:
    types: [ published ]
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Acquire sources
        uses: actions/checkout@v4

      - name: Install Poetry
        run: pipx install poetry

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          architecture: x64
          cache: poetry
          cache-dependency-path: |
            pyproject.toml
            poetry.lock

      - name: Install project
        run: .github/workflows/install.sh testing

      - name: Build
        run: poetry build

      - name: Publish
        run: poetry publish --username=__token__ --password=${{ secrets.PYPI_TOKEN }}


Dependency Review

# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
name: 'Dependency Review'
on: [pull_request]

permissions:
  contents: read

jobs:
  dependency-review:
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout Repository'
        uses: actions/checkout@v4
      - name: 'Dependency Review'
        uses: actions/dependency-review-action@v4


Lint

name: Lint

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

  # Allow job to be triggered manually.
  workflow_dispatch:

# Cancel in-progress jobs when pushing to the same branch.
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Acquire sources
        uses: actions/checkout@v4

      - name: Install Poetry
        run: pipx install poetry

      - name: Setup Python
        id: python-setup
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          architecture: x64
          cache: poetry
          cache-dependency-path: |
            pyproject.toml
            poetry.lock

      - name: Install project tooling
        run: poetry install --no-interaction --no-root --with=dev

      - name: Run code-style checks
        run: poetry run poe lint


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: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '26 4 * * 5'

jobs:
  analyze:
    name: Analyze
    # Runner size impacts CodeQL analysis time. To learn more, please see:
    #   - https://gh.io/recommended-hardware-resources-for-running-codeql
    #   - https://gh.io/supported-runners-and-hardware-resources
    #   - https://gh.io/using-larger-runners
    # Consider using larger runners for possible analysis time improvements.
    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
    timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
    permissions:
      # required for all workflows
      security-events: write

      # only required for workflows in private repositories
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        language: [ python ]
        # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
        # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
        # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

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

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      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.

        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
        # queries: security-extended,security-and-quality

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

    # ℹ Command-line programs to run using the OS shell.
    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun

    #   If the Autobuild fails above, remove it and uncomment the following three lines.
    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.

    # - run: |
    #     echo "Run, Build Application using script"
    #     ./location_of_script_within_repo/buildscript.sh

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


Release Docker

# Stage Docker images through GitHub Actions (GHA) to GitHub Container Registry (GHCR).
#
# Derived from:
# https://github.com/crate/cratedb-prometheus-adapter/blob/main/.github/workflows/release.yml
name: Release Docker

on:
  push:
    tags:
      - '*.*.*'
  pull_request:
    branches: [ main ]

  schedule:
    - cron: '0 10 * * *' # everyday at 10am

  # Allow job to be triggered manually.
  workflow_dispatch:

# Cancel in-progress jobs when pushing to the same branch.
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}

# The name for the produced image at ghcr.io.
env:
  IMAGE_NAME: "${{ github.repository }}"

jobs:
  build_and_test:
    runs-on: ubuntu-latest
    steps:
      - name: Acquire sources
        uses: actions/checkout@v4

      - name: Install Poetry
        run: pipx install poetry

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          architecture: x64
          cache: poetry

      - name: Build wheel package
        run: poetry build --format=wheel

      - name: Upload wheel package
        uses: actions/upload-artifact@v4
        with:
          name: ${{ runner.os }}-wheel-${{ github.sha }}
          path: dist/wetterdienst-*.whl
          retention-days: 7

      - name: Run tests
        run: |
          export DOCKER_BUILDKIT=1
          export COMPOSE_DOCKER_CLI_BUILD=1
          export BUILDKIT_PROGRESS=plain
          if [[ -f .github/release/test.yml ]]; then
            docker-compose --file .github/release/test.yml build
            docker-compose --file .github/release/test.yml run sut
          fi

  docker:
    needs: build_and_test
    runs-on: ubuntu-latest
    if: ${{ ! (startsWith(github.actor, 'dependabot') || github.event.pull_request.head.repo.fork ) }}

    steps:
      - name: Acquire sources
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Download wheel package
        uses: actions/download-artifact@v4
        with:
          name: ${{ runner.os }}-wheel-${{ github.sha }}
          path: dist

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          # List of Docker images to use as base name for tags
          images: |
            ghcr.io/${{ env.IMAGE_NAME }}
          # Generate Docker tags based on the following events/attributes
          tags: |
            type=schedule,pattern=nightly
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}

      - name: Inspect meta
        run: |
          echo "Tags:      ${{ steps.meta.outputs.tags }}"
          echo "Labels:    ${{ steps.meta.outputs.labels }}"

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v3

      - name: Cache Docker layers
        uses: actions/cache@v4
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-

      - name: Inspect builder
        run: |
          echo "Name:      ${{ steps.buildx.outputs.name }}"
          echo "Endpoint:  ${{ steps.buildx.outputs.endpoint }}"
          echo "Status:    ${{ steps.buildx.outputs.status }}"
          echo "Flags:     ${{ steps.buildx.outputs.flags }}"
          echo "Platforms: ${{ steps.buildx.outputs.platforms }}"

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ github.token }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          file: .github/release/Dockerfile
          platforms: linux/amd64  # in future we might want to add: linux/arm64,linux/arm/v7
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          push: true
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

      - name: Display git status
        run: |
          set -x
          git describe --tags
          git status


black

# .github/workflows/black.yml
# From https://github.com/ad-m/github-push-action#example-workflow-file
name: black

on:
  workflow_dispatch:

jobs:
  black:
    runs-on: ubuntu-latest
    steps:
    - name: STEP actions/setup-python@v5
      uses: actions/setup-python@v5
      with:
        python-version: '3.9'

    - name: STEP pip install black
      # if: steps.cache.outputs.cache-hit != 'true'
      run: |
          python -m pip install --upgrade pip
          pip install black
    - name: STEP actions/checkout@v4
      uses: actions/checkout@v4
      with:
        persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
        fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.

    - name: STEP black --check *.py
      id: black
      run: |
        mapfile -t <<<"$(find . -type f -iname "*.py")"
        ((${#MAPFILE[@]})) || { echo ERROR: No .py files found >&2 ; exit 1 ; }
        rv=0
        black --config .github/linters/.python-black --check "${MAPFILE[@]}" || rv=$?
        ((rv==0)) && echo "::set-output name=changes::false" && exit 0
        ((rv==1)) && echo "::set-output name=changes::true" && exit 0
        exit $rv
    - name: STEP black *.py
      if: steps.black.outputs.changes == 'true'
      run: |
        mapfile -t <<<"$(find . -type f -iname "*.py")"
        black --config .github/linters/.python-black "${MAPFILE[@]}"
    - name: STEP git commit -m "black" -a
      if: steps.black.outputs.changes == 'true'
      run: |
        git config --local user.email "action@github.com"
        git config --local user.name "GitHub Action"
        git commit -m "black" -a
    - name: STEP ad-m/github-push-action@master
      if: steps.black.outputs.changes == 'true'
      uses: ad-m/github-push-action@master
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        branch: ${{ github.ref }}


clean-workflows

---
# Copied from: rasa/dotfiles/.github/workflows/clean-logs.yml
# EDIT THE ABOVE FILE, NOT THIS COPY, OR YOUR CHANGES WILL BE LOST!
# yamllint disable rule:line-length
# $schema https://json.schemastore.org/github-workflow.json
# checkov:skip=CKV_GHA_7:The build output cannot be affected by user parameters
#   other than the build entry point and the top-level source location.
#   GitHub Actions workflow_dispatch inputs MUST be empty.

name: clean-workflows

on: # yamllint disable-line rule:truthy
  schedule:
  # runs at 20:02 UTC (12:02 PST) every day.
  - cron: "02 20 * * *"
  #        ┬  ┬  ┬  ┬  ┬                               # *    : run every 5 min/every 1 hour/every 1 day/every 1 mon
  #        │  │  │  │  └─ day of week (0-6 or SUN-SAT) # */X  : run every X min/every X hour/every X day/every X mon
  #        │  │  │  └──── month (1-12 or JAN-DEC)      # A,B  : run when min/hour/day/mon = A or B
  #        │  │  └─────── day of month (1-31)          # A-B  : run every min/hour/day/mon between A and B
  #        │  └────────── hour (0-23) (UTC/GMT+0 time) # A-B/X: run every X min/hour/day/mon between A and B
  #        └───────────── minute (0-59)                # Ex: '0 9-17/2 * * 1-5' : 9/11/1/3/5 on Mon/Tue/Wed/Thu/Fri
  workflow_call: # Required so this workflow can be called from another workflow
    inputs:
      retain_days:
        default: "30"
        type: string
      keep_minimum_runs:
        default: "6"
        type: string
      delete_workflow_pattern:
        type: string
      delete_workflow_by_state_pattern:
        default: ALL
        type: string
      delete_run_by_conclusion_pattern:
        default: ALL
        type: string
      dry_run:
        type: boolean
      check_branch_existence:
        default: "Keep"
        type: string
      check_pullrequest_exist:
        default: "Keep"
        type: string
      repository:
        type: string

  workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
    inputs:
      # checkov:skip=CKV_GHA_7
      retain_days:
        description: Days to keep
        # Days-worth of runs to keep for each workflow
        required: true
        default: "30"
      # checkov:skip=CKV_GHA_7
      keep_minimum_runs:
        description: Runs to keep
        # Minimum runs to keep for each workflow
        required: true
        default: "6"
      # checkov:skip=CKV_GHA_7
      delete_workflow_pattern:
        description: "Workflow name/filename (optional)"
        # Name or filename of the workflow (if not set, all workflows are targeted)
      # checkov:skip=CKV_GHA_7
      delete_workflow_by_state_pattern:
        description: State to clean
        # "Filter workflows by state: active, deleted, disabled_fork, disabled_inactivity, disabled_manually"
        required: true
        default: ALL
        type: choice
        options:
          - ALL
          - active
          - deleted
          - disabled_inactivity
          - disabled_manually
      # checkov:skip=CKV_GHA_7
      delete_run_by_conclusion_pattern:
        description: Conclusion to clean
        # "Remove runs based on conclusion: action_required, cancelled, failure, skipped, success"
        required: true
        default: ALL
        type: choice
        options:
          - ALL
          - "Unsuccessful: action_required,cancelled,failure,skipped"
          - action_required
          - cancelled
          - failure
          - skipped
          - success
      # checkov:skip=CKV_GHA_7
      dry_run:
        description: Dry run only
        type: boolean
      # checkov:skip=CKV_GHA_7
      check_branch_existence:
        description: Clean workflows of existing branches
        # If the workflow was triggered by a branch, the branch must be deleted before the workflow run is deleted as well.
        default: "Keep"
        type: choice
        options:
          - "Keep"
          - "Delete"
      # checkov:skip=CKV_GHA_7
      check_pullrequest_exist:
        description: Clean workflows for active PRs
        # If the run is linked to a pull request, the deletion is skipped.
        default: "Keep"
        type: choice
        options:
          - "Keep"
          - "Delete"
      # checkov:skip=CKV_GHA_7
      repository:
        description: Repository (user/repo) (optional)
        # (default is current repository).
        type: string
        default: ""

concurrency:
  # Group workflow jobs so new commits cancels in-progress execution triggered by previous commits.
  # Source: https://mail.python.org/archives/list/pypa-committers@python.org/thread/PCBCQMJF64JGRBOX7E2EE4YLKHT4DI55/
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}

jobs:
  clean-logs:
    runs-on: ubuntu-latest
    steps:
      # Was crazy-max/ghaction-dump-context@v2.1.0
      # but it reports a deprecation error:
      # Node.js 16 actions are deprecated. Please update the following actions to use Node.js 20: actions/github-script@v6.
      - name: rasa/ghaction-dump-context@master
        uses: rasa/ghaction-dump-context@master

      - name: Scheduled run (github.event_name == 'schedule')
        if: github.event_name == 'schedule'
        uses: rasa/delete-workflow-runs@main
        with:
          repository: ${{ github.repository }}
          token: ${{ secrets.GITHUB_TOKEN }}
          retain_days: "30" # default: 30
          keep_minimum_runs: "6" # default: 6
          check_branch_existence: "Keep" # default: Keep
          check_pullrequest_exist: "Keep" # default: Keep

      - name: Non-scheduled run (github.event_name != 'schedule')
        if: github.event_name != 'schedule'
        uses: rasa/delete-workflow-runs@main
        with:
          repository: ${{ github.event.inputs.repository || github.repository }}
          token: ${{ secrets.GITHUB_TOKEN }}
          retain_days: ${{ github.event.inputs.retain_days }}
          keep_minimum_runs: ${{ github.event.inputs.keep_minimum_runs }}
          delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }}
          delete_workflow_by_state_pattern: ${{ github.event.inputs.delete_workflow_by_state_pattern }}
          delete_run_by_conclusion_pattern: ${{
              startsWith(github.event.inputs.delete_run_by_conclusion_pattern, 'Unsuccessful:')
              && 'action_required,cancelled,failure,skipped'
              || github.event.inputs.delete_run_by_conclusion_pattern
            }}
          check_branch_existence: ${{ startsWith(github.event.inputs.check_branch_existence, 'Keep') && 'Y' || '' }}
          check_pullrequest_exist: ${{ startsWith(github.event.inputs.check_pullrequest_exist, 'Keep') && 'Y' || '' }}
          dry_run: ${{ (github.event.inputs.dry_run == true) && 'Y' || '' }}

# cSpell:ignore ghaction, Mattraks, pullrequest


codeql

---
# Copied from: rasa/dotfiles/.github/workflows/codeql.yml
# EDIT THE ABOVE FILE, NOT THIS COPY, OR YOUR CHANGES WILL BE LOST!
# $schema https://json.schemastore.org/github-workflow.json
name: codeql

on: # yamllint disable-line rule:truthy
  workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
  # push:
  # branches:
  #   - main
  # pull_request:
  # branches:
  #   - main

jobs:
  job:
    # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
    runs-on: ubuntu-latest

    permissions:
      # required for all workflows
      security-events: write

      # only required for workflows in private repositories
      actions: read
      contents: read

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4.1.1
        with:
          show-progress: false

      # Initializes the CodeQL tools for scanning.
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        # Override language selection by uncommenting this and choosing your languages
        # with:
        #   languages: go, javascript, csharp, python, cpp, java, ruby

      # Autobuild attempts to build any compiled languages (C/C++, C#, Go, 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@v3

      # ℹ Command-line programs to run using the OS shell.
      # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun

      # ✏ 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@v3
#
#

# cSpell:ignore Autobuild, autobuild