Azure Pipelines: Stages nur bei relevanten Dateiänderungen ausführen
In komplexen Azure DevOps Pipelines, die beispielsweise Infrastructure-, Database- und App-Deployments kombinieren, laufen oft alle Stages durch – auch wenn nur der Anwendungscode geändert wurde. Das kostet Zeit, blockiert Agent-Ressourcen und kann ungewollte Nebenwirkungen verursachen. Mit intelligenter Dateiänderungs-Erkennung und conditional Stage Execution kannst du deine Pipeline optimieren: Nur die Stages laufen, die wirklich benötigt werden.
Warum Stages überspringen?
Die Optimierung von Pipeline-Durchläufen bringt mehrere Vorteile:
-
Du sparst Zeit, denn ein Infrastructure-Deployment kann 10-15 Minuten dauern, selbst wenn nur App-Code geändert wurde. Bei mehreren Environments summiert sich das schnell auf 30-60 Minuten Wartezeit pro Commit.
-
Du schonst Agent-Ressourcen, denn in Teams mit parallelen Pipelines sind verfügbare Agents oft knapp. Übersprungene Stages geben Kapazitäten für andere Builds frei.
-
Du vermeidest Nebenwirkungen, denn unnötige Infrastructure-Updates können zu unerwarteten Änderungen führen oder Kosten verursachen (z.B. beim Neustart von Azure Resources).
-
Du erhältst eine bessere Pipeline-Übersicht, denn die Logs zeigen deutlich, welche Komponenten tatsächlich deployed wurden.
Git Diff: Grundlagen der Dateiänderungs-Erkennung
Die Basis für intelligente Stage-Ausführung ist die Analyse der Dateiänderungen mittels git diff. Dabei gibt es zwei wichtige Szenarien:
Szenario 1: Main Branch (nach Merge)
Beim Merge in den Main Branch, besonders bei Squash Merges, werden alle Feature-Branch-Commits zu einem zusammengefasst. Hier vergleichst du den aktuellen Commit mit dem vorherigen: HEAD~..HEAD. Der Diff zeigt alle Änderungen, die der Merge mitgebracht hat.
Szenario 2: Feature Branch / Pull Request
Bei Feature Branches oder Pull Requests vergleichst du gegen den Target Branch: origin/main..HEAD. Dies zeigt alle Änderungen des Feature Branches gegenüber dem Ziel-Branch und ist wichtig für PR Validations, die vor dem Merge laufen.
Der Git Diff Command liefert eine Liste aller geänderten Dateipfade:
git diff --name-only <base>..<head>
Mit Glob-Patterns wie eng/infrastructure/** kannst du dann filtern, ob relevante Dateien betroffen sind.
Das PowerShell-Script zur Änderungs-Erkennung
Das PowerShell-Script Test-GitDiffMatchesPattern.ps1 übernimmt die Logik zur Erkennung von Dateiänderungen:
param(
# Suchmuster als Array von Glob-Patterns
[Parameter(Mandatory = $true)]
[string[]]$SearchPatterns,
# Basis- und Head-Commits (optional)
[Parameter(Mandatory = $false)]
[string]$BaseCommit = 'HEAD~',
[Parameter(Mandatory = $false)]
[string]$HeadCommit = 'HEAD'
)
# Branch-Detection
$sourceBranch = $env:BUILD_SOURCEBRANCHNAME
if ($sourceBranch -eq "main") {
$baseCommit = "HEAD~"
$headCommit = "HEAD"
Write-Host "Comparing main branch: $baseCommit..$headCommit"
} else {
$baseCommit = "origin/main"
$headCommit = "HEAD"
Write-Host "Comparing feature branch: $baseCommit..$headCommit"
}
# Git Diff ausführen
$changedFiles = git diff --name-only $baseCommit $headCommit
# Pattern Matching
foreach ($file in $changedFiles) {
foreach ($pattern in $searchPatterns) {
# Glob Pattern zu Regex konvertieren
# ** → .* (beliebige Verzeichnisebenen)
# * → [^/]* (innerhalb einer Verzeichnisebene)
$regexPattern = $pattern -replace '\*\*', '.*' -replace '\*', '[^/]*'
if ($file -match $regexPattern) {
Write-Host "Match found: $file matches $pattern"
return $true
}
}
}
Write-Host "No matches found"
return $false
Wichtige Aspekte:
- Bei der Pattern-Normalisierung werden führende Slashes entfernt und Glob-Wildcards zu Regex konvertiert.
- Die Wildcard-Unterstützung funktioniert so, dass
*innerhalb einer Verzeichnisebene matcht, während**rekursiv über alle Ebenen matcht. - Der Return Value ist ein Boolean. Das Script gibt
$truebei einem Match zurück, sonst$false.
| Glob Pattern | Beschreibung | Beispiel-Match |
|---|---|---|
eng/infrastructure/** | Alle Dateien unter infrastructure | eng/infrastructure/main.bicep |
eng/infrastructure/*.bicep | Nur .bicep-Dateien direkt in infrastructure | eng/infrastructure/main.bicep (nicht in Unterordnern) |
src/app/**/*.cs | Alle C#-Dateien in app | src/app/Controllers/HomeController.cs |
Das Azure Pipeline Template für File-Change-Detection
Das Template check-files-changed.template.yml kapselt den Script-Aufruf und erstellt eine Output Variable:
parameters:
# Name der Output Variable
- name: outputVariableName
type: string
# Force-Flag zum manuellen Überschreiben
# Dies kann dazu genutzt werden, um die Stage immer auszuführen
# auch wenn keine Dateien geändert wurden
- name: force
type: boolean
default: false
# Suchmuster als Array von Glob-Patterns
- name: searchPatterns
type: object
steps:
- pwsh: |
if ('${{ parameters.force }}' -ieq 'True') {
Write-Host "##[section]Force flag is set. Marking files as changed."
Write-Host "##vso[task.setvariable variable=HasChanged;isOutput=true]Yes"
exit 0
}
# Azure Pipelines gibt das Array als JSON-String weiter, damit muss es konvertiert werden.
$searchPatterns = ConvertFrom-Json '${{ convertToJson(parameters.searchPatterns) }}'
# Pfad zum Script
$scriptPath = "$($env:BUILD_SOURCESDIRECTORY)/eng/scripts/Test-GitDiffMatchesPattern.ps1"
$hasChanges = & $scriptPath -SearchPatterns $searchPatterns
$variableValue = $hasChanges ? 'Yes' : 'No'
Write-Host "##[section]Files changed: $variableValue"
# Output Variable setzen
# Auf diese Weise kann die Variable in späteren Stages referenziert werden
Write-Host "##vso[task.setvariable variable=HasChanged;isOutput=true]$variableValue"
name: ${{ parameters.outputVariableName }}
displayName: 'Check Files Changed for ${{ parameters.outputVariableName }}'
Template-Features:
- Das Force-Flag erlaubt manuelles Überschreiben für Testing (z.B. bei Force-Deployments).
- Die Output Variable
HasChangedwird mitisOutput=truegesetzt. Die möglichen Werte sind'Yes'oder'No'. - Bei der JSON-Serialisierung wird
searchPatternsals Array mittelsconvertToJson()übergeben. - Der benannte Step ermöglicht durch den
nameParameter die Referenzierung in späteren Stages.
Output Variables und Dependencies nutzen
Um die Output Variable in späteren Stages zu nutzen, benötigst du zwei Dinge: dependsOn und die richtige Syntax für dependencies.
Im Build Job definierst du die File-Change-Checks:
jobs:
- job: Build_App
steps:
- template: /eng/templates/check-files-changed.template.yml
parameters:
outputVariableName: 'InfrastructureChanged'
searchPatterns:
- 'eng/infrastructure/**'
In der Deployment Stage nutzt du die Output Variable:
- stage: DEV_Infrastructure_Deploy
displayName: 'DEV Infrastructure Deploy'
dependsOn: Build
condition: and(succeeded(), eq(dependencies.Build.outputs['Build_App.InfrastructureChanged.HasChanged'], 'Yes'))
jobs:
- job: Deploy
steps:
- script: echo "Deploying infrastructure changes"
Wichtige Syntax-Elemente:
dependsOn: Buildist zwingend erforderlich, denn ohne diese Abhängigkeit istdependenciesnicht verfügbar.- Die Syntax lautet
dependencies.<STAGE>.outputs['<JOB>.<STEP>.<VARIABLE>']. Dabei istBuildder Stage-Name (bei Single-Stage-Pipelines oft implizit),Build_Appist der Job-Name,InfrastructureChangedist der Step-Name (outputVariableName) undHasChangedist der Variable-Name (im Template gesetzt). - Willst du auf die Variable innerhalb einer Job-Condition zugreifen, ist der Syntax folgender:
stageDependencies.<STAGE>.outputs['<JOB>.<STEP>.<VARIABLE>']. and(succeeded(), eq(...))kombiniert zwei Bedingungen. Die Stage wird nur ausgeführt, wenn die vorherige Stage erfolgreich war und die Variable den Wert'Yes'hat.
Für Stage-zu-Stage Dependencies mit mehreren Abhängigkeiten:
- stage: TEST_Infrastructure_Deploy
dependsOn:
- DEV_Infrastructure_Deploy
- Build
condition: and(succeeded(), eq(dependencies.Build.outputs['Build_App.InfrastructureChanged.HasChanged'], 'Yes'))
Conditional Stage Execution: Das Gesamtbild
Ein vollständiges Beispiel mit mehreren Stages zeigt das Zusammenspiel der einzelnen Komponenten:
stages:
- stage: Build
jobs:
- job: Build_App
steps:
# Check Infrastructure Changes
- template: /eng/templates/check-files-changed.template.yml
parameters:
outputVariableName: 'InfrastructureChanged'
searchPatterns:
- 'eng/infrastructure/**'
# Check Database Changes
- template: /eng/templates/check-files-changed.template.yml
parameters:
outputVariableName: 'DatabaseChanged'
searchPatterns:
- 'eng/database/**'
- 'src/**/Migrations/**'
# Check App Changes
- template: /eng/templates/check-files-changed.template.yml
parameters:
outputVariableName: 'AppChanged'
searchPatterns:
- 'src/**'
# Build steps...
- script: echo "Building application"
- stage: DEV_Infrastructure
dependsOn: Build
condition: and(succeeded(), eq(dependencies.Build.outputs['Build_App.InfrastructureChanged.HasChanged'], 'Yes'))
jobs:
- job: Deploy_Infra
steps:
- script: echo "Deploying infrastructure to DEV"
- stage: DEV_Database
dependsOn: Build
condition: and(succeeded(), eq(dependencies.Build.outputs['Build_App.DatabaseChanged.HasChanged'], 'Yes'))
jobs:
- job: Deploy_DB
steps:
- script: echo "Deploying database to DEV"
- stage: DEV_App
dependsOn:
- Build
- DEV_Infrastructure
- DEV_Database
condition: and(succeeded(), eq(dependencies.Build.outputs['Build_App.AppChanged.HasChanged'], 'Yes'))
jobs:
- job: Deploy_Application
steps:
- script: echo "Deploying app to DEV"
Ohne Conditions würden alle Stages immer ausgeführt. Mit Conditions überspringt Azure DevOps Stages automatisch, wenn die Bedingung false ist. Übersprungene Stages werden in der UI als “Skipped” angezeigt, und nachfolgende Stages mit dependsOn auf übersprungene Stages laufen trotzdem (außer sie haben eigene Conditions).
Alternative Ansätze und Best Practices
Es gibt verschiedene Wege, um ähnliche Ziele zu erreichen:
| Ansatz | Vorteile | Nachteile | Wann nutzen? |
|---|---|---|---|
| Conditional Stages (dieser Artikel) | • Flexible Kontrolle • Eine Pipeline • Vollständiger History | • Setup-Aufwand • Komplexere Conditions | Monorepos mit mehreren Deployment-Typen |
| Separate Pipelines | • Klare Trennung • Einfache Trigger | • Mehrere Pipelines verwalten • Fragmentierte History | Klare Komponenten-Grenzen |
| Path Triggers | • Native Azure DevOps Feature • Einfach zu konfigurieren | • Nur für Pipeline-Start • Keine Stage-Level-Kontrolle | Trigger verschiedener Pipelines |
| Manual Stages | • Volle Kontrolle • Keine Automatisierung nötig | • Manueller Aufwand • Verzögert Deployments | Production-Deployments mit Approval |
Best Practices für die Implementierung:
Nutze Conservative Patterns. Lieber zu viel als zu wenig deployen. Bei Unsicherheit solltest du die Stage ausführen. Implementiere ein Force-Flag für Testing, das manuelle Overrides ermöglicht (z.B. force: true als Parameter). Dokumentiere deine Pattern-Entscheidungen mit Kommentaren in der YAML und erkläre, warum bestimmte Patterns gewählt wurden. Prüfe regelmäßig die Pipeline-Logs, um zu sehen, welche Stages übersprungen werden. Beachte Shared Files, die mehrere Komponenten betreffen können, und passe deine Patterns entsprechend an.
# Beispiel mit Force-Flag für manuelle Deployments
- template: /eng/templates/check-files-changed.template.yml
parameters:
outputVariableName: 'InfrastructureChanged'
force: ${{ parameters.forceInfrastructureDeploy }} # Pipeline-Parameter
searchPatterns:
- 'eng/infrastructure/**'
Intelligentes Pipeline-Management für effiziente Deployments
Mit Conditional Stage Execution basierend auf Git Diff sparst du messbar Zeit und Ressourcen in komplexen Pipelines. Der Ansatz eignet sich besonders für Monorepos mit verschiedenen Deployment-Komponenten, wo nicht bei jeder Code-Änderung die gesamte Infrastructure neu deployed werden muss.
Git Diff erkennt automatisch, ob relevante Dateien geändert wurden. Dies funktioniert unterschiedlich für Main vs. Feature Branch. Output Variables transportieren die Information zwischen Build und Deployment Stages. dependsOn ist zwingend erforderlich, damit die dependencies-Syntax funktioniert. Conservative Patterns und Force-Flags geben dir Kontrolle bei Edge Cases.
Empfehlung: Starte mit konservativen Patterns und verfeinere sie iterativ. So kannst du erst Erfahrung mit dem Verhalten sammeln. Dokumentiere deine Pattern-Entscheidungen im Code und überwache die Pipeline-Logs regelmäßig.
Wie optimierst du deine Azure Pipelines? Nutzt du ähnliche Ansätze oder hast du andere Strategien für effizientes Pipeline-Management? Diskutiere mit uns auf LinkedIn.