
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.

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

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.

Tools and technologies
| Tool | Purpose |
|---|---|
| GitHub Actions | Build, test, scan and deploy workflows |
| Docker | Application images |
| Azure Container Registry | Image storage, tagged by commit |
| Azure App Service | Hosting with deployment slots |
| Doppler and Azure Key Vault | Secrets per environment, read by reference |
| Pytest | Tests on every pull request |
| SonarQube | Code quality gate |
| Trivy | Vulnerability gate |
| pre-commit | Local 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.