Skip to main content

CI/CD

Volume 5 — Platform Engineering

CI/CD automates the path from a committed change to a running deployment. This chapter covers the concepts, the structure of an Azure DevOps pipeline, and a real incident from Traxs history that illustrates exactly why pipeline configuration deserves the same care as application code.

1. Continuous Integration vs. Continuous Delivery vs. Continuous Deployment

These three terms describe increasingly automated stages of the same pipeline, and are worth distinguishing precisely:

TermWhat it guarantees
Continuous Integration (CI)Every change is automatically built and tested as soon as it's pushed — catches integration problems immediately rather than at a later merge
Continuous DeliveryEvery change that passes CI is automatically packaged into a deployable artifact, ready to release — but a human still triggers the actual deployment
Continuous DeploymentEvery change that passes CI is automatically deployed to production, with no manual trigger at all

The Traxs workflow — PR merges to main trigger an automatic pipeline deploy — sits at Continuous Deployment for both RoundTrip and Waypoint: there's no manual "click to release" step once a PR is merged, which is precisely why the branch/PR discipline covered in the Git Reference chapter (sequential PRs, rebase before merge) matters more here than it would in a setup with a manual gate before production.

2. Pipeline Structure: Stages, Jobs, Steps

LevelPurpose
PipelineThe whole automated process, top to bottom
StageA major phase — Build, Test, Deploy — stages can run sequentially or in parallel, and often gate on the previous stage succeeding
JobA unit of work within a stage, running on a specific agent
StepAn individual task or script command within a job

3. A Realistic azure-pipelines.yml

trigger:
branches:
include:
- main

pool:
name: traxs-self-hosted

stages:
- stage: Build
jobs:
- job: BuildAPI
steps:
- script: dotnet restore
- script: dotnet build --configuration Release
- script: dotnet publish --configuration Release --output $(Build.ArtifactStagingDirectory)
- publish: $(Build.ArtifactStagingDirectory)
artifact: api-build

- stage: Deploy
dependsOn: Build
jobs:
- job: DeployToProduction
steps:
- download: current
artifact: api-build
- task: AzureWebApp@1
inputs:
azureSubscription: "Traxs Production"
appName: "roundtrip-api"
package: "$(Pipeline.Workspace)/api-build"

trigger: branches: include: [main] is what makes this Continuous Deployment specifically — any merge to main runs the whole pipeline through to an actual production deploy, with no manual approval step in this particular configuration.

4. Self-Hosted Agents

A pipeline agent is the actual machine that executes a pipeline's jobs. Azure DevOps offers Microsoft-hosted agents (spun up fresh per run, torn down after) or self-hosted agents — a persistent machine you register and maintain yourself.

pool:
name: traxs-self-hosted

The Traxs pipelines run on a self-hosted agent (laptop, in the traxs-self-hosted pool) rather than a Microsoft-hosted one — meaning the agent persists between runs, keeps its own local caches (faster subsequent builds), but also means the agent's own health, available disk space, and installed tooling are the team's responsibility to maintain, unlike a Microsoft-hosted agent that's provisioned fresh every time.

# On the self-hosted agent machine itself
cd /path/to/agent
./config.sh --url https://dev.azure.com/traxs --auth pat --pool traxs-self-hosted --agent laptop
./run.sh

In production use, the agent typically runs as a systemd service (see the Linux Administration chapter) rather than a manually-launched foreground process, so it survives reboots and restarts automatically.

5. Environment-Specific Deploy Targeting — the TRA-195 Incident

This is worth walking through in detail because it's a real incident, not a hypothetical: two separate pipeline files, azure-pipelines-dev.yml and azure-pipelines.yml (production), both deploying to Cloudflare Pages, distinguished only by a --branch flag passed to Wrangler:

# azure-pipelines-dev.yml — CORRECT
- script: npx wrangler pages deploy ./dist --branch development

# azure-pipelines.yml (production) — CORRECT
- script: npx wrangler pages deploy ./dist --branch main

