Skip to Content
Design DecisionsShared Images per Release

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.*_tag in 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 throwaway git worktree, builds server-side via az acr build (no local Docker), pushes :<version> and :latest, and records a container release entry with the tag’s commit — so image, commit, and git tag cannot drift.
  • Each component versions independently — its own tag cadence, @version pin or latest. There is deliberately no all target: a shared-version “all” does not fit independent versioning. A future all should 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_names helper and the TypeScript azureNames() are verified byte-identical twins of one scheme (h = sha256(solutionId).hex[:12], apps orbit-<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 FileStorage shares enforce a 100 GiB minimum quota, overriding the design’s original 5 GiB.

Evidence

  • infrastructure/azure/build-images.sh — tag-worktree build + record writer
  • infrastructure/azure/lib.sh (azure_names) and implementation/aurora-webapp/codebase/src/lib/solutions/azure-names.ts — the naming twins
  • infrastructure/azure/provision-solution.sh — shares, links, reconciling create/update
  • memory/orbit-auto-deploy.md (D1, D3, D5, D9, D10), memory/release-registry.md (independent versioning, no all)