Docs · Glidepath
ADR-0023: Per-app chart through deploy.chart
Status: Accepted (2026-10-06). Slice 1 built and live (2026-10-07: cluster-defaults.yaml per cluster; the values gate follows it). Slice 2 live (2026-10-07: schema and validator). Slice 3 live (2026-10-07: the Ground ApplicationSet is rendered by glidepath-app and resolves deploy.chart/environments[].chart; it took over each live ApplicationSet in place; the deploy RBAC comes from charts/glidepath-env in each Ground environment’s Application). Requirement R6 of
envs-overhaul-requirements.md; phase 8.
Context
ADR-0018 made the chart that
renders an app’s environments per-app configuration, with
chart-contract.md as the interface. Today nothing can choose it:
every consumer hardcodes airframe/charts/airframe-application at a tag.
| Renders | Where the pin lives | What feeds that generator |
|---|---|---|
| Ground environments | lower-envs ApplicationSet in gitops-cluster-dev’s tenant chart (and apron’s copy) |
the app repo’s env files |
| Flight environments | tenant-onboarding ApplicationSet in each gitops-cluster-<name> (twice per file: template and templatePatch) |
tenants/<app>/<env>/identity.yaml in the cluster’s tenants repo |
| PR previews | glidepath-app idpServiceCatalog.chartVersion |
the app’s cicd.yaml (a values file of glidepath-app) |
The values gate |
valuesValidatorImage (the validator carries its own chart copy) |
none |
The pins already drift: on 2026-10-06 the values gate validated against
airframe-application v0.3.97 while every environment rendered v0.3.118. None of the
ApplicationSets reads the app’s cicd.yaml, so a deploy.chart field has nowhere to be
read.
Decision
The field
deploy:
chart: # app-wide; unset = the cluster default
repoURL: https://github.com/<owner>/<repo>
path: charts/<chart> # git source; or `chart:` for a Helm/OCI registry
targetRevision: v1.2.3
environments:
- name: dev
tier: ground
chart: { targetRevision: v1.2.4 } # per environment, merged over deploy.chart
Precedence, highest first: the environment’s chart, then deploy.chart, then the
cluster default. A per-environment value sets only the fields it changes (the same rule as
the per-environment cloud overrides). Only deploy.target: k8s-rollout has a chart.
Ground: the lower-envs ApplicationSet moves into glidepath-app (option B)
glidepath-app is already rendered per app with the app’s cicd.yaml as a Helm values
file; that is how the PR-preview ApplicationSet works. The lower-envs ApplicationSet moves
there, and its chart source becomes plain Helm templating: the environment’s chart
merged over deploy.chart merged over the cluster default, resolved per environment when
the chart renders. No ArgoCD generator reads cicd.yaml.
This also finishes ADR-0018’s ownership move: the environments machinery is Glidepath’s, not each cluster repo’s.
Flight: the chart is recorded in identity.yaml (option C)
The tenant-onboarding ApplicationSet already reads tenants/<app>/<env>/identity.yaml. It
gains an optional chart block there, and the template uses it with the cluster default
as fallback (default), so a missing or partial block renders today’s chart.
ApplicationEnvironment writes it when the environment is created; when deploy.chart
(or that environment’s chart) changes in cicd.yaml, Glidepath opens a PR on the
tenants repo updating it, the same two-PR pattern as adding a Flight environment
(ADR-0019). A chart change to a Flight
environment is therefore approval-gated like a release.
Amended 2026-10-08. The sync PR edits spec.chart on the environment’s
ApplicationEnvironment request (gitops-cluster-dev-tenants, tenants/<app>/xr-requests/), and
Airframe’s Composition renders it into identity.yaml (airframe#53, v0.3.119). The first version
edited identity.yaml directly; the Composition owns that file (its RepositoryFile includes the
Update policy) and reverted the edit 30 minutes after it merged (gate-api staging). With the
chart on the request there is one writer. On a cluster other than Airframe’s chart repo, the
chart’s repository must also be in that cluster’s tenant AppProject allowedChartRepos
(gitops-cluster-prod, 02-argocd-apps/tenant-appprojects/chart/values.yaml).
One default per cluster, and the gate follows the chart
Each cluster’s default chart reference lives in one place, read by both ApplicationSets
and by glidepath-app. The values gate validates against the chart that environment
will render: the default validator image is bumped in the same change as the default
chart, and an app with its own chart gets a plain helm template with that chart (Airframe’s
strict keys and component checks do not apply to a chart they do not describe). Tower shows
the raw YAML editor instead of the values form for such an app; the form is generated from
Airframe’s schema.
Deploy RBAC moves with the environment
glidepath-app’s deploy RBAC (the pipeline runner’s Role and RoleBinding in
app-<app>-<env>) is rendered by the <app>-cicd Application, but the namespace it targets
is created by that environment’s own Application, which first syncs minutes later on a new
app. Until then every <app>-cicd sync fails with “error getting namespace” and ArgoCD
retries (seen on flight-attendant, 2026-10-06; it recovered on the second retry). In slice 3
the RBAC is rendered by the environment’s Application instead, in the same sync as its
namespace, so the order cannot race. No retry tuning is added in the meantime.
Alternatives considered
A. A matrix generator that also reads the app’s cicd.yaml. The smallest change, but a
cicd.yaml that fails to parse or is removed makes the generator return zero elements, and
ArgoCD then deletes every environment’s Application, with the resources finalizer deleting
the workloads. Rejected for that failure mode.
Writing the Flight chart into the gitops repo (a chart.yaml beside values.yaml,
read by a second generator) has the same zero-element failure when the file is missing.
Procedures
Upgrading the cluster default (all apps that do not pin)
- Cut the Airframe tag; CI publishes the chart and a validator image of the same tag.
- Offline: render every live env file (Ground and Flight) on the old and the new chart
(
helm template, YAML-parse, diff), and review the per-app diffs. Treat “identical” as proven only when each render produced objects. - Canary (below) on one Ground environment, then one Flight environment.
- One PR per cluster bumps the default chart and the validator image together.
- Watch: every Application Synced, every Rollout Healthy with new pods. A green sync is not proof the workload came up.
- Rollback is reverting that PR.
Apps that pin their own chart do not move. Tower lists which apps pin what, so a pin cannot go stale unnoticed.
Canary
A canary is the override, used by the platform team: one Ground environment gets a
per-environment chart (a cicd.yaml PR), then one Flight environment (an identity.yaml
PR on the tenants repo, approval-gated). After both are healthy, the default moves (step 4)
and the canary overrides are removed. Reverting a canary is reverting its PR.
A chart canary is independent of an Argo Rollouts canary of the app’s image; a chart change reaches pods through the Rollout’s own strategy like any other spec change.
Consequences
- Moving the lower-envs ApplicationSet must not delete running environments: the old
ApplicationSet’s Applications are adopted by the new one. Same care as known-gap #29:
preserveResourcesOnDeletionfirst as its own change, verified, then the swap, on one app first. identity.yamlgains a field, rendered only by theApplicationEnvironmentComposition from the request’sspec.chart, which Glidepath’s sync PR sets (amended 2026-10-08: the first design had two writers, and the Composition’s reconcile reverted the other one).cicd.schema.jsongainsdeploy.chartandenvironments[].chart: a toolbox rebuild and atoolboxImagebump in three values files.- A custom chart must pass chart-contract.md’s conformance checklist; Glidepath does not
check that beyond the render in the
valuesgate.
Build order
| Slice | What | Verifiable by |
|---|---|---|
| 1 | One default chart reference per cluster; validator image bumped with it (fixes the v0.3.97 drift) | All consumers render the same tag; the gate validates against it |
| 2 | Schema: deploy.chart, environments[].chart |
Toolbox rebuilt and bumped; validator accepts and rejects the right shapes |
| 3 | Lower-envs ApplicationSet in glidepath-app, adopting existing Applications; the deploy RBAC (templates/env/deploy-rbac.yaml) moves into each Ground environment’s Application, beside the namespace it targets |
Every live Ground Application byte-identical before and after (dry-run, then live); a fresh onboarding syncs <app>-cicd first time, with no “error getting namespace” retries |
| 4 | identity.yaml chart + template default; Glidepath sync PR |
A Flight canary on one app renders the pinned chart; others unchanged |
| 5 | values gate and Tower for a non-Airframe chart |
A conformant scratch chart deploys to a Ground environment and passes the gate |