Shared images per release, built from immutable git tags
Status: Accepted · Date: 2026-06 (month as recorded; decision D1 of the auto-deploy design, proven live 2026-06-27/30) · Area: Deployment
Context
Every solution gets its own Orbit stack on Azure Container Apps. If each solution baked its own images, provisioning a solution would mean a build per solution, per-solution image sprawl in the registry, and no single artifact to reason about when a component is released. Separately, builds made from a developer’s working tree can drift from what the release record claims was built.
Decision
- Images are shared per release, not per solution (D1). What is
per-solution is only: the ACA apps, the Azure Files shares, and environment
values. Apps pin an image tag; a new component version rolls out by bumping
images.*_tagin the Azure config and re-running the provisioner. - Builds come from the immutable git tag, never the working tree.
build-images.sh <component>[@<version>]checks the tag<component>/v<version>out into a throwawaygit worktree, builds server-side viaaz acr build(no local Docker), pushes:<version>and:latest, and records acontainerrelease entry with the tag’s commit — so image, commit, and git tag cannot drift. - Each component versions independently — its own tag cadence,
@versionpin or latest. There is deliberately noalltarget: a shared-version “all” does not fit independent versioning. A futureallshould read a platform-version manifest (e.g.releases/platform/v<X>.yaml), not a shared version argument. - Per-solution storage follows fixed sub-decisions: two shares, three
links (D3 — customer share linked read-write to the agent and read-only
where a reader mount existed; internal share to the agent only), and the
methodology mount is the customer repo’s vendored copy (D5 —
/repo/trajectory-methodology, no third share). - Provisioning is idempotent and reconciling (D10): GET →
create-if-absent → update-on-drift, never erroring on “exists”. Re-running
deploy.sh provision <id>converges. GitHub stays the source of truth; the share holds a working clone (D9).
Consequences
- Provisioning a solution is fast and build-free; a release builds once and every solution can pick it up by tag bump.
- Nothing solution-specific can be baked into an image. This constraint later
forced the runtime-env-injection design for per-solution URLs (see
ADR-003) — baking
NEXT_PUBLIC_*values at build time would have broken image sharing. - Naming must be deterministic on both sides of the fence: the bash
azure_nameshelper and the TypeScriptazureNames()are verified byte-identical twins of one scheme (h = sha256(solutionId).hex[:12], appsorbit-<h>/agent-<h>/mc-<h>, shares<id>-customer/<id>-internal). Changing one requires changing the other; human-readable identity lives in resource tags and share metadata, not names. - Accepted trade-off: rolling out a fix to all solutions is a re-provision
sweep (each app pins a tag), and premium
FileStorageshares enforce a 100 GiB minimum quota, overriding the design’s original 5 GiB.
Evidence
infrastructure/azure/build-images.sh— tag-worktree build + record writerinfrastructure/azure/lib.sh(azure_names) andimplementation/aurora-webapp/codebase/src/lib/solutions/azure-names.ts— the naming twinsinfrastructure/azure/provision-solution.sh— shares, links, reconciling create/updatememory/orbit-auto-deploy.md(D1, D3, D5, D9, D10),memory/release-registry.md(independent versioning, noall)