Docs · Glidepath
Naming conventions
Written 2026-08-06 after auditing real inconsistencies (not guessed) - see the specific findings inline below. This is the durable reference; new catalog Tasks, Pipelines, namespaces, and files should follow this rather than the nearest existing example, since a few existing examples are exactly the inconsistencies this doc fixes.
Namespaces
One flat pattern: <type>-<app-name>-<env>. type is app (a regular Application)
or infra (a shared/platform-adjacent service onboarded with its own pipeline - e.g. a
future shared DB operator) - more types added as real cases show up, not invented
speculatively. env is whichever environment that particular namespace represents -
cicd (the Application’s own pipeline-execution namespace), dev, staging,
pr-<number>, or any other declared deploy target. All of these are siblings under
the same pattern, not a base-plus-suffix hierarchy - a deploy namespace has nothing to
do with “cicd” conceptually (it’s where the Application runs, not where its pipeline
runs), so it is never <type>-<app-name>-cicd-<env>.
Examples, all structurally identical 3-part names: app-nodejs-demo-app-cicd (pipeline
execution), app-nodejs-demo-app-dev (deploy target), app-nodejs-demo-app-staging
(release staging), app-nodejs-demo-app-pr-42 (PR ephemeral env),
infra-payments-db-cicd (an infra-type Application’s own pipeline execution).
charts/glidepath-app’s glidepath-app.envNamespace helper computes any of
these from platformIdentity.type + platformIdentity.appName + a given env value -
nothing is a separately-set, independently-typed field that could drift from the
convention.
Watch the 63-character Kubernetes namespace limit (a real DNS-1123 constraint, not a
style preference) on longer app names combined with the -pr-<number> suffix.
Platform-level namespaces keep the existing platform-* prefix - already consistent,
not changing: platform-system, platform-catalog, platform-catalog-canary,
platform-secrets.
Third-party namespaces (argocd, tekton-chains, fulcio-system,
external-secrets, observability, crossplane-system) are not ours to rename - keep
whatever that tool’s own install convention uses.
Helm release name = namespace name, exactly. helm install <type>-<app-name>-cicd charts/glidepath-app ... - one less thing to keep in sync by hand.
Catalog Task names
Verb-noun (or verb-only), kebab-case, matching the file name exactly
(build-image.yaml -> build-image). Already consistent across the whole catalog -
confirmed by auditing all 26 Task names live before writing this doc. Keep it that way.
Catalog Pipeline names
Noun matching the stage or check it represents: build, test, deploy, release,
sast-check, image-scan-check, provenance-check, sbom-check, qa-check,
governance-check, image-promotion-check, bypass-merge-check, onboarding-resync.
Not actually consistent until 2026-08-23: this doc originally claimed the list above
was “already consistent,” but missed a real gap - policy-check was the gate’s own
name, not just its Pipeline’s -check suffix (every other real gate’s name, e.g.
sast, has no suffix at all; its Pipeline adds -check). That made policy-check the
only gate whose identity redundantly baked in “check” - and it didn’t say what it
actually verified (gitsign commit signatures + SLSA provenance). Renamed the gate to
provenance (Pipeline: provenance-check, matching every other gate’s pattern) - see
docs/admin/release-guardrails.md.
Step names within a Task - the real inconsistency, now fixed
Audited live: three genuinely different patterns existed under one implied name,
emit-span/span-related steps:
- Dedicated step, sole job is sending a pre-computed span (
otel_task_span_send):build-source.yaml,run-tests.yaml,sast-scan.yaml- each has a step named exactlyemit-span. This is the standard - use this shape whenever a Task can afford a dedicated trailing step. - The same
otel_task_span_sendcall, folded into a step doing something else:build-image.yaml’semit-image-ref-resultstep also sends the Task’s span, for a real structural reason (kaniko has no shell, so the span has to be sent from whichever bash step runs after it, and that step already exists to extract kaniko’s results) - but the step’s name didn’t disclose the second job. Fixed: renamed toemit-image-ref-and-span. When a span-send has to be folded into another step for a similar structural reason, name the step to disclose both jobs - never let a name describe only one of two things a step does. - No separate step at all -
otel_child_spanwraps the live command directly:image-scan.yaml,generate-sbom.yaml. This is a genuinely different, correct mechanism (you can’t retroactively wrap a step that already finished with a span covering its execution), not a naming gap - document it as an intentional exception where it appears, don’t leave it looking like an oversight.
start-span/end-span (used by start-stage-span.yaml/end-stage-span.yaml/
start-flow-root-span.yaml/end-flow-root-span.yaml) are intentionally a different
name from emit-span - they’re a genuine two-phase begin/end pair spanning separate
Tekton Tasks (sometimes separate PipelineRuns entirely), not a one-shot send within a
single Task. Don’t unify these names - the distinction is real and worth keeping
visible.
Other step-name patterns already consistent, keep using them: resolve-* for
config/parameter-resolution steps that run before the real work (resolve-build-config,
resolve-test-command, resolve-build-script-path), and a plain verb (or
verb-noun) for the step doing the actual work (scan, build-and-push-arm64/
build-and-push-amd64, publish-multiarch-manifest, run-build-script, unit-test,
generate-and-attest). build-image.yaml’s build-and-push split into two arch-suffixed
steps + a manifest-combining step on 2026-09-22 (multi-arch build support) - kept the
same verb-noun shape per-arch rather than inventing a new pattern.
PipelineRun naming
generateName must always end in -, except the gitops-repo governance-check trigger
files, which must NOT. Two real, live-confirmed findings layered on top of each other
here - read both before touching either:
- Missing the trailing dash is the actual root cause of PipelineRun names like
sastgdn8rinstead ofsast-gdn8r- confirmed live: the five gitops-repo governance-check trigger files that existed at the time (sast,policy-check- since renamedprovenance,image-scan,sbom,bypass-check) were missing the trailing dash, while the app-repo side (build-,pr-validate-,onboarding-resync-) already had it right. First fix: add the dash everywhere. - That first fix was then partially reverted, deliberately, once tested live:
Pipelines-as-Code derives the GitHub Check context name directly from
metadata.generateName, verbatim - no trailing-dash trimming (confirmed via a real test PR againstgitops-nodejs-demo-app: adding the dash produced a real PipelineRun name likesast-gdn8r, but the check name becamesast-too, notsast). No annotation to decouple the two was found after a real search of PaC’s docs. Since the clean check name (sast, notsast-) is the explicitly stronger preference, every gitops-repo governance-check onboarding template (nine as of 2026-08-23, afteritsm/qa/policy-validation/image-promotionwere added on top of the original five - see docs/admin/release-guardrails.md) keeps its dash-freegenerateNamedeliberately - the resultingsastgdn8r-style PipelineRun name is the accepted cost of the cleaner, more visible check name. Every new gitops-repo onboarding template added since has followed this (dash-free) convention, not the general app-repo-side one. Don’t “fix” this again without a real mechanism to set the check name independently ofgenerateName- reverting the original dash-everywhere fix, for just these files, was itself the fix.
Deterministic (non-generateName) PipelineRun names fired by the broker follow
<flowName>-<index>-<stageName>-<chainSlug>-$(body.context.id) (e.g.
ci-1-test-brave-otter-a1b2c3d4), where:
indexis the step’s zero-based position in the flow’sstepslist - added so two steps sharing a stage name (e.g. twoteststeps in one flow) don’t collide, and so the CEL filter chaining one step to the next can match on the exact previous step rather than any step with that stage name (seeflow-triggers.yaml).chainSlug($(body.customData.platform.chain_slug)) is a two-word deterministic stand-in for the flow’s chain-id (seecdevents.sh’schain_id_to_slug) - derived from the first 4 hex chars of chain-id, which never changes across a flow, so every chained PipelineRun in the same flow instance shows the same two words, making them easy to spot as belonging together at a glance without reading the full chain-id.$(body.context.id)is a deterministic hash of the emitting PipelineRun’s own name plus the event type it emitted (seecdevents.sh), truncated to 8 hex chars (down from an original 20, to make room forchainSlugwithin Kubernetes’ 63-char name limit) - this is what preserves redelivery idempotency: a retried CDEvent delivery reproduces the exact same id, so the next-stage Trigger’s PipelineRun creation is a harmless no-op instead of a duplicate run.chainSlug, though also deterministic, does NOT provide this on its own, since it’s shared across an entire flow rather than unique per stage-transition - it must stay a middle segment, never a replacement forcontext.idas the name’s uniqueness key.
A flow’s git-rooted first step (trigger.source: git, e.g. build) is generated by PaC
from .tekton/*.yaml in the app’s own repo, not by the broker - at that point, no
CDEvent (and therefore no chain-id or context.id) exists yet, since chain-id itself is
minted by start-flow-root-span during build’s own execution, after Kubernetes has
already assigned build’s PipelineRun its name. So build’s name can carry neither
chainSlug nor a context.id-style hash; it instead uses a real Kubernetes
generateName of <flowName>-0-<rootStage>- (e.g. ci-0-build-), letting the API
server append its own random suffix (ci-0-build-knc2z) - structurally consistent with
the broker’s pattern (<flowName>-<index>-<stageName>-) even though the trailing
segment comes from a different mechanism and can’t share chainSlug with the rest of
the flow’s PipelineRuns.
Deferred idea, not implemented: build’s metadata.name itself can never carry
chainSlug (Kubernetes object names are immutable, and chain-id doesn’t exist until
after the name is already assigned - see above), but build’s PipelineRun object could
still be labeled with its flow’s chainSlug after the fact, since labels (unlike names)
can be patched post-creation. start-flow-root-span mints chain-id and would need one
more step to compute chain_slug (reusing cdevents.sh’s chain_id_to_slug) and
kubectl label pipelinerun $(context.pipelineRun.name) hangar.io/chain-slug=<slug> --overwrite against itself - Tekton exposes $(context.pipelineRun.name) inside a
step for exactly this. Would need confirming pipeline-runner’s ServiceAccount can
patch PipelineRuns in its own namespace first. This would let kubectl get pipelinerun -l hangar.io/chain-slug=<slug> pull back build alongside test/deploy/release even
though build’s name string alone never will. Raised and consciously left undone
2026-09-05 - worth reconsidering if build’s isolation from the rest of its flow becomes
an actual pain point (e.g. in dashboards or triage), not just a naming curiosity.
Deferred idea, not implemented: the gitops-repo governance checks (sast,
image-scan, sbom, etc. - see “PipelineRun naming” below) have no chainSlug
correlation to their release either, and unlike build there’s no existing chain-id
channel into them at all - they’re triggered by PaC straight off a GitHub PR event on
the gitops repo, not through the CDEvents broker. Two paths considered and rejected for
now:
- Bake a
hangar.io/chain-sluglabel into the static.tekton/pull-request-*.yamlcheck files whenopen-release-pr.yamlopens the release PR - rejected because that Task deliberately makes “exactly ONE commit per release PR” touching only the manifest (a prior incident, 2026-08-31, found that widening the diff re-ran every governance gate a second time); rewriting the check files every release reintroduces that same class of unwanted churn. - Have
open-release-pr.yamlattach the chain-slug as a GitHub label on the PR itself (no repo file touched), and have each governance-check Task read it via GitHub API and self-label its own PipelineRun. Workable, but touches ~9 separate catalog Tasks (sast-scan,image-scan,generate-sbom, and the rest) for a naming-correlation nicety - out of proportion to the ask right now. Raised and consciously left undone 2026-09-05, same day as the build-labeling idea above - reconsider both together if cross-flow triage pain actually shows up.
GitHub Check / status context names
Short, no trailing dash, matching the concept the file represents (sast, provenance,
image-scan, sbom, qa, itsm, policy-validation, image-promotion,
bypass-check). Confirmed-good, keep as-is.
Helm chart and file naming
- Chart names:
glidepath-<concern>(glidepath-catalog,glidepath-control-plane,glidepath-app). - Catalog Task/Pipeline/StepAction files:
<metadata.name>.yaml, exactly - already the norm, keep it exact (a mismatch is a real “which file is this Task actually defined in” trap). - Chart template subdirectories grouped by concern, not resource kind:
identity/,triggers/,env/,argocd/,governance/,sigstore/,broker/,dora-exporter/,hooks/,secretstore/. - Docs: kebab-case, descriptive noun-phrase (
catalog-versioning.md,secrets-management.md) - already consistent.
Labels and annotations
hangar.io/* is this platform’s own label namespace. As of this pass, every resource
across all three charts (116 total: 43 catalog, 38 control-plane, 35 in a full-fixture
App render) carries a full, consistent label set via a per-chart <chart>.labels
named template (templates/_helpers.tpl) - not just the handful of resources that
happened to need a label for a real selector before now.
Standard Kubernetes-recommended labels (app.kubernetes.io/* + helm.sh/chart) -
generic tooling interop (kubectl, Lens, ArgoCD’s own resource tree), not platform-
specific:
app.kubernetes.io/name: <chart name>
app.kubernetes.io/instance: <helm release name>
app.kubernetes.io/version: <Chart.yaml appVersion>
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/part-of: glidepath
helm.sh/chart: <chart name>-<chart version>
hangar.io/component: catalog | control-plane | app - which of the three
charts owns this resource. The single most useful selector for a cross-cutting audit,
e.g. kubectl get role -A -l hangar.io/component=app.
hangar.io/subcomponent: which concern within that chart - matches the template
subdirectory a resource’s file lives in (identity, triggers, env, argocd,
governance, broker, dora-exporter, sigstore, hooks, secretstore, grafana). Lets you
narrow hangar.io/component=control-plane down to just
hangar.io/subcomponent=sigstore, for example.
hangar.io/app (app chart only, on every resource it creates): most valuable on
the resources that live in a shared namespace with other Applications’ resources
(argocd’s AppProjects/Applications/ApplicationSets/Roles) - lets you
kubectl get application -n argocd -l hangar.io/app=nodejs-demo-app instead of relying
on name-pattern matching. Applied to every app-chart resource, not just the
shared-namespace ones, for consistency.
hangar.io/stub: "true" on catalog resources that are still genuinely stub
implementations - currently governance-gate-stub (Task), governance-stub
(StepAction), governance-check (Pipeline, though live-confirmed unreferenced by any
current onboarding trigger - a real, minor dead-code finding, not acted on here).
Reinforces this platform’s existing “stub-ness must be structurally loud” principle
(previously only visible in trace span attributes and docs) at the resource-selection
level too: kubectl get task -n platform-catalog -l hangar.io/stub=true now answers
“which catalog gates are still fake” directly.
Pre-existing hangar.io/* labels/annotations (kept exactly as-is, already
consistent with this scheme): hangar.io/catalog: "true" (catalog-resolvable
resources), hangar.io/dora-track: "true" (Applications the DORA exporter watches),
hangar.io/dora-pending/-app-namespace/-app/-flow-start-time/-baseline-started-at
(DORA tracking state annotations on Applications), hangar.io/stall-alerted (dedup
marker), hangar.io/ephemeral-env (PR-namespace TTL sweep target marker),
hangar.io/purpose (free-text annotation, currently only on the ClusterSecretStore’s
source namespace).
A real bug found and fixed while applying this broadly: two pre-existing resources
(fulcio-server’s Deployment, dora-exporter’s Service) already had their own
metadata.labels block for an unrelated reason (a plain app: <name> selector label) -
naively inserting a second labels: key produced invalid/duplicate-key YAML rather than
merging. Fixed by merging into one block in both cases; swept the whole of charts/ for
the same class of bug afterward (both block-style and flow-style labels: { ... }) and
confirmed zero remaining occurrences before considering this done.