Case study 11 · Engineering Lifecycle Services

A build-once release pipeline across three Azure environments

Every commit built once and the same image promoted from development to staging to production, with tests, quality and vulnerability checks blocking the merge.

Partner
A multinational advisory firm
Period
October 2024 to January 2026
Build per commit
1
The image tested in staging is the one released
Environments
3
Development, staging and production
Merge gates
3
Tests, SonarQube quality gate and Trivy

The challenge

A platform used across a large organization needed predictable releases, clear separation between environments, and security checks that could not be merged past.

What we built

GitHub Actions workflows that build one Docker image per commit, tag it with the commit SHA, push it to Azure Container Registry and promote the same image through development, staging and production. Pull requests are blocked by failing tests, a failed SonarQube quality gate or critical Trivy findings, and production releases need approval.

What was delivered

  • One image per commit, promoted unchanged to production
  • Merges blocked by tests, the quality gate and critical vulnerabilities
  • Secrets read from Key Vault, and production releases approved and swapped in without downtime

Partner background

Our partner is a multinational advisory firm whose teams work with large volumes of documents, spreadsheets, databases and email. It wanted one internal platform where those teams could build data pipelines and AI agents themselves, instead of commissioning a new application for each need. The platform had to run inside the partner's Microsoft 365 and Azure environment, keep each team's work separate, and move work from experiment to production through controlled environments. This case study covers one part of that platform; nine case studies describe the whole.

The challenge

Environments drift

Manual releases make it hard to know what is running where, and configuration differences between environments go unnoticed.

Rebuilt is not tested

Building a new image for each environment means production runs an image that was never tested in staging.

Secrets in the wrong places

Credentials copied into files, pipeline variables or application settings are hard to rotate and easy to leak.

Findings nobody acts on

Security reports that only advise are easy to merge past.

Objectives

  • Build once and promote the same image to every environment
  • Block merges that fail tests, quality or vulnerability checks
  • Keep secrets out of the repository and out of application settings
  • Require approval for production and release without downtime
  • Catch formatting and lint issues before code is committed

Our role

CharCentric provided technical leadership and architecture within a multidisciplinary engineering team, and contributed directly to implementation. The platform was built over 16 months, from October 2024 to January 2026, as a Python and FastAPI backend on Azure.

Scope and timeline

The release and scanning workflows were built between December 2024 and March 2025.

Delivery timeline from the project history, against the overall platform build
Delivery timeline from the project history, against the overall platform build

Approach

Build once, promote

Each commit produces one image tagged with its commit SHA. Merging to dev builds it and deploys it to development. Promotion then fast-forwards stage and main to that already built commit and deploys its image digest, instead of building a new one, so what reaches production is exactly what passed staging.

Gates where decisions are made

Pytest, a SonarQube quality gate and a Trivy scan run on every pull request. A failing test, a failed gate or a critical vulnerability blocks the merge, and the Trivy findings are kept in one updated comment on the pull request.

Secrets by reference

Doppler holds secrets per environment, and a custom Pulumi provider writes them into Azure Key Vault. App Service reads them through Key Vault references, so secrets never live in the code, and application settings hold only references.

Implementation

Release pipeline from commit to deployment
Release pipeline from commit to deployment

Commit checks

Pre-commit hooks lint and format only the changed code and check YAML, trailing whitespace and large files. The same checks run again in CI, so skipping them locally does not skip them.

Build

Each commit builds one Docker image, tagged with its commit SHA and pushed to Azure Container Registry.

Deploy

A matrix job deploys the image to the two API web apps in each environment. In production it deploys to a staging slot, waits for approval in a GitHub environment, then swaps the slot in without downtime.

Branch, environment, secrets configuration and deployed web apps
Branch, environment, secrets configuration and deployed web apps

Tools and technologies

ToolPurpose
GitHub ActionsBuild, test, scan and deploy workflows
DockerApplication images
Azure Container RegistryImage storage, tagged by commit
Azure App ServiceHosting with deployment slots
Doppler and Azure Key VaultSecrets per environment, read by reference
PytestTests on every pull request
SonarQubeCode quality gate
TrivyVulnerability gate
pre-commitLocal checks

What was delivered

Build per commit
1
Environments
3
Merge gates
3
Production releases
Approved
  • One image per commit, promoted through three environments
  • Merges blocked by tests, the quality gate and critical findings
  • Secrets read from Key Vault by reference
  • Approved production releases swapped in without downtime

Why it matters

Release discipline is cheapest to establish early. Building once, promoting the tested image and letting the checks block a merge keep a growing platform predictable.

If your organization is planning a platform of this kind, or needs a specific part of one designed and delivered, we would be glad to discuss it.