A maioria dos projetos Angular em que entrei em nível enterprise tinha alguma versão de CI/CD. A diferença entre uma pipeline que tecnicamente roda e uma que realmente pega problemas reais antes de chegar a produção é enorme. Depois de anos em ambientes de serviços financeiros, aqui está um setup a título de exemplo que prova seu valor.
Por Que Azure Pipelines
No BMO, Azure Pipelines e SonarQube faziam parte do setup do time. Em organizações que rodam no Azure DevOps, o Pipelines integra diretamente com os repos, work items e artifact feeds já em uso, e a decisão geralmente é tomada antes de um desenvolvedor front-end entrar no projeto. Os princípios aqui se traduzem para GitHub Actions ou GitLab CI com mudanças mínimas, então as decisões estruturais valem a pena entender independente da ferramenta.
Os Quatro Estágios Que Realmente Importam
- Lint: pegar problemas de estilo e análise estática antes dos testes rodarem. Feedback rápido, barato de corrigir. Falha rápido se alguém deu push sem rodar localmente.
- Test: testes unitários com thresholds de cobertura aplicados. Sem threshold configurado significa sem gate. Threshold sem enforcement é teatro.
- Build: builds específicos por ambiente usando fileReplacements do Angular. Não detecção de ambiente em runtime, não globals injetados. Substituição em tempo de build, resolvida antes do bundle ser escrito.
- Deploy: upload de artefato para Azure Blob ou App Service, com uma aprovação manual no ambiente de produção. Pelo menos um aprovador de fora do time que escreveu a mudança.
# 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'Fazendo Cache do npm Corretamente
A task Cache@2 com package-lock.json como chave pula a maior parte do download de dependências em toda execução, e em um projeto Angular grande isso são minutos por execução. Multiplique isso por cada PR de uma semana e você está recuperando tempo significativo de engenheiro. O cache só é invalidado quando as dependências realmente mudam, que é exatamente o que você quer.
Use npm ci com a flag --cache, não npm install. npm ci garante uma instalação limpa a partir do lockfile. npm install pode silenciosamente resolver de forma diferente e introduzir divergência entre builds locais e do CI. Já cacei bugs que se originaram dessa divergência, e é por isso que trato npm ci como inegociável no CI.
Builds Específicos por Ambiente via fileReplacements
O fileReplacements do Angular no angular.json é o mecanismo correto para configuração de ambiente. Não checks de window.location em runtime. Não globals injetados. Substituição em tempo de build significa que a configuração de ambiente errada não pode chegar a produção porque foi compilada fora no momento do 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 Como Gate Bloqueante
Em um ambiente regulado, SonarQube ou um gate equivalente de análise estática não é opcional. O erro que mais vejo é integrar SonarQube como um step de relatório que ninguém revisa. Um quality gate que não bloqueia a pipeline não é um gate. É uma sugestão.
As tasks SonarQubePrepare, SonarQubeAnalyze e SonarQubePublish ficam no estágio de Test acima. Publicar o resultado não para nada sozinho: coloque sonar.qualitygate.wait=true nas propriedades extras do step de prepare para a pipeline falhar em um gate vermelho. No Azure DevOps essa falha aparece pela task Publish Quality Gate Result, que espera o gate e então falha o build, e não pelo step de análise. Depois configure o quality gate no SonarQube para falhar se a cobertura cair abaixo do threshold ou se novos issues críticos de segurança aparecerem. A primeira vez que ele bloqueia um merge com um achado de segurança real, o time para de tratar como ruído.
Um quality gate que não bloqueia a pipeline não é um gate. É uma sugestão. Trate como bloqueante desde o primeiro dia ou não se dê ao trabalho de configurar.
Gates de Aprovação para Produção
O estágio de Deploy precisa de um requisito de aprovação manual para o ambiente de produção. No Azure Pipelines isso é uma aprovação de recurso Environment, configurada no ambiente e não no YAML, que é por isso que o job de deployment acima só o nomeia. Minha referência é pelo menos um aprovador de fora do time que escreveu a mudança. Quantos aprovadores e quais papéis assinam é uma decisão de política da organização, não de quem escreve a pipeline. A aprovação também cria uma trilha de auditoria que times de compliance exigem, e ter isso desde o início significa que você não está retrofitando governança em uma pipeline que já tem hábitos de deploy arriscados.
O setup descrito aqui é um exemplo, não uma cópia da pipeline de nenhum empregador. Os seus detalhes vão diferir, mas a estrutura não muda: lint rápido, teste com thresholds aplicados, build contra configurações explícitas de ambiente, deploy com gates humanos. Essa sequência pega os problemas que importam e não desacelera o time nos problemas que não importam.
Referências
- 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