Step termination messages accumulate all prior results, breaking per-step 4KB budget (and the docs advice)
Summary
When a TaskRun has multiple steps that each emit results, every step container's termination message contains all previous steps' results plus its own — they accumulate as steps complete. This contradicts the Tasks docs:
If your Task writes a large number of small results, you can work around this limitation by writing each result from a separate Step so that each Step has its own termination message.
Splitting results across steps actually makes the 4KB MaxContainerTerminationMessageLength limit easier to hit: the last step's container must hold the full accumulated payload. The advice was likely true ~6 years ago but is now stale.
Reported by @aThorp96, confirmed empirically: a generate-results-at-once Task succeeds while an equivalent generate-results-sequentially Task picks up only the first few results and then fails because the termination message is truncated at 4KB and no longer valid JSON.
Root cause
Two facts combine:
- All step containers share one termination file. In
pkg/pod/entrypoint.go,terminationPath = "/tekton/termination"is passed to every step (-termination_patharg +steps[i].TerminationMessagePath)./tektonis a shared volume, so all containers point at the same file. - The entrypointer appends.
pkg/termination/write.godoespro = append(existing, pro...). This append is required intra-container (results + exit code + reason are separate writes), but the shared file makes it accumulate across containers too.
The controller does not rely on the accumulation: setTaskRunStatusBasedOnStepStatus (pkg/pod/status.go) parses each container's message independently, filters step results per-step, and dedupes task results via removeDuplicateResults — dedup exists precisely to clean up this redundancy.
It's a 2019 artifact: only task results existed then, and the shared file gave "merge for free". Per-step filtering + dedup were added later instead of fixing the root coupling.
Proposed fix: per-step termination files
Give each step its own file, e.g. /tekton/termination-{stepIndex}:
pkg/pod/entrypoint.go: per-stepTerminationMessagePathand matching-termination_patharg.cmd/entrypoint/main.goalready takes-termination_pathper step; controller already reads each container's message independently — no read-side change.
This restores an independent 4KB budget per step and makes the documented workaround actually true.
Caveats to validate
- Status/pod unit tests hardcode
/tekton/terminationand the accumulated shape — need updating. - Confirm nothing in breakpoint/debug or results-sidecar paths reads the fixed path (grep suggests only
entrypoint.go+main.go). - Entrypoint image and controller ship pinned together, so the path change is internal.
removeDuplicateResultsand intra-container append remain as a harmless safety net.
Docs follow-up
Correct/remove the stale "write each result from a separate Step" workaround in docs/tasks.md; point to the compressed termination message option (tknz: prefix) instead.
cc @aThorp96
Source: tektoncd/pipeline