Skip to content

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

  1. Pipeline Overview
  2. Pipeline Architecture
  3. Stages Reference
  4. Variables and Parameters
  5. Docker Build & Caching
  6. Supply Chain Security
  7. Registry Management
  8. 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

uml diagram


Pipeline Architecture

Complete Stage Dependency Graph

uml diagram

Job Parallelism in Build Stage

uml diagram


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.

uml diagram

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

uml diagram

Stage 3: Deploy to Staging

Condition: Main branch, at least one app affected

uml diagram

Stage 4: Acceptance Tests

Condition: Main branch, loadTestOnly is false, Build succeeded, and either:

  1. 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
  2. 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 LighthouseAudit result Skipped.

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 401 errors 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 *Affected flag is true and the digest from the corresponding Package* job in this run matches sha256: + 64 hex chars. Otherwise the step fails (no attesting stale or empty digests).
  • Staging custom predicate includes verification.acceptanceTestsPassed: true only if the Acceptance stage completed as Succeeded or SucceededWithIssues; false on 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

uml diagram

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:

uml diagram


Docker Build & Caching

BuildKit Registry Caching

The pipeline uses docker buildx with registry-based caching for optimal build performance on ephemeral Azure DevOps agents.

uml diagram

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

uml diagram

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.

uml diagram

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:

  1. Downloads the cosign public key
  2. Calls cosign verify-attestation --type custom for each image
  3. Extracts and validates predicate.environment == "staging"
  4. 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

uml diagram

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)

uml diagram

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

uml diagram

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:

  1. Verify :cache tag exists in registry
  2. Check buildx is creating the builder: docker buildx inspect --bootstrap
  3. 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:

  1. COSIGN_PASSWORD secret is set in variable group
  2. cosign.key is uploaded to Secure Files
  3. File permissions are authorized for pipeline

Affected Detection Wrong

Symptoms: Apps not being detected as affected

Check:

  1. Verify full git history is fetched: fetchDepth: 0
  2. Check Nx dependency graph: pnpm nx graph
  3. Test manually: pnpm nx show projects --affected --base=HEAD~1

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