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.
make core cicdOrientation
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
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
| Piece | Carries | Syntax you will type |
|---|---|---|
| params | values down: pipeline → task → step | $(params.image) |
| workspaces | files between tasks | $(workspaces.source.path) |
| results | small strings between tasks | $(tasks.clone.results.commit) |
| when | conditional execution | when: [{input: "$(params.env)", operator: in, values: ["prod"]}] |
| finally | tasks that always run (cleanup, notify) | spec.finally[] |
| matrix | fan-out one task over a list | matrix.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.
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.
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.resolver | params | Use |
|---|---|---|
| git | url, revision, pathInRepo (anonymous clone) or org, repo, revision, pathInRepo (authenticated API) | a shared task library in a repo, pinned by revision |
| bundles | bundle (OCI ref), name, kind | tasks packaged as OCI images, signed and promoted like any image |
| hub | catalog, kind, name, version | Tekton Hub or Artifact Hub catalog tasks |
| cluster | kind, name, namespace | reference a Task in another namespace; the replacement for the deprecated ClusterTask |
| http | url | a 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
| Symptom | Most likely | Command |
|---|---|---|
| Run stays Pending | workspace PVC unbound, or quota | kubectl describe pod -l tekton.dev/pipelineRun=<name> |
| Step fails immediately | image pull, bad command, missing param | tkn pipelinerun logs <name> -f |
| "couldn't find task" | wrong namespace, or a resolver misconfigured | tkn task list |
| Permission denied pushing | SA has no registry secret | kubectl get sa <sa> -o yaml |
| Everything ran in parallel | missing runAfter | read the Pipeline, not the logs |
| Run Succeeded, nothing shipped | a step swallowed a non-zero exit | tkn 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.
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
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.
| status | reason | Meaning |
|---|---|---|
| Unknown | Started · Running · Cancelled | picked up; validated and working; cancellation requested but not finished |
| True | Succeeded | every task ran and passed |
| True | Completed | passed, but one or more tasks were skipped by when; the skipped ones are listed under status.skippedTasks with their reason |
| False | Failed | a TaskRun failed; the message counts "Tasks Completed: N (Failed: M, Cancelled 0), Skipped: K" |
| False | PipelineRunTimeout | timeouts.pipeline elapsed; child TaskRuns are canceled |
| False | Cancelled · CancelledRunFinally · StoppedRunFinally | you set spec.status |
| False | CreateRunFailed | could not create a child TaskRun (quota, admission, missing ServiceAccount) |
| False | ParameterMissing · 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 globaldefault-timeout-minutes, 60),timeouts.tasksfor the non-finally tasks together,timeouts.finallyfor the finally tasks; the constraintpipeline >= tasks + finallyis validated."0"disables a section (thenpipelinemust be 0 too).- Per task:
pipeline.spec.tasks[].timeout, or at run timetaskRunSpecs[].timeout. Whentimeouts.tasksis hit, finally tasks still run iftimeouts.finallyallows.
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: Non a pipeline task re-runs the TaskRun on failure;$(context.pipelineTask.retries)andretry-countare available inside.onError: continueon a pipeline task lets the pipeline proceed past a failure (defaultstopAndFail); on a step,onError: continuelets 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 thewhenor compose a sub-pipeline.- In
finally,$(tasks.<name>.status)isSucceeded,FailedorNone(skipped), and$(tasks.status)aggregates toSucceeded,Completed,FailedorNone; finally tasks cannot be ordered among themselves. matrix.paramsfans 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-logsfeature flag. "Termination message is above max allowed size" is the error.
"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
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 -foutputcaptured 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.
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.
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 executingThen 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.
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.
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 sayoutputcaptured 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 namespaceref. 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]}' | jqoutputcaptured 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"
}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-meoutputcaptured 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 namespaceCancelled 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}' | jqoutputcaptured 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"
]
}
]
}
]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 fanoutoutputcaptured 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 namespacefanout-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"
}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
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
- 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, andtkn 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.
make down-cicd