Tekton's core idea: a pipeline is not config for a CI server; it is Kubernetes resources executed as pods. Everything else is vocabulary, and debugging CI becomes debugging pods, which you already know how to do.

needsmake core cicd

Orientation

competency 2.2 · CI/CD pipelines integrated with Kubernetes

A Task is a sequence of steps (containers sharing one pod). A Pipeline sequences Tasks. Running either means creating a TaskRun or a PipelineRun. That is the entire object model, and every failure you will ever debug is a container in a pod that exited non-zero.

Pipeline ──instantiated by──▶ PipelineRun
   └─ tasks[]                    └─ creates one TaskRun per task
        └─ Task                       └─ creates one pod
             └─ steps[]                    └─ one container per step, run in order
                                              sharing /workspace volumes
Why "steps share a pod" matters

Steps run sequentially in the same pod, so they share volumes and the node; that is how the clone step's checkout reaches the build step. It also means step resource requests are additive: Kubernetes sums the containers even though they run one at a time (task-level computeResources on the TaskRun exists to claw that back), that a step cannot run on a different node, and that everything in a Task lives or dies together. Tasks, in contrast, are separate pods and may run in parallel.

The pieces the exam wires together

params · workspaces · results · ordering
PieceCarriesSyntax you will type
paramsvalues down: pipeline → task → step$(params.image)
workspacesfiles between tasks$(workspaces.source.path)
resultssmall strings between tasks$(tasks.clone.results.commit)
whenconditional executionwhen: [{input: "$(params.env)", operator: in, values: ["prod"]}]
finallytasks that always run (cleanup, notify)spec.finally[]
matrixfan-out one task over a listmatrix.params

Params have types (string, array, object) and defaults. Results are written by a step to $(results.<name>.path) and are deliberately small: they travel through the TaskRun's status, so a result is a digest or a count, never a build log. Workspaces are bound at run time by the PipelineRun: an emptyDir (per-pod, lost between tasks), a persistentVolumeClaim (shared, you manage it), or a volumeClaimTemplate (shared, created and deleted with the run; the sane default). ConfigMaps and Secrets can also back a workspace, which is the tidy way to hand credentials to a step.

Ordering surprises people exactly once

Tasks with no runAfter and no shared results run in parallel. Consuming another task's result creates an implicit dependency, so removing a result reference can silently parallelize a pipeline that used to be sequential. If order matters, say it with runAfter rather than relying on a data dependency you might refactor away.

Identity, credentials, and registries

The pipeline pod runs as a ServiceAccount (taskRunTemplate.serviceAccountName on the PipelineRun), and in real setups that SA carries registry credentials as an imagePullSecret or a kubernetes.io/dockerconfigjson Secret linked to the SA for Tekton's credential init. The lab's registry needs none, and kaniko pushes to it with --insecure.

One registry, two names

The pipeline pushes to kind-registry:5000/demo:v1 because it runs inside the cluster, where localhost is the pod itself. From the host, the same store answers as localhost:5001. Know which name works from where; it comes back in section 5.6 when Kyverno has to fetch a signature from inside the cluster.

Where Tasks come from

The catalog tasks make cicd installed are git-clone and kaniko; the trivy-scan task arrives when you apply the lab example. tkn task list shows what you have. Modern Tekton can also fetch task definitions at run time with resolvers: git, hub, or bundles (OCI images containing task YAML), via taskRef.resolver. That is how a platform team ships a shared, versioned task library without copying YAML into every repo, and it is worth being able to describe even if you never configure one.

Triggers: from a git push to a run

Three CRDs to say "when Gitea posts a push event, run the pipeline with that commit":

  • EventListener: a pod exposing HTTP that receives the webhook.
  • TriggerBinding: extracts fields from the payload ($(body.repository.clone_url), $(body.after)).
  • TriggerTemplate: stamps out the PipelineRun with those values.
  • Plus interceptors: filter by event type, verify the webhook secret, or evaluate a CEL expression before anything runs. Without one, your EventListener builds anything anyone posts to it.

Resolvers, StepActions and Chains, as fields

taskRef.resolverparamsUse
giturl, revision, pathInRepo (anonymous clone) or org, repo, revision, pathInRepo (authenticated API)a shared task library in a repo, pinned by revision
bundlesbundle (OCI ref), name, kindtasks packaged as OCI images, signed and promoted like any image
hubcatalog, kind, name, versionTekton Hub or Artifact Hub catalog tasks
clusterkind, name, namespacereference a Task in another namespace; the replacement for the deprecated ClusterTask
httpurla raw YAML URL

A StepAction is the reusable unit one level below a Task: image, command or script, args, env, params, results, workingDir, securityContext, volumeMounts. A step uses it with ref: { name: x } (or ref.resolver for a remote one) and must then set none of those fields itself; doing so is a validation error. StepActions are stable; check kubectl api-resources | grep stepaction for the version served on the cluster in front of you. The point for the competency: a platform team ships one kaniko-build StepAction and tenants compose Tasks from it without copying container details.

Tekton Chains is the supply-chain half. It watches completed TaskRuns and PipelineRuns, signs them and any type-hinted image results (IMAGE_URL and IMAGE_DIGEST results, or artifacts.outputs) with a key from the signing-secrets Secret in tekton-chains (cosign, x509 or KMS), produces an in-toto attestation in slsa/v1 or slsa/v2alpha4 format, stores it in the OCI registry next to the image, in Tekton annotations, or in a bucket, and marks the run chains.tekton.dev/signed: "true". Formats, storage and signer are settings in the chains-config ConfigMap (artifacts.taskrun.format, artifacts.oci.storage, transparency.enabled). Section 5.6 verifies those signatures at admission; here the fact to hold is that the pipeline pod never holds the signing key.

Debugging, which is just pod debugging

and the tkn shortcuts
SymptomMost likelyCommand
Run stays Pendingworkspace PVC unbound, or quotakubectl describe pod -l tekton.dev/pipelineRun=<name>
Step fails immediatelyimage pull, bad command, missing paramtkn pipelinerun logs <name> -f
"couldn't find task"wrong namespace, or a resolver misconfiguredtkn task list
Permission denied pushingSA has no registry secretkubectl get sa <sa> -o yaml
Everything ran in parallelmissing runAfterread the Pipeline, not the logs
Run Succeeded, nothing shippeda step swallowed a non-zero exittkn taskrun describe --last

Every step is a container named step-<name> in the TaskRun's pod, so kubectl logs <pod> -c step-build works when tkn is being unhelpful. The condition on the run carries the summary message, and kubectl get taskrun -o jsonpath='{.items[0].status.conditions[0].message}' is the scriptable form the exam's grader would use.

Tekton vs Argo Workflows

Both run DAGs of containers, and the exam lists both. The distinction to state: Tekton is made for CI: build, test, publish artifacts, triggered by git events, with a task catalog built around that. Argo Workflows (section 3.4) is general orchestration: provisioning sequences, scheduled batch jobs, anything run-to-completion. Overlapping capability, different focus, and section 3.6 makes you defend the choice.

Status vocabulary, timeouts, cancellation and control flow

what the condition says · timeouts.pipeline/tasks/finally · spec.status values · when, retries, onError

A run has one condition, type Succeeded, and the pair (status, reason) is the whole story. Learn the table and kubectl get pipelinerun -o jsonpath='{.items[*].status.conditions[0].reason}' becomes a diagnosis.

statusreasonMeaning
UnknownStarted · Running · Cancelledpicked up; validated and working; cancellation requested but not finished
TrueSucceededevery task ran and passed
TrueCompletedpassed, but one or more tasks were skipped by when; the skipped ones are listed under status.skippedTasks with their reason
FalseFaileda TaskRun failed; the message counts "Tasks Completed: N (Failed: M, Cancelled 0), Skipped: K"
FalsePipelineRunTimeouttimeouts.pipeline elapsed; child TaskRuns are canceled
FalseCancelled · CancelledRunFinally · StoppedRunFinallyyou set spec.status
FalseCreateRunFailedcould not create a child TaskRun (quota, admission, missing ServiceAccount)
FalseParameterMissing · PipelineValidationFailed · InvalidWorkspaceBinding · CouldntGetTask · InvalidTaskResultReference · ResolvingPipelineRef...permanent validation errors, often prefixed [User error] in the message; nothing ran

TaskRun reasons add the pod-level causes: TaskRunTimeout, TaskRunImagePullFailed, PodEvicted, StepOOM (a step container was OOMKilled: the run fails even if the exit code is 0), InitContainerOOM, StepFailed, ToBeRetried (a retry is pending; previous attempts sit under status.retriesStatus). Each step's terminationReason and exit code are under status.steps[].

Timeouts

  • spec.timeouts.pipeline (default: the global default-timeout-minutes, 60), timeouts.tasks for the non-finally tasks together, timeouts.finally for the finally tasks; the constraint pipeline >= tasks + finally is validated. "0" disables a section (then pipeline must be 0 too).
  • Per task: pipeline.spec.tasks[].timeout, or at run time taskRunSpecs[].timeout. When timeouts.tasks is hit, finally tasks still run if timeouts.finally allows.

Canceling and pausing, by setting spec.status

  • Cancelled: TaskRuns canceled, pods deleted, retries skipped, pending finally tasks not scheduled.
  • CancelledRunFinally: same, but finally tasks run (cleanup and notification still happen).
  • StoppedRunFinally: running TaskRuns finish normally with their retries, nothing new starts, then finally runs.
  • PipelineRunPending: create the run without starting it (a manual gate or a quota-aware queue); clear the field to start.

