Skip to content

Commit 8541212

Browse files
committed
feat(ISV-7024): Add when conditions to pipeline tasks
I reviewed the pipeline tasks to figure out if optional tasks can be skipped using native "when" conditions. In some cases this is applicable, however there are still limitation where it can't be used. To document a guidelines I am adding a doc with examples and decision tree of where the "when" can or can't be used. Signed-off-by: Ales Raszka <araszka@redhat.com>
1 parent 6be33f2 commit 8541212

3 files changed

Lines changed: 223 additions & 41 deletions

File tree

ansible/roles/operator-pipeline/templates/openshift/pipelines/operator-hosted-pipeline.yml

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,8 @@ spec:
315315
taskRef:
316316
name: yaml-lint
317317
when:
318-
- input: "$(tasks.detect-changes.results.bundle_path)"
318+
- &isBundlePathAvailable
319+
input: "$(tasks.detect-changes.results.bundle_path)"
319320
operator: notin
320321
values: [""]
321322
params:
@@ -409,30 +410,12 @@ spec:
409410
workspace: results
410411
subPath: summary
411412

412-
- name: resolve-pr-type
413-
taskRef:
414-
name: resolve-pr-type
415-
kind: Task
416-
runAfter:
417-
- read-config
418-
params:
419-
- name: pipeline_image
420-
value: "$(params.pipeline_image)"
421-
- name: bundle_path
422-
value: "$(tasks.detect-changes.results.bundle_path)"
423-
- name: affected_catalogs
424-
value: "$(tasks.detect-changes.results.affected_catalogs)"
425-
- name: affected_catalog_operators
426-
value: "$(tasks.detect-changes.results.affected_catalog_operators)"
427-
- name: fbc-enabled
428-
value: "$(tasks.read-config.results.fbc-enabled)"
429-
430413
- name: validate-catalog-format
431414
taskRef:
432415
name: validate-catalog-format
433416
kind: Task
434417
runAfter:
435-
- resolve-pr-type
418+
- read-config
436419
when:
437420
- input: "$(tasks.detect-changes.results.added_or_modified_catalogs)"
438421
operator: notin
@@ -695,6 +678,11 @@ spec:
695678
- static-tests
696679
taskRef:
697680
name: merge-registry-credentials
681+
when:
682+
- &isCertProjectRequired
683+
input: "$(params.cert_project_required)"
684+
operator: in
685+
values: ["true"]
698686
params:
699687
- name: pipeline_image
700688
value: "$(params.pipeline_image)"
@@ -717,6 +705,8 @@ spec:
717705
- merge-registry-credentials
718706
taskRef:
719707
name: digest-pinning
708+
when:
709+
- *isCertProjectRequired
720710
params:
721711
- name: pipeline_image
722712
value: "$(params.pipeline_image)"
@@ -732,26 +722,15 @@ spec:
732722
- name: registry-credentials
733723
workspace: registry-credentials-all
734724

735-
- name: verify-pinned-digest
736-
runAfter:
737-
- digest-pinning
738-
taskRef:
739-
name: verify-pinned-digest
740-
params:
741-
- name: dirty_flag
742-
value: "$(tasks.digest-pinning.results.dirty_flag)"
743-
- name: related_images_flag
744-
value: "$(tasks.digest-pinning.results.related_images_flag)"
745-
- name: related_images_message
746-
value: "$(tasks.digest-pinning.results.related_images_message)"
747-
748725
# Build images- bundle and index and push them to registry.
749726
# Those steps are also a part of the CI pipeline.
750727
- name: dockerfile-creation
751728
runAfter:
752-
- verify-pinned-digest
729+
- digest-pinning
753730
taskRef:
754731
name: dockerfile-creation
732+
when:
733+
- *isBundlePathAvailable
755734
params:
756735
- name: pipeline_image
757736
value: "$(params.pipeline_image)"
@@ -771,6 +750,8 @@ spec:
771750
taskRef:
772751
name: buildah
773752
kind: Task
753+
when:
754+
- *isBundlePathAvailable
774755
params:
775756
- name: IMAGE
776757
value: &bundleImage "$(params.registry)/$(params.image_namespace)/$(tasks.detect-changes.results.added_operator):$(tasks.detect-changes.results.added_bundle)"
@@ -1172,6 +1153,10 @@ spec:
11721153
- get-ci-results
11731154
taskRef:
11741155
name: link-pull-request
1156+
when:
1157+
- input: "$(tasks.get-ci-results.results.test_result_id)"
1158+
operator: notin
1159+
values: [""]
11751160
params:
11761161
- name: pipeline_image
11771162
value: "$(params.pipeline_image)"
@@ -1197,9 +1182,7 @@ spec:
11971182
taskRef:
11981183
name: query-publishing-checklist
11991184
when:
1200-
- input: "$(tasks.certification-project-check.results.certification_project_id)"
1201-
operator: "notin"
1202-
values: [""]
1185+
- *certProjectExists
12031186
- input: "$(tasks.detect-changes.results.affected_catalogs)"
12041187
operator: in
12051188
values: [""]
@@ -1293,6 +1276,9 @@ spec:
12931276
- input: $(tasks.merge-pr.results.pr_merged)
12941277
operator: in
12951278
values: ["true"]
1279+
- input: $(tasks.get-ci-results.results.test_result_id)
1280+
operator: notin
1281+
values: [""]
12961282
taskRef:
12971283
name: link-pull-request
12981284
params:

