Skip to main content

Customization Ladder — catalog operations

The framework, the rung definitions, and the decision procedure live in gentian-os/docs/guides/app-customization.md. Read that first. This page is the gentian-apps operational half: where files go, what CI checks, and how to characterize a new catalog entry.

The ladder in one table​

RungNameYou ownWhere it lives in this repo
L0Configurenothingprofiles/<n>/profile.yaml → spec.extraValues
L1Drop-inone fileprofiles/<n>/dropins/
L2Companiona separate deployableapps/<new>/ + contracts/<c>.yaml + profiles/<new>/
L3Extensionan addon the app loadsthe app's addon repo (odoo-modules, …)
L4Repackagechart / composition / entrypointcharts/<app>/, profiles/<n>/composition.yaml
L5Patcha patch series + rebuilt imagethe app's build repo (ocb, …)
L6Forkthe source treea dedicated fork repo
XHotfix—forbidden — see app-profile-guide.md

Pick the lowest rung that can express the change, and the narrowest scope. Scope is a separate axis: tenant (gentian-deployments) → profile (here) → platform (gentian-os, generic mechanisms only).

Profile bundle layout​

A bundle is any directory containing a profile.yaml — that file is what the catalog ApplicationSet matches, and the Application is named after the profile's metadata.name rather than the directory. Directory names are therefore free to be short: an addon lives at profiles/odoo/addons/crm-ce/ while its profile is still odoo-crm-ce. Never locate a bundle by counting path segments.

Families with addons use base/, addons/ and packages/; families without L3 have no third level, just profiles/<family>/<name>/.

profiles/<family>/[base|addons|packages/]<name>/
├── profile.yaml # the marker; also carries spec.customization
├── kustomization.yaml # required — the bundle is rendered with kustomize
├── customization.md # the app's ladder in prose, incl. the rubric score
├── dropins/ # L1 content shipped with the profile (50-89 prefixes)
└── customizations/ # Customization records (CRs) for deltas at L2+

Charts and images are deliberately not in here — charts/<name>/ and images/<name>/ are separate flat trees, because a chart is referenced by OCI coordinate rather than path and is frequently shared (charts/odoo backs 10 profiles). See app-profile-guide.md §2.

Characterizing a new app​

  1. Score the §4.1 rubric by hand (8 criteria, +1 each) — grades are manual for v0.4; automation is gentian-os roadmap item 2.13.
  2. Band it: A ≥ 7 · B 5–6 · C 3–4 · D ≤ 2.
  3. Write customization.md recording the score, the evidence, and per-rung instructions.
  4. Fill spec.customization in profile.yaml. supportedRungs must reflect what the app really offers — never list L2, it is always available and is a property of the customization, not of the target.
  5. An app with no spec.customization is treated as {grade: unknown, supportedRungs: [L0, L4]}. That is a deliberate floor, not a default to leave in place.

Customization records​

Required from L2 upward, written before the code. Customization is a namespaced CRD (gentianos.io/v1alpha1); records are authored here in git and synced to clusters like any other catalog object. The operator computes status — review overdue, upstream stale, version drift, cheaper rung available — which is what the Admin Console debt report reads.

Validate locally:

python3 scripts/validate-customizations.py
python3 scripts/customization-debt-report.py --format markdown

What CI enforces​

CheckFailure means
every profiles/*/profile.yaml has spec.customizationapp not characterized
grade matches rubricScore bandingscoring and grade disagree
supportedRungs does not contain L2L2 is not a property of the target
declared dropIns[].path is absolute; tenantEditable entries have a formattenants could get an unvalidatable mount
every customizations/*.yaml parses and satisfies the ladder policyinvalid record
reviewBy is in the futurecustomization debt past review
rung ≥ L4 records upstreamFirst.attempted: trueupstream-first obligation unmet
rung ≥ L3 names at least one artifactrecord describes nothing
justification present for every rung below the chosen onethe ladder was not actually walked