The root cause of TRA-195: the dev pipeline was, at one point, using --branch main — meaning every dev-triggered deployment was overwriting the production Cloudflare Pages deployment, regardless of which Cloudflare Pages project name the dev pipeline was otherwise targeting correctly. The project name being correct did not save it — the --branch flag is what actually determined which deployment got overwritten, independent of the project.

The broader lesson, beyond this specific flag: when two pipelines are meant to target genuinely different environments, the values that actually differentiate them (branch flags, resource group names, connection strings, target hostnames) deserve explicit review specifically for that difference — not just a glance confirming "yes, this is the dev pipeline file" — since a single incorrect parameter deep in an otherwise-correct-looking file can silently redirect an entire deployment to the wrong target.

The related, still-relevant configuration to keep in sync: the roundtrip-dev Cloudflare Pages project's production branch setting must itself be set to "development" (not "main") so that dev.roundtrips.app actually serves what the dev pipeline deploys — if dev.roundtrips.app ever appears to be serving stale content, this project-level branch mapping setting is the first thing worth checking, before assuming the pipeline itself is broken.

6. Secrets and Variables in Pipelines

Pipeline variables can be marked secret, encrypting them at rest and masking their value in logs:

variables:
- group: traxs-prod-secrets # a variable group, managed centrally in Azure DevOps Library

steps:
- script: echo "Connecting with $(DB_PASSWORD)" # value is masked in logs automatically if marked secret

For anything genuinely sensitive that the application itself needs at runtime (not just during the pipeline's own execution), a Key Vault reference (see the Azure Key Vault chapter) resolved directly by App Service is generally preferable to passing the value through as a pipeline variable at all — it keeps the secret out of the pipeline's own configuration surface entirely.

7. Artifacts

An artifact is the output of one stage, made available to a later stage (or a separate pipeline) — the compiled/published application in the example above, passed from the Build stage to the Deploy stage.

- publish: $(Build.ArtifactStagingDirectory)
artifact: api-build
- download: current
artifact: api-build

Publishing and downloading explicitly (rather than assuming build output persists automatically between stages) matters because each stage can run on a different agent — there's no guarantee the Deploy stage runs on the same physical machine, or even the same file system, as the Build stage that produced the output.

8. Troubleshooting Playbook

SymptomLikely causeWhat to check
Deploy overwrote the wrong environmentA branch/target flag mismatched between pipeline files, as in TRA-195Diff the dev and production pipeline files specifically for every differentiating parameter, not just confirm the file names
Pipeline succeeds, but the deployed change isn't visibleCDN/browser caching, or the pipeline deployed to the wrong target entirelyConfirm the actual deployed content directly (via the hosting platform, not just pipeline success) before assuming the pipeline itself is at fault
Self-hosted agent shows offlineThe agent process/service isn't running on the host machineCheck the agent's systemd service status directly on the host (systemctl status)
Build works locally, fails identically-configured in the pipelineDifferent environment on the agent — missing tooling, different OS/version, or a leftover cached artifact from a prior runConfirm the agent's installed tool versions match local; check for stale cached files the agent may have retained between runs
Secret value visible in pipeline logsVariable not actually marked as secret in the pipeline/variable group configurationConfirm the variable group setting, not just the reference syntax used in the YAML
Pipeline stuck queued, never startsNo available agent in the specified poolConfirm the self-hosted agent is online and not already occupied running another job, if the pool has limited capacity

9. Quick Reference

CategoryItemDetail
ConceptCIAutomatic build/test on every push
ConceptContinuous DeliveryAutomatic packaging, manual release trigger
ConceptContinuous DeploymentFully automatic, no manual trigger — the Traxs setup
StructureStage → Job → StepIncreasing granularity within a pipeline
AgentSelf-hostedPersistent, team-maintained machine (traxs-self-hosted pool, agent laptop)
Artifactpublish / downloadExplicit hand-off of build output between stages/agents
SecretsVariable group + secret flagMasked in logs; prefer Key Vault references for runtime app secrets
Incident lessonTRA-195A --branch flag mismatch, not the project name, caused dev to overwrite production

Part of the Traxs Engineering Handbook — Volume 5: Platform Engineering. Companion chapters in this volume: Linux Fundamentals, Linux Administration, Kubernetes Fundamentals, Observability, Secrets Management.