Crossplane XRDs, Compositions, Composition Functions, and the
airframe-application Helm chart for Dream IDP
— the service catalog a Backstage-driven Crossplane plugin turns into
self-service templates. Design lives in idp's
docs/service-catalog-design.md;
this repo is where it's actually built.
Every Bootstrap app stack now scaffolds a real cicd.yaml at onboarding — a
real self-service gap closed, 2026-09-21. NodeJSApplication/
SpringBootApplication/GoApplication/PythonApplication previously scaffolded
a src repo with no cicd.yaml at all, leaving pipeline onboarding as the one
manual, hand-authored step in an otherwise fully self-service flow — and, since
glidepath-control-plane's tenant-onboarding ApplicationSet always declares a
source reading that file, a freshly onboarded app's ArgoCD Application had
nothing to resolve until a developer added one. Each stack's own
templates/render-github-resources/cicd-yaml.yaml now commits a minimal,
build-only starter (build.agent matching the stack's own version field, no
test stage yet) alongside its other boilerplate, same create-once-then-hands-off
managementPolicies. Building this surfaced a second, separate gap: Glidepath's
schemas/cicd.schema.json build.agent enum didn't cover goVersion
1.23/1.24 or pythonVersion 3.12/3.13 at all — widened as part of the same fix
(platform-cicd-toolbox rebuilt + tag-bumped to 2026-09-21-go-python-agents).
Full write-up: Glidepath's own
ADR-0017.
InfraService's own zero-stage cicd-yaml-stub.yaml is unaffected — different,
correct use case (no source code to build).
RepositoryFile external-names follow the observed object (2026-09-24). The provider
rewrites the crossplane.io/external-name of a RepositoryFile it created to its own id
(repo:file:main); templates that rendered repo:file: made the two overwrite each
other every ~2s, each flip forcing an immediate re-reconcile (~2 GitHub calls) - about
280 calls/minute from five objects, the real cause of the repeated rate-limit
exhaustion. Every RepositoryFile template now renders the live object's annotation when
the object exists and its computed name only for new ones. Never render a different
spelling of a name the provider owns.
Scaffold files never delete, overwrite, or re-identify (2026-09-23).
The four app stacks' src-repo.yaml scaffold RepositoryFiles (Containerfile,
.gitignore, README, and the language boilerplate) no longer carry a Delete policy and
use overwriteOnCreate: false, so deleting the Kubernetes object can never remove or
clobber a developer-owned file. Previously they were reconciled with Create+Delete and
overwriteOnCreate: true, and changing a field of an already-composed object (the
2026-09-22 Dockerfile-to-Containerfile rename) put seven objects into a retry loop that
burned ~16,000 GitHub API calls/hour of a 5,000/hour budget shared with Backstage. An
existing object's file: and external-name are now pinned to what the provider reports
managing (status.atProvider.file), so a template default change only affects objects
that do not exist yet. Do not "retire" these objects by rendering nothing: a missing
object is indistinguishable from a new one, so it is recreated on the next reconcile (an
endless create/garbage-collect loop, proved live 2026-09-23). See the header comment in
each stack's templates/render-github-resources/src-repo.yaml. A composition change to a
field of an already-composed managed resource is a fleet-wide write even when its
managementPolicies has no Update - plan it as a migration, not a default change.
PythonApplication + GoApplication XRDs — third and fourth Bootstrap-tier
stacks, offline-verified, live rollout pending. xrds/pythonapplication.yaml +
compositions/pythonapplication/ and xrds/goapplication.yaml +
compositions/goapplication/ — structural ports of NodeJSApplication, built after
the TektonCICD extraction so both are simpler than SpringBootApplication was:
CI/CD onboarding is composed via a TektonCICD child, not carried inline.
PythonApplication adds spec.pythonVersion/spec.packageManager (pip/poetry/uv,
branching the dependency-file shape and Containerfile install step); GoApplication
adds only spec.goVersion (no package-manager equivalent - Go has one real
toolchain) and derives its module path from the real repo URL rather than taking it
as input. Both boilerplates keep the same zero-external-dependency philosophy
NodeJSApplication's own scaffold uses (stdlib http/http.server/net/http, no
framework); GoApplication's Containerfile is genuinely multi-stage, matching
SpringBootApplication's own precedent. The generated main.py/main.go/go.mod
were validated against the real Python/Go toolchains (py_compile, go build), not
just rendered - this also caught and fixed a pre-existing double-escaping bug
(\\n instead of \n in the Go-template printf argument piped through toJson)
before it could be copied into two more templates; NodeJSApplication's own
index.js carries the same latent bug today, cosmetic-only (affects only the /
route's trailing newline, not health checks or CI/CD status), not yet fixed there.
TektonCICD XRD + Composition — CI/CD onboarding extracted out of
NodeJSApplication/SpringBootApplication into its own XRD, live-verified on
checkout-api/nodejs-demo-app/order-api 2026-08-25. xrds/tektoncicd.yaml + compositions/tektoncicd/ replace the
~150 lines of near-verbatim-duplicated devCluster-gate/identity.yaml-commit/status
logic each of the two app XRDs carried (flagged while discussing which stack to add
next — a third stack would have copied it a third time). Named for the actual backend
(platform-cicd's Tekton/PaC pipeline), not a generic abstraction — a future
alternate CI/CD backend gets its own separate XRD, not a backend field on this one.
Composed as a normal child XR directly inside each app XRD's own Composition pipeline
(a new pattern for this catalog — every prior "one catalog XRD from within another"
relationship, e.g. SecretStore, goes through a GitOps xr-requests commit instead;
that indirection exists to let a resource land on a remote target cluster, which
TektonCICD never needs since it only calls the GitHub API). Each parent's own
cicd-onboarding-status step now proxies DevClusterReady/CicdOnboarded from the
composed child's own conditions, preserving the pre-extraction UX
(kubectl describe nodejsapplication/<app> still shows both directly on the app).
Rollout is two separate Airframe tags, not one: a managementPolicies
safety fix (excluding Delete from the identity.yaml RepositoryFile, v0.3.43,
already live-verified on all three onboarded apps) landed first, so the second tag's
orphaning of the old composition-resource-name deletes only the k8s CR, not the real
GitHub file — see idp/docs/service-catalog-design.md Item 1/2 for the full writeup.
SpringBootApplication XRD + Composition — Item 1/2's second Bootstrap-tier stack,
done, live-verified end-to-end on the dev cluster 2026-08-24. xrds/springbootapplication.yaml
compositions/springbootapplication/— a structural port ofNodeJSApplicationonto Java/Spring Boot: same devCluster-gated onboarding mechanism, same six-fieldidentity.yaml, sameCicdOnboarded-not-Readystatus convention. Stack-specific pieces:spec.javaVersion/spec.buildTool(replacingnodeVersion/packageManager) andspec.groupId(Maven groupId / Gradle group, and this app's single flat Java package — see the XRD's own header for why it's notgroupId+artifactId nested). The Containerfile is genuinely multi-stage (JDK build stage, bare JRE runtime stage), a real difference fromNodeJSApplication's single-stage script, not just a style choice. Live-verified via a throwawayspringbootapp-verify-testapp (realRepository/RepositoryFilecreation against GitHub,pom.xml/identity.yaml/xr-requests/secretstore.yamlcontent confirmed via the GitHub API,DevClusterReady/CicdOnboarded/Readyall reachedTrue); thegradlebuildToolbranch was exercised viacrossplane renderonly (example/xr-gradle-custom.yaml), not a real cluster apply. Seeidp/docs/service-catalog-design.mdItem 1/2 for the full writeup, including a revisited-and-declined "shared Function" refactor decision now that both stacks are built.
SecretStore XRD + infisical-secretstore-operator — Item 8, done, live-verified
end-to-end 2026-08-17. xrds/secretstore.yaml + compositions/secretstore/
originally rendered an InfisicalProject CR reconciled by a purpose-built kopf/Python
operator (operators/infisical-secretstore-operator/, since retired and removed - it
was replaced by provider-infisical, see hangar/docs/kind-prod-infisical-migration-plan.md;
the source is in git history at tag v0.3.81 and earlier) and an ESO ClusterSecretStore
wrapped in a provider-kubernetes Object (Crossplane v2 rejects composing a
cluster-scoped resource directly from this namespaced XR - same fix
NodeJSApplication's provider-github already needed). Full chain live-proven on
the dev cluster: real Infisical project + identity + Universal Auth credentials → a
Ready: True ClusterSecretStore → a real secret written via Infisical's API →
pulled by a real ExternalSecret into a real K8s Secret with the correct value.
Also fixed a real, pre-existing bug this surfaced in airframe-application's own
ExternalSecret template (remoteRef.property broke every pull against
Infisical).
Kubernetes Auth swap + full multi-cluster auto-provisioning (Item 8's multi-cluster revision) — done, live-verified end-to-end on both the dev and prod clusters 2026-08-17. Two real, separate builds, same day:
- Every
InfisicalProjectidentity now authenticates via Kubernetes Auth by default (noclientId/clientSecretminted or persisted anywhere - ESO's own controller SA token, verified live via TokenReview) instead of the original Universal Auth. Four real bugs found and fixed live getting this working the first time:kubernetesHostneeds the fully-qualified in-cluster DNS name (a raw c-ares query, no search-domain expansion); Infisical's SSRF guard blocks private IPs unlessALLOW_INTERNAL_IP_CONNECTIONS=true;ensure_project_membershipwas never actually idempotent (a real 400 on retry, contrary to the original, untested comment);_session's ownContent-Type: application/jsondefault broke every empty-bodyDELETEcall (Fastify rejects it, surfaced as an unhelpful 500). SecretStore's auto-provisioning (the "Not yet built" item above) is real now, with a real design change along the way:environmentSlug(already on the XRD) now has TWO modes -"shared"(the default, original behavior) or any other value, which creates ONE additional Infisical environment inside the already-existing project plus a SEPARATEClusterSecretStorenarrowed to exactly one namespace - real per-environment isolation (proven live: a correctly-scopedExternalSecretpulls the right value, a wrong-namespace one hard-fails), not asecretsPathconvention.airframe-application's own chart (templates/attached/secretstore.yaml, always-on likeNetworkPolicy) is what actually triggers this now, notApplicationEnvironment's Composition directly -ApplicationEnvironmentandNodeJSApplicationare bothprovider-github-only, neither can create a native resource on any cluster, including the dev cluster's own. NewInfisicalEnvironmentCRD (same operator) ensures one environment exists in an already-existing project. Upper-cluster stores (prod) use Universal Auth, not Kubernetes Auth - Infisical only runs on the dev cluster, and validating a token from a different cluster needs Gateway mode (Enterprise-only) - a real, deliberate, user-confirmed tradeoff, not an oversight. Proven genuinely cross-cluster, not simulated: a real secret written into Infisical on the dev cluster, pulled by a realExternalSecreton the prod cluster, over a live-verified NodePort path (infisical-nodeport.yaml) - a local-sandbox stand-in for a real routable endpoint between genuinely separate clusters, flagged as such, not assumed reusable as-is. One more real bug caught live: Crossplane's own core controller had no RBAC for the newinfisicalenvironmentsCRD on either cluster -native-resources-rbac.yamlneeded extending on both.
See idp/docs/service-catalog-design.md Item 8 for the full design writeup. (The operator-level
detail lived in the removed operators/infisical-secretstore-operator/README.md; recover it
from git history at v0.3.81 or earlier if needed.)
SecretStore provisioning moved to the Bootstrap XRs (xr-requests), off
airframe-application's chart — done, live-verified against real existing apps on both
clusters 2026-08-18. Real user objection to the above: an always-on chart
template meant secrets infrastructure only existed once someone shipped an actual
release, not when an app/env was onboarded. NodeJSApplication and
ApplicationEnvironment now each commit a SecretStore XR manifest via
xr-requests instead (airframe-application's attached/secretstore.yaml deleted
entirely) - real nuance recorded in the design doc: NodeJSApplication could have
composed it directly (same-cluster), ApplicationEnvironment structurally can't
(cross-cluster, the same "no cluster holds another's API credential" constraint
that already kept AppProject ownership out of direct composition). The prod cluster
needed its own xr-requests mechanism for the first time (Bootstrap XRs never ran
on upper clusters before). Also fixed the same day: external-secret.yaml never
actually routed secrets through the per-environment stores the prior revision
built - every secret silently still went through the old shared-store/path
convention. Fixed via ESO's real per-entry sourceRef.storeRef override.
See idp/docs/service-catalog-design.md Item 8's third revision for the full
writeup, including two real bugs found live from the old and new mechanisms
briefly coexisting mid-migration (not flaws in either design on its own).
AI-triage mechanism (Phase 2, first slice) — done, live-verified
2026-08-13. functions/function-rollout-watcher + functions/diagnosis-holmes-dispatch:
a Crossplane Composition Function that watches an Argo Rollout and, on
Degraded, dispatches a diagnosis request to a shared HolmesGPT service
instead of running a bespoke per-app Claude agent. Extracted and redesigned
from the ai-rollout prototype
(left untouched as a standalone demo). Proven end-to-end on a fresh
dev cluster: a real broken canary → real diagnosis → real fix PR,
jfillman/idp#8. See each
function's own README for the full detail.
airframe-application Helm chart — built, and as of 2026-08-13 live-verified
end-to-end on a real cluster, not just helm lint/helm template. Renders
§3's full schema (Argo Rollout, Service, ConfigMaps, ExternalSecret, PVCs, HPA,
PodDisruptionBudget, NetworkPolicy, AnalysisTemplates, components:/slos: as
generic Crossplane XRs) plus real resource-coverage/simplification passes
beyond §3: ServiceAccount, jobs:/cronJobs:, ServiceMonitor,
extraManifests:, configMap/secret consumption modes, simplified NetworkPolicy
rules. Live-verification pass: the dev cluster rebuilt from scratch under real
GitOps management (see gitops-cluster-dev), widget-api migrated onto this
chart for real off the standalone ai-rollout prototype's own Composition,
full test pass against real cluster state (NetworkPolicy enforcement,
ServiceMonitor scraping, a real canary rollout with a Prometheus-backed
AnalysisRun, checksum-triggered revisions) - found and fixed two real bugs
(rollout.canaryAnalysis didn't exist at all; analysisTemplates: silently
dropped args:). See charts/airframe-application/README.md for the full detail
of every pass, including the earlier fixture-only bugs (an envName/env
naming collision, Sprig's default silently discarding explicit false/0).
The ingress-controller namespace selector is resolved too now (Contour,
confirmed live) - remaining open items: attached-resource API group, who
provisions the image-pull Secret.
NodeJSApplication XRD + Composition — first Bootstrap-tier XRD, built and
live-verified on the dev cluster 2026-08-13/14. Item 1/2's design: given an app name,
pure provider-github commits a src repo + Node.js boilerplate (Containerfile,
package.json, index.js, README — npm/pnpm/yarn all covered), an empty,
scaffolded gitops-<appName> repo (its real <cluster>/<env>/values.yaml layout
gets populated once ApplicationEnvironment exists, immediately below), and a
tenants/<appName>/app.yaml entry in gitops-cluster-dev-tenants (read by that
repo's tenant-appprojects ApplicationSet to build the app's AppProject).
Deliberately narrow — no upper-env provisioning, no real CICD pipeline (this
platform's control plane isn't running on the dev cluster yet, a separate migration
task) — the Composition reports that gap as a real CicdOnboarded: False custom
condition rather than silently skipping it. Three real, load-bearing corrections
found only by building this for real: GitHub Apps can't create repos under a
personal (non-Org) account at all — a classic PAT (repo+delete_repo scopes)
replaces the originally-planned GitHub App reuse; Crossplane v2 rejects a
namespaced XR composing a Cluster-scoped managed resource, so provider-github's
namespaced repo.github.m.upbound.io family is required, not its Cluster-scoped
sibling; function-go-templating reserves Ready/Healthy/Synced and errors if
a Composition patches them directly — the custom-condition mechanism above is the
only supported way to express "succeeded except X."
ApplicationEnvironment XRD + Composition — second Bootstrap-tier XRD, built
2026-08-15, extended the same day for real multi-cluster support once a second
cluster (prod) existed to build and live-verify against. Item 3's design:
given an already-onboarded app (NodeJSApplication) and an env
(dev/staging/prod, enum-constrained), pure provider-github commits into the
app's own gitops-<appName> repo and a target cluster's own
gitops-cluster-<cluster>-tenants — no live cluster credential. Initial
values.yaml is an identity-only stub (rollout: null) since this platform's CICD
control plane isn't running yet — surfaced as a WorkloadDeployed: False custom
condition (direct copy of NodeJSApplication's CicdOnboarded mechanism).
cluster is a required, real spec field, not a hardcoded constant — gated live
against a new cluster registry (gitops-cluster-dev/00-bootstrap/cluster-registry/,
a ConfigMap per cluster, cluster-admin-authored) via a real Crossplane
extra-resources lookup, the first actual use of that mechanism in this catalog
(function-go-templating natively supports it — confirmed against its own v0.12.3
docs, no custom function needed). Must resolve to type: upper +
crossplaneReady: "true" or the Composition creates nothing and reports
ClusterReady: False instead — type: dev is a structural rejection (dev envs
belong to the separate platform/envs/-live-read mechanism, per gitops-strategy.md
§10). Also seeds the target cluster's own tenants/<appName>/app.yaml (§6 scopes
AppProject per cluster), with managementPolicies excluding "Delete" so one
env's teardown never deletes a file a sibling env on the same cluster still needs —
not spec.deletionPolicy: Orphan, a real live gotcha:
provider-upjet-github v0.19.1's RepositoryFile CRD has no such field at all
(confirmed via a real ReconcileError + kubectl explain, this provider version
uses managementPolicies exclusively).
env opened up from a closed enum to team-chosen names, live-verified 2026-08-15.
Was enum: ["dev", "staging", "prod"]; traced every real consumer (this Composition's
own template, the tenant-onboarding ApplicationSet, the AppProject's own
destinations wildcard) and confirmed nothing branches on the specific value — pure
path/name interpolation, not encoded business logic. Replaced with a pattern
matching Kubernetes' own DNS-1123 namespace-label rule plus maxLength: 20, so an
invalid value still fails at XR admission instead of downstream. Confirmed live on
the dev cluster: a previously-impossible custom name (perf-test) reconciles end-to-end
for real; an invalid one (Staging!) is rejected at admission with a clear
pattern-mismatch error.
Live-verified end-to-end on a real second cluster: the prod cluster bootstrapped for
real (gitops-cluster-prod, reusing its pre-existing ArgoCD instance rather
than standing up a second one — see that repo's own README), a scoped Crossplane
install (core + provider-kubernetes + function-go-templating/
function-auto-ready + just the SLO XRD — not provider-github, not
NodeJSApplication/ApplicationEnvironment, which stay dev-cluster-only
permanently). Both the rejection path (crossplaneReady: "false" → ClusterReady: False, zero resources) and the success path (real commits, the prod cluster's own
ArgoCD picking up the new tenant unprompted, a real namespace/ServiceAccount/
NetworkPolicy) proven live with a throwaway app, fully torn down after — including
confirming the orphaned app.yaml really does survive the env XR's own deletion, as
designed. See idp/docs/service-catalog-design.md §0 for the full architecture.
AppProject/Application deletion-ordering bug — fixed via protection. crossplane.io Usage, live-verified v0.3.2. A real, twice-confirmed bug: deleting
a NodeJSApplication while an ApplicationEnvironment still referenced it (via
spec.appName) could strand the dependent ArgoCD Application if its AppProject
got pruned before that Application's own finalizer finished. Originally planned as a
homegrown extra-resources lookup on NodeJSApplication's own Composition — superseded
before building it: kubectl api-resources on the dev cluster confirmed Crossplane
already ships a real primitive for exactly this, protection.crossplane.io/v1beta1
Usage ("defines a deletion blocking relationship between two resources"), enforced
by an already-installed crossplane-no-usages admission webhook (nothing new to
deploy). ApplicationEnvironment's Composition now composes one unconditionally
(spec.of = the parent NodeJSApplication, spec.by = itself) —
NodeJSApplication's own Composition needed zero changes, since the webhook and
Usage controller do all the blocking purely by watching Usage objects. Live-verified
end-to-end on the dev cluster with a real throwaway app+env: confirmed the Usage object
and the crossplane.io/in-use label it drives on the app, confirmed kubectl delete
on the app is cleanly rejected at admission time (not a finalizer hang) while the env
exists, confirmed the Usage is garbage-collected the moment the env is deleted, and
confirmed app deletion then succeeds. One real unknown resolved live along the way:
function-auto-ready's handling of a composed Usage resource hadn't been exercised
in this catalog before — confirmed it reports Ready: True correctly, no stuck
parent readiness.
SLO XRD + Composition — first XRD in the catalog, live-verified on
the dev cluster. Item 4's design (idp/docs/service-catalog-design.md), wraps
Sloth (sloth.dev) rather than hand-rolling multi-window-multi-burn-rate
PromQL: one SLO XRD (catalog.hangar.io/v1alpha1, Crossplane v2 namespaced,
just environmentRef/service/objective/indicator - no burn-rate or
window fields, Sloth computes the full canonical pattern itself) whose
Composition (function-go-templating, source: Inline - see
compositions/slo/build-composition.sh) renders a Sloth
PrometheusServiceLevel (Sloth's own controller, installed via
gitops-cluster-dev/10-crds-operators/sloth/, does the
spec→PrometheusRule translation) and a Grafana dashboard ConfigMap
(kube-prometheus-stack sidecar convention). indicator.type: availability|latency both covered.
Went through a hand-rolled revision first (matching a kube-slo-style article
the user found, own PromQL/burn-rate math, no Sloth dependency) before
switching - both were live-verified independently; see
idp/docs/service-catalog-design.md Item 4's revision history for the
reasoning. Real bugs found live along the way, worth knowing before touching
these files (full detail in the Composition/template files' own header
comments):
- A literal
<< >>mention inside a plain YAML comment collides with the templating engine's own delimiter scan and breaks the whole render - the lexer doesn't know about YAML comment syntax. Standing gotcha called out in every template file's header now. - Crossplane's controller
ServiceAccounthas no built-in RBAC for native resource kinds a Composition composes directly - needed extendingnative-resources-rbac.yamlforPrometheusServiceLevel(and, during the hand-rolled revision,PrometheusRule). - A second
Functionobject pointing at the same package as an already-installed one doesn't just mark itself unhealthy - it corrupts Crossplane v2.3.4's package-manager dependency-lock graph for every OTHER Function on the cluster. Tried giving the SLO Composition its own dedicatedfunction-go-templating-sloFunction + mount to keep its templates isolated from the ai-rollout Application Composition's own/templatesmount; on a freshly-bootstrapped cluster this leftfunction-auto-readywith no runtime Deployment at all, silently degrading the unrelatedwidget-apiRollout's Ready-status reporting. Fixed by switching tosource: Inlineinstead (templates embedded directly in the Composition, generated fromtemplates/*.yamlviabuild-composition.sh)- reuses the one shared
function-go-templatingFunction, no new registration, no collision, and the templates are now fully GitOps-tracked in the Composition itself instead of a live-cluster-only ConfigMap.
- reuses the one shared
- Sloth's own CRD claims
slo/slosascategories(not shortNames) onprometheusservicelevels- this XRD's ownsloshortName collided with that (kubectl get sloresolved to Sloth's category and 404'd looking for the wrong resource type). Removed; usekubectl get slos.catalog.hangar.ioor plainslos(this resource's actual plural, unambiguous). - kube-prometheus-stack's
PrometheusCR only loads aPrometheusRuleif the object itself carriesrelease: kube-prometheus-stack- Sloth's ownextraLabelsHelm value only stamps that onto each individual generated rule'slabels:, not the object'smetadata.labels. Fixed by having the Composition stampmetadata.labelson thePrometheusServiceLevelit generates; Sloth propagates that onto thePrometheusRuleit creates.
GitOps wiring — done, live-verified 2026-08-13.
gitops-cluster-dev/20-service-catalog/idp-service-catalog/application.yaml
(directory name unchanged in that repo — gitops-cluster-dev wasn't part of this
rebrand, only the repoURL/path fields inside it that point at this repo were
updated, in apron, not here) pins this repo to git tag v0.1.0 via a directory-source Application (same
pattern already proven for 10-crds-operators/40-observability), syncing
xrds/*.yaml + compositions/*/composition.yaml only. charts/airframe-application
stays un-synced here — it's rendered per app-release into gitops-<app-name>
repos, not installed cluster-wide — and functions/'s packages stay
registered by pinned OCI tag in 10-crds-operators/crossplane/functions.yaml,
not synced as source directories. Replaces the manual kubectl apply
verification path used until now.
Not started yet: the rest of the v1 XRD catalog (SpringBootApplication and the
Component XRDs — Redis, OAuthServer, ...) and their Compositions. Also not built:
the ClusterAnalysisTemplate golden-path library and argocd-cm Rollout
health-check config (§3 says these belong in idp-cluster-baseline), and a real
platform default canary step sequence (§3 "Still open" item 3 — the chart ships a
deliberately inert placeholder in the meantime, see its README). The rest of
airframe-application's own coverage (Rollout/Service/etc.) remains fixture-only, not
live-verified against a real ApplicationEnvironment-provisioned env with a real
rollout.image set yet — the live-verification pass above used a direct helm install, not a real ApplicationEnvironment XR (that XRD didn't exist yet at the
time).
functions/
function-rollout-watcher/ Composition Function: watches Rollout, dispatches diagnosis
diagnosis-holmes-dispatch/ Thin Job: hands the investigation off to HolmesGPT
charts/
airframe-application/ §3's Embedded+Attached tier chart - one release per (app, cluster, env)
(tests/run.sh: render tests; no workload until rollout.image is set)
tools/
build-compositions regenerates every composition.yaml's inline template blocks from templates/ (--check in CI)
test_compositions.py renders every composition's example/cases.yaml through the real functions and diffs the result with example/expected/ (CI)
airframe-validate strict values-file check (unknown keys, XRD component specs) + helm template
--xr: XR requests against the XRD schemas, unknown fields rejected (AF-XR)
validate.Containerfile the image CI runs it from (ghcr.io/jfillman/airframe-validate:<tag>, built on release tags)
AGENTS.md what an AI agent should know, today vs planned
xrds/
nodejsapplication.yaml NodeJSApplication XRD (catalog.hangar.io/v1alpha1)
applicationenvironment.yaml ApplicationEnvironment XRD (catalog.hangar.io/v1alpha1)
slo.yaml SLO XRD (catalog.hangar.io/v1alpha1)
compositions/
_shared/ the four templates every application composition links to instead of copying (see its README)
nodejsapplication/ NodeJSApplication Composition (source: Inline) + templates (built by tools/build-compositions)
applicationenvironment/ ApplicationEnvironment Composition (source: Inline) + templates (built by tools/build-compositions)
slo/ SLO Composition (source: Inline) + templates (built by tools/build-compositions)