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:
| Term | What 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 Delivery | Every change that passes CI is automatically packaged into a deployable artifact, ready to release — but a human still triggers the actual deployment |
| Continuous Deployment | Every 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
| Level | Purpose |
|---|---|
| Pipeline | The whole automated process, top to bottom |
| Stage | A major phase — Build, Test, Deploy — stages can run sequentially or in parallel, and often gate on the previous stage succeeding |
| Job | A unit of work within a stage, running on a specific agent |
| Step | An 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
| Symptom | Likely cause | What to check |
|---|---|---|
| Deploy overwrote the wrong environment | A branch/target flag mismatched between pipeline files, as in TRA-195 | Diff 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 visible | CDN/browser caching, or the pipeline deployed to the wrong target entirely | Confirm 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 offline | The agent process/service isn't running on the host machine | Check the agent's systemd service status directly on the host (systemctl status) |
| Build works locally, fails identically-configured in the pipeline | Different environment on the agent — missing tooling, different OS/version, or a leftover cached artifact from a prior run | Confirm 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 logs | Variable not actually marked as secret in the pipeline/variable group configuration | Confirm the variable group setting, not just the reference syntax used in the YAML |
| Pipeline stuck queued, never starts | No available agent in the specified pool | Confirm the self-hosted agent is online and not already occupied running another job, if the pool has limited capacity |
9. Quick Reference
| Category | Item | Detail |
|---|---|---|
| Concept | CI | Automatic build/test on every push |
| Concept | Continuous Delivery | Automatic packaging, manual release trigger |
| Concept | Continuous Deployment | Fully automatic, no manual trigger — the Traxs setup |
| Structure | Stage → Job → Step | Increasing granularity within a pipeline |
| Agent | Self-hosted | Persistent, team-maintained machine (traxs-self-hosted pool, agent laptop) |
| Artifact | publish / download | Explicit hand-off of build output between stages/agents |
| Secrets | Variable group + secret flag | Masked in logs; prefer Key Vault references for runtime app secrets |
| Incident lesson | TRA-195 | A --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.