11

Tekton Triggers

Tekton Pipelines doesn't know about git pushes. Something has to create the PipelineRun. Tekton Triggers does that with a few CRDs. An EventListener is a small HTTP server in your cluster. Interceptors decide whether an event is authentic and relevant. A TriggerBinding pulls values out of the JSON, and a TriggerTemplate turns them into a PipelineRun. This chapter sends real-looking GitHub webhooks through each stage, including the ones that get rejected.
A webhook arrives at the listener's Service. The listener replies 202 straight away, then runs the event through every Trigger it serves, each with its own interceptor chain. A Trigger that gets through its whole chain binds params, renders its template and creates a PipelineRun. One that doesn't drops the event without telling anyone.

one HTTP request, every Trigger evaluated independently

GitHub
POST · X-GitHub-Event: push
Service el-github-listener
ClusterIP :8080
EventListener sink Pod
replies before processing
trigger: push-to-main
github interceptor
cel filter
cel overlays
TriggerBinding
TriggerTemplate
create PipelineRun
trigger: pull-request
github interceptor
cel filter
TriggerBinding
TriggerTemplate
create PipelineRun

PipelineRuns in namespace ci

kubectl get pr -n ci
None yet. Only events that make it through a Trigger's whole interceptor chain create anything.
HTTP response
—
even if every Trigger drops it
this event
—
events sent
0
PipelineRuns
0
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
name: github-listener
namespace: ci
spec:
serviceAccountName: tekton-triggers-sa
triggers:
- name: push-to-main
interceptors:
- ref: { name: github }
params:
- name: secretRef
value: { secretName: github-secret, secretKey: token }
- name: eventTypes
value: ["push"]
- ref: { name: cel }
params:
- name: filter
value: body.ref == 'refs/heads/main'
- name: overlays
value:
- key: short_sha
expression: body.head_commit.id.truncate(7)
bindings:
- ref: push-binding
template:
ref: build-and-deploy
- name: pull-request
interceptors:
- ref: { name: github }
params:
- name: secretRef
value: { secretName: github-secret, secretKey: token }
- name: eventTypes
value: ["pull_request"]
- ref: { name: cel }
params:
- name: filter
value: body.action in ['opened', 'synchronize', 'reopened']
bindings:
- ref: pr-binding
template:
ref: pr-checks

Try: send a push to main (one PipelineRun), then a push to feature/login (passes the GitHub interceptor, dropped by the CEL filter), a pull_request opened (the other Trigger fires), and the wrong-secret push (both Triggers stop at the signature check). GitHub gets 202 every time; the YAML on the right lights up the lines doing the work.

GitHub signs each delivery with HMAC-SHA256 over the raw body using a secret you set in the webhook settings. The GitHub interceptor reads the same secret from a Kubernetes Secret, recomputes the digest and compares it to X-Hub-Signature-256. The hashes below are real SHA-256 computed in your browser.

GitHub signs the raw body

secret in webhook settings: s3cr3t-from-k8s-secret
HMAC-SHA256(secret, body) →
X-Hub-Signature-256: sha256=1e9c3a921403f26053c9f60596ebc9ef0f6f93cb10d8dac505ecd1d83f6a1107
body as sent:
{"ref":"refs/heads/feature/login","after":"9f2c4e81b7d03a5c6e1f42d8a0b7c3e5f9182736","repository":{"full_name":"acme/shop","clone_url":"https://github.com/acme/shop.git","default_branch":"main"},"head_commit":{"id":"9f2c4e81b7d03a5c6e1f42d8a0b7c3e5f9182736","message":"wip: login form","author":{"name":"Ada","email":"ada@acme.dev"}},"pusher":{"name":"ada"}}

GitHub interceptor recomputes it