Control flow fields

  • retries: N on a pipeline task re-runs the TaskRun on failure; $(context.pipelineTask.retries) and retry-count are available inside.
  • onError: continue on a pipeline task lets the pipeline proceed past a failure (default stopAndFail); on a step, onError: continue lets later steps run and exposes $(steps.<name>.exitCode.path).
  • when: input, operator: in|notin, values; a CEL form (cel: "'$(params.env)' == 'prod'") exists behind a feature flag. A guarded task's dependents are not automatically guarded: cascade the when or compose a sub-pipeline.
  • In finally, $(tasks.<name>.status) is Succeeded, Failed or None (skipped), and $(tasks.status) aggregates to Succeeded, Completed, Failed or None; finally tasks cannot be ordered among themselves.
  • matrix.params fans one pipeline task out over every combination (cap 256 by default, default-max-matrix-combinations-count); TaskRuns are named <run>-<task>-<n> and array results from a fanned-out task are consumed with $(tasks.x.results.y[*]).
  • Results travel in the container termination message, capped at 4096 bytes per step including Tekton's own metadata; larger results need the results-from: sidecar-logs feature flag. "Termination message is above max allowed size" is the error.
How this gets tested

"The pipeline must always publish a report even when the build fails, and a failed scan must not stop the report" is two fields: the report task goes in finally, and either the scan task carries onError: continue or the report reads $(tasks.scan.status) to decide what to say. "Stop the running pipeline but let cleanup run" is spec.status: CancelledRunFinally, not deleting the PipelineRun.

Exercises

tick the dot when its check passes

Read examples/tekton/pipeline.yaml first: pipeline build-and-scan, params repo-url and image, one workspace shared carried clone → build → scan, and a ready-made PipelineRun with generateName. One catch the file does not solve for you: the seeded demo-app repo contains only a README, and kaniko needs a Dockerfile. Supplying one is the exercise:

source lab.env   # push needs credentials
git clone "http://lab:${GITEA_PASS}@gitea.lab:3000/lab/demo-app.git" /tmp/demo-app && cd /tmp/demo-app
cat > Dockerfile <<'EOF'
FROM ghcr.io/nginxinc/nginx-unprivileged:1.27-alpine
COPY README.md /usr/share/nginx/html/index.html
EOF
git add . && git commit -m "make it buildable" && git push
cd - && kubectl create -f examples/tekton/pipeline.yaml   # Task + Pipeline + one PipelineRun
tkn pipelinerun logs --last -f
outputcaptured 2026-08-26
$ source lab.env   # push needs credentials
$ git clone "http://lab:${GITEA_PASS}@gitea.lab:3000/lab/demo-app.git" /tmp/demo-app && cd /tmp/demo-app
Cloning into '/tmp/demo-app'...
$ cat > Dockerfile <<'EOF'
FROM ghcr.io/nginxinc/nginx-unprivileged:1.27-alpine
COPY README.md /usr/share/nginx/html/index.html
EOF
$ git add . && git commit -m "make it buildable" && git push
[main 1bfbf17] make it buildable
 1 file changed, 2 insertions(+)
 create mode 100644 Dockerfile
To http://gitea.lab:3000/lab/demo-app.git
   8f7fd2d..1bfbf17  main -> main
$ cd - && kubectl create -f examples/tekton/pipeline.yaml   # Task + Pipeline + one PipelineRun
task.tekton.dev/trivy-scan created
pipeline.tekton.dev/build-and-scan created
pipelinerun.tekton.dev/build-and-scan-2pz8k created
$ tkn pipelinerun logs --last -f
[clone : clone] + '[' false '=' true ]
[clone : clone] + '[' false '=' true ]
[clone : clone] + '[' false '=' true ]
[clone : clone] + CHECKOUT_DIR=/workspace/output/
[clone : clone] + '[' true '=' true ]
[clone : clone] + cleandir
[clone : clone] + '[' -d /workspace/output/ ]
[clone : clone] + rm -rf '/workspace/output//*'
[clone : clone] + rm -rf '/workspace/output//.[!.]*'
[clone : clone] + rm -rf '/workspace/output//..?*'
[clone : clone] + test -z 
[clone : clone] + test -z 
[clone : clone] + test -z 
[clone : clone] + git config --global --add safe.directory /workspace/output
[clone : clone] + /ko-app/git-init '-url=http://gitea.lab:3000/lab/demo-app.git' '-revision=' '-refspec=' '-path=/workspace/output/' '-sslVerify=true' '-submodules=true' '-depth=1' '-sparseCheckoutDirectories='
[clone : clone] {"level":"info","ts":1787798994.1044528,"caller":"git/git.go:176","msg":"Successfully cloned http://gitea.lab:3000/lab/demo-app.git @ 1bfbf17970f14a2cc50bd19a49c9ff2a835c3519 (grafted, HEAD) in path /workspace/output/"}
[clone : clone] {"level":"info","ts":1787798994.127507,"caller":"git/git.go:215","msg":"Successfully initialized and updated submodules in path /workspace/output/"}
[clone : clone] + cd /workspace/output/
[clone : clone] + git rev-parse HEAD
[clone : clone] + RESULT_SHA=1bfbf17970f14a2cc50bd19a49c9ff2a835c3519
[clone : clone] + EXIT_CODE=0
[clone : clone] + '[' 0 '!=' 0 ]
[clone : clone] + git log -1 '--pretty=%ct'
[clone : clone] + RESULT_COMMITTER_DATE=1787798875
[clone : clone] + printf '%s' 1787798875
[clone : clone] + printf '%s' 1bfbf17970f14a2cc50bd19a49c9ff2a835c3519
[clone : clone] + printf '%s' http://gitea.lab:3000/lab/demo-app.git

[build : build-and-push] INFO[0000] Retrieving image manifest ghcr.io/nginxinc/nginx-unprivileged:1.27-alpine 
[build : build-and-push] INFO[0000] Retrieving image ghcr.io/nginxinc/nginx-unprivileged:1.27-alpine from registry ghcr.io 
[build : build-and-push] INFO[0000] Built cross stage deps: map[]                
[build : build-and-push] INFO[0000] Retrieving image manifest ghcr.io/nginxinc/nginx-unprivileged:1.27-alpine 
[build : build-and-push] INFO[0000] Returning cached image manifest              
[build : build-and-push] INFO[0000] Executing 0 build triggers                   
[build : build-and-push] INFO[0000] Unpacking rootfs as cmd COPY README.md /usr/share/nginx/html/index.html requires it. 
[build : build-and-push] INFO[0002] COPY README.md /usr/share/nginx/html/index.html 
[build : build-and-push] INFO[0002] Taking snapshot of files...                  
[build : build-and-push] INFO[0002] Pushing image to kind-registry:5000/demo:v1  
[build : build-and-push] INFO[0003] Pushed image to 1 destinations               

[build : write-url] kind-registry:5000/demo:v1

