[3.13] Convert change detection to a Python script (GH-129627) (#130367)

Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
This commit is contained in:
Hugo van Kemenade 2025-02-22 02:31:45 +02:00 committed by GitHub
parent 8ef89474b9
commit 019918a626
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 328 additions and 209 deletions

View file

@ -22,32 +22,32 @@ env:
FORCE_COLOR: 1 FORCE_COLOR: 1
jobs: jobs:
check_source: build-context:
name: Change detection name: Change detection
# To use boolean outputs from this job, parse them as JSON. # To use boolean outputs from this job, parse them as JSON.
# Here's some examples: # Here's some examples:
# #
# if: fromJSON(needs.check_source.outputs.run-docs) # if: fromJSON(needs.build-context.outputs.run-docs)
# #
# ${{ # ${{
# fromJSON(needs.check_source.outputs.run_tests) # fromJSON(needs.build-context.outputs.run-tests)
# && 'truthy-branch' # && 'truthy-branch'
# || 'falsy-branch' # || 'falsy-branch'
# }} # }}
# #
uses: ./.github/workflows/reusable-change-detection.yml uses: ./.github/workflows/reusable-context.yml
check-docs: check-docs:
name: Docs name: Docs
needs: check_source needs: build-context
if: fromJSON(needs.check_source.outputs.run-docs) if: fromJSON(needs.build-context.outputs.run-docs)
uses: ./.github/workflows/reusable-docs.yml uses: ./.github/workflows/reusable-docs.yml
check_abi: check_abi:
name: 'Check if the ABI has changed' name: 'Check if the ABI has changed'
runs-on: ubuntu-22.04 # 24.04 causes spurious errors runs-on: ubuntu-22.04 # 24.04 causes spurious errors
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' if: needs.build-context.outputs.run-tests == 'true'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -96,8 +96,8 @@ jobs:
container: container:
image: ghcr.io/python/autoconf:2024.10.16.11360930377 image: ghcr.io/python/autoconf:2024.10.16.11360930377
timeout-minutes: 60 timeout-minutes: 60
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' if: needs.build-context.outputs.run-tests == 'true'
steps: steps:
- name: Install Git - name: Install Git
run: | run: |
@ -137,8 +137,8 @@ jobs:
# reproducible: to get the same tools versions (autoconf, aclocal, ...) # reproducible: to get the same tools versions (autoconf, aclocal, ...)
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
timeout-minutes: 60 timeout-minutes: 60
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' if: needs.build-context.outputs.run-tests == 'true'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -153,7 +153,7 @@ jobs:
with: with:
path: config.cache path: config.cache
# Include env.pythonLocation in key to avoid changes in environment when setup-python updates Python # Include env.pythonLocation in key to avoid changes in environment when setup-python updates Python
key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.check_source.outputs.config_hash }}-${{ env.pythonLocation }} key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}-${{ env.pythonLocation }}
- name: Install dependencies - name: Install dependencies
run: sudo ./.github/workflows/posix-deps-apt.sh run: sudo ./.github/workflows/posix-deps-apt.sh
- name: Add ccache to PATH - name: Add ccache to PATH
@ -196,8 +196,8 @@ jobs:
name: >- name: >-
Windows Windows
${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} ${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }}
needs: check_source needs: build-context
if: fromJSON(needs.check_source.outputs.run_tests) if: fromJSON(needs.build-context.outputs.run-tests)
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -227,8 +227,8 @@ jobs:
build_windows_msi: build_windows_msi:
name: >- # ${{ '' } is a hack to nest jobs under the same sidebar category name: >- # ${{ '' } is a hack to nest jobs under the same sidebar category
Windows MSI${{ '' }} Windows MSI${{ '' }}
needs: check_source needs: build-context
if: fromJSON(needs.check_source.outputs.run-win-msi) if: fromJSON(needs.build-context.outputs.run-windows-msi)
strategy: strategy:
matrix: matrix:
arch: arch:
@ -243,8 +243,8 @@ jobs:
name: >- name: >-
macOS macOS
${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} ${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }}
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' if: needs.build-context.outputs.run-tests == 'true'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -269,7 +269,7 @@ jobs:
free-threading: true free-threading: true
uses: ./.github/workflows/reusable-macos.yml uses: ./.github/workflows/reusable-macos.yml
with: with:
config_hash: ${{ needs.check_source.outputs.config_hash }} config_hash: ${{ needs.build-context.outputs.config-hash }}
free-threading: ${{ matrix.free-threading }} free-threading: ${{ matrix.free-threading }}
os: ${{ matrix.os }} os: ${{ matrix.os }}
@ -277,8 +277,8 @@ jobs:
name: >- name: >-
Ubuntu Ubuntu
${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} ${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }}
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' if: needs.build-context.outputs.run-tests == 'true'
strategy: strategy:
matrix: matrix:
free-threading: free-threading:
@ -286,15 +286,15 @@ jobs:
- true - true
uses: ./.github/workflows/reusable-ubuntu.yml uses: ./.github/workflows/reusable-ubuntu.yml
with: with:
config_hash: ${{ needs.check_source.outputs.config_hash }} config_hash: ${{ needs.build-context.outputs.config-hash }}
free-threading: ${{ matrix.free-threading }} free-threading: ${{ matrix.free-threading }}
build_ubuntu_ssltests: build_ubuntu_ssltests:
name: 'Ubuntu SSL tests with OpenSSL' name: 'Ubuntu SSL tests with OpenSSL'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 60 timeout-minutes: 60
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' if: needs.build-context.outputs.run-tests == 'true'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -315,7 +315,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: config.cache path: config.cache
key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.check_source.outputs.config_hash }} key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}
- name: Register gcc problem matcher - name: Register gcc problem matcher
run: echo "::add-matcher::.github/problem-matchers/gcc.json" run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Install dependencies - name: Install dependencies
@ -352,18 +352,18 @@ jobs:
build_wasi: build_wasi:
name: 'WASI' name: 'WASI'
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' if: needs.build-context.outputs.run-tests == 'true'
uses: ./.github/workflows/reusable-wasi.yml uses: ./.github/workflows/reusable-wasi.yml
with: with:
config_hash: ${{ needs.check_source.outputs.config_hash }} config_hash: ${{ needs.build-context.outputs.config-hash }}
test_hypothesis: test_hypothesis:
name: "Hypothesis tests on Ubuntu" name: "Hypothesis tests on Ubuntu"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
timeout-minutes: 60 timeout-minutes: 60
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' && needs.check_source.outputs.run_hypothesis == 'true' if: needs.build-context.outputs.run-tests == 'true'
env: env:
OPENSSL_VER: 3.0.15 OPENSSL_VER: 3.0.15
PYTHONSTRICTEXTENSIONBUILD: 1 PYTHONSTRICTEXTENSIONBUILD: 1
@ -410,7 +410,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ${{ env.CPYTHON_BUILDDIR }}/config.cache path: ${{ env.CPYTHON_BUILDDIR }}/config.cache
key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.check_source.outputs.config_hash }} key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}
- name: Configure CPython out-of-tree - name: Configure CPython out-of-tree
working-directory: ${{ env.CPYTHON_BUILDDIR }} working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: | run: |
@ -477,8 +477,8 @@ jobs:
name: 'Address sanitizer' name: 'Address sanitizer'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 60 timeout-minutes: 60
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' if: needs.build-context.outputs.run-tests == 'true'
strategy: strategy:
matrix: matrix:
os: [ubuntu-24.04] os: [ubuntu-24.04]
@ -496,7 +496,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: config.cache path: config.cache
key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.check_source.outputs.config_hash }} key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}
- name: Register gcc problem matcher - name: Register gcc problem matcher
run: echo "::add-matcher::.github/problem-matchers/gcc.json" run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Install dependencies - name: Install dependencies
@ -540,8 +540,8 @@ jobs:
name: >- name: >-
Thread sanitizer Thread sanitizer
${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} ${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }}
needs: check_source needs: build-context
if: needs.check_source.outputs.run_tests == 'true' if: needs.build-context.outputs.run-tests == 'true'
strategy: strategy:
matrix: matrix:
free-threading: free-threading:
@ -549,7 +549,7 @@ jobs:
- true - true
uses: ./.github/workflows/reusable-tsan.yml uses: ./.github/workflows/reusable-tsan.yml
with: with:
config_hash: ${{ needs.check_source.outputs.config_hash }} config_hash: ${{ needs.build-context.outputs.config-hash }}
free-threading: ${{ matrix.free-threading }} free-threading: ${{ matrix.free-threading }}
# CIFuzz job based on https://google.github.io/oss-fuzz/getting-started/continuous-integration/ # CIFuzz job based on https://google.github.io/oss-fuzz/getting-started/continuous-integration/
@ -557,8 +557,8 @@ jobs:
name: CIFuzz name: CIFuzz
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
needs: check_source needs: build-context
if: needs.check_source.outputs.run_cifuzz == 'true' if: needs.build-context.outputs.run-ci-fuzz == 'true'
permissions: permissions:
security-events: write security-events: write
strategy: strategy:
@ -597,7 +597,7 @@ jobs:
if: always() if: always()
needs: needs:
- check_source # Transitive dependency, needed to access `run_tests` value - build-context # Transitive dependency, needed to access `run-tests` value
- check-docs - check-docs
- check_autoconf_regen - check_autoconf_regen
- check_generated_files - check_generated_files
@ -625,14 +625,14 @@ jobs:
test_hypothesis, test_hypothesis,
allowed-skips: >- allowed-skips: >-
${{ ${{
!fromJSON(needs.check_source.outputs.run-docs) !fromJSON(needs.build-context.outputs.run-docs)
&& ' && '
check-docs, check-docs,
' '
|| '' || ''
}} }}
${{ ${{
needs.check_source.outputs.run_tests != 'true' needs.build-context.outputs.run-tests != 'true'
&& ' && '
check_autoconf_regen, check_autoconf_regen,
check_generated_files, check_generated_files,
@ -643,21 +643,15 @@ jobs:
build_windows, build_windows,
build_asan, build_asan,
build_tsan, build_tsan,
test_hypothesis,
' '
|| '' || ''
}} }}
${{ ${{
!fromJSON(needs.check_source.outputs.run_cifuzz) !fromJSON(needs.build-context.outputs.run-ci-fuzz)
&& ' && '
cifuzz, cifuzz,
' '
|| '' || ''
}} }}
${{
!fromJSON(needs.check_source.outputs.run_hypothesis)
&& '
test_hypothesis,
'
|| ''
}}
jobs: ${{ toJSON(needs) }} jobs: ${{ toJSON(needs) }}

