diff --git a/.github/actions/check-changed-files/action.yml b/.github/actions/check-changed-files/action.yml index 4e44385e177..f012557ea88 100644 --- a/.github/actions/check-changed-files/action.yml +++ b/.github/actions/check-changed-files/action.yml @@ -1,12 +1,19 @@ name: 'Check Changed Files' description: | - Check if all changed files in a PR match provided regex patterns. + Check if all changed files in a PR match provided glob patterns. - This action compares changed files in a pull request against one or more regex patterns + This action compares changed files in a pull request against one or more glob patterns and determines if all changed files match at least one of the provided patterns. + It only supports pull_request events. Inputs: - - patterns: List of regex patterns (multiline string) to match against changed file paths + - patterns_file: Path to a file containing glob patterns (relative to repository root). + Lines starting with '#' and blank lines are ignored. + + Pattern syntax: + - ** matches any path including directory separators (recursive) + - * matches any characters except a directory separator + - . is treated as a literal dot (no escaping needed) Outputs: - only_changed: Boolean indicating if all changed files matched the patterns @@ -15,8 +22,8 @@ description: | - matched_files: JSON array of files that matched at least one pattern - unmatched_files: JSON array of files that didn't match any pattern inputs: - patterns: - description: 'List of regex patterns to match against changed files' + patterns_file: + description: 'Path to a file containing glob patterns (relative to repository root)' required: true outputs: @@ -57,12 +64,60 @@ runs: exit 1 fi - # Read patterns from input (multiline string) - PATTERNS_INPUT="${{ inputs.patterns }}" + # Convert a glob pattern to an anchored ERE regex pattern. + # Glob syntax supported: + # ** matches any path including directory separators + # * matches any characters except a directory separator + # . is treated as a literal dot + # All other characters are treated as literals. + glob_to_regex() { + local glob="$1" + local result="$glob" + # Replace ** and * with placeholders before escaping + result="${result//\*\*/__DOUBLESTAR__}" + result="${result//\*/__STAR__}" + # Escape regex metacharacters that could appear in file paths. + # Note: { } ^ $ are not escaped because they are either not special + # in ERE mid-pattern or cannot appear in file paths. + result="${result//\\/\\\\}" + result="${result//./\\.}" + result="${result//+/\\+}" + result="${result//\?/\\?}" + result="${result//\[/\\[}" + result="${result//\]/\\]}" + result="${result//\(/\\(}" + result="${result//\)/\\)}" + result="${result//|/\\|}" + # Restore glob placeholders as regex + result="${result//__STAR__/[^/]*}" + result="${result//__DOUBLESTAR__/.*}" + # Anchor to full path + echo "^${result}$" + } + + PATTERNS=() + + PATTERNS_FILE="${{ inputs.patterns_file }}" - # Validate patterns input - if [ -z "$PATTERNS_INPUT" ]; then - echo "Error: patterns input is required" + # Read glob patterns from file, skip comments and blank lines + FULL_PATH="${GITHUB_WORKSPACE}/${PATTERNS_FILE}" + if [ ! -f "$FULL_PATH" ]; then + echo "Error: patterns_file '$FULL_PATH' not found" + exit 1 + fi + while IFS= read -r line; do + # Remove leading/trailing whitespace + line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + # Skip blank lines and comments + if [ -z "$line" ] || [[ "$line" == \#* ]]; then + continue + fi + PATTERNS+=("$(glob_to_regex "$line")") + done < "$FULL_PATH" + + # Check if we have any valid patterns + if [ ${#PATTERNS[@]} -eq 0 ]; then + echo "Error: No valid patterns provided" exit 1 fi @@ -77,23 +132,6 @@ runs: echo "Changed files:" echo "$CHANGED_FILES" - # Convert patterns to array and filter out empty lines - PATTERNS=() - while IFS= read -r pattern; do - # Remove leading/trailing whitespace - pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - # Skip empty patterns - if [ -n "$pattern" ]; then - PATTERNS+=("$pattern") - fi - done <<< "$PATTERNS_INPUT" - - # Check if we have any valid patterns - if [ ${#PATTERNS[@]} -eq 0 ]; then - echo "Error: No valid patterns provided" - exit 1 - fi - # Initialize arrays MATCHED_FILES=() UNMATCHED_FILES=() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0007e8030f..bc39f93bf69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,24 +37,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} uses: ./.github/actions/check-changed-files with: - # Patterns that do NOT require CI to run - patterns: | - \.md$ - eng/pipelines/.* - eng/test-configuration.json - \.github/workflows/apply-test-attributes.yml - \.github/workflows/backport.yml - \.github/workflows/dogfood-comment.yml - \.github/workflows/generate-api-diffs.yml - \.github/workflows/generate-ats-diffs.yml - \.github/workflows/labeler-*.yml - \.github/workflows/markdownlint*.yml - \.github/workflows/refresh-manifests.yml - \.github/workflows/pr-review-needed.yml - \.github/workflows/specialized-test-runner.yml - \.github/workflows/tests-outerloop.yml - \.github/workflows/tests-quarantine.yml - \.github/workflows/update-*.yml + patterns_file: eng/testing/github-ci-trigger-patterns.txt - id: compute_version_suffix name: Compute version suffix for PRs diff --git a/docs/ci/ci-trigger-patterns.md b/docs/ci/ci-trigger-patterns.md new file mode 100644 index 00000000000..ab7f4cbc67d --- /dev/null +++ b/docs/ci/ci-trigger-patterns.md @@ -0,0 +1,71 @@ +# CI Trigger Patterns + +## Overview + +The file `eng/testing/github-ci-trigger-patterns.txt` lists glob patterns for files whose changes do **not** require the full CI to run. + +When a pull request is opened or updated, the CI workflow (`ci.yml`) checks whether **all** changed files match at least one pattern in the file. If they do, the workflow is skipped (no build or test jobs run). This keeps CI fast for changes that only affect documentation, pipeline configuration, or unrelated workflow files. + +> **Note:** This mechanism applies only to **pull requests**. Pushes to `main` or `release/*` branches always run the full CI pipeline. The `check-changed-files` action explicitly rejects non-`pull_request` events. + +## Why a Separate File? + +Previously the patterns were inlined in `.github/workflows/ci.yml`. Any change to that file (even just adding a new pattern to skip CI) would trigger CI on itself. Moving the patterns to `eng/testing/github-ci-trigger-patterns.txt` decouples pattern maintenance from the workflow definition. + +## Pattern Syntax + +Patterns use a simple **glob** style: + +| Syntax | Meaning | +|--------|---------| +| `**` | Matches any path including directory separators (recursive) | +| `*` | Matches any characters except a directory separator | +| `.` | Treated as a literal dot — no backslash escaping needed | + +All other characters (letters, digits, `-`, `_`, `/`, etc.) are treated as literals. + +Lines starting with `#` and blank lines are ignored. + +### Examples + +```text +# All Markdown files anywhere in the repo +**.md + +# All files under eng/pipelines/ recursively +eng/pipelines/** + +# A specific file +eng/test-configuration.json + +# Workflow files matching a glob (e.g. labeler-promote.yml, labeler-train.yml) +.github/workflows/labeler-*.yml +``` + +## How to Add a New Pattern + +To add files whose changes should not trigger CI: + +1. Open `eng/testing/github-ci-trigger-patterns.txt`. +2. Add one pattern per line, optionally preceded by a comment. +3. Submit a PR — CI will not run for that PR if all changed files match the patterns. + +> **Tip:** Changing the patterns file itself is listed as a skippable change (`eng/testing/github-ci-trigger-patterns.txt`), so a PR that only updates this file will not trigger CI. + +## How It Works + +The `.github/actions/check-changed-files` composite action: + +1. Reads `eng/testing/github-ci-trigger-patterns.txt` from the checked-out repository. +2. Converts each glob pattern to an anchored ERE (Extended Regular Expression) regex: + - `**` → `.*` + - `*` → `[^/]*` + - `.` and other regex metacharacters (`+`, `?`, `[`, `]`, `(`, `)`, `|`) → escaped with `\` +3. For every file changed in the PR, checks whether the file path matches at least one of the converted regexes. +4. Outputs `only_changed=true` when every changed file matched, allowing the calling workflow to skip further jobs. + +## Related Files + +- `eng/testing/github-ci-trigger-patterns.txt` — the patterns file described on this page +- `.github/actions/check-changed-files/action.yml` — the composite action that reads and evaluates the patterns +- `.github/workflows/ci.yml` — the CI workflow that calls the action diff --git a/eng/testing/github-ci-trigger-patterns.txt b/eng/testing/github-ci-trigger-patterns.txt new file mode 100644 index 00000000000..0d0ae248697 --- /dev/null +++ b/eng/testing/github-ci-trigger-patterns.txt @@ -0,0 +1,49 @@ +# CI trigger patterns +# +# This file lists glob patterns for files whose changes do NOT require the full CI +# to run (e.g. documentation, non-build pipeline scripts, or specific workflow files +# that are unrelated to the build and test process). +# +# When all files changed in a pull request match at least one pattern here, the CI +# workflow is skipped. +# +# Pattern syntax: +# ** matches any path including directory separators (recursive) +# * matches any characters except a directory separator +# . is treated as a literal dot (no escaping needed) +# All other characters are treated as literals. +# +# Lines starting with '#' and blank lines are ignored. + +# This file itself - changing CI-skip patterns doesn't require a CI run. +# Note: this also means a syntax error introduced here won't be caught by CI, +# so take care when editing. Pattern conversion is validated by the +# check-changed-files action at runtime. +eng/testing/github-ci-trigger-patterns.txt + +# Documentation +**.md + +# Engineering pipeline scripts (Azure DevOps, not used in the GitHub CI build) +eng/pipelines/** +eng/test-configuration.json + +.github/instructions/** +.github/skills/** + +# GitHub workflow files that do not affect the CI build or test process +.github/workflows/apply-test-attributes.yml +.github/workflows/backmerge-release.yml +.github/workflows/backport.yml +.github/workflows/dogfood-comment.yml +.github/workflows/generate-api-diffs.yml +.github/workflows/generate-ats-diffs.yml +.github/workflows/labeler-*.yml +.github/workflows/markdownlint*.yml +.github/workflows/pr-review-needed.yml +.github/workflows/refresh-manifests.yml +.github/workflows/reproduce-flaky-tests.yml +.github/workflows/specialized-test-runner.yml +.github/workflows/tests-outerloop.yml +.github/workflows/tests-quarantine.yml +.github/workflows/update-*.yml