From ae80ddc8dd523c5dcc7c6b2f3ee7569a0cd023f8 Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:13:59 -0700 Subject: [PATCH 1/2] ci: Skip the build and tests on docs-only PRs Every release blog PR changes only documentation, yet ran the full build, all eight test projects, coverage and CodeQL, about 17 runner-minutes each. The build and test jobs can't be left out with paths-ignore, because Build Solution and Test Suite are required checks and a check that never reports blocks the merge. A new Detect Changes job classifies the PR's files. When every one is Markdown or under docs/, the build, test matrix and coverage are skipped, which counts as passing for a required check. Anything else, including workflows and scripts, runs the full suite, as do pushes and manual runs. Test Suite still fails on any failed or cancelled job, and also when Detect Changes itself didn't succeed, so the build can't be skipped by mistake. CodeQL's pull_request trigger ignores docs and Markdown; main's push and weekly runs still cover the code. Fixes #173 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 68 +++++++++++++++++++++++++-- .github/workflows/codeql-analysis.yml | 5 ++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 348093eb..1a94093a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,8 @@ permissions: # No paths-ignore here: "Build Solution" and "Test Suite" below are # required status checks on main's ruleset. A docs-only PR that this # workflow never runs against would leave them permanently missing, - # blocking the PR from merging. + # blocking the PR from merging. The changes job skips the build and tests + # for such a PR instead; a skipped required check counts as passing. pull_request: types: [opened, synchronize, reopened, ready_for_review] @@ -54,8 +55,60 @@ env: DOTNET_CLI_TELEMETRY_OPTOUT: true jobs: + # Whether the PR changes anything the build or tests depend on. A PR that + # changes only Markdown and files under docs/ (such as a release blog PR) + # skips the build, the test matrix and coverage. Everything else counts as + # code, including workflows and scripts, so an unexpected path runs the full + # suite. Pushes and manual runs always run it. + changes: + name: Detect Changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + code: ${{ steps.diff.outputs.code }} + steps: + - name: Checkout code + if: github.event_name == 'pull_request' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Classify changed files + id: diff + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + if [ "$EVENT_NAME" != "pull_request" ]; then + echo "code=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # HEAD is the PR merged into its base. Diffing against the base the + # event names can also pick up commits that reached main since, + # which only ever adds paths, so it errs toward running the suite. + mapfile -t files < <(git diff --name-only "$BASE_SHA" HEAD) + code=false + for file in "${files[@]}"; do + case "$file" in + docs/*|*.md) ;; + *) code=true; echo "Code change: $file"; break ;; + esac + done + if [ "${#files[@]}" -eq 0 ]; then + code=true + fi + + echo "code=$code" >> "$GITHUB_OUTPUT" + if [ "$code" = "false" ]; then + echo "::notice::Docs-only change (${#files[@]} files); the build, tests and coverage are skipped." + fi + build: name: Build Solution + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest timeout-minutes: 15 @@ -464,6 +517,7 @@ jobs: pull-requests: write issues: read needs: + - changes - build - discover-tests - test @@ -503,6 +557,8 @@ jobs: # PR-controlled project paths: read from the environment, not # interpolated into the script. TEST_MATRIX: ${{ needs.discover-tests.outputs.matrix }} + CHANGES_STATUS: ${{ needs.changes.result }} + CODE_CHANGED: ${{ needs.changes.outputs.code }} BUILD_STATUS: ${{ needs.build.result }} DISCOVER_STATUS: ${{ needs.discover-tests.result }} TEST_STATUS: ${{ needs.test.result }} @@ -512,6 +568,9 @@ jobs: echo "## Test Execution Summary" echo "" echo "### Job Status" + if [ "$CHANGES_STATUS" = "success" ] && [ "$CODE_CHANGED" = "false" ]; then + echo "- **Docs-only change:** the build, tests and coverage were skipped." + fi echo "- **Build:** ${BUILD_STATUS}" echo "- **Discovered Tests:** ${DISCOVER_STATUS}" echo "- **Test Matrix:** ${TEST_STATUS}" @@ -530,8 +589,11 @@ jobs: # This job is the required status check that stands in for the # dynamically named "Tests: …" matrix jobs and the 60% coverage # gate, so it must fail when any of them does. A skipped test - # matrix (no test projects) still passes. - if [[ "$BUILD_STATUS" =~ ^(failure|cancelled)$ \ + # matrix (no test projects, or a docs-only change) still passes, but + # only when the changes job itself succeeded: if it failed, the build + # was skipped for the wrong reason. + if [[ "$CHANGES_STATUS" != "success" \ + || "$BUILD_STATUS" =~ ^(failure|cancelled)$ \ || "$DISCOVER_STATUS" =~ ^(failure|cancelled)$ \ || "$TEST_STATUS" =~ ^(failure|cancelled)$ \ || "$COVERAGE_STATUS" =~ ^(failure|cancelled)$ ]]; then diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 924d0548..f1522eba 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,9 +21,14 @@ name: "CodeQL" - "**.csproj" - ".github/workflows/**" + # Not a required check, and main's push and weekly runs still cover the + # code, so a docs-only PR (such as a release blog PR) skips the analysis. pull_request: branches: - "**" + paths-ignore: + - "docs/**" + - "**/*.md" workflow_dispatch: inputs: From 763819379b8cadc4fee27b9d9d8b6139234c8582 Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:26:48 -0700 Subject: [PATCH 2/2] ci: Classify both paths of a renamed file git diff --name-only reports a rename under its new path only, so moving a source file into docs/ or to a .md name looked docs-only and skipped the build and tests. --no-renames lists the removed path too. Addresses Copilot review on #175. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a94093a..e581183d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,9 @@ jobs: # HEAD is the PR merged into its base. Diffing against the base the # event names can also pick up commits that reached main since, # which only ever adds paths, so it errs toward running the suite. - mapfile -t files < <(git diff --name-only "$BASE_SHA" HEAD) + # --no-renames lists a renamed file under both paths, so moving code + # into docs/ or to a .md name still counts as a code change. + mapfile -t files < <(git diff --name-only --no-renames "$BASE_SHA" HEAD) code=false for file in "${files[@]}"; do case "$file" in