View file

@ -1,158 +0,0 @@
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
with:
persist-credentials: false
- 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/get-changed-files@v2.3.0
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/get-changed-files@v2.3.0
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}"

100
.github/workflows/reusable-context.yml vendored Normal file
View file

@ -0,0 +1,100 @@
name: Reusable build context
on: # yamllint disable-line rule:truthy
workflow_call:
outputs:
# Every referenced step MUST always set its output variable,
# either via ``Tools/build/compute-changes.py`` or in this workflow file.
# Boolean outputs (generally prefixed ``run-``) can then later be used
# safely through the following idiom in job conditionals and other
# expressions. Here's some examples:
#
# if: fromJSON(needs.build-context.outputs.run-tests)
#
# ${{
# fromJSON(needs.build-context.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 }} # bool
run-tests:
description: Whether to run the regular tests
value: ${{ jobs.compute-changes.outputs.run-tests }} # bool
run-windows-msi:
description: Whether to run the MSI installer smoke tests
value: ${{ jobs.compute-changes.outputs.run-windows-msi }} # bool
run-ci-fuzz:
description: Whether to run the CIFuzz job
value: ${{ jobs.compute-changes.outputs.run-ci-fuzz }} # bool
jobs:
compute-changes:
name: Create context from changed files
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
config-hash: ${{ steps.config-hash.outputs.hash }}
run-ci-fuzz: ${{ steps.changes.outputs.run-ci-fuzz }}
run-docs: ${{ steps.changes.outputs.run-docs }}
run-tests: ${{ steps.changes.outputs.run-tests }}
run-windows-msi: ${{ steps.changes.outputs.run-windows-msi }}
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3"
- run: >-
echo '${{ github.event_name }}'
- uses: actions/checkout@v4
with:
persist-credentials: false
ref: >-
${{
github.event_name == 'pull_request'
&& github.event.pull_request.head.sha
|| ''
}}
# Adapted from https://github.com/actions/checkout/issues/520#issuecomment-1167205721
- name: Fetch commits to get branch diff
if: github.event_name == 'pull_request'
run: |
set -eux
# Fetch enough history to find a common ancestor commit (aka merge-base):
git fetch origin "${refspec_pr}" --depth=$(( commits + 1 )) \
--no-tags --prune --no-recurse-submodules
# This should get the oldest commit in the local fetched history (which may not be the commit the PR branched from):
COMMON_ANCESTOR=$( git rev-list --first-parent --max-parents=0 --max-count=1 "${branch_pr}" )
DATE=$( git log --date=iso8601 --format=%cd "${COMMON_ANCESTOR}" )
# Get all commits since that commit date from the base branch (eg: main):
git fetch origin "${refspec_base}" --shallow-since="${DATE}" \
--no-tags --prune --no-recurse-submodules
env:
branch_pr: 'origin/${{ github.event.pull_request.head.ref }}'
commits: ${{ github.event.pull_request.commits }}
refspec_base: '+${{ github.event.pull_request.base.sha }}:remotes/origin/${{ github.event.pull_request.base.ref }}'
refspec_pr: '+${{ github.event.pull_request.head.sha }}:remotes/origin/${{ github.event.pull_request.head.ref }}'
# We only want to run tests on PRs when related files are changed,
# or when someone triggers a manual workflow run.
- name: Compute changed files
id: changes
run: python Tools/build/compute-changes.py
env:
GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
- name: Compute hash for config cache key
id: config-hash
run: |
echo "hash=${{ hashFiles('configure', 'configure.ac', '.github/workflows/build.yml') }}" >> "$GITHUB_OUTPUT"

