From 39b68aa4bc1412c6c76c34805e435462644209ae Mon Sep 17 00:00:00 2001 From: David Engel Date: Fri, 17 Jul 2026 21:35:29 +0000 Subject: [PATCH 1/3] ci: skip macOS validation on PRs with no macOS-relevant changes Refactor the PR-only EvaluateDuplicate stage into a single EvaluateRequirements gate that runs both the duplicate-run check and a new macOS relevance check. The macOS jobs (Build_MacOS, Test_MacOS, Build_mssql_python_MacOS) now skip on PRs whose diff touches no macOS-relevant paths, and always run on non-PR builds. macOS-relevant paths (moderate allowlist): mssql-tds security/transport modules, the macOS Kerberos test, Cargo manifests/lock, rust-toolchain, and .pipeline/**. The guard fails safe (runs macOS) on any error or missing PR context. --- .pipeline/scripts/evaluate-duplicate-pr.py | 2 +- .pipeline/scripts/evaluate-macos-relevance.py | 150 ++++++++++++++++++ .pipeline/templates/validation-stages.yml | 41 +++-- 3 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 .pipeline/scripts/evaluate-macos-relevance.py diff --git a/.pipeline/scripts/evaluate-duplicate-pr.py b/.pipeline/scripts/evaluate-duplicate-pr.py index e41cdcfd..8758e6f1 100644 --- a/.pipeline/scripts/evaluate-duplicate-pr.py +++ b/.pipeline/scripts/evaluate-duplicate-pr.py @@ -8,7 +8,7 @@ Any missing prerequisite (access token, source commit) or API error falls back to ``skipDuplicate=false`` so full validation proceeds (safe default). -Consumed by the ``EvaluateDuplicate`` stage in +Consumed by the ``SetDuplicateState`` step of the ``EvaluateRequirements`` stage in ``.pipeline/templates/validation-stages.yml``. """ import json diff --git a/.pipeline/scripts/evaluate-macos-relevance.py b/.pipeline/scripts/evaluate-macos-relevance.py new file mode 100644 index 00000000..7e35b82d --- /dev/null +++ b/.pipeline/scripts/evaluate-macos-relevance.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""macOS validation relevance guard. + +Determines whether a pull request's changes can affect the macOS build/test jobs. +Only a subset of the workspace has macOS-specific behavior (Apple Security.framework +TLS/crypto and GSSAPI, plus dependency manifests and pipeline definitions). When a PR +touches none of those paths, the macOS jobs add wall-clock time without adding coverage, +so this guard emits ``macRelevant=false`` to let them short-circuit. + +Any missing prerequisite (target branch, git failure) or unexpected error falls back to +``macRelevant=true`` so the macOS jobs run (safe default: never skip on uncertainty). + +Consumed by the ``SetMacRelevance`` step of the ``EvaluateRequirements`` stage in +``.pipeline/templates/validation-stages.yml``. The macOS jobs also always run on non-PR +builds regardless of this variable (that gating lives in the pipeline conditions). +""" +import fnmatch +import os +import subprocess +import sys + +# Paths whose changes require a macOS validation run (moderate allowlist). +# fnmatch-style globs matched against repo-relative POSIX paths. Note fnmatch's +# ``*`` also spans ``/``, so a trailing ``/*`` matches arbitrarily deep subtrees. +MAC_RELEVANT_GLOBS = [ + # macOS-specific security libraries: Security.framework/CommonCrypto crypto and TLS. + "mssql-tds/src/security/*", + "mssql-tds/src/connection/transport.rs", + "mssql-tds/src/connection/transport/*", + # macOS GSSAPI/Kerberos integration test. + "mssql-tds/tests/test_kerberos_gssapi.rs", + # Dependency graph / toolchain: a resolution change can break the macOS build. + "*Cargo.toml", + "Cargo.lock", + "rust-toolchain*", + # Pipeline definitions themselves (templates, scripts) — high-impact, run macOS. + ".pipeline/*", +] + + +def set_mac_relevant(value): + print(f"##vso[task.setvariable variable=macRelevant;isOutput=true]{value}") + + +def matches_allowlist(path): + return any(fnmatch.fnmatch(path, pattern) for pattern in MAC_RELEVANT_GLOBS) + + +def main(): + build_reason = os.environ.get("BUILD_REASON", "") + if build_reason != "PullRequest": + # Non-PR builds always run macOS in full; the guard is a no-op here. + print( + f"Build.Reason='{build_reason or ''}' is not PullRequest; " + "macOS jobs run in full." + ) + set_mac_relevant("true") + return + + target_branch = os.environ.get("SYSTEM_PULLREQUEST_TARGETBRANCH", "") + if not target_branch: + print( + "##vso[task.logissue type=warning]System.PullRequest.TargetBranch is unavailable; " + "running macOS validation." + ) + set_mac_relevant("true") + return + + # TargetBranch arrives as e.g. 'refs/heads/main' or 'main'; normalize to a ref + # the local clone can resolve. ADO checks out the PR merge ref with the target + # branch tip available as origin/. + short_branch = target_branch + for prefix in ("refs/heads/", "refs/remotes/origin/"): + if short_branch.startswith(prefix): + short_branch = short_branch[len(prefix):] + break + + candidate_refs = [ + f"origin/{short_branch}", + short_branch, + target_branch, + ] + + diff_output = None + used_ref = None + for ref in candidate_refs: + try: + # Three-dot diff against the merge base isolates the PR's own changes + # from unrelated commits already on the target branch. + diff_output = subprocess.check_output( + ["git", "diff", "--name-only", f"{ref}...HEAD"], + stderr=subprocess.STDOUT, + text=True, + ) + used_ref = ref + break + except subprocess.CalledProcessError: + continue + except OSError as exc: + print( + f"##vso[task.logissue type=warning]Unable to invoke git ({exc}); " + "running macOS validation." + ) + set_mac_relevant("true") + return + + if diff_output is None: + print( + "##vso[task.logissue type=warning]Could not compute PR diff against target " + f"branch '{target_branch}' (tried {candidate_refs}); running macOS validation." + ) + set_mac_relevant("true") + return + + changed_files = [line.strip() for line in diff_output.splitlines() if line.strip()] + print(f"Comparing against '{used_ref}'; {len(changed_files)} changed file(s).") + + if not changed_files: + # An empty diff is unexpected for a PR; do not skip on that ambiguity. + print( + "##vso[task.logissue type=warning]PR diff is empty; running macOS validation." + ) + set_mac_relevant("true") + return + + matched = [path for path in changed_files if matches_allowlist(path)] + if matched: + preview = ", ".join(matched[:10]) + suffix = "" if len(matched) <= 10 else f" (+{len(matched) - 10} more)" + print(f"macOS-relevant change(s) detected: {preview}{suffix}") + set_mac_relevant("true") + else: + print( + "No macOS-relevant paths changed; skipping macOS validation jobs for this PR." + ) + set_mac_relevant("false") + + +if __name__ == "__main__": + # Safety net: the guard is best-effort, so any unexpected failure must fall back + # to running macOS validation (macRelevant=true) rather than skipping coverage. + try: + main() + except Exception as exc: # noqa: BLE001 - deliberate catch-all for safe fallback + print( + f"##vso[task.logissue type=warning]macOS relevance guard failed unexpectedly " + f"({type(exc).__name__}: {exc}); running macOS validation." + ) + set_mac_relevant("true") + sys.exit(0) diff --git a/.pipeline/templates/validation-stages.yml b/.pipeline/templates/validation-stages.yml index c97e5d52..0df73c49 100644 --- a/.pipeline/templates/validation-stages.yml +++ b/.pipeline/templates/validation-stages.yml @@ -40,12 +40,15 @@ parameters: default: '2025-latest' stages: -- stage: EvaluateDuplicate - displayName: Evaluate PR duplicate +# Requirements gate: a single PR-only stage that computes which downstream work is +# needed. Today it covers (1) duplicate-run detection and (2) macOS relevance; it is +# the home for future "smart" skip optimizations. +- stage: EvaluateRequirements + displayName: Evaluate PR requirements condition: and(eq(variables['Build.Reason'], 'PullRequest'), eq('${{ parameters.RunFuzz }}', 'false'), eq('${{ parameters.RunLongHaul }}', 'false')) jobs: - job: Evaluate - displayName: Check prior successful PR validation + displayName: Evaluate PR validation requirements pool: name: RUST-1ES-POOL-WUS3 demands: @@ -58,11 +61,18 @@ stages: displayName: Check duplicate PR head commit env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) + # Determine whether the PR touches macOS-relevant paths. Emits macRelevant; + # the macOS jobs gate on it (and always run on non-PR builds). + - bash: | + set -euo pipefail + python3 .pipeline/scripts/evaluate-macos-relevance.py + name: SetMacRelevance + displayName: Check macOS validation relevance - stage: Build displayName: Build Stage dependsOn: - - EvaluateDuplicate + - EvaluateRequirements condition: >- and( not(canceled()), @@ -70,7 +80,7 @@ stages: eq('${{ parameters.RunLongHaul }}', 'false'), or( ne(variables['Build.Reason'], 'PullRequest'), - ne(dependencies.EvaluateDuplicate.outputs['Evaluate.SetDuplicateState.skipDuplicate'], 'true') + ne(dependencies.EvaluateRequirements.outputs['Evaluate.SetDuplicateState.skipDuplicate'], 'true') ) ) jobs: @@ -276,6 +286,9 @@ stages: architecture: ARM64 - job: Build_MacOS displayName: Build MacOS + # Skip on PRs whose diff touches no macOS-relevant paths (see + # EvaluateRequirements/SetMacRelevance). Always runs on non-PR builds. + condition: and(succeeded(), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) pool: name: Azure Pipelines vmImage: macOS-latest @@ -304,6 +317,9 @@ stages: - job: Test_MacOS displayName: Test MacOS + # Skip on PRs whose diff touches no macOS-relevant paths (see + # EvaluateRequirements/SetMacRelevance). Always runs on non-PR builds. + condition: and(succeeded(), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) pool: name: Azure Pipelines vmImage: macOS-latest @@ -333,8 +349,8 @@ stages: - stage: Kerberos_Test_PR displayName: Kerberos Test (PR) dependsOn: - - EvaluateDuplicate - condition: and(not(canceled()), eq(variables['Build.Reason'], 'PullRequest'), eq('${{ parameters.RunFuzz }}', 'false'), eq('${{ parameters.RunLongHaul }}', 'false'), ne(dependencies.EvaluateDuplicate.outputs['Evaluate.SetDuplicateState.skipDuplicate'], 'true')) + - EvaluateRequirements + condition: and(not(canceled()), eq(variables['Build.Reason'], 'PullRequest'), eq('${{ parameters.RunFuzz }}', 'false'), eq('${{ parameters.RunLongHaul }}', 'false'), ne(dependencies.EvaluateRequirements.outputs['Evaluate.SetDuplicateState.skipDuplicate'], 'true')) jobs: - template: kerberos-test-template.yml parameters: @@ -350,7 +366,7 @@ stages: dependsOn: - Build # Scope success to the Build stage explicitly. Bare succeeded() evaluates the - # whole dependency graph, so the PR-only EvaluateDuplicate stage (skipped on + # whole dependency graph, so the PR-only EvaluateRequirements stage (skipped on # non-PR CI runs) would otherwise make this stage skip in CI. condition: and(succeeded('Build'), ne(variables['Build.Reason'], 'PullRequest'), eq('${{ parameters.RunFuzz }}', 'false'), eq('${{ parameters.RunLongHaul }}', 'false')) jobs: @@ -464,8 +480,8 @@ stages: - stage: Build_mssql_python dependsOn: - - EvaluateDuplicate - condition: and(not(canceled()), eq(variables['Build.Reason'], 'PullRequest'), eq('${{ parameters.RunFuzz }}', 'false'), eq('${{ parameters.RunLongHaul }}', 'false'), ne(dependencies.EvaluateDuplicate.outputs['Evaluate.SetDuplicateState.skipDuplicate'], 'true')) + - EvaluateRequirements + condition: and(not(canceled()), eq(variables['Build.Reason'], 'PullRequest'), eq('${{ parameters.RunFuzz }}', 'false'), eq('${{ parameters.RunLongHaul }}', 'false'), ne(dependencies.EvaluateRequirements.outputs['Evaluate.SetDuplicateState.skipDuplicate'], 'true')) displayName: Build mssql-python jobs: - job: Build_mssql_python_Linux @@ -481,6 +497,9 @@ stages: - job: Build_mssql_python_MacOS displayName: Build mssql-python macOS (cross-repo) + # Skip on PRs whose diff touches no macOS-relevant paths (see + # EvaluateRequirements/SetMacRelevance). Always runs on non-PR builds. + condition: and(succeeded(), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) pool: name: Azure Pipelines vmImage: macOS-latest @@ -730,7 +749,7 @@ stages: dependsOn: - Build # Scope success to the Build stage explicitly. Bare succeeded() evaluates the - # whole dependency graph, so the PR-only EvaluateDuplicate stage (skipped on + # whole dependency graph, so the PR-only EvaluateRequirements stage (skipped on # non-PR CI runs) would otherwise make this stage skip in CI. condition: and(succeeded('Build'), ne(variables['Build.Reason'], 'PullRequest'), eq('${{ parameters.RunFuzz }}', 'false'), eq('${{ parameters.RunLongHaul }}', 'false')) jobs: From cd39ce0b28006d380f931ea5d3db4fb187e03c71 Mon Sep 17 00:00:00 2001 From: David Engel Date: Fri, 17 Jul 2026 22:03:16 +0000 Subject: [PATCH 2/3] ci: use not(canceled()) for macOS job conditions The EvaluateRequirements stage is PR-only and is skipped in CI (non-PR). A bare succeeded() on the macOS jobs risks cascading to skipped when that stage is skipped, mirroring the earlier downstream-stage incident. not(canceled()) keeps the macOS jobs running on non-PR builds and matches the fail-safe pattern used by the Build and Build_mssql_python stage conditions. --- .pipeline/templates/validation-stages.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.pipeline/templates/validation-stages.yml b/.pipeline/templates/validation-stages.yml index 0df73c49..9bc70ddc 100644 --- a/.pipeline/templates/validation-stages.yml +++ b/.pipeline/templates/validation-stages.yml @@ -288,7 +288,9 @@ stages: displayName: Build MacOS # Skip on PRs whose diff touches no macOS-relevant paths (see # EvaluateRequirements/SetMacRelevance). Always runs on non-PR builds. - condition: and(succeeded(), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) + # not(canceled()) (rather than succeeded()) so a skipped EvaluateRequirements + # stage in CI does not cascade this job to skipped. + condition: and(not(canceled()), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) pool: name: Azure Pipelines vmImage: macOS-latest @@ -319,7 +321,9 @@ stages: displayName: Test MacOS # Skip on PRs whose diff touches no macOS-relevant paths (see # EvaluateRequirements/SetMacRelevance). Always runs on non-PR builds. - condition: and(succeeded(), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) + # not(canceled()) (rather than succeeded()) so a skipped EvaluateRequirements + # stage in CI does not cascade this job to skipped. + condition: and(not(canceled()), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) pool: name: Azure Pipelines vmImage: macOS-latest @@ -499,7 +503,9 @@ stages: displayName: Build mssql-python macOS (cross-repo) # Skip on PRs whose diff touches no macOS-relevant paths (see # EvaluateRequirements/SetMacRelevance). Always runs on non-PR builds. - condition: and(succeeded(), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) + # not(canceled()) (rather than succeeded()) so a skipped EvaluateRequirements + # stage in CI does not cascade this job to skipped. + condition: and(not(canceled()), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) pool: name: Azure Pipelines vmImage: macOS-latest From 4c490a04cffcb92a9c2b6888017629d3ba7d2a97 Mon Sep 17 00:00:00 2001 From: David Engel Date: Fri, 17 Jul 2026 22:22:23 +0000 Subject: [PATCH 3/3] ci: revert macOS job conditions to succeeded() Revert the not(canceled()) change and drop the accompanying comments. Whether a job-level succeeded() cascades to skipped when the PR-only EvaluateRequirements stage is skipped will be validated empirically in the PR run rather than assumed in the YAML. The stage-level not(canceled()) fail-safes are unchanged. --- .pipeline/templates/validation-stages.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.pipeline/templates/validation-stages.yml b/.pipeline/templates/validation-stages.yml index 9bc70ddc..0df73c49 100644 --- a/.pipeline/templates/validation-stages.yml +++ b/.pipeline/templates/validation-stages.yml @@ -288,9 +288,7 @@ stages: displayName: Build MacOS # Skip on PRs whose diff touches no macOS-relevant paths (see # EvaluateRequirements/SetMacRelevance). Always runs on non-PR builds. - # not(canceled()) (rather than succeeded()) so a skipped EvaluateRequirements - # stage in CI does not cascade this job to skipped. - condition: and(not(canceled()), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) + condition: and(succeeded(), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) pool: name: Azure Pipelines vmImage: macOS-latest @@ -321,9 +319,7 @@ stages: displayName: Test MacOS # Skip on PRs whose diff touches no macOS-relevant paths (see # EvaluateRequirements/SetMacRelevance). Always runs on non-PR builds. - # not(canceled()) (rather than succeeded()) so a skipped EvaluateRequirements - # stage in CI does not cascade this job to skipped. - condition: and(not(canceled()), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) + condition: and(succeeded(), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) pool: name: Azure Pipelines vmImage: macOS-latest @@ -503,9 +499,7 @@ stages: displayName: Build mssql-python macOS (cross-repo) # Skip on PRs whose diff touches no macOS-relevant paths (see # EvaluateRequirements/SetMacRelevance). Always runs on non-PR builds. - # not(canceled()) (rather than succeeded()) so a skipped EvaluateRequirements - # stage in CI does not cascade this job to skipped. - condition: and(not(canceled()), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) + condition: and(succeeded(), or(ne(variables['Build.Reason'], 'PullRequest'), eq(stageDependencies.EvaluateRequirements.Evaluate.outputs['SetMacRelevance.macRelevant'], 'true'))) pool: name: Azure Pipelines vmImage: macOS-latest