Release Registry
The releases/ directory at the repo root is the platform’s per-component
registry of release records. For every released version of every component
built in this repo (aurora-webapp, orbit-webapp, maxq-orbit-agent,
maxq-orbit-agent-webapp, and the registry services customer-service,
tenant-service, solution-service), there is one small release.yaml that
pins the immutable source (git tag + commit) and a reference to the
built output — a container image in Azure Container Registry.
Reference, not bytes
A release record never holds build artifacts. It points at them:
- Source is pinned by an immutable git tag (
<component>/v<semver>) and the commit SHA it resolves to. Git already stores the exact source snapshot. - Output is pinned by a container image reference —
registry / repository / tag (and optionally a
sha256:digest). ACR retains every pushed image.
Committing build output to the repo would duplicate what git and ACR already retain immutably, and would bloat history. So the record is a thin, validated YAML pointer connecting the two — the authoritative “what was released, from which commit, as which image” ledger that deployment tooling and (eventually) the Aurora portfolio view can read.
Directory layout
One directory per component, one directory per version, one record per
version. Version folders use the v<semver> form (e.g. v1.3.0), echoing
the methodology repo’s releases/v1.8/ convention.
releases/
├── release.schema.json # JSON Schema (draft 2020-12) for every release.yaml
├── aurora-webapp/
│ ├── v0.1.0/
│ │ ├── release.yaml # the record (managed by build-images.sh)
│ │ └── release-notes.md # optional human narrative
│ ├── v1.0.0/release.yaml
│ ├── v1.1.0/release.yaml
│ ├── v1.2.0/release.yaml
│ └── v1.3.0/release.yaml
├── orbit-webapp/
│ ├── v1.0.0/release.yaml
│ ├── v1.1.0/release.yaml
│ └── v1.2.0/release.yaml
├── maxq-orbit-agent/
│ ├── v1.0.0/release.yaml
│ └── v1.1.0/release.yaml
├── maxq-orbit-agent-webapp/
│ └── v1.1.0/release.yaml
├── customer-service/v0.1.0/release.yaml
├── tenant-service/v0.1.0/release.yaml
└── solution-service/v0.1.0/release.yamlAlongside release.yaml, a version directory may hold an optional
release-notes.md (human narrative). A package/ subfolder is a reserved
escape hatch for release-only material (deploy config, env template, migration
notes) — it is never created today and must never contain build output or a
source copy.
The release.yaml schema
Every record validates against releases/release.schema.json (JSON Schema
draft 2020-12, $id: https://maxqlabs.be/orbit/release.schema.json). The
schema sets additionalProperties: false at every level — unknown keys are
rejected.
House convention: release records do not carry a $schema key inside the
data file (matching solution.yaml). Validation is external — use
check-jsonschema if available, else python3 with jsonschema + pyyaml.
Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
component | string | yes | Component slug; matches the implementation/ directory name. Pattern ^[a-z][a-z0-9-]*[a-z0-9]$. |
version | string | yes | Semantic version, no leading v (e.g. 1.3.0). Pattern allows a pre-release suffix (-…). |
status | string | yes | One of draft, preview, production, superseded, rolled-back (see lifecycle below). |
released_at | string (date) | yes | Date the entry was cut, YYYY-MM-DD. Set by the operator/script, not derived. |
released_by | string (email) | yes | Person who cut/owns the release. |
previous_version | string or null | no | The version this one supersedes, or null for the first release. |
source | object | yes | The immutable input: where the code came from. |
artifact | object | conditional | Reference to the built/deployed output. Required once status is anything other than draft (enforced by an allOf/if rule). |
Status lifecycle (from the schema’s own descriptions):
| Status | Meaning |
|---|---|
draft | Cut, not yet deployed. |
preview | Deployed to a preview URL. |
production | Promoted to the production domain. |
superseded | Replaced by a newer production release. |
rolled-back | Was production, reverted. |
source fields
All three are required; additionalProperties: false.
| Field | Type | Description |
|---|---|---|
source.repo | string | Git repo the component lives in. Records written since the 2026-07-02 repo consolidation say orbit; older aurora-webapp records say aurora — historical, left as written. |
source.git_tag | string | The scoped tag component/vX.Y.Z (pattern-enforced), so multiple components in one repo tag independently. |
source.commit | string | Full or abbreviated commit SHA the tag points at (7–40 hex chars). |
artifact fields
artifact.target is required and discriminates the shape; the schema’s
conditional rules make the target-specific fields mandatory.
| Field | Type | Applies to | Description |
|---|---|---|---|
artifact.target | string enum | — | container or vercel. Everything new is container; vercel survives only for historical records. |
artifact.registry | string | container (required) | Registry login server the image was pushed to (e.g. acrorbit.azurecr.io). |
artifact.repository | string | container (required) | Image repository/name within the registry (e.g. orbit-webapp). |
artifact.tag | string | container (required) | The immutable version tag pushed (e.g. 1.2.0). The image is also pushed as latest. |
artifact.digest | string | container (optional) | Content-addressable image digest (sha256: + 64 hex) — the permanently re-deployable artifact. |
artifact.project | string | vercel (required) | Vercel project name or id. |
artifact.deployment_id | string | vercel (required) | Immutable Vercel deployment id (dpl_…). |
artifact.url | string | vercel (required) | Deployment URL returned by Vercel. |
Quote the date. released_at must be a string ('2026-06-26'). A bare
released_at: 2026-06-26 is parsed by PyYAML as a datetime.date, and schema
validation then fails on type: string. The script writes it quoted;
keep it quoted when hand-editing.
A real record
releases/aurora-webapp/v1.3.0/release.yaml, verbatim:
# release.yaml — managed by infrastructure/azure/build-images.sh.
# Schema: releases/release.schema.json
# A `container` artifact: the immutable image pushed to ACR (also tagged :latest),
# built from the git tag below.
component: aurora-webapp
version: 1.3.0
status: draft
released_at: '2026-06-26'
released_by: [email protected]
previous_version: null
source:
repo: orbit
git_tag: aurora-webapp/v1.3.0
commit: 8d214a1a67b4e155241dfea69d68ad9435c3f0b4
artifact:
target: container
registry: acrorbit.azurecr.io
repository: aurora-webapp
tag: 1.3.0The registry-service records look the same shape — e.g.
releases/customer-service/v0.1.0/release.yaml records customer-service/v0.1.0
at commit 586b6e9… as acrorbit.azurecr.io/customer-service:0.1.0.
Who writes the records: build-images.sh
Records are written (and reconciled) by
infrastructure/azure/build-images.sh — the container build worker. Its
release discipline: the image is built from an immutable git tag, not your
working tree, so the image, the recorded commit, and the git_tag can never
drift.
commit → git tag <component>/v<version> → build-images.sh <target> → deployWhat the script does per target:
- Resolves the version — an explicit
@<version>pin, or the component’s latestcomponent/v*tag (sorted by-v:refname). Dies with tag-creation instructions if the tag doesn’t exist. - Resolves the tag to a commit (dereferencing annotated tags).
- Checks the tag out into a throwaway
git worktree— your checkout is left untouched; worktrees are cleaned up on exit. - Builds server-side via
az acr build(ACR Tasks — no local Docker): the tagged tree is uploaded to ACR, built inwestcentralus, and on success pushed tagged both:<version>and:latest. - Writes/reconciles the record at
releases/<component>/v<version>/release.yamlvia an embedded Python snippet: acontainerartifact (registry/repository/tag) plus the tag’s commit, always withstatus: draft— the image exists in the registry but isn’t yet promoted to a running app; deploy flips the status later. An existing file is patched in place, preserving its comment header andprevious_version. The cut date comes from aRELEASED_ATconstant in the script (deliberately not shelled out fromdate), owner fromRELEASED_BY.
The script is idempotent: re-building a version re-pushes the image (same tag,
same code) and reconciles the existing release.yaml in place.
Build-context details worth knowing (encoded in the script’s image_meta):
orbit-webapp, orbit-agent, and the three registry services build with
context implementation/ (not their own codebase/) because their Dockerfiles
copy shared packages (shared/trajectory-loader, shared/registry-kit);
aurora and orbit-agent-webapp build from their own
implementation/<name>/codebase context.
Versioning conventions
- Per-component semver. Each component has its own version line and its own
tag cadence — components are not upgraded in lockstep. That’s why tags are
scoped (
aurora-webapp/v1.3.0,orbit-webapp/v1.2.0) rather than repo-wide. - No
alltarget.build-images.sh alldeliberately errors: building everything at one shared version doesn’t match independent versioning. A futureallshould read a platform-version manifest (e.g.releases/platform/v<X>.yaml) that pins each component’s version — not a shared version argument. - Per-repo registry.
releases/lives with the code it describes; the YAML files are the authoritative record,build-images.shthe worker that writes them.
Connection to Azure deployment
Azure Container Apps is the sole deployment target, and images are shared
per release: one image per component per version in the shared ACR
(acrorbit.azurecr.io), consumed by the singleton Aurora app and by every
per-solution Orbit stack (reader + agent + Mission Control) that the
auto-deploy provisions. After a build, the operator bumps the corresponding
images.*_tag in infrastructure/azure/config.local.yaml and runs
deploy.sh aurora (or per-solution provisioning simply picks up the new tag).
The release record is the ledger tying that deployed tag back to its exact
commit.
What was retired: the Vercel path
The Vercel deploy path ended on 2026-07-02: the
infrastructure/vercel/release.sh drive tool (with its
cut / deploy / promote / rollback / validate subcommands) was retired
and deleted along with the Vercel project. Old aurora-webapp records with
target: vercel remain in the registry as history, and the schema keeps the
vercel enum value so they stay valid. Everything new records a container
artifact.
Cutting a release, step by step
Commit and tag
Land the change on the repo, then create and push the scoped, immutable tag:
git tag aurora-webapp/v1.3.0 <commit-ish>
git push origin aurora-webapp/v1.3.0Build, push, and record
Run the build worker with one or more targets (requires an az session —
managed identity in ACA, or az login):
infrastructure/azure/build-images.sh aurora # latest aurora-webapp/v* tag
infrastructure/azure/build-images.sh [email protected] # or pin explicitly
infrastructure/azure/build-images.sh aurora orbit-webapp # multiple, each at its OWN latest tagValid targets: aurora, orbit-webapp, orbit-agent, orbit-agent-webapp,
customer-service, tenant-service, solution-service. This builds from the
tag via az acr build, pushes :<version> + :latest, and writes
releases/<component>/v<version>/release.yaml with status: draft.
Commit the record
The new/updated release.yaml is a working-tree change — commit it like any
other file.
Deploy
Bump the matching images.*_tag in infrastructure/azure/config.local.yaml
and run deploy.sh aurora; per-solution Orbit stacks pick up the new tag at
provisioning time.
Status promotion beyond draft (to preview/production, and later
superseded/rolled-back) is currently a manual edit of the record — the
Vercel-era tool that automated promotion was retired, and the container-path
scripts only ever write draft.