View file

@ -0,0 +1,183 @@
"""Determine which GitHub Actions workflows to run.
Called by ``.github/workflows/reusable-context.yml``.
We only want to run tests on PRs when related files are changed,
or when someone triggers a manual workflow run.
This improves developer experience by not doing (slow)
unnecessary work in GHA, and saves CI resources.
"""
from __future__ import annotations
import os
import subprocess
from dataclasses import dataclass
from pathlib import Path
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Set
GITHUB_DEFAULT_BRANCH = os.environ["GITHUB_DEFAULT_BRANCH"]
GITHUB_CODEOWNERS_PATH = Path(".github/CODEOWNERS")
GITHUB_WORKFLOWS_PATH = Path(".github/workflows")
CONFIGURATION_FILE_NAMES = frozenset({
".pre-commit-config.yaml",
".ruff.toml",
"mypy.ini",
})
SUFFIXES_C_OR_CPP = frozenset({".c", ".h", ".cpp"})
SUFFIXES_DOCUMENTATION = frozenset({".rst", ".md"})
@dataclass(kw_only=True, slots=True)
class Outputs:
run_ci_fuzz: bool = False
run_docs: bool = False
run_tests: bool = False
run_windows_msi: bool = False
def compute_changes() -> None:
target_branch, head_branch = git_branches()
if target_branch and head_branch:
# Getting changed files only makes sense on a pull request
files = get_changed_files(
f"origin/{target_branch}", f"origin/{head_branch}"
)
outputs = process_changed_files(files)
else:
# Otherwise, just run the tests
outputs = Outputs(run_tests=True)
outputs = process_target_branch(outputs, target_branch)
if outputs.run_tests:
print("Run tests")
if outputs.run_ci_fuzz:
print("Run CIFuzz tests")
else:
print("Branch too old for CIFuzz tests; or no C files were changed")
if outputs.run_docs:
print("Build documentation")
if outputs.run_windows_msi:
print("Build Windows MSI")
print(outputs)
write_github_output(outputs)
def git_branches() -> tuple[str, str]:
target_branch = os.environ.get("GITHUB_BASE_REF", "")
target_branch = target_branch.removeprefix("refs/heads/")
print(f"target branch: {target_branch!r}")
head_branch = os.environ.get("GITHUB_HEAD_REF", "")
head_branch = head_branch.removeprefix("refs/heads/")
print(f"head branch: {head_branch!r}")
return target_branch, head_branch
def get_changed_files(
ref_a: str = GITHUB_DEFAULT_BRANCH, ref_b: str = "HEAD"
) -> Set[Path]:
"""List the files changed between two Git refs, filtered by change type."""
args = ("git", "diff", "--name-only", f"{ref_a}...{ref_b}", "--")
print(*args)
changed_files_result = subprocess.run(
args, stdout=subprocess.PIPE, check=True, encoding="utf-8"
)
changed_files = changed_files_result.stdout.strip().splitlines()
return frozenset(map(Path, filter(None, map(str.strip, changed_files))))
def process_changed_files(changed_files: Set[Path]) -> Outputs:
run_tests = False
run_ci_fuzz = False
run_docs = False
run_windows_msi = False
for file in changed_files:
# Documentation files
doc_or_misc = file.parts[0] in {"Doc", "Misc"}
doc_file = file.suffix in SUFFIXES_DOCUMENTATION or doc_or_misc
if file.parent == GITHUB_WORKFLOWS_PATH:
if file.name == "build.yml":
run_tests = run_ci_fuzz = True
if file.name == "reusable-docs.yml":
run_docs = True
if file.name == "reusable-windows-msi.yml":
run_windows_msi = True
if not (
doc_file
or file == GITHUB_CODEOWNERS_PATH
or file.name in CONFIGURATION_FILE_NAMES
):
run_tests = True
# The fuzz tests are pretty slow so they are executed only for PRs
# changing relevant files.
if file.suffix in SUFFIXES_C_OR_CPP:
run_ci_fuzz = True
if file.parts[:2] in {
("configure",),
("Modules", "_xxtestfuzz"),
}:
run_ci_fuzz = True
# Check for changed documentation-related files
if doc_file:
run_docs = True
# Check for changed MSI installer-related files
if file.parts[:2] == ("Tools", "msi"):
run_windows_msi = True
return Outputs(
run_ci_fuzz=run_ci_fuzz,
run_docs=run_docs,
run_tests=run_tests,
run_windows_msi=run_windows_msi,
)
def process_target_branch(outputs: Outputs, git_branch: str) -> Outputs:
if not git_branch:
outputs.run_tests = True
# CIFuzz / OSS-Fuzz compatibility with older branches may be broken.
if git_branch != GITHUB_DEFAULT_BRANCH:
outputs.run_ci_fuzz = False
if os.environ.get("GITHUB_EVENT_NAME", "").lower() == "workflow_dispatch":
outputs.run_docs = True
outputs.run_windows_msi = True
return outputs
def write_github_output(outputs: Outputs) -> None:
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-output-parameter
if "GITHUB_OUTPUT" not in os.environ:
print("GITHUB_OUTPUT not defined!")
return
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
f.write(f"run-ci-fuzz={bool_lower(outputs.run_ci_fuzz)}\n")
f.write(f"run-docs={bool_lower(outputs.run_docs)}\n")
f.write(f"run-tests={bool_lower(outputs.run_tests)}\n")
f.write(f"run-windows-msi={bool_lower(outputs.run_windows_msi)}\n")
def bool_lower(value: bool, /) -> str:
return "true" if value else "false"
if __name__ == "__main__":
compute_changes()