body as received:
{"ref":"refs/heads/feature/login","after":"9f2c4e81b7d03a5c6e1f42d8a0b7c3e5f9182736","repository":{"full_name":"acme/shop","clone_url":"https://github.com/acme/shop.git","default_branch":"main"},"head_commit":{"id":"9f2c4e81b7d03a5c6e1f42d8a0b7c3e5f9182736","message":"wip: login form","author":{"name":"Ada","email":"ada@acme.dev"}},"pusher":{"name":"ada"}}
HMAC-SHA256(Secret github-secret/token, received) →
sha256=1e9c3a921403f26053c9f60596ebc9ef0f6f93cb10d8dac505ecd1d83f6a1107
header:
sha256=1e9c3a921403f26053c9f60596ebc9ef0f6f93cb10d8dac505ecd1d83f6a1107
hmac.Equal(computed, header) → continue: true
verdict
continue
hex chars differing
0/71
one changed byte flips about half
apiVersion: v1
kind: Secret
metadata:
name: github-secret
stringData:
token: s3cr3t-from-k8s-secret

Try: switch to "tamper: ref" to pretend a feature push is a push to main, and see that the signature no longer matches. Then put the cluster secret out of sync with GitHub (edit it, or pick "GitHub has old secret"). This is the usual reason for "my webhook does nothing" after a secret rotation.