[scan : scan] 2026-08-27T02:50:26Z	INFO	[vulndb] Need to update DB
[scan : scan] 2026-08-27T02:50:26Z	INFO	[vulndb] Downloading vulnerability DB...
[scan : scan] 2026-08-27T02:50:26Z	INFO	[vulndb] Downloading artifact...	repo="mirror.gcr.io/aquasec/trivy-db:2"
[scan : scan] 2026-08-27T02:50:38Z	INFO	[vulndb] Artifact successfully downloaded	repo="mirror.gcr.io/aquasec/trivy-db:2"
[scan : scan] 2026-08-27T02:50:38Z	INFO	[vuln] Vulnerability scanning is enabled
[scan : scan] 2026-08-27T02:50:38Z	INFO	[secret] Secret scanning is enabled
[scan : scan] 2026-08-27T02:50:38Z	INFO	[secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning
[scan : scan] 2026-08-27T02:50:38Z	INFO	[secret] Please see https://trivy.dev/docs/v0.74/guide/scanner/secret#recommendation for faster secret detection
[scan : scan] 2026-08-27T02:50:40Z	INFO	Detected OS	family="alpine" version="3.21.3"
[scan : scan] 2026-08-27T02:50:40Z	INFO	[alpine] Detecting vulnerabilities...	os_version="3.21" repository="3.21" pkg_num=67
[scan : scan] 2026-08-27T02:50:40Z	INFO	Number of language-specific files	num=0
[scan : scan] 2026-08-27T02:50:40Z	WARN	Using severities from other vendors for some vulnerabilities. Read https://trivy.dev/docs/v0.74/guide/scanner/vulnerability#severity-selection for details.
[scan : scan] 
[scan : scan] Report Summary
[scan : scan] 
[scan : scan] ┌────────────────────────────────────────────┬────────┬─────────────────┬─────────┐
[scan : scan] │                   Target                   │  Type  │ Vulnerabilities │ Secrets │
[scan : scan] ├────────────────────────────────────────────┼────────┼─────────────────┼─────────┤
[scan : scan] │ kind-registry:5000/demo:v1 (alpine 3.21.3) │ alpine │       36        │    -    │
[scan : scan] └────────────────────────────────────────────┴────────┴─────────────────┴─────────┘
[scan : scan] Legend:
[scan : scan] - '-': Not scanned
[scan : scan] - '0': Clean (no security findings detected)
[scan : scan] 
[scan : scan] 
[scan : scan] kind-registry:5000/demo:v1 (alpine 3.21.3)
[scan : scan] ==========================================
[scan : scan] Total: 36 (HIGH: 34, CRITICAL: 2)
[scan : scan] 
[scan : scan] ┌──────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐
[scan : scan] │   Library    │ Vulnerability  │ Severity │ Status │ Installed Version │ Fixed Version │                            Title                             │
[scan : scan] ├──────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │ c-ares       │ CVE-2026-33630 │ HIGH     │ fixed  │ 1.34.3-r0         │ 1.34.8-r0     │ c-ares: c-ares: Use-after-free / double-free in              │
[scan : scan] │              │                │          │        │                   │               │ query-completion handling                                    │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-33630                   │
[scan : scan] ├──────────────┼────────────────┼──────────┤        ├───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │ libcrypto3   │ CVE-2026-31789 │ CRITICAL │        │ 3.3.3-r0          │ 3.3.7-r0      │ openssl: OpenSSL: Heap buffer overflow on 32-bit systems     │
[scan : scan] │              │                │          │        │                   │               │ from large X.509 certificate...                              │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-31789                   │
[scan : scan] │              ├────────────────┼──────────┤        │                   ├───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │              │ CVE-2025-15467 │ HIGH     │        │                   │ 3.3.6-r0      │ openssl: OpenSSL: Remote code execution or Denial of Service │
[scan : scan] │              │                │          │        │                   │               │ via oversized Initialization...                              │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2025-15467                   │
[scan : scan] │              ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
[scan : scan] │              │ CVE-2025-69421 │          │        │                   │               │ openssl: OpenSSL: Denial of Service via malformed PKCS#12    │
[scan : scan] │              │                │          │        │                   │               │ file processing                                              │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2025-69421                   │
[scan : scan] │              ├────────────────┤          │        │                   ├───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │              │ CVE-2026-28387 │          │        │                   │ 3.3.7-r0      │ openssl: OpenSSL: Arbitrary code execution due to            │
[scan : scan] │              │                │          │        │                   │               │ use-after-free in DANE TLSA authentication...                │
… (80 lines omitted)
[scan : scan] │              ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
[scan : scan] │              │ CVE-2025-32415 │          │        │                   │               │ libxml2: Out-of-bounds Read in xmlSchemaIDCFillNodeTables    │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2025-32415                   │
[scan : scan] │              ├────────────────┤          │        │                   ├───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │              │ CVE-2025-49794 │          │        │                   │ 2.13.9-r0     │ libxml: Heap use after free (UAF) leads to Denial of service │
[scan : scan] │              │                │          │        │                   │               │ (DoS)...                                                     │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2025-49794                   │
[scan : scan] │              ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
[scan : scan] │              │ CVE-2025-49795 │          │        │                   │               │ libxml: Null pointer dereference leads to Denial of service  │
[scan : scan] │              │                │          │        │                   │               │ (DoS)                                                        │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2025-49795                   │
[scan : scan] │              ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
[scan : scan] │              │ CVE-2025-49796 │          │        │                   │               │ libxml: Type confusion leads to Denial of service (DoS)      │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2025-49796                   │
[scan : scan] │              ├────────────────┤          │        │                   ├───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │              │ CVE-2026-6732  │          │        │                   │ 2.13.9-r1     │ libxml2: libxml2: Denial of Service via crafted              │
[scan : scan] │              │                │          │        │                   │               │ XSD-validated document                                       │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-6732                    │
[scan : scan] ├──────────────┼────────────────┤          │        ├───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │ musl         │ CVE-2026-40200 │          │        │ 1.2.5-r9          │ 1.2.5-r11     │ musl: musl libc: Arbitrary code execution and denial of      │
[scan : scan] │              │                │          │        │                   │               │ service via stack-based...                                   │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-40200                   │
[scan : scan] ├──────────────┤                │          │        │                   │               │                                                              │
[scan : scan] │ musl-utils   │                │          │        │                   │               │                                                              │
[scan : scan] │              │                │          │        │                   │               │                                                              │
[scan : scan] │              │                │          │        │                   │               │                                                              │
[scan : scan] ├──────────────┼────────────────┤          │        ├───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │ nghttp2-libs │ CVE-2026-27135 │          │        │ 1.64.0-r0         │ 1.68.1        │ nghttp2: nghttp2: Denial of Service via malformed HTTP/2     │
[scan : scan] │              │                │          │        │                   │               │ frames after session termination...                          │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-27135                   │
[scan : scan] ├──────────────┼────────────────┤          │        ├───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │ xz-libs      │ CVE-2025-31115 │          │        │ 5.6.3-r0          │ 5.6.3-r1      │ xz: XZ has a heap-use-after-free bug in threaded .xz decoder │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2025-31115                   │
[scan : scan] ├──────────────┼────────────────┤          │        ├───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
[scan : scan] │ zlib         │ CVE-2026-22184 │          │        │ 1.3.1-r2          │ 1.3.2-r0      │ zlib: zlib: Arbitrary code execution via buffer overflow in  │
[scan : scan] │              │                │          │        │                   │               │ untgz utility                                                │
[scan : scan] │              │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-22184                   │
[scan : scan] └──────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘

failed to get logs for task scan : container step-scan has failed  : [{"key":"StartedAt","value":"2026-08-27T02:50:26.400Z","type":3}]

If the scan step fails instead, that is the CVE gate doing its job on today's base image; read which CVEs and decide as a platform engineer would (bump the base, or ignore-unfixed). Rerun later with tkn pipeline start build-and-scan --last.

verify: the run either Succeeds or stops at the scan gate (the capture above did exactly that); either way clone and build ran, and the push was real: skopeo inspect --tls-verify=false docker://localhost:5001/demo:v1 | jq .Digest.

Write a Task manifest-lint that takes a param path, mounts workspace source, and runs kustomize build $(params.path) from an image that has kustomize (registry.k8s.io/kustomize/kustomize:v5.0.0 works; if pulls are slow, busybox counting grep -c '^kind:' over the cloned manifests is an honest substitute). Emit a result objects containing the object count. Run it with tkn task start against the platform repo cloned by a git-clone task, or standalone with a fresh clone step.

verify: tkn taskrun describe --last shows your result value, non-zero.

The trivy-scan task exits 1 on HIGH/CRITICAL findings, and an old image guarantees some:

tkn task start trivy-scan -p image=nginx:1.19 -w name=source,emptyDir="" --showlog
tkn taskrun list | head -3
kubectl get taskrun -o jsonpath='{.items[0].status.conditions[0].message}'
outputcaptured 2026-08-26
$ tkn task start trivy-scan -p image=nginx:1.19 -w name=source,emptyDir="" --showlog
TaskRun started: trivy-scan-run-v5vpv
Waiting for logs to be available...
[scan] 2026-08-27T02:51:18Z	INFO	[vulndb] Need to update DB
[scan] 2026-08-27T02:51:18Z	INFO	[vulndb] Downloading vulnerability DB...
[scan] 2026-08-27T02:51:18Z	INFO	[vulndb] Downloading artifact...	repo="mirror.gcr.io/aquasec/trivy-db:2"
[scan] 2026-08-27T02:51:31Z	INFO	[vulndb] Artifact successfully downloaded	repo="mirror.gcr.io/aquasec/trivy-db:2"
[scan] 2026-08-27T02:51:31Z	INFO	[vuln] Vulnerability scanning is enabled
[scan] 2026-08-27T02:51:31Z	INFO	[secret] Secret scanning is enabled
[scan] 2026-08-27T02:51:31Z	INFO	[secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning
[scan] 2026-08-27T02:51:31Z	INFO	[secret] Please see https://trivy.dev/docs/v0.74/guide/scanner/secret#recommendation for faster secret detection
[scan] 2026-08-27T02:51:37Z	INFO	[javadb] Downloading Java DB...
[scan] 2026-08-27T02:51:37Z	INFO	[javadb] Downloading artifact...	repo="mirror.gcr.io/aquasec/trivy-java-db:1"
[scan] 2026-08-27T02:52:29Z	INFO	[javadb] Artifact successfully downloaded	repo="mirror.gcr.io/aquasec/trivy-java-db:1"
[scan] 2026-08-27T02:52:29Z	INFO	[javadb] Java DB is cached for 3 days. If you want to update the database more frequently, "trivy clean --java-db" command clears the DB cache.
[scan] 2026-08-27T02:52:29Z	INFO	Detected OS	family="debian" version="10.9"
[scan] 2026-08-27T02:52:29Z	INFO	[debian] Detecting vulnerabilities...	os_version="10" pkg_num=135
[scan] 2026-08-27T02:52:29Z	INFO	Number of language-specific files	num=0
[scan] 2026-08-27T02:52:29Z	WARN	Using severities from other vendors for some vulnerabilities. Read https://trivy.dev/docs/v0.74/guide/scanner/vulnerability#severity-selection for details.
[scan] 2026-08-27T02:52:29Z	WARN	This OS version is no longer supported by the distribution	family="debian" version="10.9"
[scan] 2026-08-27T02:52:29Z	WARN	The vulnerability detection may be insufficient because security updates are not provided
[scan] 
[scan] Report Summary
[scan] 
[scan] ┌──────────────────────────┬────────┬─────────────────┬─────────┐
[scan] │          Target          │  Type  │ Vulnerabilities │ Secrets │
[scan] ├──────────────────────────┼────────┼─────────────────┼─────────┤
[scan] │ nginx:1.19 (debian 10.9) │ debian │       189       │    -    │
[scan] └──────────────────────────┴────────┴─────────────────┴─────────┘
[scan] Legend:
[scan] - '-': Not scanned
[scan] - '0': Clean (no security findings detected)
[scan] 
[scan] 
[scan] nginx:1.19 (debian 10.9)
[scan] ========================
[scan] Total: 189 (HIGH: 147, CRITICAL: 42)
[scan] 
[scan] ┌─────────────────────┬────────────────┬──────────┬──────────────┬───────────────────────────┬───────────────────────────┬──────────────────────────────────────────────────────────────┐
[scan] │       Library       │ Vulnerability  │ Severity │    Status    │     Installed Version     │       Fixed Version       │                            Title                             │
[scan] ├─────────────────────┼────────────────┼──────────┼──────────────┼───────────────────────────┼───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │ curl                │ CVE-2022-32221 │ CRITICAL │ fixed        │ 7.64.0-4+deb10u2          │ 7.64.0-4+deb10u4          │ curl: POST following PUT confusion                           │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-32221                   │
[scan] │                     ├────────────────┼──────────┤              │                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2021-22946 │ HIGH     │              │                           │ 7.64.0-4+deb10u3          │ curl: Requirement to use TLS not properly enforced for IMAP, │
[scan] │                     │                │          │              │                           │                           │ POP3, and...                                                 │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2021-22946                   │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2022-22576 │          │              │                           │                           │ curl: OAUTH2 bearer bypass in connection re-use              │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-22576                   │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2022-27781 │          │              │                           │                           │ curl: CERTINFO never-ending busy-loop                        │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-27781                   │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2022-27782 │          │              │                           │                           │ curl: TLS and SSH connection too eager reuse                 │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-27782                   │
[scan] │                     ├────────────────┤          │              │                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2023-27533 │          │              │                           │ 7.64.0-4+deb10u6          │ curl: TELNET option IAC injection                            │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2023-27533                   │
[scan] │                     ├────────────────┤          │              │                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2023-27534 │          │              │                           │ 7.64.0-4+deb10u9          │ curl: SFTP path ~ resolving discrepancy                      │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2023-27534                   │
[scan] ├─────────────────────┼────────────────┼──────────┤              ├───────────────────────────┼───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │ dpkg                │ CVE-2022-1664  │ CRITICAL │              │ 1.19.7                    │ 1.19.8                    │ Dpkg::Source::Archive in dpkg, the Debian package management │
[scan] │                     │                │          │              │                           │                           │ system, b ...                                                │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-1664                    │
[scan] ├─────────────────────┼────────────────┼──────────┼──────────────┼───────────────────────────┼───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │ e2fsprogs           │ CVE-2022-1304  │ HIGH     │ affected     │ 1.44.5-1+deb10u3          │                           │ e2fsprogs: out-of-bounds read/write via crafted filesystem   │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-1304                    │
[scan] ├─────────────────────┼────────────────┤          ├──────────────┼───────────────────────────┼───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │ gcc-8-base          │ CVE-2018-12886 │          │ will_not_fix │ 8.3.0-6                   │                           │ gcc: spilling of stack protection address in cfgexpand.c and │
[scan] │                     │                │          │              │                           │                           │ function.c leads to...                                       │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2018-12886                   │
[scan] │                     ├────────────────┤          │              │                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2019-15847 │          │              │                           │                           │ gcc: POWER9 "DARN" RNG intrinsic produces repeated output    │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2019-15847                   │
[scan] ├─────────────────────┼────────────────┤          ├──────────────┼───────────────────────────┼───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │ gzip                │ CVE-2022-1271  │          │ fixed        │ 1.9-3                     │ 1.9-3+deb10u1             │ gzip: arbitrary-file-write vulnerability                     │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-1271                    │
[scan] ├─────────────────────┼────────────────┼──────────┤              ├───────────────────────────┼───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │ libc-bin            │ CVE-2021-33574 │ CRITICAL │              │ 2.28-10                   │ 2.28-10+deb10u2           │ glibc: mq_notify does not handle separately allocated thread │
[scan] │                     │                │          │              │                           │                           │ attributes                                                   │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2021-33574                   │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2021-35942 │          │              │                           │                           │ glibc: Arbitrary read in wordexp()                           │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2021-35942                   │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2022-23218 │          │              │                           │                           │ glibc: Stack-based buffer overflow in svcunix_create via     │
[scan] │                     │                │          │              │                           │                           │ long pathnames                                               │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-23218                   │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2022-23219 │          │              │                           │                           │ glibc: Stack-based buffer overflow in sunrpc clnt_create via │
[scan] │                     │                │          │              │                           │                           │ a long pathname                                              │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-23219                   │
[scan] │                     ├────────────────┼──────────┼──────────────┤                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2020-1751  │ HIGH     │ will_not_fix │                           │                           │ glibc: array overflow in backtrace functions for powerpc     │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2020-1751                    │
[scan] │                     ├────────────────┤          ├──────────────┤                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2020-1752  │          │ fixed        │                           │ 2.28-10+deb10u2           │ glibc: use-after-free in glob() function when expanding      │
[scan] │                     │                │          │              │                           │                           │ ~user                                                        │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2020-1752                    │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2020-6096  │          │              │                           │                           │ glibc: signed comparison vulnerability in the ARMv7 memcpy   │
[scan] │                     │                │          │              │                           │                           │ function                                                     │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2020-6096                    │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2021-3326  │          │              │                           │                           │ glibc: Assertion failure in ISO-2022-JP-3 gconv module       │
[scan] │                     │                │          │              │                           │                           │ related to combining characters                              │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2021-3326                    │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
… (554 lines omitted)
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2023-0215  │          │              │                           │                           │ openssl: use-after-free following BIO_new_NDEF               │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2023-0215                    │
[scan] │                     ├────────────────┤          │              │                           │                           ├──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2023-0286  │          │              │                           │                           │ openssl: X.400 address type confusion in X.509 GeneralName   │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2023-0286                    │
[scan] │                     ├────────────────┤          │              │                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2023-0464  │          │              │                           │ 1.1.1n-0+deb10u5          │ openssl: Denial of service by excessive resource usage in    │
[scan] │                     │                │          │              │                           │                           │ verifying X509 policy...                                     │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2023-0464                    │
[scan] ├─────────────────────┼────────────────┤          ├──────────────┼───────────────────────────┼───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │ perl-base           │ CVE-2020-16156 │          │ affected     │ 5.28.1-6+deb10u1          │                           │ perl-CPAN: Bypass of verification of signatures in CHECKSUMS │
[scan] │                     │                │          │              │                           │                           │ files                                                        │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2020-16156                   │
[scan] │                     ├────────────────┤          │              │                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2023-31484 │          │              │                           │                           │ perl: CPAN.pm does not verify TLS certificates when          │
[scan] │                     │                │          │              │                           │                           │ downloading distributions over HTTPS...                      │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2023-31484                   │
[scan] ├─────────────────────┼────────────────┼──────────┼──────────────┼───────────────────────────┼───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │ zlib1g              │ CVE-2022-37434 │ CRITICAL │ fixed        │ 1:1.2.11.dfsg-1           │ 1:1.2.11.dfsg-1+deb10u2   │ zlib: heap-based buffer over-read and overflow in inflate()  │
[scan] │                     │                │          │              │                           │                           │ in inflate.c via a...                                        │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2022-37434                   │
[scan] │                     ├────────────────┤          ├──────────────┤                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2023-45853 │          │ will_not_fix │                           │                           │ zlib: integer overflow and resultant heap-based buffer       │
[scan] │                     │                │          │              │                           │                           │ overflow in zipOpenNewFileInZip4_6                           │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2023-45853                   │
[scan] │                     ├────────────────┼──────────┼──────────────┤                           ├───────────────────────────┼──────────────────────────────────────────────────────────────┤
[scan] │                     │ CVE-2018-25032 │ HIGH     │ fixed        │                           │ 1:1.2.11.dfsg-1+deb10u1   │ zlib: A flaw found in zlib when compressing (not             │
[scan] │                     │                │          │              │                           │                           │ decompressing) certain inputs...                             │
[scan] │                     │                │          │              │                           │                           │ https://avd.aquasec.com/nvd/cve-2018-25032                   │
[scan] └─────────────────────┴────────────────┴──────────┴──────────────┴───────────────────────────┴───────────────────────────┴──────────────────────────────────────────────────────────────┘

[sbom] 2026/08/27 02:52:30 Skipping step because a previous step failed

$ tkn taskrun list | head -3
NAME                         STARTED         DURATION   STATUS
trivy-scan-run-v5vpv         1 minute ago    1m28s      Failed
build-and-scan-2pz8k-scan    2 minutes ago   29s        Failed
$ kubectl get taskrun -o jsonpath='{.items[0].status.conditions[0].message}'
All Steps have completed executing

Then reason one level up: in the full pipeline, a scan failure stops anything sequenced after it via runAfter, which is the entire supply-chain argument for putting the gate in the pipeline instead of in a ticket.

verify: the run is Failed and the condition message names the step that exited non-zero. Naming which task killed a run from kubectl output alone, without the dashboard, is the competency.

EventListener + TriggerBinding (extract the clone URL and SHA from Gitea's push payload) + TriggerTemplate (PipelineRun with those as params). Expose the EventListener service as a LoadBalancer so the Gitea container can reach it across the kind network, then add the webhook in the Gitea UI (repo settings → webhooks, target http://<EXTERNAL-IP>:8080). Push a commit to demo-app. Budget an hour; the payoff is having debugged webhook → listener → run once before an exam asks you to.

verify: tkn pipelinerun list grows by one without you touching kubectl, and kubectl get eventlistener shows Ready.

A StepAction is a step you can version and share the way a Task shares a whole sequence. The rule that catches people is that a step referencing one may not also carry its own image; the validation message says it plainly.

kubectl api-resources | grep -i stepaction
kubectl apply -f - <<'EOF'
apiVersion: tekton.dev/v1beta1
kind: StepAction
metadata: { name: say, namespace: default }
spec:
  image: busybox:1.36
  params:
    - name: text
      type: string
  env:
    - name: TEXT
      value: $(params.text)
  script: |
    echo "$TEXT"
EOF
kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: TaskRun
metadata: { generateName: say-run-, namespace: default }
spec:
  taskSpec:
    steps:
      - name: say
        ref: { name: say }
        params: [{ name: text, value: hello from a stepaction }]
EOF
sleep 45
tkn taskrun logs --last
kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: TaskRun
metadata: { generateName: say-bad-, namespace: default }
spec:
  taskSpec:
    steps:
      - name: say
        image: busybox:1.36
        ref: { name: say }
        params: [{ name: text, value: this will not apply }]
EOF
kubectl delete stepaction say
outputcaptured 2026-09-12
$ kubectl api-resources | grep -i stepaction
stepactions                                                                           tekton.dev/v1beta1                        true         StepAction
$ kubectl apply -f - <<'EOF'
apiVersion: tekton.dev/v1beta1
kind: StepAction
metadata: { name: say, namespace: default }
spec:
  image: busybox:1.36
  params:
    - name: text
      type: string
  env:
    - name: TEXT
      value: $(params.text)
  script: |
    echo "$TEXT"
EOF
stepaction.tekton.dev/say created
$ kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: TaskRun
metadata: { generateName: say-run-, namespace: default }
spec:
  taskSpec:
    steps:
      - name: say
        ref: { name: say }
        params: [{ name: text, value: hello from a stepaction }]
EOF
taskrun.tekton.dev/say-run-cl79p created
$ sleep 45
$ tkn taskrun logs --last
[say] hello from a stepaction
$ kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: TaskRun
metadata: { generateName: say-bad-, namespace: default }
spec:
  taskSpec:
    steps:
      - name: say
        image: busybox:1.36
        ref: { name: say }
        params: [{ name: text, value: this will not apply }]
EOF
Error from server (BadRequest): error when creating "STDIN": admission webhook "validation.webhook.pipeline.tekton.dev" denied the request: validation failed: image cannot be used with Ref: spec.taskSpec.steps[0].image
$ kubectl delete stepaction say
stepaction.tekton.dev "say" deleted from default namespace
verify: the referencing run prints the parameter, and the second create is rejected at admission with a message naming the fields that may not appear alongside ref. That list is worth memorizing; it is short.

A PipelineRun has three clocks: the whole run, the non-finally tasks, and the finally tasks. They must add up, and the one that fires decides the reason string you will be asked to explain.

kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata: { generateName: slow-, namespace: default }
spec:
  timeouts: { pipeline: 1m, tasks: 40s, finally: 20s }
  pipelineSpec:
    tasks:
      - name: sleeper
        taskSpec:
          steps:
            - name: sleep
              image: busybox:1.36
              script: |
                sleep 90
    finally:
      - name: cleanup
        taskSpec:
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo cleanup ran
EOF
sleep 90
tkn pipelinerun describe --last
kubectl get pipelinerun --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].status.conditions[0]}' | jq
outputcaptured 2026-09-13
$ kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata: { generateName: slow-, namespace: default }
spec:
  timeouts: { pipeline: 1m, tasks: 40s, finally: 20s }
  pipelineSpec:
    tasks:
      - name: sleeper
        taskSpec:
          steps:
            - name: sleep
              image: busybox:1.36
              script: |
                sleep 90
    finally:
      - name: cleanup
        taskSpec:
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo cleanup ran
EOF
pipelinerun.tekton.dev/slow-5vv4l created
$ sleep 90
$ tkn pipelinerun describe --last
Name:              slow-5vv4l
Namespace:         default
Service Account:   default
Labels:
 tekton.dev/pipeline=slow-5vv4l
Annotations:
 chains.tekton.dev/cert-pipelinerun-8afc9a0d-8b68-40eb-8199-22ca1d923be1=
 chains.tekton.dev/chain-pipelinerun-8afc9a0d-8b68-40eb-8199-22ca1d923be1=
 chains.tekton.dev/payload-pipelinerun-8afc9a0d-8b68-40eb-8199-22ca1d923be1=eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsICJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YwLjIiLCAicHJlZGljYXRlIjp7ImJ1aWxkQ29uZmlnIjp7InRhc2tzIjpbeyJmaW5pc2hlZE9uIjoiMjAyNi0wOS0xM1QxMTozOToxOFoiLCAiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnt9LCAiZW52aXJvbm1lbnQiOnsiYW5ub3RhdGlvbnMiOnsicGlwZWxpbmUudGVrdG9uLmRldi9yZWxlYXNlIjoiZmNiYTUyMiJ9LCAibGFiZWxzIjp7ImFwcC5rdWJlcm5ldGVzLmlvL21hbmFnZWQtYnkiOiJ0ZWt0b24tcGlwZWxpbmVzIiwgInRla3Rvbi5kZXYvbWVtYmVyT2YiOiJ0YXNrcyIsICJ0ZWt0b24uZGV2L3BpcGVsaW5lIjoic2xvdy01dnY0bCIsICJ0ZWt0b24uZGV2L3BpcGVsaW5lUnVuIjoic2xvdy01dnY0bCIsICJ0ZWt0b24uZGV2L3BpcGVsaW5lUnVuVUlEIjoiOGFmYzlhMGQtOGI2OC00MGViLTgxOTktMjJjYTFkOTIzYmUxIiwgInRla3Rvbi5kZXYvcGlwZWxpbmVUYXNrIjoic2xlZXBlciJ9fSwgInBhcmFtZXRlcnMiOnt9fSwgIm5hbWUiOiJzbGVlcGVyIiwgInJlZiI6e30sICJzZXJ2aWNlQWNjb3VudE5hbWUiOiJkZWZhdWx0IiwgInN0YXJ0ZWRPbiI6IjIwMjYtMDktMTNUMTE6Mzg6MzlaIiwgInN0YXR1cyI6IkZhaWxlZCIsICJzdGVwcyI6W3siYW5ub3RhdGlvbnMiOm51bGwsICJhcmd1bWVudHMiOm51bGwsICJlbnRyeVBvaW50Ijoic2xlZXAgOTBcbiIsICJlbnZpcm9ubWVudCI6eyJjb250YWluZXIiOiJzbGVlcCIsICJpbWFnZSI6Im9jaTovL2RvY2tlci5pby9saWJyYXJ5L2J1c3lib3hAc2hhMjU2OjczYWFmMDkwZjNkODVhYTM0ZWUxOTk4NTdmMDNmYTNhOTVjOGVkZTJmZmQ0Y2MyY2RiNWI5NGU1NjZiMTE2NjIifX1dfV19LCAiYnVpbGRUeXBlIjoidGVrdG9uLmRldi92MS9QaXBlbGluZVJ1biIsICJidWlsZGVyIjp7ImlkIjoiaHR0cHM6Ly90ZWt0b24uZGV2L2NoYWlucy92MiJ9LCAiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnt9LCAiZW52aXJvbm1lbnQiOnsibGFiZWxzIjp7InRla3Rvbi5kZXYvcGlwZWxpbmUiOiJzbG93LTV2djRsIn19LCAicGFyYW1ldGVycyI6e319LCAibWF0ZXJpYWxzIjpbeyJkaWdlc3QiOnsic2hhMjU2IjoiNzNhYWYwOTBmM2Q4NWFhMzRlZTE5OTg1N2YwM2ZhM2E5NWM4ZWRlMmZmZDRjYzJjZGI1Yjk0ZTU2NmIxMTY2MiJ9LCAidXJpIjoib2NpOi8vZG9ja2VyLmlvL2xpYnJhcnkvYnVzeWJveCJ9XSwgIm1ldGFkYXRhIjp7ImJ1aWxkRmluaXNoZWRPbiI6IjIwMjYtMDktMTNUMTE6Mzk6MThaIiwgImJ1aWxkU3RhcnRlZE9uIjoiMjAyNi0wOS0xM1QxMTozODozOFoiLCAiY29tcGxldGVuZXNzIjp7ImVudmlyb25tZW50IjpmYWxzZSwgIm1hdGVyaWFscyI6ZmFsc2UsICJwYXJhbWV0ZXJzIjpmYWxzZX0sICJyZXByb2R1Y2libGUiOmZhbHNlfX19
 chains.tekton.dev/signature-pipelinerun-8afc9a0d-8b68-40eb-8199-22ca1d923be1=eyJwYXlsb2FkVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5pbi10b3RvK2pzb24iLCJwYXlsb2FkIjoiZXlKZmRIbHdaU0k2SW1oMGRIQnpPaTh2YVc0dGRHOTBieTVwYnk5VGRHRjBaVzFsYm5RdmRqQXVNU0lzSUNKd2NtVmthV05oZEdWVWVYQmxJam9pYUhSMGNITTZMeTl6YkhOaExtUmxkaTl3Y205MlpXNWhibU5sTDNZd0xqSWlMQ0FpY0hKbFpHbGpZWFJsSWpwN0ltSjFhV3hrUTI5dVptbG5JanA3SW5SaGMydHpJanBiZXlKbWFXNXBjMmhsWkU5dUlqb2lNakF5Tmkwd09TMHhNMVF4TVRvek9Ub3hPRm9pTENBaWFXNTJiMk5oZEdsdmJpSTZleUpqYjI1bWFXZFRiM1Z5WTJVaU9udDlMQ0FpWlc1MmFYSnZibTFsYm5RaU9uc2lZVzV1YjNSaGRHbHZibk1pT25zaWNHbHdaV3hwYm1VdWRHVnJkRzl1TG1SbGRpOXlaV3hsWVhObElqb2labU5pWVRVeU1pSjlMQ0FpYkdGaVpXeHpJanA3SW1Gd2NDNXJkV0psY201bGRHVnpMbWx2TDIxaGJtRm5aV1F0WW5raU9pSjBaV3QwYjI0dGNHbHdaV3hwYm1Weklpd2dJblJsYTNSdmJpNWtaWFl2YldWdFltVnlUMllpT2lKMFlYTnJjeUlzSUNKMFpXdDBiMjR1WkdWMkwzQnBjR1ZzYVc1bElqb2ljMnh2ZHkwMWRuWTBiQ0lzSUNKMFpXdDBiMjR1WkdWMkwzQnBjR1ZzYVc1bFVuVnVJam9pYzJ4dmR5MDFkblkwYkNJc0lDSjBaV3QwYjI0dVpHVjJMM0JwY0dWc2FXNWxVblZ1VlVsRUlqb2lPR0ZtWXpsaE1HUXRPR0kyT0MwME1HVmlMVGd4T1RrdE1qSmpZVEZrT1RJelltVXhJaXdnSW5SbGEzUnZiaTVrWlhZdmNHbHdaV3hwYm1WVVlYTnJJam9pYzJ4bFpYQmxjaUo5ZlN3Z0luQmhjbUZ0WlhSbGNuTWlPbnQ5ZlN3Z0ltNWhiV1VpT2lKemJHVmxjR1Z5SWl3Z0luSmxaaUk2ZTMwc0lDSnpaWEoyYVdObFFXTmpiM1Z1ZEU1aGJXVWlPaUprWldaaGRXeDBJaXdnSW5OMFlYSjBaV1JQYmlJNklqSXdNall0TURrdE1UTlVNVEU2TXpnNk16bGFJaXdnSW5OMFlYUjFjeUk2SWtaaGFXeGxaQ0lzSUNKemRHVndjeUk2VzNzaVlXNXViM1JoZEdsdmJuTWlPbTUxYkd3c0lDSmhjbWQxYldWdWRITWlPbTUxYkd3c0lDSmxiblJ5ZVZCdmFXNTBJam9pYzJ4bFpYQWdPVEJjYmlJc0lDSmxiblpwY205dWJXVnVkQ0k2ZXlKamIyNTBZV2x1WlhJaU9pSnpiR1ZsY0NJc0lDSnBiV0ZuWlNJNkltOWphVG92TDJSdlkydGxjaTVwYnk5c2FXSnlZWEo1TDJKMWMzbGliM2hBYzJoaE1qVTJPamN6WVdGbU1Ea3daak5rT0RWaFlUTTBaV1V4T1RrNE5UZG1NRE5tWVROaE9UVmpPR1ZrWlRKbVptUTBZMk15WTJSaU5XSTVOR1UxTmpaaU1URTJOaklpZlgxZGZWMTlMQ0FpWW5WcGJHUlVlWEJsSWpvaWRHVnJkRzl1TG1SbGRpOTJNUzlRYVhCbGJHbHVaVkoxYmlJc0lDSmlkV2xzWkdWeUlqcDdJbWxrSWpvaWFIUjBjSE02THk5MFpXdDBiMjR1WkdWMkwyTm9ZV2x1Y3k5Mk1pSjlMQ0FpYVc1MmIyTmhkR2x2YmlJNmV5SmpiMjVtYVdkVGIzVnlZMlVpT250OUxDQWlaVzUyYVhKdmJtMWxiblFpT25zaWJHRmlaV3h6SWpwN0luUmxhM1J2Ymk1a1pYWXZjR2x3Wld4cGJtVWlPaUp6Ykc5M0xUVjJkalJzSW4xOUxDQWljR0Z5WVcxbGRHVnljeUk2ZTMxOUxDQWliV0YwWlhKcFlXeHpJanBiZXlKa2FXZGxjM1FpT25zaWMyaGhNalUySWpvaU56TmhZV1l3T1RCbU0yUTROV0ZoTXpSbFpURTVPVGcxTjJZd00yWmhNMkU1TldNNFpXUmxNbVptWkRSall6SmpaR0kxWWprMFpUVTJObUl4TVRZMk1pSjlMQ0FpZFhKcElqb2liMk5wT2k4dlpHOWphMlZ5TG1sdkwyeHBZbkpoY25rdlluVnplV0p2ZUNKOVhTd2dJbTFsZEdGa1lYUmhJanA3SW1KMWFXeGtSbWx1YVhOb1pXUlBiaUk2SWpJd01qWXRNRGt0TVROVU1URTZNems2TVRoYUlpd2dJbUoxYVd4a1UzUmhjblJsWkU5dUlqb2lNakF5Tmkwd09TMHhNMVF4TVRvek9Eb3pPRm9pTENBaVkyOXRjR3hsZEdWdVpYTnpJanA3SW1WdWRtbHliMjV0Wlc1MElqcG1ZV3h6WlN3Z0ltMWhkR1Z5YVdGc2N5STZabUZzYzJVc0lDSndZWEpoYldWMFpYSnpJanBtWVd4elpYMHNJQ0p5WlhCeWIyUjFZMmxpYkdVaU9tWmhiSE5sZlgxOSIsInNpZ25hdHVyZXMiOlt7ImtleWlkIjoiU0hBMjU2OklEeXcwSkYzYmlVOXlmL3lCQUhKdU1JTm42Q3FtTUh6RWdZMm9ieTAyQTQiLCJzaWciOiJNRVFDSUZUUUU3VWhvUUZHcFJubm01M0VkOERCZ3hxSGhjT3R2NEx1cDNxejJWODlBaUJjUjdQMGRGQ3Y3dUN0aWs4YjlLaTFkQkJwL3ZBSmFmdGJqR2hrQlFYQ1FBPT0ifV19
 chains.tekton.dev/signed=true

Status

STARTED        DURATION   STATUS
1 minute ago   40s        Failed(PipelineRunTimeout)

Message

PipelineRun "slow-5vv4l" failed due to tasks failed to finish within "40s"
TaskRun(s) cancelled: slow-5vv4l-sleeper

Timeouts
 Pipeline:   1m0s
 Tasks:      40s
 Finally:    20s

Taskruns

 NAME                 TASK NAME   STARTED        DURATION   STATUS
 slow-5vv4l-sleeper   sleeper     1 minute ago   39s        Cancelled(TaskRunCancelled)
$ kubectl get pipelinerun --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].status.conditions[0]}' | jq
{
  "lastTransitionTime": "2026-09-13T11:39:18Z",
  "message": "PipelineRun \"slow-5vv4l\" failed due to tasks failed to finish within \"40s\"",
  "reason": "PipelineRunTimeout",
  "status": "False",
  "type": "Succeeded"
}
verify: the run fails on the tasks clock rather than the pipeline clock, the reason names which timeout expired, and the finally task still gets its own twenty seconds. Say what happens if tasks plus finally exceeds pipeline.

