Set azure pipelines variables based on changed paths (#748)
authorSantiago Fernandez Madero <safern@microsoft.com>
Thu, 12 Dec 2019 14:17:37 +0000 (06:17 -0800)
committerGitHub <noreply@github.com>
Thu, 12 Dec 2019 14:17:37 +0000 (06:17 -0800)
* Set variables based on changed paths

* PR Feedback

* PR feedback

eng/pipelines/common/checkout-job.yml
eng/pipelines/common/evaluate-changed-paths.yml [new file with mode: 0644]
eng/pipelines/common/xplat-setup.yml
eng/pipelines/evaluate-changed-paths.sh [new file with mode: 0755]
eng/pipelines/runtime.yml

index 5cadb5a..a46268e 100644 (file)
@@ -1,5 +1,32 @@
 ### Check out job creating a git bundle and publishing it
 ### into an Azure artifact for reuse by the subsequent build and test execution phases.
+### If paths is specified, we will create a job using evaluate-changed-paths.yml template
+### for each path specified.
+
+parameters:
+  # Object containing subset include and exclude paths in an array form.
+  # Scenarios:
+  #  1. exclude paths are specified
+  #     Will include all paths except the ones in the exclude list.
+  #  2. include paths are specified
+  #     Will only include paths specified in the list.
+  #  3. exclude + include:
+  #     1st we evaluate changes for all paths except ones in excluded list. If we can't find
+  #     any applicable changes like that, then we evaluate changes for incldued paths
+  #     if any of these two finds changes, then a variable will be set to true.
+  #  In order to consume this variable you need to reference it via: $[ dependencies.checkout.outputs['SetPathVars_<subset>.containschange'] ]
+  #
+  #  Array form example
+  #  paths:
+  #  - subset: coreclr
+  #    include:
+  #    - src/libraries/System.Private.CoreLib/*
+  #    exclude:
+  #    - src/libraries/*
+  #
+  #  This example will include ALL path changes except the ones under src/libraries/*!System.Private.CoreLib/*
+  paths: []
+
 jobs:
 - job: checkout
   displayName: Checkout
@@ -10,7 +37,7 @@ jobs:
 
     ${{ if ne(variables['System.TeamProject'], 'public') }}:
       name: Hosted Mac Internal
-  
+
   steps:
   - checkout: self
     clean: true
@@ -22,3 +49,16 @@ jobs:
   - publish: $(Build.StagingDirectory)/Checkout.bundle
     artifact: Checkout_bundle
     displayName: Upload Checkout.bundle
+
+  - ${{ if and(ne(parameters.paths[0], ''), eq(variables['Build.Reason'], 'PullRequest')) }}:
+    - ${{ each path in parameters.paths }}:
+      - template: evaluate-changed-paths.yml
+        parameters:
+          subsetName: ${{ path.subset }} 
+          arguments:
+          - --difftarget origin/$(System.PullRequest.TargetBranch)
+          - --subset ${{ path.subset }}
+          - ${{ if ne(path.include[0], '') }}:
+            - --includepaths '${{ join('+', path.include) }}'
+          - ${{ if ne(path.exclude[0], '') }}:
+            - --excludepaths '${{ join('+', path.exclude) }}'
diff --git a/eng/pipelines/common/evaluate-changed-paths.yml b/eng/pipelines/common/evaluate-changed-paths.yml
new file mode 100644 (file)
index 0000000..28f8490
--- /dev/null
@@ -0,0 +1,19 @@
+# This step template evaluates git changes using git based on a include/exclude path filter.
+# For more information on how the path evaluation works look at evaluate-changed-paths.sh docs
+# at the beginning of that file.
+
+parameters:
+  # Name for the subset that we're evaluating changes for.
+  # It is required to name the step correctly and so the variable created can be consumable.
+  subsetName: ''
+  # Array containing the arguments that are to be passed down to evaluate-changed-paths.sh
+  # Note that --azurevariable is always set to containschange, no need to pass it down.
+  arguments: []
+
+steps:
+  - ${{ if ne(parameters.arguments[0], '') }}:
+    - script: eng/pipelines/evaluate-changed-paths.sh
+              --azurevariable containsChange
+              ${{ join(' ', parameters.arguments) }}
+      displayName: Evaluate paths for ${{ parameters.subsetName }}
+      name: ${{ format('SetPathVars_{0}', parameters.subsetName) }} # need a name to access output variable
index e799ba9..6c293ee 100644 (file)
@@ -12,6 +12,13 @@ jobs:
 - template: ${{ coalesce(parameters.helixQueuesTemplate, parameters.jobTemplate) }}
   parameters:
     variables:
+      - name: _containsCoreClrChange
+        value: $[ dependencies.checkout.outputs['SetPathVars_coreclr.containsChange'] ]
+      - name: _containsLibrariesChange
+        value: $[ dependencies.checkout.outputs['SetPathVars_libraries.containsChange'] ]
+      - name: _containsInstallerChange
+        value: $[ dependencies.checkout.outputs['SetPathVars_installer.containsChange'] ]
+
       - ${{ if eq(parameters.osGroup, 'Windows_NT') }}:
         - name: archiveExtension
           value: '.zip'
diff --git a/eng/pipelines/evaluate-changed-paths.sh b/eng/pipelines/evaluate-changed-paths.sh
new file mode 100755 (executable)
index 0000000..0df6189
--- /dev/null
@@ -0,0 +1,185 @@
+#!/usr/bin/env bash
+: '
+Scenarios:
+  1. exclude paths are specified
+      Will include all paths except the ones in the exclude list.
+  2. include paths are specified
+      Will only include paths specified in the list.
+  3. exclude + include:
+      1st we evaluate changes for all paths except ones in excluded list. If we can not find
+      any applicable changes like that, then we evaluate changes for incldued paths
+      if any of these two finds changes, then a variable will be set to true.
+  In order to consume this variable in a yaml pipeline, reference it via: $[ dependencies.<JobName>.outputs["<StepName>_<subset>.containschange"] ]
+
+  Example:
+  -difftarget ''HEAD^1'' -subset coreclr -includepaths ''src/libraries/System.Private.CoreLib/*'' -excludepaths ''src/libraries/*+src/installer/*''
+
+  This example will include ALL path changes except the ones under src/libraries/*!System.Private.CoreLib/*
+'
+
+# Disable globbing in this bash script since we iterate over path patterns
+set -f
+
+# Stop script if unbound variable found (use ${var:-} if intentional)
+set -u
+
+# Stop script if command returns non-zero exit code.
+# Prevents hidden errors caused by missing error code propagation.
+set -e
+
+usage()
+{
+  echo "Script that evaluates changed paths and emits an azure devops variable if the changes contained in the current HEAD against the difftarget meet the includepahts/excludepaths filters:"
+  echo "  --difftarget <value>       SHA or branch to diff against. (i.e: HEAD^1, origin/master, 0f4hd36, etc.)"
+  echo "  --excludepaths <value>     Escaped list of paths to exclude from diff separated by '+'. (i.e: 'src/libraries/*+'src/installer/*')"
+  echo "  --includepaths <value>     Escaped list of paths to include on diff separated by '+'. (i.e: 'src/libraries/System.Private.CoreLib/*')"
+  echo "  --subset                   Subset name for which we're evaluating in order to include it in logs"
+  echo "  --azurevariable            Name of azure devops variable to create if change meets filter criteria"
+  echo ""
+
+  echo "Arguments can also be passed in with a single hyphen."
+}
+
+source="${BASH_SOURCE[0]}"
+
+# resolve $source until the file is no longer a symlink
+while [[ -h "$source" ]]; do
+  scriptroot="$( cd -P "$( dirname "$source" )" && pwd )"
+  source="$(readlink "$source")"
+  # if $source was a relative symlink, we need to resolve it relative to the path where the
+  # symlink file was located
+  [[ $source != /* ]] && source="$scriptroot/$source"
+done
+
+scriptroot="$( cd -P "$( dirname "$source" )" && pwd )"
+eng_root=`cd -P "$scriptroot/.." && pwd`
+
+exclude_paths=()
+include_paths=()
+subset_name=''
+azure_variable=''
+diff_target=''
+
+while [[ $# > 0 ]]; do
+  opt="$(echo "${1/#--/-}" | awk '{print tolower($0)}')"
+  case "$opt" in
+    -help|-h)
+      usage
+      exit 0
+      ;;
+    -difftarget)
+      diff_target=$2
+      shift
+      ;;
+    -excludepaths)
+      IFS='+' read -r -a tmp <<< $2
+      exclude_paths+=($tmp)
+      shift
+      ;;
+    -includepaths)
+      IFS='+' read -r -a tmp <<< $2
+      include_paths+=($tmp)
+      shift
+      ;;
+    -subset)
+      subset_name=$2
+      shift
+      ;;
+    -azurevariable)
+      azure_variable=$2
+      shift
+      ;;
+  esac
+
+  shift
+done
+
+ci=true # Needed in order to use pipeline-logging-functions.sh
+. "$eng_root/common/pipeline-logging-functions.sh"
+
+# -- expected args --
+# $@: git diff arguments
+customGitDiff() {
+  (
+    set -x
+    git diff -M -C -b --ignore-cr-at-eol --ignore-space-at-eol "$@"
+  )
+}
+
+# runs git diff with supplied filter.
+# -- exit codes --
+# 0: No match was found
+# 1: At least 1 match was found
+#
+# -- expected args --
+# $@: filter string
+probePathsWithExitCode() {
+  local _filter=$@
+  echo ""
+  customGitDiff --exit-code --quiet $diff_target -- $_filter
+}
+
+# -- expected args --
+# $@: filter string
+printMatchedPaths() {
+  local _subset=$subset_name
+  local _filter=$@
+  echo ""
+  echo "----- Matching files for $_subset -----"
+  customGitDiff --name-only $diff_target -- $_filter
+}
+
+probePaths() {
+  local _subset=$subset_name
+  local _azure_devops_var_name=$azure_variable
+  local exclude_path_string=""
+  local include_path_string=""
+  local found_applying_changes=false
+  
+  if [[ ${#exclude_paths[@]} -gt 0 ]]; then
+    echo ""
+    echo "******* Probing $_subset exclude paths *******";
+    for _path in "${exclude_paths[@]}"; do
+      echo "$_path"
+      if [[ "$exclude_path_string" == "" ]]; then
+        exclude_path_string=":!$_path"
+      else
+        exclude_path_string="$exclude_path_string :!$_path"
+      fi
+    done
+
+    if ! probePathsWithExitCode $exclude_path_string; then
+      found_applying_changes=true
+      printMatchedPaths $exclude_path_string
+    fi
+  fi
+
+  if [[ $found_applying_changes != true && ${#include_paths[@]} -gt 0 ]]; then
+    echo ""
+    echo "******* Probing $_subset include paths *******";
+    for _path in "${include_paths[@]}"; do
+      echo "$_path"
+      if [[ "$include_path_string" == "" ]]; then
+        include_path_string=":$_path"
+      else
+        include_path_string="$exclude_path_string :$_path"
+      fi
+    done
+
+    if ! probePathsWithExitCode $include_path_string; then
+      found_applying_changes=true
+      printMatchedPaths $include_path_string
+    fi
+  fi
+
+  if [[ $found_applying_changes == true ]]; then
+    echo ""
+    echo "Setting pipeline variable $_azure_devops_var_name=true"
+    Write-PipelineSetVariable -name $_azure_devops_var_name -value true
+  else
+    echo ""
+    echo "No changed files for $_subset"
+  fi
+}
+
+probePaths
index e4f5135..a23ead1 100644 (file)
@@ -24,6 +24,30 @@ jobs:
 # Checkout repository
 #
 - template: /eng/pipelines/common/checkout-job.yml
+  parameters:
+    paths:
+    - subset: coreclr
+      include:
+      - src/libraries/System.Private.CoreLib/*
+      exclude:
+      - src/installer/*
+      - src/libraries/*
+      - eng/pipelines/installer/*
+      - eng/pipelines/libraries/*
+    - subset: libraries
+      exclude:
+      - src/installer/*
+      - src/coreclr/*
+      - eng/pipelines/coreclr/*
+      - eng/pipelines/installer/*
+    - subset: installer
+      include:
+      - docs/manpages/*
+      exclude:
+      - src/coreclr/*
+      - src/libraries/*
+      - eng/pipelines/coreclr/*
+      - eng/pipelines/libraries/*
 
 #
 # Build CoreCLR