The listener's URL is public by design. Without a secretRef, anyone who finds it can POST a payload with their own clone_url and have your cluster build and run their code with your pipeline's ServiceAccount.
Each interceptor is an HTTPS service. For built-in and custom ClusterInterceptors, the listener POSTs an InterceptorRequest (body, headers, extensions so far, the interceptor's params, context) and gets back continue: true/false plus any new extensions. CEL overlays are how you compute fields the payload doesn't have.
provider
event
CEL filter
overlays

the chain (click a hop to see what goes over the wire)

event
X-GitHub-Event: push
→
→
→
bindings
event + extensions
POST https://tekton-triggers-core-interceptors.tekton-pipelines.svc:8443/cel
body.ref.startsWith('refs/heads/release/') → true
InterceptorRequest
{
  "body": "…same body…",
  "header": "…same headers…",
  "extensions": {},
  "interceptor_params": {
    "filter": "body.ref.startsWith('refs/heads/release/')",
    "overlays": [
      {
        "key": "short_sha",
        "expression": "body.head_commit.id.truncate(7)"
      },
      {
        "key": "branch",
        "expression": "body.ref.replace('refs/heads/', '')"
      }
    ]
  },
  "context": {
    "event_url": "http://el-shop-listener.ci.svc:8080",
    "event_id": "c2a8f-41d7b",
    "trigger_id": "namespaces/ci/triggers/shop-push"
  }
}
InterceptorResponse
{
  "continue": true,
  "extensions": {
    "short_sha": "e4c1a90",
    "branch": "release/1.4"
  }
}
result
continue
hops called
2
chain stops at first false
extensions for bindings
$(extensions.short_sha) = "e4c1a90"
$(extensions.branch) = "release/1.4"
interceptors:
- ref:
name: github
kind: ClusterInterceptor
params:
- name: secretRef
value: { secretName: github-secret, secretKey: token }
- name: eventTypes
value: ["push"]
- ref: { name: cel }
params:
- name: filter
value: >-
body.ref.startsWith('refs/heads/release/')
- name: overlays
value:
- key: short_sha
expression: body.head_commit.id.truncate(7)
- key: branch
expression: body.ref.replace('refs/heads/', '')

Try: push release/1.4 with the release/* filter and the branch overlay. Then switch the provider: the event header, signature header and JSON paths all change, but the chain works the same way. Turn off "secret matches" and the CEL interceptor is never called. Push a tag with the filter on main and the chain stops at CEL.

A TriggerBinding is a list of name/value pairs where the values are expressions over the event. The TriggerTemplate declares the params it expects, with optional defaults, and uses them as $(tt.params.name) in its resourcetemplates. Pick a template param, then click a value in the payload to bind it.
binding for template param

the event, as the binding sees it

hover a value
header {
}
body {
repository {
}
head_commit {
author {
}
}
pusher {
}
commits [
[0] {
modified [
]
}
]
}
extensions {
}

extensions.short_sha was added by a CEL overlay earlier in the chain. Click any value to bind it to git-url.

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
name: push-binding
spec:
params:
# click a field in the payload →
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
name: build-and-deploy
spec:
params:
- name: git-url
- name: git-revision
- name: short-sha
default: latest
- name: event-type
default: push
resourcetemplates:
- apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: build-and-deploy-
labels:
acme.dev/event: $(tt.params.event-type)
spec:
pipelineRef: { name: build-and-deploy }
params:
- name: repo-url
value: $(tt.params.git-url)
- name: revision
value: $(tt.params.git-revision)
- name: image-tag
value: $(tt.params.short-sha)

PipelineRun the EventListener would create

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: build-and-deploy-
labels:
acme.dev/event: push
spec:
pipelineRef: { name: build-and-deploy }
params:
- name: repo-url
value: $(tt.params.git-url)
- name: revision
value: $(tt.params.git-revision)
- name: image-tag
value: latest
git-url
unset
placeholder left as-is
git-revision
unset
placeholder left as-is
short-sha
default
latest
event-type
default
push

Try: click repository.clone_url, then head_commit.id, and watch the PipelineRun fill in. Unbind git-url: there is no default, so the literal $(tt.params.git-url) goes into the PipelineRun (in red). short-sha and event-type fall back to their defaults until you bind them. Header lookups are case-insensitive.

One event, one PipelineRun: naming under load

triggers-resourcetemplatestriggers-generatename
Triggers keeps no state. Each event independently renders the template and asks the API server to create the objects. When a burst of pushes and GitHub redeliveries arrives at the same time, the only thing that decides what happens is the name in the template's metadata.
metadata

webhooks hitting the EventListener Pods

Service spreads requests across replicas
pod-0pod-1delivery h2qg2 · sha 2dc31ec → build-and-deploy-x2hvddelivery 9dwg4 · sha 2f1e1de → build-and-deploy-tzq7jdelivery hqzbd · sha 2f1e1de (redelivery) → build-and-deploy-4zwbqRdelivery gqsp6 · sha 1810e93 → build-and-deploy-l7k2zdelivery 2gsq9 · sha 1810e93 (redelivery) → build-and-deploy-jb4hkRdelivery 9l8g4 · sha 392dcd6 → build-and-deploy-fq9jddelivery tksln · sha 90c6883 → build-and-deploy-5lp9kdelivery 2b7kp · sha 29ebad6 → build-and-deploy-nh6z6delivery fjpv7 · sha c04842d → build-and-deploy-r4wfwdelivery zsk5q · sha 2dc31ec (redelivery) → build-and-deploy-tl4vbRdelivery h84bn · sha c0768a3 → build-and-deploy-8mm6jdelivery t4nq4 · sha 6c98730 → build-and-deploy-g5k9b
● PipelineRun created● created, but that commit was already built● create failed: AlreadyExistsR = GitHub redelivery of an earlier event

kubectl get pipelineruns (12)

build-and-deploy-x2hvd sha 2dc31ec
build-and-deploy-tzq7j sha 2f1e1de
build-and-deploy-4zwbq sha 2f1e1de
build-and-deploy-l7k2z sha 1810e93
build-and-deploy-jb4hk sha 1810e93
build-and-deploy-fq9jd sha 392dcd6
build-and-deploy-5lp9k sha 90c6883
build-and-deploy-nh6z6 sha 29ebad6
build-and-deploy-r4wfw sha c04842d
build-and-deploy-tl4vb sha 2dc31ec
build-and-deploy-8mm6j sha c0768a3
build-and-deploy-g5k9b sha 6c98730
events
12
PipelineRuns
12
AlreadyExists
0
event lost
commit built twice
3
wasted run
resourcetemplates:
- apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: build-and-deploy-
spec: …

Try: switch to a fixed name and replay. Only the first event gets a PipelineRun. Naming by commit SHA drops redeliveries (that is deduplication), but it also blocks you from deliberately re-running a commit. generateName never collides: every event gets its own run, redeliveries included. Adding replicas spreads the load but changes nothing about naming.

Which Triggers does a listener serve?

triggers-el-selectorstriggers-trigger
Inline triggers and triggerRefs are always served. Standalone Trigger CRDs are found through selectors: labelSelector filters by labels, and namespaceSelector reaches into other namespaces. What the listener can actually read is decided by its ServiceAccount's RBAC.
namespaceSelector
labelSelector
ServiceAccount RBAC

Trigger CRDs in the cluster

namespace ci · EventListener lives here
✓ push-to-maininline
inline in spec.triggers
✓ nightlyprovider=github
spec.triggers[].triggerRef: nightly
✓ docs-previewprovider=github
matched by labelSelector
namespace team-a
✗ api-pushprovider=github
other namespace (no namespaceSelector)
✗ api-mrprovider=gitlab
other namespace (no namespaceSelector)
namespace team-b
✗ web-pushprovider=github
other namespace (no namespaceSelector)
namespace sandbox
✗ experimentprovider=github
other namespace (no namespaceSelector)
Triggers served
3/7
RBAC errors
0
in the EL Pod's logs
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
name: github-listener
namespace: ci
spec:
serviceAccountName: tekton-triggers-sa
triggers:
- name: push-to-main
bindings: [{ ref: push-binding }]
template: { ref: build-and-deploy }
- triggerRef: nightly
labelSelector:
matchLabels:
provider: github
kind: RoleBinding # namespace ci only
metadata: { name: triggers-sa-eventlistener-binding, namespace: ci }
roleRef:
kind: ClusterRole
name: tekton-triggers-eventlistener-roles
subjects: [{ kind: ServiceAccount, name: tekton-triggers-sa, namespace: ci }]
# both setups also bind ...-eventlistener-clusterroles
# (ClusterInterceptors, ClusterTriggerBindings)

Try: with only the labelSelector set, the listener picks up the labelled Triggers in its own namespace. Set namespaceSelector to team-a, team-b and those Triggers go red: the ServiceAccount can only read objects in ci. Switch to the ClusterRoleBinding and they are served. Unset the labelSelector to see api-mr (provider=gitlab) join in.

Exposing and scaling the listener

triggers-el-serviceeventlistener
The EventListener controller turns your CR into a Deployment and a Service named el-<name> on port 8080. Getting traffic in from GitHub is up to you. The sink holds no state, so you can scale it with replicas, and it can report what it did as CloudEvents.
expose via

what the EventListener controller creates for you

GitHubwebhookIngresshooks.acme.devServiceel-github-listener:8080pod-00 eventspod-10 eventsCEsink

log

Press Send webhook.
ready pods
2/2
reachable
yes
spec:
serviceAccountName: tekton-triggers-sa
cloudEventURI: http://event-display.ci.svc
resources:
kubernetesResource:
replicas: 2
spec:
template:
spec:
containers:
- resources:
requests: { cpu: 100m, memory: 64Mi }
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: github-listener }
spec:
tls: [{ hosts: [hooks.acme.dev], secretName: hooks-tls }]
rules:
- host: hooks.acme.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: el-github-listener
port: { number: 8080 }

Try: choose "nothing" and send: the listener is running but GitHub can't reach it, which is a common first-day problem. Expose it, raise replicas and send a few times to see requests spread across Pods. Kill pod-0 and the Service stops sending it traffic. With the CloudEvents sink on, every event also produces started / successful / done CloudEvents.

Triggers reference bindings and templates by name. Namespaced objects resolve in the Trigger's own namespace; a ClusterTriggerBinding resolves from anywhere. Changes take effect on the next event, because the listener reads them when the event arrives.
web-push binding kind
click any object in the graph

Triggers reference bindings and templates by name

namespace team-anamespace team-bTriggerapi-pushTriggerapi-nightlyTriggerweb-pushTriggerweb-docsClusterTriggerBindinggithub-pushTriggerBindingteam-labelsTriggerTemplatebuild-and-deployTriggerTemplatebuild-and-deploy
users of github-push
4
one definition
edit affects
4
Triggers, next event
ClusterTriggerBinding github-push
apiVersion: triggers.tekton.dev/v1beta1
kind: ClusterTriggerBinding # cluster-scoped
metadata:
name: github-push # no namespace
spec:
params:
- name: git-url
value: $(body.repository.clone_url)
- name: git-revision
value: $(body.head_commit.id) # v1

Try: select github-push and press Edit. All four Triggers, across two namespaces, pick up the change on their next event, with nothing to redeploy. Then switch web-push to a plain TriggerBinding: the name now resolves in team-b, where no such binding exists.

  • takeawayA 202 from the EventListener only means the request was received. Whether a PipelineRun was created depends on each Trigger's interceptor chain, and a rejected event leaves no trace except in the listener's logs.
  • takeawayInterceptors are the security boundary and the router. Validate the HMAC first, filter with CEL, and use overlays to compute what the bindings need.
  • takeawayBindings read the event, templates shape the run. Use generateName, give required params a value or a default, and share mappings through ClusterTriggerBindings.

Key concepts · 20

book chapter 11
  1. EventListener

    A CRD for a long-running HTTP sink that receives events and evaluates every Trigger it serves against each one.

    It is the only part that is always running. It replies 202 Accepted right away and processes the event asynchronously, so a 202 from the listener does not mean a PipelineRun was created.

    triggers.tekton.dev/v1beta1spec.triggerstkn eventlistener describe
    ↑ see it in the visualization
  2. EventListener Service & Deployment

    For each EventListener the controller creates a Deployment and a Service named el-<name>, listening on port 8080.

    The Service is ClusterIP by default, so you must expose it yourself (Ingress, OpenShift Route or a LoadBalancer serviceType) before GitHub can reach it. Replicas scale the sink horizontally, and cloudEventURI makes it emit CloudEvents about each event it handles.

    el-<name>:8080spec.resources.kubernetesResource.replicasserviceType: LoadBalancerspec.cloudEventURI
    ↑ see it in the visualization
  3. Trigger

    Groups interceptors, bindings and a template into one unit. It can be written inline in an EventListener or as its own CRD.

    As standalone CRDs, Triggers can live in team namespaces and be picked up by a shared listener through selectors. Each one can set its own serviceAccountName for the objects it creates.

    kind: TriggertriggerRefspec.serviceAccountName
    ↑ see it in the visualization
  4. TriggerBinding

    A namespaced list of params whose values are expressions over the event, like $(body.head_commit.id).

    It separates reading the payload from the PipelineRun shape, so the same template can serve GitHub, GitLab or a cron job as long as each one has its own binding.

    kind: TriggerBindingspec.params[].value
    ↑ see it in the visualization
  5. ClusterTriggerBinding

    A cluster-scoped TriggerBinding that any Trigger in any namespace can reference with kind: ClusterTriggerBinding.

    Mappings for a provider's payload are the same everywhere, so you define them once. A ref without kind looks for a namespaced TriggerBinding in the Trigger's own namespace, which is a common cause of 'not found'.

    kind: ClusterTriggerBindingbindings[].kind
    ↑ see it in the visualization
  6. TriggerTemplate

    Declares params (optionally with defaults) and resourcetemplates, the Kubernetes objects to create for each event.

    It is the blueprint for a run. The listener substitutes $(tt.params.x) into it and creates the result with its own ServiceAccount, so that account needs RBAC to create PipelineRuns.

    kind: TriggerTemplatespec.params[].default
    ↑ see it in the visualization
  7. $(tt.params.…)

    The placeholder syntax inside resourcetemplates that is replaced with the bound or default value of a template param.

    A param that is neither bound nor defaulted is not an error: the literal placeholder ends up in the created object, which then fails later in a confusing way.

    $(tt.params.git-revision)
    ↑ see it in the visualization
  8. resourcetemplates

    The list of objects in a TriggerTemplate. Usually one PipelineRun, but any Tekton or Kubernetes resource the ServiceAccount may create.

    One event creates every object in the list, every time. Triggers keeps no memory of earlier events, so deduplication has to come from naming or from the pipeline itself.

    spec.resourcetemplates[]apiVersion: tekton.dev/v1
    ↑ see it in the visualization
  9. generateName

    metadata.generateName asks the API server to add a random 5-character suffix, so every created PipelineRun gets a unique name.

    A fixed name works once and then fails with AlreadyExists. A name derived from the commit SHA dedupes redeliveries but blocks reruns. generateName never collides, but builds a redelivered commit twice.

    generateName: build-AlreadyExists
    ↑ see it in the visualization
  10. Interceptor

    A step in a Trigger's chain that can validate, filter or enrich the event before bindings run. Any interceptor can stop the chain.

    Interceptors run in order and each sees the extensions added by earlier ones. The chain stops at the first continue: false, so cheap checks like signature validation should come first.

    spec.triggers[].interceptors[]continue: false
    ↑ see it in the visualization
  11. ClusterInterceptor

    A cluster-scoped CRD that registers an interceptor as an HTTPS service. The listener POSTs it an InterceptorRequest and reads back an InterceptorResponse.

    The built-in github, gitlab, bitbucket, cel and slack interceptors are ClusterInterceptors served by tekton-triggers-core-interceptors. You can register your own the same way via spec.clientConfig.service.

    kind: ClusterInterceptorref: { name: cel }InterceptorRequesttekton-triggers-core-interceptors
    ↑ see it in the visualization
  12. GitHub interceptor

    Validates X-Hub-Signature-256 against a secret and filters on X-GitHub-Event with an eventTypes list.

    Without it, anyone who finds your webhook URL can start pipelines with any payload they like. eventTypes keeps push-only Triggers from firing on ping or issue events.

    params: secretRef, eventTypesX-GitHub-Event
    ↑ see it in the visualization
  13. GitLab & Bitbucket interceptors

    Provider-specific equivalents of the GitHub interceptor. GitLab compares the X-Gitlab-Token shared token; Bitbucket Server checks an HMAC in X-Hub-Signature.

    Each provider authenticates and names events differently (Push Hook, repo:refs_changed). The interceptor hides that so the rest of the chain looks the same.

    X-Gitlab-TokenX-Gitlab-EventX-Event-Key
    ↑ see it in the visualization
  14. CEL interceptor

    Evaluates a Common Expression Language filter over body and header. The event continues only if it returns true.

    Branch, path and action filtering without writing a service. Triggers adds functions such as match, truncate, split, compareSecret and parseJSON to standard CEL.

    filter: body.ref == 'refs/heads/main'header.match('X-GitHub-Event','push')
    ↑ see it in the visualization
  15. CEL overlays

    Key/expression pairs on the CEL interceptor whose results are added to the event's extensions.

    Bindings can only copy fields. Overlays compute new ones (a 7-char SHA, a branch name without refs/heads/) so the template stays simple.

    overlays: [{key, expression}]body.head_commit.id.truncate(7)
    ↑ see it in the visualization
  16. Webhook interceptor

    The older style of custom interceptor: a plain Kubernetes Service that receives the raw HTTP event and returns 200 to continue.

    Its response body and headers replace the event's, so it can rewrite payloads. It doesn't use the InterceptorRequest protocol, and new interceptors should usually be ClusterInterceptors instead.

    webhook.objectRef: { kind: Service }
    ↑ see it in the visualization
  17. Webhook secret

    A shared secret configured both in the Git provider's webhook settings and in a Kubernetes Secret that the interceptor reads via secretRef.

    The two copies have to match. After a rotation that misses one side, every event is dropped silently while the provider still sees a 202.

    secretRef: { secretName, secretKey }
    ↑ see it in the visualization
  18. HMAC signature

    sha256=HMAC-SHA256(secret, raw body), sent in X-Hub-Signature-256 and recomputed by the interceptor.

    It proves the sender knows the secret and that the body wasn't changed in transit. One changed byte gives a completely different digest, and the comparison is constant-time so timing doesn't leak the expected value.

    X-Hub-Signature-256hmac.Equal
    ↑ see it in the visualization
  19. $(body…), $(header…), $(extensions…)

    The three roots a binding value can read: the JSON body (JSONPath-like, with [i] indexes), HTTP headers (case-insensitive), and interceptor extensions.

    Values can be mixed with literals ("pr-$(body.number)"), and $(body) on its own gives the whole payload as a JSON string. A path that doesn't exist fails the binding for that event.

    $(body.head_commit.id)$(header.X-GitHub-Event)$(extensions.short_sha)
    ↑ see it in the visualization
  20. namespaceSelector, labelSelector & listener RBAC

    Selectors that let one EventListener serve Trigger CRDs from other namespaces (matchNames, "*") and/or only those with matching labels.

    This is how platform teams run one listener for many teams. The listener's ServiceAccount must be allowed to read Triggers, bindings and templates in those namespaces, or they are silently skipped and the errors show up only in its logs.

    namespaceSelector.matchNameslabelSelector.matchLabelstekton-triggers-eventlistener-rolestekton-triggers-eventlistener-clusterroles
    ↑ see it in the visualization