There are two cancellations. One stops everything now; the other stops the work but lets finally run, which is what you want when finally releases a lock or posts a status back to git.

# a fixed name needs a pre-delete or the second run reads a leftover from the first
kubectl delete pipelinerun cancel-me --ignore-not-found
kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata: { name: cancel-me, namespace: default }
spec:
  pipelineSpec:
    tasks:
      - name: sleeper
        taskSpec:
          steps:
            - name: sleep
              image: busybox:1.36
              script: |
                sleep 600
    finally:
      - name: cleanup
        taskSpec:
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo finally ran after cancellation
EOF
sleep 30
kubectl patch pipelinerun cancel-me --type merge -p '{"spec":{"status":"CancelledRunFinally"}}'
sleep 45
kubectl get pipelinerun cancel-me -o jsonpath='{.status.conditions[0]}' | jq
tkn taskrun list | head -5
tkn taskrun logs cancel-me-cleanup 2>/dev/null | tail -3
kubectl delete pipelinerun cancel-me
outputcaptured 2026-09-12
$ # a fixed name needs a pre-delete or the second run reads a leftover from the first
$ kubectl delete pipelinerun cancel-me --ignore-not-found
$ kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata: { name: cancel-me, namespace: default }
spec:
  pipelineSpec:
    tasks:
      - name: sleeper
        taskSpec:
          steps:
            - name: sleep
              image: busybox:1.36
              script: |
                sleep 600
    finally:
      - name: cleanup
        taskSpec:
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo finally ran after cancellation
EOF
pipelinerun.tekton.dev/cancel-me created
$ sleep 30
$ kubectl patch pipelinerun cancel-me --type merge -p '{"spec":{"status":"CancelledRunFinally"}}'
pipelinerun.tekton.dev/cancel-me patched
$ sleep 45
$ kubectl get pipelinerun cancel-me -o jsonpath='{.status.conditions[0]}' | jq
{
  "lastTransitionTime": "2026-09-13T16:39:38Z",
  "message": "PipelineRun \"cancel-me\" was cancelled",
  "reason": "Cancelled",
  "status": "False",
  "type": "Succeeded"
}
$ tkn taskrun list | head -5
NAME                   STARTED          DURATION   STATUS
cancel-me-cleanup      45 seconds ago   7s         Succeeded
cancel-me-sleeper      1 minute ago     30s        Cancelled(TaskRunCancelled)
skipper-c2xp8-always   4 hours ago      8s         Succeeded
slow-5vv4l-sleeper     5 hours ago      39s        Cancelled(TaskRunCancelled)
$ tkn taskrun logs cancel-me-cleanup 2>/dev/null | tail -3
[say] finally ran after cancellation
$ kubectl delete pipelinerun cancel-me
pipelinerun.tekton.dev "cancel-me" deleted from default namespace
verify: the PipelineRun ends with reason Cancelled and message PipelineRun "cancel-me" was cancelled, the sleeper TaskRun shows Cancelled(TaskRunCancelled), and cancel-me-cleanup shows Succeeded with finally ran after cancellation in its logs. CancelledRunningFinally is only the reason while finally is still running; read it inside those few seconds and you will see it. The other value of spec.status is Cancelled, which would have killed cancel-me-cleanup along with the sleeper and left you no logs to read.

