Azure DevOps
Stages, jobs, templates, variables, environments, approbations.
Vérifié en septembre 2026 · outils aux versions citées dans le cours · environ 18 min
Azure Pipelines, le service d'intégration et de livraison continues d'Azure DevOps, exécute un fichier YAML versionné avec le code : à chaque push ou pull request, il compile, teste, publie, puis déploie d'un environment à l'autre, en s'arrêtant là où quelqu'un doit approuver. Le fichier se lit facilement. Ce qui le rend traître, c'est qu'il est traité en deux temps : une première passe, à la compilation, développe les templates et évalue les expressions ${{ }} ; l'exécution vient ensuite, job par job, sur des machines qui ne partagent rien. Presque toutes les surprises viennent de ces deux faits.
Stages, jobs, steps et déclencheurs
Un pipeline est un arbre à trois niveaux. Une étape (steps) est un script — script, bash, pwsh — ou une tâche, task: Nom@version, une action empaquetée dont la version majeure fait partie du nom. Un job est l'unité confiée à un agent : ses étapes s'exécutent dans l'ordre, sur la même machine. Un stage regroupe des jobs et marque les frontières où le pipeline peut attendre, car c'est avant un stage que s'appliquent les approbations. Les stages s'enchaînent dans l'ordre du fichier, chacun dépendant du précédent, sauf dependsOn: [] ; les jobs d'un même stage n'ont aucune dépendance implicite et tournent en parallèle si l'organisation dispose d'assez de jobs parallèles. Un pipeline d'un seul stage peut écrire jobs à la racine, et un pipeline d'un seul job, directement steps.
# azure-pipelines.yml, a la racine du depot
trigger: # integration continue : un push sur main
branches:
include:
- main
paths:
exclude:
- docs # un commit qui ne touche que docs/ ne declenche rien
pr: # validation de pull request : GitHub et Bitbucket Cloud seulement
- main
pool:
vmImage: ubuntu-latest # valable pour tous les jobs qui n'en disent pas plus
stages:
- stage: Build
jobs:
- job: Api
steps:
- script: dotnet build src/Boutique.Api -c Release
displayName: Compiler l'API
- job: Front # aucun lien avec Api : les deux peuvent tourner en meme temps
steps:
- script: npm ci && npm run build
workingDirectory: web
displayName: Construire le front
- stage: Controle # attend Build, comme tout stage attend le precedent
jobs:
- job: Annoncer
steps:
- script: echo "Build $(Build.BuildNumber) termine"trigger lance un run à chaque push sur les branches retenues. Sans trigger, toutes les branches déclenchent, à moins que l'organisation ou le projet n'ait activé le paramètre « Disable implied YAML CI trigger ». pr valide les pull requests, mais la référence du schéma le réserve à GitHub et à Bitbucket Cloud : sur un dépôt Azure Repos, la clé reste sans effet, et la validation passe par une stratégie de branche, « Build validation », posée sur la branche cible. Les déclencheurs se lisent avant tout run : ils n'acceptent pas de variables, et un template ne peut pas en porter.
Où tournent les jobs : agents hébergés et auto-hébergés
Un agent hébergé par Microsoft vient du pool « Azure Pipelines » ; on choisit une image par vmImage. ubuntu-latest, l'image par défaut quand aucun pool n'est donné, désigne aujourd'hui Ubuntu 24.04, et windows-latest Windows Server 2025 : ces étiquettes glissent d'une version à l'autre, et une étiquette datée comme ubuntu-24.04 fige la version du système, pas le contenu de l'image, que Microsoft continue de mettre à jour. ubuntu-26.04 est encore en préversion. Chaque job reçoit une machine virtuelle neuve, jetée à la fin du job. Ces agents n'existent que dans Azure DevOps Services, pas dans Azure DevOps Server.
Un agent auto-hébergé est une machine sur laquelle on installe soi-même le logiciel d'agent. Il se justifie quand le job doit atteindre un réseau privé, un cluster ou une base qui n'est pas exposée, ou quand il lui faut un outillage que les images n'ont pas. Les agents sont rangés en pools ; un job désigne son pool par name et exprime des demands, que l'agent satisfait par ses capacités, avec deux opérations seulement : l'existence et l'égalité. L'agent sert d'un run à l'autre : les caches restent chauds, les restes du run précédent aussi, d'où workspace: clean.
jobs:
- job: Linux
pool:
vmImage: ubuntu-latest # Ubuntu 24.04 en septembre 2026
steps:
- script: uname -a
- job: Windows
pool:
vmImage: windows-latest # Windows Server 2025 en septembre 2026
steps:
- pwsh: $PSVersionTable
- job: Interne
pool:
name: Agents-Boutique # pool auto-heberge, cree dans les parametres du projet
demands:
- Agent.OS -equals Linux # capacite systeme, relevee par l'agent
- docker # capacite qui doit simplement exister
workspace:
clean: all # sinon le dossier du run precedent est encore la
steps:
- script: docker versionLa machine neuve par job a une conséquence que l'on oublie une fois : deux jobs ne partagent aucun fichier, même quand l'un dépend de l'autre. Ce job cherche un dossier produit sur une autre machine.
jobs:
- job: Compiler
steps:
- script: dotnet publish src/Boutique.Api -c Release -o $(Build.ArtifactStagingDirectory)/api
- job: Verifier
dependsOn: Compiler
steps:
# Une autre machine, neuve : le dossier publie par Compiler n'y existe pas
- script: ls $(Build.ArtifactStagingDirectory)/apiCe qui doit passer d'un job à l'autre voyage comme artefact de pipeline, publié par l'un et téléchargé par l'autre.
jobs:
- job: Compiler
steps:
- script: dotnet publish src/Boutique.Api -c Release -o $(Build.ArtifactStagingDirectory)/api
- publish: $(Build.ArtifactStagingDirectory)/api # raccourci de PublishPipelineArtifact@1
artifact: api
- job: Verifier
dependsOn: Compiler
steps:
- checkout: none # les sources ne servent pas ici
- download: current # artefacts de ce run
artifact: api # arrive dans $(Pipeline.Workspace)/api
- script: ls $(Pipeline.Workspace)/apiVariables : trois syntaxes, trois moments
Une variable se définit à la racine, sur un stage ou sur un job, et la portée la plus proche l'emporte. Toutes sont des chaînes. Un groupe de variables, créé dans la « Library » du projet, les partage entre pipelines ; comme il peut contenir des secrets, un pipeline doit être autorisé à s'en servir. Chaque variable ordinaire devient aussi une variable d'environnement des scripts.
variables: # racine : visibles de tous les stages et jobs
- name: configuration
value: Release
- group: boutique-commun # groupe de la Library ; le pipeline doit y etre autorise
- template: variables/communes.yml
stages:
- stage: Build
variables:
- name: configuration # la portee la plus proche l'emporte
value: Debug
jobs:
- job: Api
steps:
- script: dotnet build -c $(configuration) # Debug
# Dans le script, la meme valeur est aussi la variable d'environnement
# CONFIGURATION : nom en majuscules, points remplaces par des soulignes.Un secret ne s'écrit jamais dans le YAML : il se déclare dans l'interface du pipeline ou dans un groupe, chiffré au repos. Azure Pipelines masque sa valeur dans les journaux, mais pas ses fragments : un secret qui contient du JSON laisse voir ses parties. Et, contrairement aux autres variables, il n'est pas exporté dans l'environnement des scripts.
variables:
- group: boutique-secrets # contient cleApi, marquee secrete
steps:
# fumee.sh lit $CLEAPI, qui est vide : un secret n'est jamais exporte
# dans l'environnement d'un script sans qu'on le demande.
- bash: ./scripts/fumee.sh
# Et surtout pas sur la ligne de commande, que certains systemes journalisent :
# - bash: ./scripts/fumee.sh --cle "$(cleApi)" Le secret se passe à une étape par son bloc env, ou à une tâche par une de ses entrées.
variables:
- group: boutique-secrets
steps:
- bash: ./scripts/fumee.sh # le script lit $CLE_API
env:
CLE_API: $(cleApi) # mappage explicite, pour cette etape seulementReste à lire la valeur, et c'est là que se trouve le piège classique d'Azure Pipelines : trois syntaxes, évaluées à trois moments différents.
| Syntaxe | Évaluée | Où elle s'écrit | Variable introuvable |
|---|---|---|---|
${{ variables.nom }} | à la compilation, avant tout job | clé ou valeur | chaîne vide |
$(nom) | à l'exécution, juste avant chaque tâche | valeur seulement | reste $(nom) |
$[ variables.nom ] | à l'exécution, pour une variable ou une condition | toute la valeur, rien d'autre | chaîne vide |
La macro $(nom) est la forme courante des scripts et des entrées de tâche. Introuvable, elle reste telle quelle, parce que $() peut avoir un sens pour le programme qui la reçoit — dans un script bash, $(nom) lance alors la commande nom. Elle n'est pas développée dans les clés que la compilation résout, comme trigger ou resources. L'expression ${{ }}, elle, réécrit le texte du YAML avant l'exécution : son contexte ne contient que les variables écrites dans le fichier et les variables prédéfinies marquées disponibles dans les templates — Build.SourceBranch et Build.Reason le sont, Build.BuildId ne l'est pas. Il ne contient ni les variables définies dans l'interface ou au lancement, ni celles d'un groupe, ni celles qu'un script modifie. L'exemple de la documentation, repris ici, montre l'écart.
variables:
- name: version
value: '1.0'
- name: estMain # $[ ] occupe toute la valeur, evaluee a l'execution
value: $[ eq(variables['Build.SourceBranch'], 'refs/heads/main') ]
steps:
# Dans un bloc |, les # suivants font partie du script : ce sont des commentaires bash
- script: |
echo "${{ variables.version }}" # 1.0 : ecrit dans le YAML a la compilation
echo "$(version)" # 1.0 : remplace juste avant l'etape
displayName: Premier passage
- bash: echo "##vso[task.setvariable variable=version]2.0"
displayName: Changer la version
- script: |
echo "${{ variables.version }}" # 1.0 : ce texte est fige depuis la compilation
echo "$(version)" # 2.0
displayName: Second passageLe piège survient quand une décision de structure — insérer ou non un stage — dépend d'une valeur qui n'existe qu'à l'exécution. Cette variable, définie dans l'interface, vaut une chaîne vide au moment où la condition est évaluée, et rien ne le signale.
# deployerProduction est definie dans l'interface du pipeline,
# modifiable au lancement d'un run.
stages:
- stage: Build
jobs:
- job: Api
steps:
- script: dotnet build -c Release
# Evaluee a la compilation : deployerProduction n'existe pas encore et vaut ''.
# Le stage n'est jamais insere, et le run finit en vert sans rien deployer.
- ${{ if eq(variables.deployerProduction, 'true') }}:
- stage: Production
jobs:
- job: Deployer
steps:
- script: echo Deploiement Un choix fait au lancement se déclare en paramètre de pipeline, typé et connu à la compilation. Une valeur que seule l'exécution produit se teste dans une condition, évaluée au moment de démarrer le stage.
# Un parametre de pipeline est type et connu des la compilation ;
# le formulaire de lancement d'un run le propose a celui qui lance.
parameters:
- name: deployerProduction
type: boolean
default: false
stages:
- stage: Build
jobs:
- job: Api
steps:
- script: dotnet build -c Release
- ${{ if eq(parameters.deployerProduction, true) }}:
- stage: Production
jobs:
- job: Deployer
steps:
- script: echo Deploiement Une valeur calculée pendant un job en sort par une variable de sortie : isOutput=true dans la commande de journalisation, un name sur l'étape, et $[ ] pour la relire dans le job qui en dépend.
jobs:
- job: Versionner
steps:
- bash: echo "##vso[task.setvariable variable=version;isOutput=true]1.4.$(Build.BuildId)"
name: calcul # le nom de l'etape fait partie du chemin
- job: Publier
dependsOn: Versionner # sans elle, les deux jobs demarreraient ensemble
variables:
version: $[ dependencies.Versionner.outputs['calcul.version'] ]
steps:
- script: echo "Version $(version)"
# D'un stage a l'autre, le chemin passe par stageDependencies :
# $[ stageDependencies.Build.Versionner.outputs['calcul.version'] ]Templates et paramètres typés
Un template est un fichier YAML inséré à la compilation. Il en existe quatre sortes, selon l'endroit où on l'appelle : des étapes, des jobs, des stages ou des variables. Le chemin est relatif au fichier qui inclut, ou absolu depuis la racine du dépôt s'il commence par / ; un template d'un autre dépôt se désigne par fichier@alias, l'alias étant déclaré dans resources.repositories. Azure Pipelines n'accepte ni les ancres, ni les clés complexes, ni les ensembles, dit la référence du schéma : les ancres et la clé de fusion présentées dans le cours YAML n'y ont pas cours, et la réutilisation passe par les templates, qui traversent les fichiers et les dépôts et reçoivent des paramètres.
Ces paramètres sont typés : string, number, boolean, object, et des types de structure, step, stepList, job, jobList, deployment, deploymentList, stage, stageList ; un paramètre de pipeline accepte en plus stringList, absent des templates. Une valeur du mauvais type, ou hors de la liste values, fait échouer le run avant qu'il commence. Le défaut ne peut être qu'un littéral. ${{ if }} insère un bloc sous condition, ${{ each }} en répète un par élément.
# templates/etapes-dotnet.yml : un template d'etapes
parameters:
- name: solution
type: string
- name: configuration
type: string
default: Release
values: # toute autre valeur fait echouer le run avant son debut
- Debug
- Release
- name: lancerTests
type: boolean
default: true
steps:
- task: UseDotNet@2
inputs:
packageType: sdk
version: '10.0.x'
- script: dotnet build ${{ parameters.solution }} -c ${{ parameters.configuration }}
displayName: Compiler
- ${{ if eq(parameters.lancerTests, true) }}:
- script: dotnet test ${{ parameters.solution }} -c ${{ parameters.configuration }} --no-build
displayName: Tester# azure-pipelines.yml : le template s'insere a la place d'une etape
steps:
- checkout: self
- template: templates/etapes-dotnet.yml
parameters:
solution: Boutique.slnx
configuration: Debug
- script: echo "Etape propre a ce pipeline"
# Meme mecanisme un niveau plus haut : un template de jobs se place sous jobs:,
# un template de stages sous stages:, un template de variables sous variables:. Le type n'est pas une formalité. La documentation prévient qu'un paramètre scalaire sans type est une chaîne, et qu'une chaîne non vide vaut true dans un contexte booléen.
# templates/deployer-simulation.yml
parameters:
- name: simulation # aucun type : le parametre est une chaine
default: false
steps:
# 'false' est une chaine non vide, que eq convertit en booleen true :
# la condition est toujours vraie, et le deploiement jamais fait.
- ${{ if eq(true, parameters.simulation) }}:
- script: echo "Simulation, rien n'est deploye"
- ${{ else }}:
- script: ./scripts/deployer.shDéclaré boolean, le paramètre refuse ce qui n'est pas un booléen.
# templates/deployer-simulation.yml
parameters:
- name: simulation
type: boolean # une valeur comme 'oui' fait echouer le run
default: false
steps:
- ${{ if eq(parameters.simulation, true) }}:
- script: echo "Simulation, rien n'est deploye"
- ${{ else }}:
- script: ./scripts/deployer.shextends renverse la relation. Le pipeline n'inclut plus un morceau de template : il devient le template, et ne fournit que les paramètres que celui-ci déclare. Une équipe plateforme y fixe ce qui doit toujours s'exécuter, avant et après les étapes de l'application. Seul, extends reste une convention, qu'un pipeline peut ignorer ; associé à la vérification « Required template » sur une ressource, il devient une règle, présentée plus bas.
# Depot Boutique/modeles, fichier socle.yml : le squelette impose
parameters:
- name: etapesBuild
type: stepList
default: []
stages:
- stage: Build
pool:
vmImage: ubuntu-latest
jobs:
- job: Build
steps:
- script: echo "Restauration depuis le flux interne" # impose, avant
- ${{ each etape in parameters.etapesBuild }}:
- ${{ etape }}
- script: echo "Analyse des dependances" # impose, apres# Depot de l'application, azure-pipelines.yml
trigger:
- main
resources:
repositories:
- repository: modeles
type: git # Azure Repos : name vaut <projet>/<depot>
name: Boutique/modeles
ref: refs/tags/v2.1.0 # sans ref, c'est refs/heads/main qui est lu
extends:
template: socle.yml@modeles
parameters:
etapesBuild:
- script: dotnet build Boutique.slnx -c Release
- script: dotnet test Boutique.slnx -c Release --no-buildEnvironments, approbations et vérifications
Un environment est une cible logique — recette, production — qui garde l'historique de ce qui y a été déployé, avec les commits et les éléments de travail. On le vise depuis un job de déploiement, deployment, qui porte un environment et une stratégie. runOnce exécute une fois ses crochets preDeploy, deploy, routeTraffic, postRouteTraffic, puis on.success ou on.failure ; rolling vise des machines virtuelles, canary Kubernetes. Un job de déploiement ne récupère pas les sources, mais télécharge dans deploy tous les artefacts du run, sauf étape download explicite.
Les approbations et les vérifications ne s'écrivent pas dans le YAML. Le propriétaire d'une ressource — environment, service connection, groupe de variables, pool, dépôt, fichier sécurisé — les règle dans l'interface, et celui qui modifie le YAML ne peut pas les contourner. Avant qu'un stage démarre, Azure Pipelines rassemble toutes les ressources qu'il consomme et attend que leurs vérifications passent ; un refus, ou un délai dépassé, et le stage ne s'exécute pas. C'est aussi pourquoi un nom d'environment ou de service connection ne peut venir d'aucune variable lue à l'exécution, ni d'un groupe de variables : les ressources sont autorisées avant. Il faut une valeur connue à la compilation, comme un paramètre de template.
| Vérification | Ce qu'elle garantit |
|---|---|
| Branch control | les ressources viennent de branches autorisées, protégées au besoin |
| Required template | le pipeline étend le template désigné, sinon il échoue |
| Approvals | une personne ou un membre d'un groupe a approuvé, dans le délai fixé |
| Business hours | le stage ne démarre que dans une plage horaire |
| Invoke REST API, Invoke Azure Function | un service externe répond favorablement |
| Exclusive lock | un seul run à la fois utilise la ressource |
L'ordre est fixe : d'abord les vérifications statiques (branche, template requis, évaluation d'artefact), puis les approbations « pre-check », les vérifications dynamiques — dont l'approbation ordinaire, les heures ouvrées et les appels REST ou Azure Function —, les approbations « post-check », et le verrou exclusif en dernier. Face à ce verrou, lockBehavior choisit entre runLatest, le défaut, qui ne laisse passer que le dernier run, et sequential, qui les fait passer tous, l'un après l'autre. Écrit sur un stage qui porte un identifiant, lockBehavior crée en plus un verrou propre à ce stage, sans vérification à poser sur une ressource : c'est ce que fait le template de déploiement plus bas. À ce niveau, runLatest n'annule que les runs de la même branche.
Mieux vaut créer les environments à la main, avec leurs vérifications, avant le premier run. Un environment inconnu n'est créé automatiquement que si Azure Pipelines connaît l'utilisateur, par exemple quand le YAML est enregistré dans l'éditeur web, et il naît alors sans aucune vérification ; lancé depuis un push, le run échoue simplement.
Reste la condition d'un stage de production, où une erreur de logique déploie après un échec. Celle-ci ne teste que la branche.
stages:
- stage: Recette
jobs:
- deployment: Api
environment: boutique-recette
strategy:
runOnce:
deploy:
steps:
- script: ./scripts/deployer.sh recette
- stage: Production
dependsOn: Recette
# Une condition ecrite remplace la condition par defaut, succeeded().
# Celle-ci ne regarde que la branche : Production demarre sur main
# meme quand Recette a echoue.
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
jobs:
- deployment: Api
environment: boutique-production
strategy:
runOnce:
deploy:
steps:
- script: ./scripts/deployer.sh productionsucceeded() doit être repris explicitement, parce que la condition écrite remplace entièrement celle par défaut.
stages:
- stage: Recette
jobs:
- deployment: Api
environment: boutique-recette
strategy:
runOnce:
deploy:
steps:
- script: ./scripts/deployer.sh recette
- stage: Production
dependsOn: Recette
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: Api
environment: boutique-production
strategy:
runOnce:
deploy:
steps:
- script: ./scripts/deployer.sh productionUn pipeline complet pour l'API et le front
Le pipeline suivant construit l'API ASP.NET Core 10 et le front Angular dans deux jobs parallèles d'un même stage, publie deux artefacts, puis déploie en recette et en production par le même template de stages. Une pull request vers main, qu'elle soit validée par pr sur GitHub ou par une stratégie de branche sur Azure Repos, compile et teste sans déployer, car Build.SourceBranch y vaut refs/pull/…/merge ; un push sur main déploie en recette, puis en production une fois l'approbation posée sur l'environment boutique-production accordée. Les vérifications s'évaluent au niveau du stage : une seule approbation couvre les deux jobs de déploiement. La commande de publication suit la règle du cours YAML : >- pour une commande unique coupée sur plusieurs lignes.
# azure-pipelines.yml : l'API .NET 10 (src/, tests/) et le front Angular (web/)
trigger:
- main
pr:
- main
pool:
vmImage: ubuntu-latest
variables:
- name: configuration
value: Release
stages:
- stage: Build
jobs:
- job: Api
steps:
- task: UseDotNet@2
inputs:
packageType: sdk
version: '10.0.x'
- script: dotnet restore Boutique.slnx
displayName: Restaurer
- script: dotnet build Boutique.slnx -c $(configuration) --no-restore
displayName: Compiler
- task: DotNetCoreCLI@2
displayName: Tester
inputs:
command: test
projects: Boutique.slnx
arguments: -c $(configuration) --no-build
publishTestResults: true # publie les resultats de test dans le run
- script: >-
dotnet publish src/Boutique.Api/Boutique.Api.csproj
-c $(configuration) --no-build
-o $(Build.ArtifactStagingDirectory)/api
displayName: Publier
- publish: $(Build.ArtifactStagingDirectory)/api
artifact: api
- job: Front
steps:
- task: UseNode@1
inputs:
versionSource: spec
version: '24.x' # @angular/core 22.1 : ^22.22.3 || ^24.15.0 || >=26.0.0
- script: npm ci
workingDirectory: web
displayName: Installer
- script: npx ng test --watch=false
workingDirectory: web
displayName: Tester
- script: npm run build
workingDirectory: web
displayName: Construire
- publish: web/dist/web/browser
artifact: front
- template: templates/deployer.yml
parameters:
nom: Recette
dependDe: Build
environnement: boutique-recette
appApi: boutique-api-recette
groupe: boutique-recette
- template: templates/deployer.yml
parameters:
nom: Production
dependDe: Recette
environnement: boutique-production
appApi: boutique-api-production
groupe: boutique-production# templates/deployer.yml : un template de stages, appele une fois par environment
parameters:
- name: nom
type: string
- name: dependDe
type: string
- name: environnement
type: string
- name: appApi
type: string
- name: groupe
type: string
stages:
- stage: ${{ parameters.nom }}
dependsOn: ${{ parameters.dependDe }}
# Une pull request compile et teste, mais ne deploie jamais
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
lockBehavior: sequential # un run a la fois dans ce stage, dans l'ordre d'arrivee
variables:
- group: ${{ parameters.groupe }} # jetonStaticWebApp, secret
jobs:
- deployment: Api
environment: ${{ parameters.environnement }}
strategy:
runOnce:
deploy:
steps:
- download: current # remplace le telechargement automatique
artifact: api
- task: AzureWebApp@1
inputs:
azureSubscription: connexion-azure-boutique
appType: webAppLinux
appName: ${{ parameters.appApi }}
runtimeStack: 'DOTNETCORE|10.0'
package: $(Pipeline.Workspace)/api
- deployment: Front
environment: ${{ parameters.environnement }}
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: front
# Le routeur Angular a besoin de web/public/staticwebapp.config.json, que ng build
# copie a la racine de l'artefact : "navigationFallback": { "rewrite": "/index.html" }.
# Sans lui, recharger /commandes/42 cherche un fichier qui n'existe pas.
- task: AzureStaticWebApp@0 # agent Linux obligatoire
inputs:
workingDirectory: $(Pipeline.Workspace)
app_location: front
output_location: ''
skip_app_build: true # deja construit par le job Front
skip_api_build: true
azure_static_web_apps_api_token: $(jetonStaticWebApp)L'API part vers Azure App Service, le front vers Azure Static Web Apps, dont le jeton de déploiement vit dans le groupe de variables propre à chaque environment. Le cours Docker emballe les mêmes applications en images ; ce pipeline, lui, livre des fichiers.
Les pipelines de ce cours n'ont pas été exécutés : leurs clés et leurs entrées de tâche suivent la référence du schéma et des tâches de learn.microsoft.com, telle qu'elle se lit en septembre 2026. Un parseur YAML ne prouve que la syntaxe : une clé mal orthographiée ou une entrée de tâche inventée passent. Sans exécuter le pipeline, l'éditeur web d'Azure DevOps valide le fichier et ses conditions, et son action « Download full YAML » rend le pipeline une fois les templates développés, ce que fait aussi l'API REST de prévisualisation des runs. C'est le moyen le plus direct de voir ce que la compilation a fait des expressions ${{ }}.