Azure DevOps Pipeline Reference¶
Version: 3.5
Last Updated: 21 March 2026
Pipeline File:azure-pipelines.yml
This document provides a detailed technical reference for the Forma3D.Connect CI/CD pipeline, including architecture, stages, caching strategies, and supply chain security.
Table of Contents¶
- Pipeline Overview
- Pipeline Architecture
- Stages Reference
- Variables and Parameters
- Docker Build & Caching
- Supply Chain Security
- Registry Management
- Conditional Execution
Pipeline Overview¶
The pipeline implements trunk-based development with the following philosophy:
| Branch Type | Stages Executed | Purpose |
|---|---|---|
| Feature branches | Validate & Test | Fast feedback loop |
main branch |
Validate & Test → Build & Package → Deploy Staging → Acceptance (when applicable) → Lighthouse (web only) → Registry Maintenance → Attest Staging → optional Load Test → Deploy Production (when enabled) | Full deployment + supply-chain attestations |
High-Level Flow¶
Pipeline Architecture¶
Complete Stage Dependency Graph¶
Job Parallelism in Build Stage¶
Stages Reference¶
Stage 1: Validate & Test¶
Condition: Always runs (except loadTestOnly mode)
All 4 jobs run across MS-hosted and self-hosted agents. Lint, TypeCheck, and UnitTests run in parallel; CodeQuality runs after UnitTests:
| Job | Description | Pool | Services | Depends On |
|---|---|---|---|---|
| Lint | License check + ESLint on all/affected | MS-hosted | — | — |
| TypeCheck | TypeScript compilation | DO-Build-Agents | — | — |
| UnitTests | Jest/Vitest unit tests | DO-Build-Agents | PostgreSQL 16 (container) | — |
| CodeQuality | SonarCloud analysis | MS-hosted | — | UnitTests |
Outputs:
- JUnit XML test results
- Cobertura code coverage (Azure DevOps)
- lcov coverage reports (published as pipeline artifact for SonarCloud)
- SonarCloud quality gate result
CodeQuality Job (SonarCloud)¶
The CodeQuality job runs SonarCloud static analysis after unit tests complete. It downloads the lcov coverage artifacts from the UnitTests job and feeds them to SonarCloud for combined code quality + coverage analysis.
Configuration:
| Setting | Value | Rationale |
|---|---|---|
configMode |
file |
All config in sonar-project.properties (not YAML) |
pollingTimeoutSec |
300 |
SonarCloud processing can take up to 5 minutes |
| Pool | MS-hosted (ubuntu-latest) |
Analysis is network-bound, saves self-hosted capacity |
dependsOn |
UnitTests |
Needs coverage artifacts |
Rule suppression: False positives and won't-fix items are managed in sonar-project.properties via sonar.issue.ignore.multicriteria. Inline // NOSONAR comments do not work for TypeScript/JavaScript in SonarCloud.
Stage 2: Build & Package¶
Condition: Main branch only, after Validate & Test succeeds
Stage 3: Deploy to Staging¶
Condition: Main branch, at least one app affected
Stage 4: Acceptance Tests¶
Condition: Main branch, loadTestOnly is false, Build succeeded, and either:
- Deploy Staging succeeded and at least one of these was affected: Gateway, Web, Order / Print / Shipping / GridFlock services, or Slicer (docs and EventCatalog are not in this list), or
- Deploy Staging was skipped and acceptance tests were affected (run Gherkin against the existing staging deployment without a new deploy).
Jobs:
- Verify Deployment Health & Version — runs only when an app service from (1) was deployed (same service set as above; excludes docs/EventCatalog).
- Run Gherkin Acceptance Tests — runs after verification completes or was skipped (covers both full deploy and acceptance-tests-only flows).
Outputs: Cucumber HTML report, Playwright traces, JUnit results.
Stage 5: Lighthouse Audit¶
Condition: Main branch, Deploy Staging succeeded, and Web was affected.
- Runs Lighthouse CI against
WEB_URL(self-hosted agent, Chrome). - Publishes HTML report tab and build artifact.
- Skipped when Web was not deployed in the run (for example docs-only or API-only). Attest Staging still allows
LighthouseAuditresultSkipped.
Stage 6: Registry Maintenance¶
Condition: Main branch (ordering after Build and Deploy Staging; runs even when deploy was skipped).
- Registry cleanup script and DigitalOcean garbage collection.
- Attest Staging waits for this stage to finish (success, failure, or skipped) so cosign operations are less likely to hit transient registry
401errors during GC.
Stage 7: Attest Staging Promotion¶
Condition: Main branch, enableSigning true, loadTestOnly false, LighthouseAudit in Succeeded / SucceededWithIssues / Skipped, RegistryMaintenance finished (including Failed), and either:
- Acceptance Test succeeded (full E2E path), or
- Deploy Staging succeeded and docs and/or EventCatalog were in the affected set (static-site path; Acceptance stage is usually Skipped for those).
Job: AttestStagingPromotion (Microsoft-hosted ubuntu-latest, checkout: none).
- Runs only when at least one deployable target was affected (
deploymentHappened). - Per-image steps run only when that service’s
*Affectedflag istrueand the digest from the correspondingPackage*job in this run matchessha256:+ 64 hex chars. Otherwise the step fails (no attesting stale or empty digests). - Staging custom predicate includes
verification.acceptanceTestsPassed:trueonly if the Acceptance stage completed asSucceededorSucceededWithIssues;falseon the docs/EventCatalog-only path (Acceptance Skipped).
Stage 8: Load Tests (Optional)¶
Condition: runLoadTests parameter is true (and upstream stages per azure-pipelines.yml)
- Runs K6 load tests
- Can run in baseline mode (no threshold failures)
Stage 9: Deploy to Production¶
Condition: When the production stage is enabled in azure-pipelines.yml: depends on Build, AcceptanceTest, AttestStaging, and LoadTest; manual approval gate on the production environment.
Variables and Parameters¶
Pipeline Parameters¶
Pipeline Variables¶
| Variable | Value | Description |
|---|---|---|
nodeVersion |
20.x |
Node.js version |
pnpmVersion |
9 |
pnpm version |
isMain |
$[eq(...)] |
True if on main branch |
dockerRegistry |
registry.digitalocean.com/forma-3d |
Container registry URL |
gatewayImageName |
forma3d-connect-gateway |
API Gateway image name |
orderServiceImageName |
forma3d-connect-order-service |
Order Service image name |
printServiceImageName |
forma3d-connect-print-service |
Print Service image name |
shippingServiceImageName |
forma3d-connect-shipping-service |
Shipping Service image name |
gridflockServiceImageName |
forma3d-connect-gridflock-service |
GridFlock Service image name |
slicerImageName |
forma3d-connect-slicer |
Slicer Container image name |
webImageName |
forma3d-connect-web |
Web image name |
docsImageName |
forma3d-connect-docs |
Docs image name |
eventcatalogImageName |
forma3d-connect-eventcatalog |
EventCatalog static site image |
imageTag |
$(Build.BuildNumber) |
Tag format: YYYYMMDDHHmmss |
Variable Group: forma3d-staging¶
The forma3d-staging variable group contains all secrets and configuration:
Docker Build & Caching¶
BuildKit Registry Caching¶
The pipeline uses docker buildx with registry-based caching for optimal build performance on ephemeral Azure DevOps agents.
Build Command Structure¶
docker buildx build \
--file apps/api/Dockerfile \
--tag $(registry)/$(image):$(tag) \
--tag $(registry)/$(image):latest \
--cache-from type=registry,ref=$(registry)/$(image):cache \
--cache-to type=registry,ref=$(registry)/$(image):cache,mode=max \
--build-arg BUILD_NUMBER=$(tag) \
--build-arg BUILD_DATE=$(date) \
--build-arg GIT_COMMIT=$(commit) \
--label "org.label-schema.version=$(tag)" \
--push \
.
Cache Behavior¶
| Flag | Purpose |
|---|---|
--cache-from type=registry,ref=... |
Pull existing cache from registry |
--cache-to type=registry,ref=...,mode=max |
Push ALL layers (including intermediate) |
--push |
Build and push in single operation |
Expected Build Times¶
| Scenario | First Build | Cached Build |
|---|---|---|
| API Image | 3-4 min | 1-2 min |
| Web Image | 2-3 min | 1-2 min |
| Docs Image | 5-6 min | 1-2 min |
Supply Chain Security¶
Image Signing Flow¶
Dependency License Check¶
Before any code is built or packaged, the Lint job runs a license compliance check against the full npm dependency tree using license-checker-rseidelsohn:
- script: pnpm run license-check
displayName: 'Check dependency licenses (fail on non-permissive)'
The check is implemented in scripts/check-licenses.js and scans all installed packages for non-permissive licenses:
| Pattern matched | Examples |
|---|---|
GPL |
GPL-2.0, GPL-3.0, GPL-3.0-only, LGPL-2.1 |
AGPL |
AGPL-3.0, AGPL-3.0-or-later |
SSPL |
Server Side Public License |
Commons Clause |
Any license with Commons Clause restriction |
The check runs in ~1 second and exits with code 1 (failing the pipeline) if any dependency matches a disallowed license. Private packages (like the project root with "license": "UNLICENSED") are excluded.
See ADR-068: Dependency License Compliance Check for the full decision record.
CVE Scanning with Grype¶
After the SBOM is generated and attached, each image's SBOM is scanned by Grype for known vulnerabilities:
grype sbom:<service>-sbom.cdx.json --output table --fail-on high --only-fixed
| Flag | Purpose |
|---|---|
--fail-on high |
Pipeline fails if any High or Critical severity CVEs are found |
--only-fixed |
Only reports CVEs that have a fix available — prevents failures from unfixable vulnerabilities |
--output table |
Human-readable output in the pipeline log |
Grype output columns:
| Column | Meaning |
|---|---|
| NAME | Vulnerable package name |
| INSTALLED | Version found in the image |
| FIXED IN | Version that resolves the CVE |
| TYPE | Package type (npm, go-module, deb, apk, python) |
| VULNERABILITY | CVE or GHSA identifier |
| SEVERITY | CVSS-based rating (Critical, High, Medium, Low) |
| EPSS | Exploit Prediction Scoring System — probability of exploitation within 30 days (percentage + percentile rank) |
| RISK | Combined risk score factoring EPSS and severity |
EPSS (Exploit Prediction Scoring System) is a data-driven model by FIRST.org that predicts the likelihood a CVE will be exploited in the wild. It complements CVSS severity: a High-severity CVE with 0.1% EPSS (1st percentile) is far less urgent than a Medium-severity CVE with 30% EPSS (96th percentile). Grype includes EPSS data to help prioritize remediation.
Exclusions (.grype.yaml):
Some CVEs cannot be fixed at the project level. These are excluded in .grype.yaml at the repository root with documented rationale:
ignore:
# docker/cli v28.5.1 → needs 29.2.0 (High, EPSS <0.1%)
# Compiled into Alpine's docker-cli package; awaiting upstream update
- vulnerability: GHSA-p436-gjf2-799p
package:
type: go-module
Current exclusions are all Go module CVEs from Alpine's docker-cli and containerd packages, affecting only the Gateway image. All have EPSS scores at the 0th–1st percentile.
Slicer exception: The Slicer's grype scan is disabled entirely (commented out in the pipeline) because its linuxserver/bambustudio:01.08.03 base image has 800+ unfixable CVEs from system packages. See TODO.md for the BambuStudio v2 upgrade plan.
Remediation strategies used:
| Strategy | What it fixes |
|---|---|
pnpm overrides in package.json |
Transitive npm dependency CVEs (cross-spawn, minimatch, file-type, lodash, ajv, bn.js, serialize-javascript) |
apk upgrade --no-cache in Dockerfiles |
Alpine system package CVEs (zlib, partial docker-cli) |
rm -rf /usr/local/lib/node_modules/npm in Dockerfiles |
Bundled npm CVEs in node:20-alpine (tar, glob, cross-spawn) — npm is not needed at runtime |
.grype.yaml ignore rules |
Go module CVEs baked into Alpine binaries (awaiting upstream update) |
Verification¶
# Verify image signature
cosign verify --key cosign.pub registry.digitalocean.com/forma-3d/forma3d-connect-api:latest
# Verify SBOM attestation
cosign verify-attestation --key cosign.pub --type cyclonedx \
registry.digitalocean.com/forma-3d/forma3d-connect-api:latest
Image Metadata Labels¶
All images include Label Schema metadata:
| Label | Value |
|---|---|
org.label-schema.version |
Build number (YYYYMMDDHHmmss) |
org.label-schema.build-date |
ISO 8601 timestamp |
org.label-schema.vcs-ref |
Git commit SHA |
org.label-schema.vcs-url |
Repository URL |
org.label-schema.vcs-branch |
Branch name |
Environment Promotion Attestations¶
The pipeline uses signed attestations to track image promotions through environments. This creates an auditable chain of custody and prevents deploying images that haven't passed earlier stages.
Attestation Types¶
| Attestation | When Created | Purpose |
|---|---|---|
| Signature | After build | Proves image integrity, created by authorized pipeline |
| SBOM (cyclonedx) | After build | Lists all components/dependencies in image |
| Staging (custom) | After Acceptance succeeds, or after docs/EventCatalog staging deploy (no E2E) | Proves image promoted to staging; acceptanceTestsPassed in predicate reflects whether Gherkin ran |
| Production (custom) | After production deploy | Proves image promoted to production |
Attestation Predicate Schema (staging / production custom type)¶
Shape produced by Attest Staging / production promotion steps (see azure-pipelines.yml). The verification block is the source of truth for whether Gherkin acceptance ran in the same pipeline run.
{
"_type": "https://forma3d.com/attestations/promotion/v1",
"environment": "staging",
"promotedAt": "2026-03-21T12:00:00+00:00",
"build": {
"number": "20260321120000",
"pipeline": "forma3d-connect",
"pipelineId": "123",
"runId": "456",
"runUrl": "https://dev.azure.com/..."
},
"source": {
"commit": "abc123",
"branch": "main",
"repository": "forma-3d-connect"
},
"verification": {
"healthCheckPassed": true,
"acceptanceTestsPassed": true
}
}
acceptanceTestsPassed is true only when the Acceptance Test stage completed as Succeeded or SucceededWithIssues. For a docs-only or EventCatalog-only staging deploy, Acceptance is skipped and this field is false while the staging promotion attestation still records the build and registry digest that was pushed.
Verification Commands¶
# Verify staging attestation exists
cosign verify-attestation \
--key cosign.pub \
--type custom \
registry.digitalocean.com/forma-3d/forma3d-connect-api:20260201143000
# Extract attestation content
cosign verify-attestation --key cosign.pub --type custom \
registry.digitalocean.com/forma-3d/forma3d-connect-api:20260201143000 \
| jq -r '.payload | @base64d | fromjson | .predicate'
Production Gate¶
Before production deployment, the pipeline verifies staging attestation:
- Downloads the cosign public key
- Calls
cosign verify-attestation --type customfor each image - Extracts and validates
predicate.environment == "staging" - Fails the pipeline if any image lacks valid staging attestation
This ensures images cannot bypass staging and go directly to production. Automation that interprets the predicate should not assume verification.acceptanceTestsPassed is always true: validate it explicitly if production should require a Gherkin run for that image.
Registry Management¶
Image Tags¶
Cleanup Script: scripts/cleanup-registry.sh¶
The cleanup script maintains registry hygiene by using the promotion attestations to determine image importance:
| Attestation Status | Cleanup Behavior |
|---|---|
| Has PRODUCTION attestation | Never delete (unlimited retention) |
| Has STAGING attestation | Keep most recent 5 per repository |
| No promotion attestation | Delete (unless currently deployed) |
| Currently deployed | Never delete (checked via health endpoint) |
Cleanup Command¶
./scripts/cleanup-registry.sh \
--key cosign.pub \
--api-url https://staging-connect-api.forma3d.be \
--web-url https://staging-connect.forma3d.be \
--docs-url https://staging-docs.forma3d.be \
--max-staging 5 \
--dry-run # Remove for actual cleanup
Conditional Execution¶
Nx Affected Detection¶
Stage Conditions Summary¶
| Stage / Job | Condition |
|---|---|
| ValidateAndTest | Always (unless loadTestOnly) |
| — Lint | Always |
| — TypeCheck | Always |
| — UnitTests | Always |
| — CodeQuality | After UnitTests succeeds (SonarCloud analysis) |
| Build | isMain AND ValidateAndTest succeeded |
| DeployStaging | isMain AND any app affected (includes docs, EventCatalog, gateway, web, services, slicer) |
| AcceptanceTest | isMain, not loadTestOnly, Build succeeded, and (DeployStaging succeeded with gateway/web/services/slicer affected) or (DeployStaging skipped and acceptance tests affected) — excludes docs/EventCatalog as triggers |
| UpdateDeployTag | DeployStaging succeeded (ordering parallel to downstream stages) |
| LighthouseAudit | isMain, DeployStaging succeeded, web affected |
| RegistryMaintenance | isMain (not canceled, not loadTestOnly; ordered after Build + DeployStaging) |
| AttestStaging | isMain, enableSigning, not loadTestOnly, LighthouseAudit in succeeded/succeeded-with-issues/Skipped, RegistryMaintenance finished (including Failed), and (AcceptanceTest succeeded or (DeployStaging succeeded and docs or EventCatalog affected)) |
| LoadTest | loadTestOnly=true or (isMain, runLoadTests, Build succeeded, and AcceptanceTest result is Succeeded, SucceededWithIssues, or Skipped) |
| DeployProduction | When enabled: depends on Build, AcceptanceTest, AttestStaging, LoadTest; approval on production environment |
Exact expressions live in azure-pipelines.yml; use this table as a guide, not a substitute for the YAML.
Troubleshooting¶
Cache Not Being Used¶
Symptoms: Build times not improving on subsequent runs
Check:
- Verify
:cachetag exists in registry - Check buildx is creating the builder:
docker buildx inspect --bootstrap - Verify registry authentication succeeded
Solution:
# Manually populate cache
docker buildx build \
--cache-to type=registry,ref=registry/image:cache,mode=max \
--push .
Signing Failures¶
Symptoms: cosign sign fails with authentication error
Check:
COSIGN_PASSWORDsecret is set in variable groupcosign.keyis uploaded to Secure Files- File permissions are authorized for pipeline
Affected Detection Wrong¶
Symptoms: Apps not being detected as affected
Check:
- Verify full git history is fetched:
fetchDepth: 0 - Check Nx dependency graph:
pnpm nx graph - Test manually:
pnpm nx show projects --affected --base=HEAD~1
Related Documentation¶
| Document | Description |
|---|---|
| Trunk-Based Development Workflow | Git branching strategy and workflow |
| Self-Hosted Build Agent | DO droplet agent setup and strategy |
| Staging Deployment Guide | Setup and configuration |
| Cosign Setup Guide | Key generation and setup |
| Runbook | Operations procedures |
| Troubleshooting | Common issues |
| SonarCloud Triage Reports | Issue triage and duplication reports |
| CodeCharta Research | City visualization research and options analysis |
Revision History:
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-02-01 | AI-generated | Initial pipeline reference |
| 2.0 | 2026-02-15 | AI-generated | Updated for microservice architecture (7 services) |
| 3.0 | 2026-03-13 | AI-generated | Added CodeQuality job (SonarCloud) to ValidateAndTest stage |
| 3.1 | 2026-03-14 | AI-generated | Added GenerateCodeCharta job to Build & Package stage; updated PackageDocs dependency and condition |
| 3.2 | 2026-03-14 | AI-generated | Decoupled CodeCharta from docs image; serve via bind-mount volume instead of baking into Docker image |
| 3.3 | 2026-03-18 | AI-generated | Added AttestStaging to stage conditions; fixed docs/eventcatalog included in attestation gate |
| 3.4 | 2026-03-18 | AI-generated | Moved UpdateDeployTag to run after DeployStaging (parallel with AcceptanceTest); tag tracks deployment, not test results |
| 3.5 | 2026-03-21 | AI-generated | Attest Staging: document E2E or docs/EventCatalog staging-deploy path; digest gating and verification.acceptanceTestsPassed semantics; stage graph and conditions aligned with azure-pipelines.yml |