A when that evaluates false does not fail a pipeline and does not hide either. The run succeeds and records what it skipped, which is the evidence a task asks for when the question is "why did the deploy step not run".

kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata: { generateName: skipper-, namespace: default }
spec:
  params: [{ name: env, value: staging }]
  pipelineSpec:
    params: [{ name: env, type: string }]
    tasks:
      - name: always
        taskSpec:
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo this always runs
      - name: prod-only
        when:
          - input: $(params.env)
            operator: in
            values: [prod]
        taskSpec:
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo this should not run
EOF
sleep 60
kubectl get pipelinerun --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].status.conditions[0]}' | jq
kubectl get pipelinerun --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].status.skippedTasks}' | jq
outputcaptured 2026-09-13
$ kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata: { generateName: skipper-, namespace: default }
spec:
  params: [{ name: env, value: staging }]
  pipelineSpec:
    params: [{ name: env, type: string }]
    tasks:
      - name: always
        taskSpec:
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo this always runs
      - name: prod-only
        when:
          - input: $(params.env)
            operator: in
            values: [prod]
        taskSpec:
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo this should not run
EOF
pipelinerun.tekton.dev/skipper-c2xp8 created
$ sleep 60
$ kubectl get pipelinerun --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].status.conditions[0]}' | jq
{
  "lastTransitionTime": "2026-09-13T11:44:16Z",
  "message": "Tasks Completed: 1 (Failed: 0, Cancelled 0), Skipped: 1",
  "reason": "Completed",
  "status": "True",
  "type": "Succeeded"
}
$ kubectl get pipelinerun --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].status.skippedTasks}' | jq
[
  {
    "name": "prod-only",
    "reason": "When Expressions evaluated to false",
    "whenExpressions": [
      {
        "input": "staging",
        "operator": "in",
        "values": [
          "prod"
        ]
      }
    ]
  }
]
verify: the condition is True with reason Completed and skippedTasks names prod-only with the reason it was skipped. A skipped task is a success, not a failure; be able to say so without hesitating.

A matrix turns one task into one TaskRun per combination, named predictably. It is the cheapest way to run the same check against three versions, and the naming is how you find the one that failed.

kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata: { name: fanout, namespace: default }
spec:
  pipelineSpec:
    tasks:
      - name: check
        matrix:
          params:
            - name: version
              value: ["1.35", "1.36", "1.37"]
        taskSpec:
          params: [{ name: version, type: string }]
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo checking $(params.version)
EOF
sleep 75
tkn taskrun list | head -6
kubectl get pipelinerun fanout -o jsonpath='{.status.childReferences[*].name}{"\n"}'
kubectl delete pipelinerun fanout
outputcaptured 2026-09-12
$ kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata: { name: fanout, namespace: default }
spec:
  pipelineSpec:
    tasks:
      - name: check
        matrix:
          params:
            - name: version
              value: ["1.35", "1.36", "1.37"]
        taskSpec:
          params: [{ name: version, type: string }]
          steps:
            - name: say
              image: busybox:1.36
              script: |
                echo checking $(params.version)
EOF
pipelinerun.tekton.dev/fanout created
$ sleep 75
$ tkn taskrun list | head -6
NAME               STARTED         DURATION   STATUS
fanout-check-1     1 minute ago    28s        Succeeded
fanout-check-2     1 minute ago    29s        Succeeded
fanout-check-0     1 minute ago    10s        Succeeded
say-run-cl79p      2 minutes ago   10s        Succeeded
provenance-lpwcp   5 hours ago     11s        Succeeded
$ kubectl get pipelinerun fanout -o jsonpath='{.status.childReferences[*].name}{"\n"}'
fanout-check-1 fanout-check-2 fanout-check-0
$ kubectl delete pipelinerun fanout
pipelinerun.tekton.dev "fanout" deleted from default namespace
verify: three TaskRuns exist, named fanout-check-0 through -2, and the PipelineRun's child references list all three. One failure in a matrix fails the whole task; say what that means for a matrix over environments.

