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: