cpython/.github/workflows/reusable-change-detection.yml

name: Reusable change detection

on:  # yamllint disable-line rule:truthy
  workflow_call:
    outputs:
      # Some of the referenced steps set outputs conditionally and there may be
      # cases when referencing them evaluates to empty strings. It is nice to
      # work with proper booleans so they have to be evaluated through JSON
      # conversion in the expressions. However, empty strings used like that
      # may trigger all sorts of undefined and hard-to-debug behaviors in
      # GitHub Actions CI/CD. To help with this, all of the outputs set here
      # that are meant to be used as boolean flags (and not arbitrary strings),
      # MUST have fallbacks with default values set. A common pattern would be
      # to add ` || false` to all such expressions here, in the output
      # definitions. They can then later be safely used through the following
      # idiom in job conditionals and other expressions. Here's some examples:
      #
      #   if: fromJSON(needs.change-detection.outputs.run-docs)
      #
      #   ${{
      #        fromJSON(needs.change-detection.outputs.run-tests)
      #        && 'truthy-branch'
      #        || 'falsy-branch'
      #   }}
      #
      config_hash:
        description: Config hash value for use in cache keys
        value: ${{ jobs.compute-changes.outputs.config-hash }}  # str
      run-docs:
        description: Whether to build the docs
        value: ${{ jobs.compute-changes.outputs.run-docs || false }}  # bool
      run_tests:
        description: Whether to run the regular tests
        value: ${{ jobs.compute-changes.outputs.run-tests || false }}  # bool
      run-win-msi:
        description: Whether to run the MSI installer smoke tests
        value: >-  # bool
          ${{ jobs.compute-changes.outputs.run-win-msi || false }}
      run_hypothesis:
        description: Whether to run the Hypothesis tests
        value: >-  # bool
          ${{ jobs.compute-changes.outputs.run-hypothesis || false }}
      run_cifuzz:
        description: Whether to run the CIFuzz job
        value: >-  # bool
          ${{ jobs.compute-changes.outputs.run-cifuzz || false }}

jobs:
  compute-changes:
    name: Compute changed files
    runs-on: ubuntu-latest
    timeout-minutes: 10
    outputs:
      config-hash: ${{ steps.config-hash.outputs.hash }}
      run-cifuzz: ${{ steps.check.outputs.run-cifuzz }}
      run-docs: ${{ steps.docs-changes.outputs.run-docs }}
      run-hypothesis: ${{ steps.check.outputs.run-hypothesis }}
      run-tests: ${{ steps.check.outputs.run-tests }}
      run-win-msi: ${{ steps.win-msi-changes.outputs.run-win-msi }}
    steps:
    - run: >-
        echo '${{ github.event_name }}'
    - uses: actions/checkout@v4
    - name: Check for source changes
      id: check
      run: |
        if [ -z "$GITHUB_BASE_REF" ]; then
          echo "run-tests=true" >> "$GITHUB_OUTPUT"
        else
          git fetch origin "$GITHUB_BASE_REF" --depth=1
          # git diff "origin/$GITHUB_BASE_REF..." (3 dots) may be more
          # reliable than git diff "origin/$GITHUB_BASE_REF.." (2 dots),
          # but it requires to download more commits (this job uses
          # "git fetch --depth=1").
          #
          # git diff "origin/$GITHUB_BASE_REF..." (3 dots) works with Git
          # 2.26, but Git 2.28 is stricter and fails with "no merge base".
          #
          # git diff "origin/$GITHUB_BASE_REF.." (2 dots) should be enough on
          # GitHub, since GitHub starts by merging origin/$GITHUB_BASE_REF
          # into the PR branch anyway.
          #
          # https://github.com/python/core-workflow/issues/373
          git diff --name-only "origin/$GITHUB_BASE_REF.." | grep -qvE '(\.rst$|^Doc|^Misc|^\.pre-commit-config\.yaml$|\.ruff\.toml$|\.md$|mypy\.ini$)' && echo "run-tests=true" >> "$GITHUB_OUTPUT" || true
        fi

        # Check if we should run hypothesis tests
        GIT_BRANCH=${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}
        echo "$GIT_BRANCH"
        if $(echo "$GIT_BRANCH" | grep -q -w '3\.\(8\|9\|10\|11\)'); then
          echo "Branch too old for hypothesis tests"
          echo "run-hypothesis=false" >> "$GITHUB_OUTPUT"
        else
          echo "Run hypothesis tests"
          echo "run-hypothesis=true" >> "$GITHUB_OUTPUT"
        fi

        # oss-fuzz maintains a configuration for fuzzing the main branch of
        # CPython, so CIFuzz should be run only for code that is likely to be
        # merged into the main branch; compatibility with older branches may
        # be broken.
        FUZZ_RELEVANT_FILES='(\.c$|\.h$|\.cpp$|^configure$|^\.github/workflows/build\.yml$|^Modules/_xxtestfuzz)'
        if [ "$GITHUB_BASE_REF" = "main" ] && [ "$(git diff --name-only "origin/$GITHUB_BASE_REF.." | grep -qE $FUZZ_RELEVANT_FILES; echo $?)" -eq 0 ]; then
          # The tests are pretty slow so they are executed only for PRs
          # changing relevant files.
          echo "Run CIFuzz tests"
          echo "run-cifuzz=true" >> "$GITHUB_OUTPUT"
        else
          echo "Branch too old for CIFuzz tests; or no C files were changed"
          echo "run-cifuzz=false" >> "$GITHUB_OUTPUT"
        fi
    - name: Compute hash for config cache key
      id: config-hash
      run: |
        echo "hash=${{ hashFiles('configure', 'configure.ac', '.github/workflows/build.yml') }}" >> "$GITHUB_OUTPUT"
    - name: Get a list of the changed documentation-related files
      if: github.event_name == 'pull_request'
      id: changed-docs-files
      uses: Ana06/[email protected]
      with:
        filter: |
          Doc/**
          Misc/**
          .github/workflows/reusable-docs.yml
        format: csv  # works for paths with spaces
    - name: Check for docs changes
      # We only want to run this on PRs when related files are changed,
      # or when user triggers manual workflow run.
      if: >-
        (
          github.event_name == 'pull_request'
          && steps.changed-docs-files.outputs.added_modified_renamed != ''
        ) || github.event_name == 'workflow_dispatch'
      id: docs-changes
      run: |
        echo "run-docs=true" >> "${GITHUB_OUTPUT}"
    - name: Get a list of the MSI installer-related files
      if: github.event_name == 'pull_request'
      id: changed-win-msi-files
      uses: Ana06/[email protected]
      with:
        filter: |
          Tools/msi/**
          .github/workflows/reusable-windows-msi.yml
        format: csv  # works for paths with spaces
    - name: Check for changes in MSI installer-related files
      # We only want to run this on PRs when related files are changed,
      # or when user triggers manual workflow run.
      if: >-
        (
          github.event_name == 'pull_request'
          && steps.changed-win-msi-files.outputs.added_modified_renamed != ''
        ) || github.event_name == 'workflow_dispatch'
      id: win-msi-changes
      run: |
        echo "run-win-msi=true" >> "${GITHUB_OUTPUT}"