Tekton Chains is not part of make cicd, so this block installs it, generates a signing key, and removes nothing afterwards: uninstall it when you are finished, or leave it if you want to keep experimenting with signing.

The release ships an empty signing-secrets, so the block replaces it with a real key pair. cosign generate-key-pair prompts for a password unless COSIGN_PASSWORD is set, which is why it is set to an empty one here; do not do that where the key matters. The point is that no pipeline change is needed: Chains watches completed TaskRuns and signs what they report.

kubectl apply -f https://storage.googleapis.com/tekton-releases/chains/latest/release.yaml
kubectl -n tekton-chains rollout status deploy/tekton-chains-controller --timeout=300s
kubectl -n tekton-chains get secret signing-secrets -o jsonpath='{.data}{"\n"}'
kubectl -n tekton-chains delete secret signing-secrets
COSIGN_PASSWORD='' cosign generate-key-pair k8s://tekton-chains/signing-secrets
kubectl -n tekton-chains get secret signing-secrets -o json | jq '.data | keys'
kubectl -n tekton-chains patch cm chains-config --type merge -p '{"data":{"artifacts.taskrun.format":"in-toto","artifacts.taskrun.storage":"tekton","artifacts.oci.storage":"tekton"}}'
kubectl -n tekton-chains rollout restart deploy/tekton-chains-controller
kubectl -n tekton-chains rollout status deploy/tekton-chains-controller --timeout=180s
kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: TaskRun
metadata: { generateName: signed-, namespace: default }
spec:
  taskSpec:
    steps:
      - name: build
        image: busybox:1.36
        script: |
          echo pretend this built something
EOF
sleep 60
kubectl get taskrun --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].metadata.annotations}' | jq 'with_entries(select(.key | startswith("chains")))'
outputcaptured 2026-09-12
$ kubectl apply -f https://storage.googleapis.com/tekton-releases/chains/latest/release.yaml
namespace/tekton-chains unchanged
secret/signing-secrets created
configmap/chains-config unchanged
deployment.apps/tekton-chains-controller unchanged
clusterrolebinding.rbac.authorization.k8s.io/tekton-chains-controller-cluster-access unchanged
clusterrole.rbac.authorization.k8s.io/tekton-chains-controller-cluster-access unchanged
clusterrole.rbac.authorization.k8s.io/tekton-chains-controller-tenant-access unchanged
clusterrolebinding.rbac.authorization.k8s.io/tekton-chains-controller-tenant-access unchanged
serviceaccount/tekton-chains-controller unchanged
role.rbac.authorization.k8s.io/tekton-chains-leader-election unchanged
rolebinding.rbac.authorization.k8s.io/tekton-chains-controller-leaderelection unchanged
role.rbac.authorization.k8s.io/tekton-chains-info unchanged
rolebinding.rbac.authorization.k8s.io/tekton-chains-info unchanged
configmap/chains-info unchanged
configmap/tekton-chains-config-leader-election unchanged
configmap/config-logging unchanged
configmap/tekton-chains-config-observability unchanged
service/tekton-chains-metrics unchanged
$ kubectl -n tekton-chains rollout status deploy/tekton-chains-controller --timeout=300s
deployment "tekton-chains-controller" successfully rolled out
$ kubectl -n tekton-chains get secret signing-secrets -o jsonpath='{.data}{"\n"}'
$ kubectl -n tekton-chains delete secret signing-secrets
secret "signing-secrets" deleted from tekton-chains namespace
$ COSIGN_PASSWORD='' cosign generate-key-pair k8s://tekton-chains/signing-secrets
Successfully created secret signing-secrets in namespace tekton-chains
Public key written to cosign.pub
$ kubectl -n tekton-chains get secret signing-secrets -o json | jq '.data | keys'
[
  "cosign.key",
  "cosign.password",
  "cosign.pub"
]
$ kubectl -n tekton-chains patch cm chains-config --type merge -p '{"data":{"artifacts.taskrun.format":"in-toto","artifacts.taskrun.storage":"tekton","artifacts.oci.storage":"tekton"}}'
configmap/chains-config patched
$ kubectl -n tekton-chains rollout restart deploy/tekton-chains-controller
deployment.apps/tekton-chains-controller restarted
$ kubectl -n tekton-chains rollout status deploy/tekton-chains-controller --timeout=180s
Waiting for deployment spec update to be observed...
Waiting for deployment "tekton-chains-controller" rollout to finish: 0 out of 1 new replicas have been updated...
Waiting for deployment "tekton-chains-controller" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "tekton-chains-controller" rollout to finish: 1 old replicas are pending termination...
deployment "tekton-chains-controller" successfully rolled out
$ kubectl create -f - <<'EOF'
apiVersion: tekton.dev/v1
kind: TaskRun
metadata: { generateName: signed-, namespace: default }
spec:
  taskSpec:
    steps:
      - name: build
        image: busybox:1.36
        script: |
          echo pretend this built something
