Run an Azure Pipelines Job only, if source code has changed
When using DevOps pipelines like Azure Pipelines, you might want to skip certain stages or jobs, if your source code has not changed since the last run.
Detecting source code changes
Your pipeline might do more than just building code, like running some Terraform script, updating Kubernetes Deployments or anything else. Building code, that has not changed since the last run can be time and cost consuming, so here is how to skip a specific Azure Pipeline Job when no source code has changed.
If your repository hosts more than just your source code, but also some environment configuration files, we need to detect what exactly has changed since the last pipeline execution. The following Shell script lists all Git changes and compares them to a path filter:
#!/bin/sh
PATH_FILTER="src/"
CHANGED_FILES=$(git diff HEAD HEAD~ --name-only)
MATCH_COUNT=0
echo "Checking for file changes..."
for FILE in $CHANGED_FILES
do
if [[ $FILE == *$PATH_FILTER* ]]; then
echo "MATCH: ${FILE} changed"
MATCH_COUNT=$(($MATCH_COUNT+1))
else
echo "IGNORE: ${FILE} changed"
fi
done
echo "$MATCH_COUNT match(es) for filter '$PATH_FILTER' found."
if [[ $MATCH_COUNT -gt 0 ]]; then
echo "##vso[task.setvariable variable=SOURCE_CODE_CHANGED;isOutput=true]true"
else
echo "##vso[task.setvariable variable=SOURCE_CODE_CHANGED;isOutput=true]false"
fi
Take a look at the last block, which sets an Azure Pipelines variable SOURCE_CODE_CHANGED
to true
, if one or more changed files match the src/
path filter. It's also worth noting, that we used isOutput=true
to mark the variable as an Output Variable, to make it reusable across other pipeline jobs.
Set changed code as a condition for a job
To use the SOURCE_CODE_CHANGED
variable as a condition for another job, we need to set the job that executes the Shell script as a dependency. Afterwards, we can create a condition like this:
# job name step name variable name
condition: eq(dependencies.CheckChanges.outputs['check_changes.SOURCE_CODE_CHANGED'], 'true')
A full example pipeline could look like this:
name: $(BuildID)
trigger:
paths:
include:
- src/*
- env/*
- tests/*
stages:
- stage: 'Build'
jobs:
- job: CheckChanges
displayName: 'Check changes'
steps:
- bash: |
PATH_FILTER="src/"
CHANGED_FILES=$(git diff HEAD HEAD~ --name-only)
MATCH_COUNT=0
echo "Checking for file changes..."
for FILE in $CHANGED_FILES
do
if [[ $FILE == *$PATH_FILTER* ]]; then
echo "MATCH: ${FILE} changed"
MATCH_COUNT=$(($MATCH_COUNT+1))
else
echo "IGNORE: ${FILE} changed"
fi
done
echo "$MATCH_COUNT match(es) for filter '$PATH_FILTER' found."
if [[ $MATCH_COUNT -gt 0 ]]; then
echo "##vso[task.setvariable variable=SOURCE_CODE_CHANGED;isOutput=true]true"
else
echo "##vso[task.setvariable variable=SOURCE_CODE_CHANGED;isOutput=true]false"
fi
name: check_changes
displayName: 'Check changed files'
- job: Build
displayName: 'Only when source code changed'
dependsOn: CheckChanges # <- Important: Mark previous job as dependency
condition: eq(dependencies.CheckChanges.outputs['check_changes.SOURCE_CODE_CHANGED'], 'true')
steps:
- # Add your build steps here
- job: Rest
displayName: 'Will always be execured'
steps:
- # ...
I hope, that helps to save you some build time and costs!
☝️ Advertisement Block: I will buy myself a pizza every time I make enough money with these ads to do so. So please feed a hungry developer and consider disabling your Ad Blocker.