La plupart des projets Angular que j'ai rejoints au niveau entreprise avaient une version de CI/CD. L'écart entre un pipeline qui tourne techniquement et un pipeline qui détecte réellement les vrais problèmes avant la production est énorme. Après des années dans les services financiers, voici une configuration à titre d'exemple qui fait ses preuves.
Pourquoi Azure Pipelines
Chez BMO, Azure Pipelines et SonarQube faisaient partie de l'outillage de l'équipe. Dans les organisations sur Azure DevOps, Pipelines s'intègre directement avec les repos, les work items et les feeds d'artefacts déjà en usage, et la décision est généralement prise avant qu'un développeur front-end ne rejoigne le projet. Les principes ici se transposent à GitHub Actions ou GitLab CI avec des changements minimaux, donc les décisions structurelles valent la peine d'être comprises quel que soit l'outillage.
Les quatre étapes qui comptent vraiment
- Lint : détectez les problèmes de style et d'analyse statique avant que les tests ne tournent. Feedback rapide, peu coûteux à corriger. Échoue vite si quelqu'un a poussé sans lancer localement.
- Test : tests unitaires avec des seuils de couverture appliqués. Pas de seuil configuré signifie pas de gate. Un seuil sans application est du théâtre.
- Build : builds spécifiques à l'environnement utilisant les fileReplacements d'Angular. Pas de détection d'environnement à l'exécution, pas de globales injectées. Substitution au moment du build, résolue avant que le bundle ne soit écrit.
- Deploy : upload d'artefact vers Azure Blob ou App Service, encadré par une approbation manuelle sur l'environnement de production. Au moins un approbateur extérieur à l'équipe qui a écrit le changement.
# Illustrative azure-pipelines.yml: a generic starting point, not any employer's pipeline
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
variables:
nodeVersion: '20.x'
npmCacheDir: $(Pipeline.Workspace)/.npm
buildEnvironment: 'production' # or 'staging'; must match a configuration in angular.json
sonarConnection: 'sonarqube-service-connection'
azureConnection: 'azure-service-connection'
webAppName: 'my-angular-app-prod'
stages:
- stage: Lint
jobs:
- job: LintJob
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- task: Cache@2
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
restoreKeys: 'npm | "$(Agent.OS)"'
path: $(npmCacheDir)
- script: npm ci --cache $(npmCacheDir)
displayName: 'Install dependencies'
- script: npm run lint
displayName: 'Run ESLint'
- stage: Test
dependsOn: Lint
jobs:
- job: TestJob
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- task: Cache@2
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
restoreKeys: 'npm | "$(Agent.OS)"'
path: $(npmCacheDir)
- script: npm ci --cache $(npmCacheDir)
# Task major versions track the SonarQube extension installed in your org. Check its
# marketplace page before copying the @N suffixes.
- task: SonarQubePrepare@6
displayName: 'Prepare SonarQube analysis'
inputs:
SonarQube: $(sonarConnection)
scannerMode: 'CLI'
configMode: 'manual'
cliProjectKey: 'my-angular-app'
cliSources: 'src'
extraProperties: |
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.qualitygate.wait=true
- script: npm test -- --ci --coverage
displayName: 'Run unit tests'
# Jest writes these files only if 'cobertura' and 'lcov' are in its coverageReporters
- task: PublishCodeCoverageResults@2
inputs:
summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage/**/cobertura-coverage.xml'
- task: SonarQubeAnalyze@6
displayName: 'Run SonarQube analysis'
# This is the step that turns a red quality gate into a failed pipeline on Azure DevOps
- task: SonarQubePublish@6
displayName: 'Publish Quality Gate Result'
inputs:
pollingTimeoutSec: '300'
- stage: Build
dependsOn: Test
jobs:
- job: BuildJob
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- task: Cache@2
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
restoreKeys: 'npm | "$(Agent.OS)"'
path: $(npmCacheDir)
- script: npm ci --cache $(npmCacheDir)
- script: npm run build -- --configuration=$(buildEnvironment)
displayName: 'Build Angular app'
- task: PublishPipelineArtifact@1
inputs:
targetPath: dist
artifact: angular-dist
- stage: Deploy
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
# A deployment job, not a plain job: the environment is what carries the approval
- deployment: DeployProduction
# Approvals and checks are configured on the environment in Azure DevOps, not in this file
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: angular-dist
- task: AzureWebApp@1
displayName: 'Deploy to App Service'
inputs:
azureSubscription: $(azureConnection)
appType: 'webAppLinux'
appName: $(webAppName)
# The application builder writes dist/<project>/browser
package: '$(Pipeline.Workspace)/angular-dist/my-angular-app/browser'Mettre en cache npm correctement
La tâche Cache@2 avec package-lock.json comme clé évite l'essentiel du téléchargement des dépendances à chaque exécution, et sur un grand projet Angular cela représente des minutes par exécution. Multipliez cela par toutes les PR d'une semaine et vous récupérez un temps ingénieur significatif. Le cache n'est invalidé que quand les dépendances changent réellement, ce qui est exactement ce que vous voulez.
Utilisez npm ci avec le flag --cache, pas npm install. npm ci garantit une installation propre depuis le lockfile. npm install peut résoudre silencieusement différemment et introduire une dérive entre les builds locaux et CI. J'ai traqué des bogues qui trouvaient leur origine dans cette dérive, et c'est pour ça que je considère npm ci comme non négociable en CI.
Builds spécifiques à l'environnement via fileReplacements
Les fileReplacements d'Angular dans angular.json sont le mécanisme correct pour la configuration d'environnement. Pas de vérifications window.location à l'exécution. Pas de globales injectées. La substitution au moment du build signifie que la mauvaise configuration d'environnement ne peut pas atteindre la production parce qu'elle a été compilée hors du build.
{
"configurations": {
"staging": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
],
"optimization": true,
"sourceMap": false
},
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"sourceMap": false,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
}
]
}
}
}SonarQube comme gate bloquant
Dans un environnement réglementé, SonarQube ou un gate d'analyse statique équivalent n'est pas optionnel. L'erreur que je vois le plus souvent est d'intégrer SonarQube comme une étape de rapport que personne ne consulte. Un quality gate qui ne bloque pas le pipeline n'est pas un gate. C'est une suggestion.
Les tâches SonarQubePrepare, SonarQubeAnalyze et SonarQubePublish se trouvent dans l'étape Test ci-dessus. Publier le résultat n'arrête rien en soi : mettez sonar.qualitygate.wait=true dans les propriétés supplémentaires de l'étape prepare pour que le pipeline échoue sur un gate rouge. Sur Azure DevOps, cet échec remonte par la tâche Publish Quality Gate Result, qui attend le gate puis fait échouer le build, et non par l'étape d'analyse. Configurez ensuite le quality gate dans SonarQube pour échouer si la couverture tombe sous le seuil ou si de nouveaux problèmes de sécurité critiques apparaissent. La première fois qu'il bloque un merge avec une vraie découverte de sécurité, l'équipe arrête de le traiter comme du bruit.
Un quality gate qui ne bloque pas le pipeline n'est pas un gate. C'est une suggestion. Traitez-le comme bloquant dès le premier jour ou ne prenez pas la peine de le configurer.
Gates d'approbation pour la production
L'étape Deploy nécessite une exigence d'approbation manuelle pour l'environnement de production. Dans Azure Pipelines c'est une approbation de ressource Environment, configurée sur l'environnement plutôt que dans le YAML, ce qui explique que le job de déploiement ci-dessus se contente de le nommer. Ma base de référence, c'est au moins un approbateur extérieur à l'équipe qui a écrit le changement. Combien d'approbateurs et quels rôles signent relève de la politique de l'organisation, pas de l'auteur du pipeline. L'approbation crée aussi une piste d'audit que les équipes de conformité exigent, et l'avoir dès le début signifie que vous n'avez pas à ajouter la gouvernance après coup sur un pipeline qui a déjà des habitudes de déploiement risquées.
La configuration décrite ici est un exemple, pas une copie du pipeline d'un employeur. Vos spécificités différeront, mais la structure ne change pas : lint rapide, test avec seuils appliqués, build contre des configs d'environnement explicites, déployer avec des gates humains. Cette séquence détecte les problèmes qui comptent et ne ralentit pas l'équipe sur ceux qui ne comptent pas.
Références
- Azure Pipelines documentation. https://learn.microsoft.com/en-us/azure/devops/pipelines/
- SonarQube documentation. https://docs.sonarsource.com/sonarqube/
- Angular CLI build. https://angular.dev/tools/cli/build
- npm ci. https://docs.npmjs.com/cli/v10/commands/npm-ci