Most deployment pipelines fail in the same way:
production gets deployed too easily.
A merge to main triggers a deploy.
A hotfix goes live unintentionally.
Nobody can answer, “What exact code is running in prod?”
After rebuilding my Azure DevOps pipeline from the ground up, I landed on a tag-based, infra-templated release model that trades a bit of upfront complexity for long-term safety and clarity.
This post documents what I built, why it works, and what I’d do again.
The Core Idea
Production releases should be intentional, immutable, and auditable.
In practical terms:
- Merging code ≠ deploying to prod
- Only Git tags create production releases
- Deployment logic lives outside application repos
- Pipelines are infrastructure, not app code
High-Level Architecture
I split responsibilities across two repositories:
App Repo (Tanolis.Corp.App)
└─ azure-pipelines.yml ← thin entry point (locked)
└─ listens for Git tags only
Infra Repo (Tanolis.Corp.Infra)
└─ pipelines/prod/
└─ prod-release-template.yml ← real build & deploy logic
Separation of Concerns
| Responsibility | Location |
|---|---|
| When to deploy | App repo (tags) |
| How to deploy | Infra repo (templates) |
| Build steps | Infra repo |
| Deployment steps | Infra repo |
| Release decision | Git tag |
This separation is the single most important design decision.
Why Tag-Based Releases?
What tags give you automatically
- Immutability
A tag points to a specific commit. That release never changes. - Auditability
You can always answer: “What version is live?” - Safety
No accidental prod deploys from merges, rebases, or force pushes. - Clarity
Release intent is explicit: someone created a tag.
What I deliberately avoided
- Branch-based auto-deploys
- “Merge to main = prod”
- Release pipelines that require clicking buttons in the UI
The Thin Wrapper Pipeline (App Repo)
The App repo contains exactly one pipeline file at the root:
azure-pipelines.yml
Its responsibilities are minimal:
- Register the pipeline with Azure DevOps
- Listen for
v*Git tags - Delegate execution to the Infra repo
It does not:
- Build
- Deploy
- Know about Azure resources
This file is locked behind PRs and treated as infrastructure.
The Infra Template (Infra Repo)
All real work happens here:
- Checkout app code
- Build the Blazor WebAssembly app
- Inject release metadata
- Deploy to Azure Static Web Apps
Because this lives in a separate repo:
- Platform changes don’t touch app code
- Multiple apps can reuse the same template
- Deployment logic evolves independently
This is how platform teams work — even in a solo setup.
Versioning: What Changed (and Why It’s Better)
Before this design, my UI displayed versions like:
main+ce16b0564f9...
That came from assembly metadata.
After moving to tag-based releases:
- Assembly-time versioning no longer made sense
- Static Web Apps don’t preserve pipeline variables at runtime
The fix: runtime version injection
During the pipeline, I write a simple file:
wwwroot/version.json
Example:
{ "version": "v2.0.5" }
The UI reads this at runtime.
Why this is better:
- Works for static hosting
- Decoupled from build tooling
- Easy to extend (commit hash, timestamp, environment)
This is a subtle but important architectural upgrade.
The Release Flow (Day-to-Day)
Once everything is wired:
git checkout main
git pull
git tag v2.0.6
git push origin v2.0.6
That’s it.
No UI clicks.
No “deploy” buttons.
No ambiguity.
Rollbacks
Because releases are immutable:
- Re-run the same pipeline → redeploy
- Or re-tag a previous version (if policy allows)
Rollbacks become predictable instead of stressful.
Pros and Cons (Honest Assessment)
Pros
- Zero accidental production deployments
- Clear audit trail
- Strong separation of app vs platform
- Reusable across future projects
- Scales from solo → team → regulated environments
Cons
- Azure DevOps YAML has sharp edges
- Initial setup is non-trivial
- Pipeline must exist before tags will trigger
- Requires “platform thinking” up front
For serious production systems, the tradeoff is worth it.
When This Pattern Makes Sense
Good fit for:
- Solo founders building long-lived products
- Gov / enterprise / audit-sensitive work
- Teams that value safety over speed
Not ideal for:
- Hackathons
- Throwaway prototypes
- “Move fast and break prod” cultures
Key Lessons Learned
- Pipelines are objects, not just YAML
- Entry-point pipelines must exist before tags
- Templates cannot trigger pipelines
- Production versioning belongs at runtime
- Infrastructure deserves PR discipline — even for one person
Final Takeaway
Production releases should be boring, predictable, and intentional.
This tag-based, infra-templated approach makes that the default.
It took longer to set up than a typical pipeline —
but I won’t be rebuilding it again.

Add to favorites
Leave a Reply
You must be logged in to post a comment.