ansible/roles/operator-pipeline/templates/openshift/tasks/digest-pinning.yml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ spec:
1212
- name: digest_pinning_tool_image
1313
description: Digest pinning tool image
1414
default: "quay.io/redhat-isv/digest-pinning-tool@sha256:7e9dd84000e597e5591d94e791e7c0be1f1efe7e60ab915b200c5c0dff2fa6ba"
15+
- name: enforce_pinning
16+
description: A flag to enforce digest pinning
17+
default: "true"
1518
results:
1619
- name: dirty_flag
1720
- name: related_images_flag
@@ -104,6 +107,10 @@ spec:
104107
if [[ $REPLACEMENT_COUNT -gt 0 ]]; then
105108
echo "Manifests were not pinned."
106109
echo -n "true" | tee $(results.dirty_flag.path)
110+
if [[ "$(params.enforce_pinning)" == "true" ]]; then
111+
echo "Digest pinning is enforced. Failing the task."
112+
exit 1
113+
fi
107114
else
108115
echo "Manifests are pinned."
109116
echo -n "false" | tee $(results.dirty_flag.path)
@@ -135,13 +142,20 @@ spec:
135142
CSVFILE=$(find $BUNDLE_PATH -name "*clusterserviceversion.yaml" -o -name "*clusterserviceversion.yml")
136143
RELATED_IMAGE_COUNT=$(yq -e '.spec.relatedImages | length' $CSVFILE)
137144
145+
PASS=true
138146
if [[ $RELATED_IMAGE_COUNT -ge $REFERENCE_COUNT ]]; then
139147
echo -n "Related images section exists." | tee $(results.related_images_message.path)
140-
echo -n "true" | tee $(results.related_images_flag.path)
141-
elif [[ $RELATED_IMAGE_COUNT -lt $REFERENCE_COUNT && $RELATED_IMAGE_COUNT -gt 0 ]]; then
148+
elif [[ $RELATED_IMAGE_COUNT -gt 0 ]]; then
142149
echo -n "The relatedImages section in your CSV covers only $RELATED_IMAGE_COUNT of the $REFERENCE_COUNT images detected in your CSV." | tee $(results.related_images_message.path)
143-
echo -n "false" | tee $(results.related_images_flag.path)
150+
PASS=false
144151
else
145152
echo -n "The relatedImages section is missing from the CSV" | tee $(results.related_images_message.path)
146-
echo -n "false" | tee $(results.related_images_flag.path)
153+
PASS=false
154+
fi
155+
156+
echo -n "$PASS" | tee $(results.related_images_flag.path)
157+
158+
if [[ "$PASS" == "false" && "$(params.enforce_pinning)" == "true" ]]; then
159+
echo "Digest pinning is enforced. Failing the task."
160+
exit 1
147161
fi
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Guidelines for Using Tekton `when` Conditions
2+
3+
This document defines when pipeline-level `when` conditions can and cannot be used in this
4+
project's Tekton pipelines. These rules exist because a skipped task still participates in the
5+
pipeline graph: its results become empty strings, `runAfter` dependents still execute, and any
6+
downstream task that consumes those empty results may silently misbehave or fail.
7+
8+
---
9+
10+
## Rules at a Glance
11+
12+
| Scenario | Allowed |
13+
| ---------------------------------------------------------------------------------------------------------------------------- | ------- |
14+
| Task has no downstream dependents | ✅ Yes |
15+
| `when` input is a pipeline parameter | ✅ Yes |
16+
| `when` input is a task result, the source task cannot be skipped, AND the guarded task has no task results in its own params | ✅ Yes |
17+
| `when` input is a task result AND the source task can itself be skipped | ❌ No |
18+
| `when` input is a task result AND the guarded task also consumes task results in its params | ❌ No |
19+
20+
---
21+
22+
## Rule 1 — No downstream dependents
23+
24+
A `when` condition is always safe when no other task depends on the guarded task's results or
25+
uses it in a `runAfter` chain. There is no cascading effect if the task is skipped.
26+
27+
```yaml
28+
# Safe: nothing downstream reads this task's results
29+
- name: notify-slack
30+
taskRef:
31+
name: send-slack-notification
32+
when:
33+
- input: $(tasks.check.results.failed)
34+
operator: in
35+
values: ["true"]
36+
params:
37+
- name: message
38+
value: "Pipeline failed"
39+
```
40+
41+
---
42+
43+
## Rule 2 — `when` input is a pipeline parameter
44+
45+
Using a pipeline parameter as the `when` condition input is always safe. Pipeline parameters are
46+
resolved before any task runs and are guaranteed to be non-empty (or have a known default). No
47+
task result propagation is involved.
48+
49+
```yaml
50+
# Safe: condition is a pipeline param, not a task result
51+
- name: publish-pyxis-data
52+
taskRef:
53+
name: publish-pyxis-data
54+
when:
55+
- input: $(params.cert_project_required)
56+
operator: in
57+
values: ["true"]
58+
params:
59+
- name: cert_project_id
60+
value: $(params.cert_project_id)
61+
```
62+
63+
---
64+
65+
## Rule 3 — `when` input is a task result, source task cannot be skipped, guarded task has no task results in params
66+
67+
A task result can be used as a `when` condition input when **all three** of the following hold:
68+
69+
1. The task that produces the result **cannot itself be skipped** — it has no `when` condition and
70+
always runs. If the source task can be skipped, its result becomes an empty string, making the
71+
`when` condition unreliable (it may silently evaluate as if the condition were false, with no
72+
indication that the source task was skipped rather than producing a genuine empty value).
73+
2. The guarded task's own params **contain no task results** — all param values come from pipeline
74+
parameters, literal strings, or workspace references.
75+
3. Downstream tasks that receive the guarded task's results must already handle empty values (via
76+
their own `when` conditions or inner script guards), since a skipped task's results become empty
77+
strings.
78+
79+
```yaml
80+
# NOT allowed: source task (some-upstream-task) has its own when condition and can be skipped.
81+
# If it is skipped, its result is "", and the when condition below becomes meaningless.
82+
- name: some-upstream-task
83+
when:
84+
- input: $(params.flag)
85+
operator: in
86+
values: ["true"]
87+
...
88+
89+
- name: guarded-task
90+
when:
91+
- input: $(tasks.some-upstream-task.results.value) # ❌ source can be skipped
92+
operator: notin
93+
values: [""]
94+
...
95+
```
96+
97+
```yaml
98+
# Safe: when uses a task result, source task always runs (no when condition), guarded task
99+
# params are all pipeline params
100+
- name: link-pull-request-with-open-status
101+
taskRef:
102+
name: link-pull-request
103+
when:
104+
- input: $(tasks.get-ci-results.results.test_result_id) # get-ci-results always runs ✓
105+
operator: notin
106+
values: [""]
107+
params:
108+
- name: pipeline_image
109+
value: $(params.pipeline_image) # pipeline param — OK
110+
- name: pyxis_url
111+
value: $(params.pyxis_url) # pipeline param — OK
112+
```
113+
114+
---
115+
116+
## Rule 4 — `when` input is a task result, guarded task also consumes task results in params ❌
117+
118+
This combination is **not allowed**. When the `when` condition is based on a task result and the
119+
guarded task also consumes task results in its own params, skipping the task creates an ambiguous
120+
state in the result propagation chain:
121+
122+
1. The upstream tasks that produced the param results ran and completed.
123+
2. The guarded task is skipped — those results are not consumed and not transformed.
124+
3. Any downstream task expecting the guarded task's output receives empty strings, with no clear
125+
signal about whether the skip was intentional.
126+
127+
This makes the pipeline hard to reason about and can cause silent failures downstream.
128+
129+
```yaml
130+
# NOT allowed: when uses a task result, AND params also reference task results
131+
- name: get-pyxis-certification-data
132+
taskRef:
133+
name: get-pyxis-certification-data
134+
when:
135+
- input: $(tasks.certification-project-check.results.certification_project_id) # task result
136+
operator: notin
137+
values: [""]
138+
params:
139+
- name: cert_project_id
140+
value: $(tasks.certification-project-check.results.certification_project_id) # also task result ❌
141+
- name: pyxis_url
142+
value: $(tasks.set-env.results.pyxis_url) # also task result ❌
143+
```
144+
145+
**How to fix this:** instead, add an inner guard in the task script and rely on the task's own
146+
early-exit logic to handle the empty-input case, as was the established workaround pattern in
147+
this project.
148+
149+
```bash
150+
# In the task script — guard against empty cert_project_id at the script level
151+
if [ "$(params.cert_project_id)" == "" ]; then
152+
echo -n | tee "$(results.foo.path)"
153+
exit 0
154+
fi
155+
```
156+
157+
---
158+
159+
## Summary Decision Tree
160+
161+
```
162+
Is the task being considered for a when condition?
163+
164+
├── Does it have no downstream dependents?
165+
│ └── YES → ✅ Use when condition freely
166+
167+
├── Is the when condition input a pipeline parameter?
168+
│ └── YES → ✅ Use when condition freely
169+
170+
└── Is the when condition input a task result?
171+
172+
├── Can the source task itself be skipped (does it have a when condition)?
173+
│ └── YES → ❌ Do NOT use when condition
174+
│ Use an inner script guard (exit 0) instead
175+
176+
└── Does the guarded task's params contain any task results?
177+
├── NO → ✅ Use when condition (self-contained skip)
178+
└── YES → ❌ Do NOT use when condition
179+
Use an inner script guard (exit 0) instead
180+
```
181+
182+
---

0 commit comments

Comments
 (0)