diff --git a/eng/pipelines/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml index f5f2269494..fcf15f8af0 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -9,6 +9,13 @@ parameters: type: boolean default: false + # When true, add a gate stage that detects redundant PR triggers (e.g. Draft <-> Open state + # transitions with no new commits). When a redundant trigger is detected, all subsequent stages + # are skipped and the pipeline succeeds without performing any work. + - name: skipRedundantTrigger + type: boolean + default: false + # The target frameworks to build and run tests for on Windows. # # These are _not_ the target frameworks to build the driver packages for. @@ -123,6 +130,29 @@ variables: value: SqlServer.Artifacts stages: + # When skipRedundantTrigger is enabled, add a gate stage that detects PR triggers caused by + # state-only transitions (Draft <-> Open). If the trigger is redundant, the no-op stage's + # condition evaluates to false (skipped), which causes secrets_stage to implicitly skip via + # its default succeeded() condition, cascading to all downstream stages. + - ${{ if eq(parameters.skipRedundantTrigger, true) }}: + - template: /eng/pipelines/stages/detect-redundant-trigger-stage.yml@self + parameters: + debug: ${{ parameters.debug }} + + - stage: skip_redundant_trigger_stage + displayName: Skip Redundant Trigger + dependsOn: detect_redundant_trigger_stage + condition: eq(dependencies.detect_redundant_trigger_stage.outputs['detect_redundant_trigger.detect.meaningful'], 'true') + jobs: + - job: proceed + displayName: Build is meaningful + pool: + vmImage: ubuntu-latest + steps: + - checkout: none + - bash: echo "PR Trigger gate passed — proceeding with build and test." + displayName: Confirm + # Generate secrets used throughout the pipeline. - template: /eng/pipelines/stages/generate-secrets-ci-stage.yml@self parameters: diff --git a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml index be7e2e9e3b..5cbde97772 100644 --- a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml @@ -130,6 +130,7 @@ extends: buildPlatforms: ${{ parameters.buildPlatforms }} referenceType: Package debug: ${{ parameters.debug }} + skipRedundantTrigger: true targetFrameworks: ${{ parameters.targetFrameworks }} targetFrameworksUnix: ${{ parameters.targetFrameworksUnix }} testJobTimeout: ${{ parameters.testJobTimeout }} diff --git a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml index 30950ec21c..77aa782c49 100644 --- a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml @@ -130,6 +130,7 @@ extends: buildPlatforms: ${{ parameters.buildPlatforms }} referenceType: Project debug: ${{ parameters.debug }} + skipRedundantTrigger: true targetFrameworks: ${{ parameters.targetFrameworks }} targetFrameworksUnix: ${{ parameters.targetFrameworksUnix }} testJobTimeout: ${{ parameters.testJobTimeout }} diff --git a/eng/pipelines/stages/detect-redundant-trigger-stage.yml b/eng/pipelines/stages/detect-redundant-trigger-stage.yml new file mode 100644 index 0000000000..2f02133c16 --- /dev/null +++ b/eng/pipelines/stages/detect-redundant-trigger-stage.yml @@ -0,0 +1,128 @@ +#################################################################################################### +# Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this +# file to you under the MIT license. See the LICENSE file in the project root for more information. +#################################################################################################### + +# This stage gates PR pipeline runs to avoid redundant builds triggered by state-only transitions +# (e.g. Draft <-> Open). It compares the current merge commit (Build.SourceVersion) against the +# most recent successful build for the same PR in this pipeline definition. +# +# If the merge commit is unchanged and the prior build succeeded, the build is considered redundant +# and the output variable 'meaningful' is set to 'false'. Downstream stages should check this +# variable and skip when it is 'false'. +# +# Output variable reference (for use in stage conditions): +# +# dependencies.detect_redundant_trigger_stage.outputs['detect_redundant_trigger.detect.meaningful'] +# +# This stage defines: +# +# Stage name: detect_redundant_trigger_stage +# Job name: detect_redundant_trigger +# Step name: detect +# Variable: meaningful (true|false) + +parameters: + + # True to emit debug information. + - name: debug + type: boolean + default: false + +stages: + + - stage: detect_redundant_trigger_stage + displayName: Detect Redundant Trigger + jobs: + + - job: detect_redundant_trigger + displayName: Detect Redundant Trigger + pool: + vmImage: ubuntu-latest + + steps: + + - checkout: none + + - bash: | + set -euo pipefail + + # If this is not a PR-triggered run (e.g., manual queue), always proceed. + if [[ "$(Build.Reason)" != "PullRequest" ]]; then + echo "##[section]Build is MEANINGFUL — not a PR trigger (Build.Reason=$(Build.Reason))." + echo "##vso[task.setvariable variable=meaningful;isOutput=true]true" + exit 0 + fi + + # The current merge commit SHA for this PR build. + CURRENT_SOURCE_VERSION="$(Build.SourceVersion)" + + # The Azure DevOps REST API base URL. + ORG_URL="$(System.CollectionUri)" + PROJECT="$(System.TeamProject)" + DEFINITION_ID="$(System.DefinitionId)" + BUILD_ID="$(Build.BuildId)" + + # The branch name used for PR builds (refs/pull/{number}/merge). + BRANCH="$(Build.SourceBranch)" + + echo "Current source version: $CURRENT_SOURCE_VERSION" + echo "Branch: $BRANCH" + echo "Definition ID: $DEFINITION_ID" + echo "Build ID: $BUILD_ID" + + # Query the most recent successful build for this PR in this pipeline, + # excluding the current build. + API_URL="${ORG_URL}${PROJECT}/_apis/build/builds" + API_URL="${API_URL}?definitions=${DEFINITION_ID}" + API_URL="${API_URL}&branchName=${BRANCH}" + API_URL="${API_URL}&resultFilter=succeeded" + API_URL="${API_URL}&\$top=5" + API_URL="${API_URL}&queryOrder=finishTimeDescending" + API_URL="${API_URL}&api-version=7.1" + + # Call the API using the mapped env var (avoids macro expansion/logging). + # On HTTP error, default to meaningful=true so the build proceeds. + HTTP_CODE=$(curl -s -o /tmp/api_response.json -w "%{http_code}" \ + -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" "$API_URL") + + if [[ "$HTTP_CODE" -lt 200 || "$HTTP_CODE" -ge 300 ]]; then + echo "##[warning]Builds API returned HTTP $HTTP_CODE — defaulting to meaningful build." + echo "##vso[task.setvariable variable=meaningful;isOutput=true]true" + exit 0 + fi + + RESPONSE=$(cat /tmp/api_response.json) + + # Find the first successful build that is not the current build. + LAST_SUCCESSFUL_VERSION=$(echo "$RESPONSE" | \ + jq -r --argjson buildId "$BUILD_ID" \ + '[.value[] | select(.id != $buildId)] | .[0].sourceVersion // empty') + + echo "Last successful source version: ${LAST_SUCCESSFUL_VERSION:-}" + + if [[ -n "$LAST_SUCCESSFUL_VERSION" && "$LAST_SUCCESSFUL_VERSION" == "$CURRENT_SOURCE_VERSION" ]]; then + echo "" + echo "##[section]Build is REDUNDANT — source version unchanged and prior build succeeded." + echo "This is likely a Draft/Open state transition. Skipping build." + echo "##vso[task.setvariable variable=meaningful;isOutput=true]false" + else + echo "" + echo "##[section]Build is MEANINGFUL — proceeding." + if [[ -z "$LAST_SUCCESSFUL_VERSION" ]]; then + echo " Reason: No prior successful build found for this PR." + elif [[ "$LAST_SUCCESSFUL_VERSION" != "$CURRENT_SOURCE_VERSION" ]]; then + echo " Reason: Source version changed (new commits or target branch update)." + fi + echo "##vso[task.setvariable variable=meaningful;isOutput=true]true" + fi + name: detect + displayName: Check for redundant PR trigger + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - ${{ if eq(parameters.debug, true) }}: + - bash: | + echo "API Response (first 2000 chars):" + head -c 2000 /tmp/api_response.json 2>/dev/null || echo "" + displayName: Debug - Show API Response