Docs · Glidepath
Onboarding mechanics
How a repo actually gets from “nothing” to “a real, ArgoCD-managed CI/CD Application” - the operator-facing counterpart to ../user/install-guide.md.
cicd.yaml is the app chart’s own values file: every resource the platform creates
for an Application conditionally renders off what cicd.yaml declares (see
charts/glidepath-app/templates/_helpers.tpl’s glidepath-app.hasStage).
Onboarding is ArgoCD-managed end to end. An ApplicationSet
(charts/glidepath-control-plane/templates/argocd/tenant-onboarding-applicationset.yaml)
generates one Application per tenant from a git files generator scanning
tenants/*/identity.yaml. Onboarding a new Application, or changing an existing one’s
platformIdentity, is “add/edit a file in tenants/, get it reviewed” - never a manual
CLI command.
One-time platform setup
The control plane and catalog charts are cluster-wide singletons, installed once per cluster - see installation.md.
Onboarding one Application
-
Install the platform’s GitHub App on the app’s repo - the one manual, GitHub-side prerequisite. The Pipelines-as-Code
RepositoryCR itself is rendered by the chart (charts/glidepath-app/templates/pipelines-as-code/repository.yaml, gated onplatformIdentity.registerPipelinesAsCode, defaulttrue) - not a separate manual step.If
cicd.yamlwill declare areleasestage, the App also needs installing on thegitops-<app-name>repo (release.md step 3) - a second, easy-to-miss repo, not covered by installing on the app repo alone. This bites private gitops repos specifically: a GitHub App installed with “selected repositories” doesn’t automatically gain access to a new repo regardless of its visibility, anddeliver-onboarding-files’s push to that repo fails outright until the App is explicitly added there. Confirmed live 2026-08-22 against a real privategitops-checkout-apirepo. -
The developer adds
cicd.yamlto their repo root. Thetenant-onboardingApplicationSetreads it live from there - a push tomaintakes effect on ArgoCD’s next sync, with no separate copy to keep up to date. -
An operator adds
tenants/<app-name>/identity.yamlto this repo (via a reviewed PR, never the app repo) - the handful of infra-identity values that aren’t pipeline configuration, soschemas/cicd.schema.jsonnever carries them and a developer’s own commit can never influence them (they flow into governance policy-config lookups, secret resolution, and whichgitops-<app>repo a release PR targets):platformIdentity: appName: <app-name> type: app # or infra - see naming-conventions.md gitopsRepoUrl: https://github.com/<org>/gitops-<app-name> # "" if no release stage appRepoUrl: https://github.com/<org>/<app-name> # always required githubOwner: <org> catalogNamespace: platform-catalog # extraGitopsRepos: [https://github.com/<org>/gitops-cluster-<name>] # optionalAll six top-level fields must be present (even as
"") - theApplicationSet’s own Go-template evaluation errors on a genuinely-missing key.appRepoUrlcan’t be""for any tenant: theApplicationSet’s second source readscicd.yamllive from exactly this URL. There’s notenantNamespacefield - the execution namespace is computed fromtype+appNameas<type>-<app-name>-cicd, so it can’t drift from convention (see naming-conventions.md).extraGitopsReposis optional - full URLs of extra gitops repos (beyondgitops-<app-name>) this app’s CI needs write access to, e.g.gitops-image-bumpfor an app whose deploy target isn’t its owngitops-<app-name>repo (a hand-deployed singleton writing into a sharedgitops-cluster-*repo instead - seedocs/backstage-design.mdin theidprepo for a real example). Rendered as an extraRepositoryCR grant byglidepath-app’srepository.yaml. It’s a list, so - unlike the six scalar fields above - it can’t be forwarded through theApplicationSet’s HelmvaluesObject(goTemplate’s per-field substitution can’t emit array items); theApplicationSet’s third source $refs this wholeidentity.yamlfile as an extravalueFilesentry instead, specifically so list-shaped fields like this one reach the chart. A real bug where this field silently vanished (missing fromvaluesObject, no third source yet) was found and fixed 2026-08-29.platformIdentityisn’t a keyschemas/cicd.schema.jsonpermits -additionalProperties: falserejects anycicd.yamlthat tries to define it. This is enforced structurally, not just by convention: theApplicationSet’s HelmvaluesObject(built only from this identity file) takes precedence overvalueFiles(the app repo’s livecicd.yaml, and this identity file itself) for any overlapping key - verified directly against a live cluster, not just a template diff. -
Provision the target Deployment first.
deploy-manifests.yamlpatches an existingDeploymentnamed<app-name>in<type>-<app-name>-<env>for each environmentcicd.yamldeclares - it doesn’t create the Deployment or its namespace. Apply a minimal Deployment/Service by hand before step 5, or the sync fails outright (env/deploy-rbac.yamlneeds the namespace to already exist to grant namespaced RBAC in it). Ifcicd.yamlalso declaresrelease, pre-seed the gitops repo too - see release.md. -
ArgoCD installs the app chart - nothing to run by hand. Once step 3’s identity file lands on this repo’s default branch, the
ApplicationSetcreates/syncs a three-sourceApplicationnamed<app-name>-cicd: one source is this platform’s own chart, one readscicd.yamllive fromappRepoUrl, and one reads this sameidentity.yamllive from the tenants repo (seeextraGitopsReposabove). This provisionspipeline-runner+ RBAC, only the stage-transition Triggerscicd.yamlactually declares, env-deploy RBAC per declared environment, the build-cache PVC, the release AppProject/Application/DORA RBAC (ifreleaseis declared), the ephemeral-envsApplicationSet(if enabled), the governance policy ConfigMap, and anExternalSecretforregistry-credentials. Editingcicd.yamland letting ArgoCD auto-sync is how an Application’s shape changes later -prune: trueremoves whatever a stage removal makes unnecessary. -
.tekton/boilerplate delivery. A push to the app repo’scicd.yamlfiresonboarding-resync.yaml(a real Pipelines-as-Code CEL extension,"cicd.yaml".pathChanged()), which opens a PR against the app repo (and gitops repo, if any) with the generated.tekton/*.yamlfiles. Merge it once - these files are never hand-edited.git status --porcelaininside that delivery step skips opening a PR when nothing changed, so this is safe to fire often.Bootstrapping this for a brand-new app is automatic, not a step you do by hand:
.tekton/onboarding-resync.yamlis itself one of the files this mechanism delivers, so on a repo where.tekton/doesn’t exist yet, nocicd.yamlpush (past or future) can trigger it - PaC only matches against.tekton/*.yamlfiles already committed at the pushed ref.templates/hooks/onboarding-resync-bootstrap.yaml(an ArgoCDPostSynchook on this Application, same shape ascharts/glidepath-pr-preview-notify’s own hook) closes this gap: it fires on every sync, checks whether.tekton/onboarding-resync.yamlalready exists on the app repo, and only if not, creates the same bootstrapPipelineRunthis section used to have you run by hand (see git history for the old manual command). Past the first real delivery, PaC’s owncicd.yaml-push resync owns.tekton/and this hook is a no-op on every later sync. -
Push to
main. Watch the Grafana dashboard.
Keeping onboarding boilerplate in sync
Two things used to go stale silently: an Application’s cicd.yaml changing (nothing
re-rendered the app chart), and the platform’s own onboarding templates changing
(nothing re-delivered already-onboarded repos’ copies). Both are solved the same way
now - onboarding-resync.yaml fires on any cicd.yaml push and re-delivers current
templates regardless of which side went stale. Templates live in a ConfigMap
(charts/glidepath-app/templates/configmaps/onboarding-templates.yaml, one per
Application, not baked into the toolbox image), so an edit takes effect on that
Application’s next helm upgrade - though it does mean updating N already-onboarded
Applications individually to propagate a template change.
The app chart’s own resources (Triggers, RBAC) re-render on a cicd.yaml change too,
for the same reason: the ApplicationSet’s generated Application reads cicd.yaml
live from the app repo, so one push reaches both halves without a manual step.
A real deadlock this mechanism can fall into (found and fixed live)
onboarding-resync’s own trigger file - the thing that would deliver a fix - is
itself one of the files this mechanism regenerates. If a required param gets added
to onboarding-resync.yaml’s Pipeline (or release.yaml’s) after a tenant already
onboarded, that tenant’s stale trigger file omits the new param, so the very
PipelineRun that would regenerate it fails Tekton admission (ParameterMissing) before
running - nothing can self-heal from that state. Fixed by making
onboarding-resync.yaml’s params optional wherever derivable (e.g. app-type from
app-namespace’s own naming convention). Lesson for next time a required param is
added to a Pipeline a stale committed trigger file might invoke: give it a derivable
default, or every already-onboarded tenant needs one manual resync PipelineRun before
the fix reaches it on its own.
A quieter sibling of the same bug, found live 2026-08-23: gitops-repo-url was
already optional (default: ""), so it never caused the hard deadlock above - but it
had the same underlying gap, no derivable fallback, just a softer failure mode: a
tenant whose committed trigger file ever got a wrong/stale value (found via a real
tenant, checkout-api, whose .tekton/onboarding-resync.yaml was bootstrapped by
copying a donor app’s files and never got this one field corrected) silently skips
gitops-repo delivery on every resync, forever, with only a log line
("gitops repo: no gitops-repo-url param given (release stage not declared), skipping") as a clue - easy to misread as a real failure when it’s actually a graceful,
permanent no-op. Fixed the same way as app-type: deliver-onboarding-files.yaml’s
deliver-app-repo-files step now derives gitops-repo-url from the
gitops-<app-name> convention whenever it’s empty AND cicd.yaml actually declares a
release stage (checked across every flow’s steps, not just each flow’s first step -
release is typically the last stage in a chain). Same self-heal shape as app-type:
fixes the committed trigger file for next time, not the current run’s own delivery -
merge the resulting PR and push cicd.yaml once more to see gitops-repo delivery
actually happen. Same lesson applies more broadly: any param here without a
derivable fallback can go silently stale forever, not just hard-deadlock - worth a
derivation whenever one’s safely possible, not only when a missing param would fail
admission outright.