EOF
taskrun.tekton.dev/signed-86hrq created
$ sleep 60
$ kubectl get taskrun --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].metadata.annotations}' | jq 'with_entries(select(.key | startswith("chains")))'
{
  "chains.tekton.dev/cert-taskrun-94974ec7-8759-49b2-bbc8-d8b1b6784ee7": "",
  "chains.tekton.dev/chain-taskrun-94974ec7-8759-49b2-bbc8-d8b1b6784ee7": "",
  "chains.tekton.dev/payload-taskrun-94974ec7-8759-49b2-bbc8-d8b1b6784ee7": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsICJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YwLjIiLCAicHJlZGljYXRlIjp7ImJ1aWxkQ29uZmlnIjp7InN0ZXBzIjpbeyJhbm5vdGF0aW9ucyI6bnVsbCwgImFyZ3VtZW50cyI6bnVsbCwgImVudHJ5UG9pbnQiOiJlY2hvIHByZXRlbmQgdGhpcyBidWlsdCBzb21ldGhpbmdcbiIsICJlbnZpcm9ubWVudCI6eyJjb250YWluZXIiOiJidWlsZCIsICJpbWFnZSI6Im9jaTovL2RvY2tlci5pby9saWJyYXJ5L2J1c3lib3hAc2hhMjU2OjczYWFmMDkwZjNkODVhYTM0ZWUxOTk4NTdmMDNmYTNhOTVjOGVkZTJmZmQ0Y2MyY2RiNWI5NGU1NjZiMTE2NjIifX1dfSwgImJ1aWxkVHlwZSI6InRla3Rvbi5kZXYvdjEvVGFza1J1biIsICJidWlsZGVyIjp7ImlkIjoiaHR0cHM6Ly90ZWt0b24uZGV2L2NoYWlucy92MiJ9LCAiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnt9LCAiZW52aXJvbm1lbnQiOnsiYW5ub3RhdGlvbnMiOnsicGlwZWxpbmUudGVrdG9uLmRldi9yZWxlYXNlIjoiZmNiYTUyMiJ9LCAibGFiZWxzIjp7ImFwcC5rdWJlcm5ldGVzLmlvL21hbmFnZWQtYnkiOiJ0ZWt0b24tcGlwZWxpbmVzIn19LCAicGFyYW1ldGVycyI6e319LCAibWF0ZXJpYWxzIjpbeyJkaWdlc3QiOnsic2hhMjU2IjoiNzNhYWYwOTBmM2Q4NWFhMzRlZTE5OTg1N2YwM2ZhM2E5NWM4ZWRlMmZmZDRjYzJjZGI1Yjk0ZTU2NmIxMTY2MiJ9LCAidXJpIjoib2NpOi8vZG9ja2VyLmlvL2xpYnJhcnkvYnVzeWJveCJ9XSwgIm1ldGFkYXRhIjp7ImJ1aWxkRmluaXNoZWRPbiI6IjIwMjYtMDktMTJUMTk6MzI6NDhaIiwgImJ1aWxkU3RhcnRlZE9uIjoiMjAyNi0wOS0xMlQxOTozMjoyN1oiLCAiY29tcGxldGVuZXNzIjp7ImVudmlyb25tZW50IjpmYWxzZSwgIm1hdGVyaWFscyI6ZmFsc2UsICJwYXJhbWV0ZXJzIjpmYWxzZX0sICJyZXByb2R1Y2libGUiOmZhbHNlfX19",
  "chains.tekton.dev/signature-taskrun-94974ec7-8759-49b2-bbc8-d8b1b6784ee7": "eyJwYXlsb2FkVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5pbi10b3RvK2pzb24iLCJwYXlsb2FkIjoiZXlKZmRIbHdaU0k2SW1oMGRIQnpPaTh2YVc0dGRHOTBieTVwYnk5VGRHRjBaVzFsYm5RdmRqQXVNU0lzSUNKd2NtVmthV05oZEdWVWVYQmxJam9pYUhSMGNITTZMeTl6YkhOaExtUmxkaTl3Y205MlpXNWhibU5sTDNZd0xqSWlMQ0FpY0hKbFpHbGpZWFJsSWpwN0ltSjFhV3hrUTI5dVptbG5JanA3SW5OMFpYQnpJanBiZXlKaGJtNXZkR0YwYVc5dWN5STZiblZzYkN3Z0ltRnlaM1Z0Wlc1MGN5STZiblZzYkN3Z0ltVnVkSEo1VUc5cGJuUWlPaUpsWTJodklIQnlaWFJsYm1RZ2RHaHBjeUJpZFdsc2RDQnpiMjFsZEdocGJtZGNiaUlzSUNKbGJuWnBjbTl1YldWdWRDSTZleUpqYjI1MFlXbHVaWElpT2lKaWRXbHNaQ0lzSUNKcGJXRm5aU0k2SW05amFUb3ZMMlJ2WTJ0bGNpNXBieTlzYVdKeVlYSjVMMkoxYzNsaWIzaEFjMmhoTWpVMk9qY3pZV0ZtTURrd1pqTmtPRFZoWVRNMFpXVXhPVGs0TlRkbU1ETm1ZVE5oT1RWak9HVmtaVEptWm1RMFkyTXlZMlJpTldJNU5HVTFOalppTVRFMk5qSWlmWDFkZlN3Z0ltSjFhV3hrVkhsd1pTSTZJblJsYTNSdmJpNWtaWFl2ZGpFdlZHRnphMUoxYmlJc0lDSmlkV2xzWkdWeUlqcDdJbWxrSWpvaWFIUjBjSE02THk5MFpXdDBiMjR1WkdWMkwyTm9ZV2x1Y3k5Mk1pSjlMQ0FpYVc1MmIyTmhkR2x2YmlJNmV5SmpiMjVtYVdkVGIzVnlZMlVpT250OUxDQWlaVzUyYVhKdmJtMWxiblFpT25zaVlXNXViM1JoZEdsdmJuTWlPbnNpY0dsd1pXeHBibVV1ZEdWcmRHOXVMbVJsZGk5eVpXeGxZWE5sSWpvaVptTmlZVFV5TWlKOUxDQWliR0ZpWld4eklqcDdJbUZ3Y0M1cmRXSmxjbTVsZEdWekxtbHZMMjFoYm1GblpXUXRZbmtpT2lKMFpXdDBiMjR0Y0dsd1pXeHBibVZ6SW4xOUxDQWljR0Z5WVcxbGRHVnljeUk2ZTMxOUxDQWliV0YwWlhKcFlXeHpJanBiZXlKa2FXZGxjM1FpT25zaWMyaGhNalUySWpvaU56TmhZV1l3T1RCbU0yUTROV0ZoTXpSbFpURTVPVGcxTjJZd00yWmhNMkU1TldNNFpXUmxNbVptWkRSall6SmpaR0kxWWprMFpUVTJObUl4TVRZMk1pSjlMQ0FpZFhKcElqb2liMk5wT2k4dlpHOWphMlZ5TG1sdkwyeHBZbkpoY25rdlluVnplV0p2ZUNKOVhTd2dJbTFsZEdGa1lYUmhJanA3SW1KMWFXeGtSbWx1YVhOb1pXUlBiaUk2SWpJd01qWXRNRGt0TVRKVU1UazZNekk2TkRoYUlpd2dJbUoxYVd4a1UzUmhjblJsWkU5dUlqb2lNakF5Tmkwd09TMHhNbFF4T1Rvek1qb3lOMW9pTENBaVkyOXRjR3hsZEdWdVpYTnpJanA3SW1WdWRtbHliMjV0Wlc1MElqcG1ZV3h6WlN3Z0ltMWhkR1Z5YVdGc2N5STZabUZzYzJVc0lDSndZWEpoYldWMFpYSnpJanBtWVd4elpYMHNJQ0p5WlhCeWIyUjFZMmxpYkdVaU9tWmhiSE5sZlgxOSIsInNpZ25hdHVyZXMiOlt7ImtleWlkIjoiU0hBMjU2OklEeXcwSkYzYmlVOXlmL3lCQUhKdU1JTm42Q3FtTUh6RWdZMm9ieTAyQTQiLCJzaWciOiJNRVlDSVFEMnpTQ2NCZmJWY3Y0S2NFVGVqeEZQdEF2bkV6S0JmbVNjZG5WRGVRMVRMZ0loQU5CWmpyMTNjbTNXd0k2QlNQUzdCMmVqYmp2ZlNKODVJeXhTblMxK3pNenUifV19",
  "chains.tekton.dev/signed": "true"
}
verify: the completed TaskRun carries a chains.tekton.dev/signed annotation set to true, added by a controller the pipeline author never mentioned. If it says failed instead, read the chains-controller log: the reason is almost always the key or the storage backend.

Self-check

answer before opening
Two tasks in a pipeline run at the same time and you did not expect it. Why?

No runAfter and no result dependency between them, so Tekton runs them concurrently by design. Order comes from explicit runAfter or from consuming a result; nothing else implies sequence.

How do files get from the clone task to the build task, and what happens if you bind the workspace to an emptyDir?

Through a workspace backed by a PVC or volumeClaimTemplate, mounted by both tasks. An emptyDir is per-pod, so each task gets an empty one and the build finds nothing: a classic "why is my workspace empty" bug.

Where do you put a step that must run whether the pipeline passed or failed?

spec.finally. It runs after all other tasks regardless of outcome, and it can inspect $(tasks.status) to branch; cleanup, notifications, and teardown belong there rather than as a last runAfter task that never executes on failure.

Why does the pipeline push to kind-registry:5000 and not localhost:5001?

Because it runs in a pod, where localhost is the pod's own network namespace. The registry has an in-cluster DNS name wired into CoreDNS and containerd; the host reaches the same store through a published port. Same content, two names, and the digest is identical from both.

Your EventListener is Ready and a push produces nothing. Diagnostic ladder?

Did the webhook deliver (Gitea's webhook delivery log)? Did the listener receive it (its pod logs)? Did an interceptor filter it out (event type, secret mismatch, CEL expression)? Did the binding extract fields the template expects (a missing param yields an invalid PipelineRun)? Each rung produces a different error; check them in order.

A PipelineRun shows status True with reason Completed rather than Succeeded. What happened?

It passed, but at least one task was skipped by a when expression; the skipped tasks and the expressions that evaluated false are listed under status.skippedTasks. Graders and scripts that check for reason == Succeeded will miss this, so check status == "True" on the Succeeded condition instead.

Which spec.status value stops new tasks, lets running ones finish and still runs finally?

StoppedRunFinally. CancelledRunFinally kills running TaskRuns but still runs finally; plain Cancelled kills everything and skips finally. PipelineRunPending is the opposite direction: a run created but not yet started.

A step referencing a StepAction also sets image. What happens, and why is the design that way?

Validation error: a step with ref may not set image, command, args, script, env or volumeMounts, because the StepAction owns them and the step only supplies params and results wiring. That separation is what lets a platform team publish one vetted StepAction and let tenants compose Tasks around it without copying container details.

Docs to know your way around

study time, not exam time
  • tekton.dev: Tasks, Pipelines, and the workspaces page (volumeClaimTemplate binding especially); Triggers' TriggerBinding examples for payload paths; the resolvers page.
  • Offline: kubectl explain pipelinerun.spec, tkn <verb> --help, and tkn task describe <name> to read a catalog task's params and workspaces without leaving the terminal.
  • tekton.dev/docs/pipelines/pipelineruns ("PipelineRun status", "Configuring a failure timeout", "Canceling"): the status/reason table and the three spec.status cancellation values.
  • tekton.dev/docs/pipelines/stepactions and /resolution: StepAction fields and the ref rules; the git, bundles, hub, cluster and http resolver parameters. tekton.dev/docs/chains/config for the chains-config keys.
free before 2.5make down-cicd