From a7c8230b15d74bb54eb436c6b930faeaa591f64c Mon Sep 17 00:00:00 2001 From: Pedro Brochado Date: Mon, 1 Sep 2025 16:49:22 -0300 Subject: [PATCH] Skip running test-suite if there are only doc changes This requires plugins to use only the 'ready-to-ship' job as a required check in their github repository settings. If there are other checks such as 'test / test (pulp)' the CI will report there are pending checks and will never be green. --- templates/github/.ci/scripts/skip_tests.py | 113 +++++++++++++++++++ templates/github/.github/workflows/ci.yml.j2 | 50 +++++++- 2 files changed, 158 insertions(+), 5 deletions(-) create mode 100755 templates/github/.ci/scripts/skip_tests.py diff --git a/templates/github/.ci/scripts/skip_tests.py b/templates/github/.ci/scripts/skip_tests.py new file mode 100755 index 00000000..2a21a723 --- /dev/null +++ b/templates/github/.ci/scripts/skip_tests.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +skip_tests.py - Check if only documentation files were changed in a git branch + +Usage: + ./skip_tests.py + +Arguments: + git_root: The root directory of the git project + reference_branch: The branch to compare against + +Returns: + 0: Skip + 1: NoSkip + *: Error +""" + +import sys +import os +import re +import git +import textwrap +import argparse + +DOC_PATTERNS = [ + r"^docs/", + r"\.md$", + r"\.txt$", + r"LICENSE.*", + r"CHANGELOG.*", + r"CHANGES.*", + r"CONTRIBUTING.*", +] + +# Exit codes +CODE_SKIP = 0 +CODE_NO_SKIP = 1 +CODE_ERROR = 2 + + +def main() -> int: + git_root, reference_branch = get_args() + changed_files = get_changed_files(git_root, reference_branch) + if not changed_files: + return CODE_SKIP + doc_files = [f for f in changed_files if is_doc_file(f)] + not_doc_files = set(changed_files) - set(doc_files) + print_changes(doc_files, not_doc_files) + if not_doc_files: + return CODE_NO_SKIP + else: + return CODE_SKIP + + +# Utils + + +def get_changed_files(git_root: str, reference_branch: str) -> list[str]: + """Get list of files changed between current branch and reference branch.""" + repo = git.Repo(git_root) + diff_index = repo.git.diff("--name-only", reference_branch).strip() + if not diff_index: + return [] + return [f.strip() for f in diff_index.split("\n") if f.strip()] + + +def is_doc_file(file_path: str) -> bool: + """Check if a file is a documentation file.""" + for pattern in DOC_PATTERNS: + if re.search(pattern, file_path): + return True + return False + + +def print_changes(doc_files: list[str], not_doc_files: list[str]) -> None: + display_doc = " \n".join(doc_files) + print(f"doc_files({len(doc_files)})") + if doc_files: + display_doc = "\n".join(doc_files) + print(textwrap.indent(display_doc, " ")) + + print(f"non_doc_files({len(not_doc_files)})") + if not_doc_files: + display_non_doc = " \n".join(not_doc_files) + print(textwrap.indent(display_non_doc, " ")) + + +def get_args() -> tuple[str, str]: + """Parse command line arguments and validate them.""" + parser = argparse.ArgumentParser(description="Check if CI can skip tests for a git branch") + parser.add_argument("git_root", help="The root directory of the git project") + parser.add_argument("reference_branch", help="The branch to compare against") + args = parser.parse_args() + git_root = os.path.abspath(args.git_root) + ref_branch = args.reference_branch + + if not os.path.exists(git_root): + raise ValueError(f"Git root directory does not exist: {git_root}") + if not os.path.isdir(git_root): + raise ValueError(f"Git root is not a directory: {git_root}") + try: + git.Repo(git_root) + except git.InvalidGitRepositoryError: + raise ValueError(f"Directory is not a git repository: {git_root}") + return git_root, ref_branch + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as e: + print(e) + sys.exit(CODE_ERROR) diff --git a/templates/github/.github/workflows/ci.yml.j2 b/templates/github/.github/workflows/ci.yml.j2 index ddd680c7..d86f9652 100644 --- a/templates/github/.github/workflows/ci.yml.j2 +++ b/templates/github/.github/workflows/ci.yml.j2 @@ -38,6 +38,34 @@ jobs: .github/workflows/scripts/check_commit.sh {%- endif %} {%- endif %} + + check-changes: + runs-on: ubuntu-latest + outputs: + run_tests: {{ '${{ steps.check.outputs.run_tests }}' }} + steps: + {{ checkout(depth=0, path=plugin_name) | indent(6) }} + + {{ setup_python("3.12") | indent(6) }} + + {{ install_python_deps(["gitpython"]) | indent(6) }} + + - name: Analyze changed files + shell: bash + id: check + run: | + set +e + BASE_REF={{ "${{ github.event.pull_request.base.sha }}" }} + echo "Checking against:" + git name-rev $BASE_REF + python3 .ci/scripts/skip_tests.py . $BASE_REF + exit_code=$? + if [ $exit_code -ne 0 ] && [ $exit_code -ne 1 ]; then + echo "Error: skip_tests.py returned unexpected exit code $exit_code" + exit $exit_code + fi + echo "run_tests=$exit_code" >> $GITHUB_OUTPUT + {%- if is_pulpdocs_member %} docs: @@ -45,9 +73,12 @@ jobs: {%- endif %} lint: - {%- if pre_job_template %} - needs: {{ pre_job_template.name }} - {%- endif %} + needs: + - "check-changes" + {%- if pre_job_template %} + - "{{ pre_job_template.name }}" + {%- endif %} + if: needs.check-changes.outputs.run_tests == '1' uses: "./.github/workflows/lint.yml" build: @@ -87,6 +118,7 @@ jobs: # This is a dummy dependent task to have a single entry for the branch protection rules. runs-on: "ubuntu-latest" needs: + - "check-changes" {%- if check_commit_message or lint_requirements %} - "check-commits" {%- endif %} @@ -100,9 +132,17 @@ jobs: - name: "Collect needed jobs results" working-directory: "." run: | - echo {{ "'${{toJson(needs)}}'" }} | jq -r 'to_entries[]|select(.value.result!="success")|.key + ": " + .value.result' - echo {{ "'${{toJson(needs)}}'" }} | jq -e 'to_entries|map(select(.value.result!="success"))|length == 0' + if [ {{ "${{ needs.check-changes.outputs.run_tests }}" }} == "1" ]; then + # Full test run - check all jobs + echo {{ "'${{toJson(needs)}}'" }} | jq -r 'to_entries[]|select(.value.result!="success")|.key + ": " + .value.result' + echo {{ "'${{toJson(needs)}}'" }} | jq -e 'to_entries|map(select(.value.result!="success"))|length == 0' + else + # Docs-only run - check only required jobs (exclude lint and test) + echo {{ "'${{toJson(needs)}}'" }} | jq -r 'to_entries[]|select(.key != "lint" and .key != "test")|select(.value.result!="success")|.key + ": " + .value.result' + echo {{ "'${{toJson(needs)}}'" }} | jq -e 'to_entries|map(select(.key != "lint" and .key != "test"))|map(select(.value.result!="success"))|length == 0' + fi echo "CI says: Looks good!" + {%- if post_job_template %} {% include post_job_template.path | indent (2) %} {%- endif %}