CI/CD & Ops

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 version

La 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)/api

Ce 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)/api

Variables : 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 seulement

Reste à 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éeOù elle s'écritVariable introuvable
${{ variables.nom }}à la compilation, avant tout jobclé ou valeurchaîne vide
$(nom)à l'exécution, juste avant chaque tâchevaleur seulementreste $(nom)
$[ variables.nom ]à l'exécution, pour une variable ou une conditiontoute la valeur, rien d'autrechaî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 passage

Le 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.sh

Dé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.sh

extends 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-build

Environments, 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érificationCe qu'elle garantit
Branch controlles ressources viennent de branches autorisées, protégées au besoin
Required templatele pipeline étend le template désigné, sinon il échoue
Approvalsune personne ou un membre d'un groupe a approuvé, dans le délai fixé
Business hoursle stage ne démarre que dans une plage horaire
Invoke REST API, Invoke Azure Functionun service externe répond favorablement
Exclusive lockun 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 production

succeeded() 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 production

Un 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 ${{ }}.

Ce cours vous a servi ? Offrir un café Signaler une erreur