status | title | creation-date | last-updated | authors | see-also | ||||
---|---|---|---|---|---|---|---|---|---|
implemented |
Skipping Strategies |
2021-03-24 |
2021-08-23 |
|
|
- Summary
- Motivation
- Requirements
- Proposal
- Test Plan
- Design Evaluation
- Upgrade & Migration Strategy
- Alternatives
- References
This TEP addresses skipping strategies to give users the flexibility to skip a single guarded Task
only and unblock
execution of its dependent Tasks
.
Today, WhenExpressions
are specified within Tasks
but they guard the Task
and its dependent Tasks
.
To provide flexible skipping strategies, we propose changing the scope of WhenExpressions
from guarding a Task
and its dependent Tasks
to guarding the Task
only. If a user wants to guard a Task
and its dependent
Tasks
, they can:
- cascade the
WhenExpressions
to the dependentTasks
- compose the
Task
and its dependentTasks
as a sub-Pipeline
that's guarded and executed together usingPipelines
inPipelines
When WhenExpressions
evaluate to False
, the guarded Task
is skipped and its dependent Tasks
are skipped as well while the rest of the Pipeline
executes. Users need the flexibility to skip that guarded Task
only and unblock the execution of the dependent Tasks
.
Pipelines
are directed acyclic graphs where:
Nodes
areTasks
Edges
are defined using ordering (runAfter
) and resource (e.g.Results
) dependenciesBranches
are made up ofNodes
that are connected byEdges
WhenExpressions
are specified within Tasks
, but they guard the Task
and its dependent Tasks
. Thus,
the WhenExpressions
(and Conditions
)
can be visualized to be along the edges between two Tasks
in a Pipeline
graph.
Take this example:
tasks:
- name: previous-task
taskRef:
name: previous-task
- name: current-task
runAfter: [ previous-task ]
when:
- input: "foo"
operator: in
values: [ "bar" ]
taskRef:
name: current-task
- name: next-task
runAfter: [ current-task ]
taskRef:
name: next-task
The visualization/workflow of the Pipeline
graph would be:
previous-task # executed
|
(guard) # false
|
v
current-task # skipped
|
v
next-task # skipped
This TEP aims to support WhenExpressions
that are specified within Tasks
to guard the Task
only (not its dependent
Tasks
). Thus, this visualization/workflow of the Pipeline
graph would be possible:
previous-task # executed
|
v
(guard) current-task # false and skipped
|
v
next-task # executed
The main goal of this TEP is to provide the flexibility to skip a guarded Task
when its WhenExpressions
evaluate
to False
while unblocking the execution of its dependent Tasks
.
Providing the flexibility to skip a Task
and unblock execution of its dependent Tasks
when it was skipped for other
reasons besides its WhenExpressions
evaluating to False
is out of scope for this TEP.
Today, the other reasons that a Task
is skipped include:
- its
Conditions
fail (deprecated) - its parent
Task
is skipped - its
Results
references cannot be resolved - the
PipelineRun
is in a stopping state
By scoping this skipping strategy to WhenExpressions
only, we can provide the flexibility safely with a minimal
change. Moreover, it allows us to limit the number possible Pipeline
graph execution paths and make the workflow
predictable. If needed, we can explore adding this skipping strategy for the other reasons in the future.
A user needs to design a Pipeline
with a manual approval Task
that is executed when merging a pull request only.
The execution of the manual approval Task
is guarded using WhenExpressions
. To reuse the same Pipeline
when
merging and not merging, the user needs the dependent Tasks
to execute when the guarded manual approval Task
is
skipped.
lint unit-tests
| |
v v
report-linter-output integration-tests
|
v
manual-approval
|
v
build-image
|
v
deploy-image
If the WhenExpressions
in manual-approval
evaluate to True
, then manual-approval
is executed and:
- if
manual-approval
succeeds, thenbuild-image
anddeploy-image
are executed - if
manual-approval
fails, thenbuild-image
anddeploy-image
are not executed because thePipeline
fails
Today, if the WhenExpressions
in manual-approval
evaluate to False
, then manual-approval
, build-image
and deploy-image
are all skipped. In this TEP, we'll provide the flexibility to execute build-image
and deploy-image
when manual-approval
is skipped. This would allow the user to reuse the Pipeline
in both
scenarios (merging and not merging).
Building on the above use case, the user adds slack-msg
which sends a notification to slack that it was manually
approved with the name of the approver that is passed as a Result
from manual-approval
to slack-msg
.
lint unit-tests
| |
v v
report-linter-output integration-tests
|
v
manual-approval
| |
v (approver)
build-image |
| v
v slack-msg
deploy-image
If the guarded manual-approval
is skipped, then build-image
and deploy-image
needs to be executed similarly to
above. However, slack-msg
should be skipped because of the missing Result
reference to the approver name.
Users should be able to specify that a guarded Task
only should be skipped when its WhenExpressions
evaluate
to False
to unblock the execution of its dependent Tasks
- ordering-dependent
Tasks
, based onrunAfter
, should execute as expected - resource-dependent
Tasks
, based on resources such asResults
, should be attempted but might be skipped if they can't resolve missing resources
Today, WhenExpressions
are specified within Tasks
but they guard the Task
and its dependent Tasks
.
To provide flexible skipping strategies, we propose changing the scope of WhenExpressions
from guarding a Task
and its dependent Tasks
to guarding the Task
only. If a user wants to guard a Task
and its dependent
Tasks
, they can:
- cascade the
WhenExpressions
to the dependentTasks
- compose the
Task
and its dependentTasks
as a sub-Pipeline
that's guarded and executed together usingPipelines
inPipelines
To enable guarding a Task
only, we'll change the scope of WhenExpressions
to guard the Task
only. The
migration strategy for this change is discussed in Upgrade & Migration Strategy
below.
A Pipeline
to solve for the use case described above would be designed as such:
tasks:
...
- name: manual-approval
runAfter:
- integration-tests
when:
- input: $(params.git-action)
operator: in
values:
- merge
taskRef:
name: manual-approval
- name: slack-msg
params:
- name: approver
value: $(tasks.manual-approval.results.approver)
taskRef:
name: slack-msg
- name: build-image
runAfter:
- manual-approval
taskRef:
name: build-image
- name: deploy-image
runAfter:
- build-image
taskRef:
name: deploy-image
If user wants to guard a Task
and its dependent Tasks
, they have two options:
- cascade the
WhenExpressions
to the specific dependentTasks
they want to guard as well - compose the
Task
and its dependentTasks
as a unit to be guarded and executed together usingPipelines
inPipelines
Cascading WhenExpressions
to specific dependent Tasks
gives users more control to design their workflow. Today,
we skip all dependent Tasks
. With this TEP, they can pick and choose which dependent Tasks
to guard as well,
empowering them to solve for more complex CI/CD use cases.
A user who wants to guard manual-approval
and its dependent Tasks
can design the Pipeline
as such:
tasks:
...
- name: manual-approval
runAfter:
- integration-tests
when:
- input: $(params.git-action)
operator: in
values:
- merge
taskRef:
name: manual-approval
- name: slack-msg
params:
- name: approver
value: $(tasks.manual-approval.results.approver)
taskRef:
name: slack-msg
- name: build-image
when:
- input: $(params.git-action)
operator: in
values:
- merge
runAfter:
- manual-approval
taskRef:
name: build-image
- name: deploy-image
when:
- input: $(params.git-action)
operator: in
values:
- merge
runAfter:
- build-image
taskRef:
name: deploy-image
Cascading is more verbose, but it provides clarity and flexibility in guarded execution by being explicit.
Composing a set of Tasks
as a unit of execution using Pipelines
in Pipelines
will allow users to guard a Task
and its dependent Tasks
(as a sub-Pipeline
) using WhenExpressions
.
If a user wants to guard manual-approval
and its dependent Tasks
, they can combine them in a sub-Pipeline
which
we'll refer to as approve-slack-build-deploy
, as such:
tasks:
- name: manual-approval
runAfter:
- integration-tests
taskRef:
name: manual-approval
- name: slack-msg
params:
- name: approver
value: $(tasks.manual-approval.results.approver)
taskRef:
name: slack-msg
- name: build-image
runAfter:
- manual-approval
taskRef:
name: build-image
- name: deploy-image
runAfter:
- build-image
taskRef:
name: deploy-image
Pipelines
in Pipelines
is currently available through Custom Tasks
, so it would be used in the main-Pipeline
as such:
tasks:
...
- name: approve-slack-build-deploy
runAfter:
- integration-tests
when:
- input: $(params.git-action)
operator: in
values:
- merge
taskRef:
apiVersion: tekton.dev/v1beta1
kind: Pipeline
name: approve-slack-build-deploy
After we promote Pipelines
in Pipelines
from experimental to a top-level feature, then this is a possible syntax:
tasks:
...
- name: approve-slack-build-deploy
runAfter:
- integration-tests
when:
- input: $(params.git-action)
operator: in
values:
- merge
pipelineRef:
name: approve-slack-build-deploy
Pipelines
in Pipelines
would provide the flexible skipping strategies needed to solve for the use cases without
verbosity.
Unit and integration tests for guarded execution of Tasks
in a Pipeline
with different skipping strategies in
different combinations.
By unblocking the execution of dependent Tasks
when a guarded Task
is skipped, we enable execution to continue
when the guarded Task
is either successful or skipped, making the Pipeline
reusable for more scenarios or use
cases.
By cascading WhenExpressions
or composing a set of Tasks
as a Pipeline
in a Pipeline
, we reuse existing features
to provide flexible skipping strategies.
By scoping the skipping strategy to WhenExpressions
only, we provide the flexibility safely with a minimal change.
We also limit the interleaving of Pipeline
graph execution paths and maintain the simplicity of the workflows.
WhenExpressions
and Pipelines
in Pipelines
are the bare minimum features so solve for the CI/CD use cases that
need skipping strategies.
This TEP will give users the flexibility to either guard a Task
only or guard a Task
and its dependent Tasks
.
Moreover, it gives users the flexibility to guard some dependent Tasks
while executing other dependent Tasks
.
Changing the scope of WhenExpressions
to guard the Task
only is backwards-incompatible, so to make the
transition smooth:
- we'll provide a feature flag,
scope-when-expressions-to-task
, which:- will default to
scope-when-expressions-to-task
: "false" to guard aTask
and its dependentTasks
- can be set to
scope-when-expressions-to-task
: "true" to guard aTask
only
- will default to
- after 9 months, per the Tekton API compatibility policy,
we'll flip the feature flag and default to
scope-when-expressions-to-task
:true
[February 2022] - in the next release, we'll remove the feature flag and
WhenExpressions
will be scoped to guard aTask
only going forward [March 2022] - when we do v1 release (projected for early 2022), we will have
when
expressions guarding aTask
only both in beta and v1
We will over-communicate during the migration in Slack, email and working group meetings.
Pipelines
in Pipelines
is available through Custom Tasks
- we are iterating on it as we work towards promoting it
to a top level feature. This work will be discussed separately in TEP-0056: Pipelines in Pipelines.
What if we don't change the scope of WhenExpressions
and want to use Pipelines
in Pipelines
only?
In this case, we'd have to lean on Finally Tasks
to execute the dependent Task
in the sub-Pipelines
-- which
leads to convoluted Pipeline
designs, such as:
tasks:
...
- name: approve-build-deploy-notify
runAfter:
- integration-tests
pipelineRef:
- name: approve-build-deploy-notify
---
# approve-build-deploy-notify (sub-pipeline)
tasks:
- name: manual-approval
when:
- input: $(params.git-action)
operator: in
values:
- merge
runAfter:
- integration-tests
taskRef:
- name: manual-approval
- name: slack-msg
params:
- name: approver
value: $(tasks.manual-approval.results.approver)
taskRef:
- name: slack-msg
finally:
- name: build-and-deploy
when:
- input: $(tasks.manual-approval.status)
operator: notin
values:
- Failed
pipelineRef:
- name: build-and-deploy
---
# build-and-deploy (sub-pipeline)
- name: build-image
runAfter:
- manual-approval
taskRef:
name: build-image
- name: deploy-image
runAfter:
- build-image
taskRef:
name: deploy-image
Today, we support specifying a list of WhenExpressions
through the when
field as such:
when:
- input: 'foo'
operator: in
values: [ 'bar' ]
To provide the flexibility to skip a guarded Task
when its WhenExpressions
evaluate to False
while unblocking the
execution of its dependent Tasks
, we could change the when
field from a list to a dictionary and add scope
and
expressions
fields under the when
field.
- The
scope
field would be used to specify whether theWhenExpressions
guard theTask
only or the wholeBranch
( theTask
and its dependencies). To unblock execution of subsequentTasks
, users would setscope
toTask
. Settingscope
toBranch
matches the current behavior. - The
expressions
field would be used to specify the list ofWhenExpressions
, each of which hasinput
,operator
andvalues
fields, as it is currently.
when:
scope: Task
expressions:
- input: 'foo'
operator: in
values: [ 'bar' ]
---
when:
scope: Branch
expressions:
- input: 'foo'
operator: notin
values: [ 'bar' ]
To support both syntaxes under when
, we'll detect whether it's a list or dictionary in UnmarshalJSON
function that
implements the json.Unmarshaller
interface, using the first character. This is how similar scenarios have been handled
elsewhere, including:
- Tekton does the same thing in
Parameters
to detect whether the type of the value is aString
orArray
(code) - Kubernetes does the same thing in
IntOrString
to detect whether the type isInt
orString
(code)
A Pipeline
to solve for the use case described above would be designed as such:
tasks:
...
- name: manual-approval
runAfter:
- integration-tests
when:
scope: Task
expressions:
- input: $(params.git-action)
operator: in
values:
- merge
taskRef:
name: manual-approval
- name: slack-msg
params:
- name: approver
value: $(tasks.manual-approval.results.approver)
taskRef:
name: slack-msg
- name: build-image
runAfter:
- manual-approval
taskRef:
name: build-image
- name: deploy-image
runAfter:
- build-image
taskRef:
name: deploy-image
If the WhenExpressions
in manual-approval
evaluate to False
, then manual-approval
would be skipped and:
build-image
anddeploy-image
would be executedslack-msg
would be skipped due to missing resource frommanual-approval
Add a field - whenSkipped
- that can be set to runBranch
to unblock or skipBranch
to block the execution
of Tasks
that are dependent on the guarded Task
.
type SkippingPolicy string
const (
RunBranch SkippingPolicy = "runBranch"
SkipBranch SkippingPolicy = "skipBranch"
)
tasks:
- name: task
when:
- input: foo
operator: in
values: [ bar ]
whenSkipped: runBranch / skipBranch
taskRef:
- name: task
Another option would be a field - whenScope
- than can be set to Task
to unblock or Branch
to block the execution
of Tasks
that are dependent on the guarded Task
.
type WhenScope string
const (
Task WhenScope = "task"
Branch WhenScope = "branch"
)
tasks:
- name: task
when:
- input: foo
operator: in
values: [ bar ]
whenScope: task / branch
taskRef:
- name: task
However, it won't be clear that the skipping policies are related to WhenExpressions
specifically and can be confusing
to reason about when they are specified separately.
Add a field - executionPolicies
- that takes a list of execution policies for the skipping and failure strategies for
given Task
. This would align well
with TEP-0050: Ignore Task Failures
and is easily extensible.
type ExecutionPolicy string
const (
IgnoreFailure ExecutionPolicy = "ignoreFailure"
ContinueAfterSkip ExecutionPolicy = "continueAfterSkip"
...
)
tasks:
- name: task
when:
- input: foo
operator: in
values: [ bar ]
executionPolicies:
- ignoreFailure
- continueAfterSkip
taskRef:
- name: task
However, it won't be clear that the skipping policies are related to WhenExpressions
specifically and can be confusing
to reason about when they are specified separately.
Add a field - continueAfterSkip
- that can be set to true
to unblock or false
to block the execution of Tasks
that are dependent on the guarded Task
.
tasks:
- name: task
when:
- input: foo
operator: in
values: [ bar ]
continueAfterSkip: true / false
taskRef:
- name: task
However, it won't be clear that the boolean flag is related to WhenExpressions
specifically and can be confusing to
reason about when they are specified separately. In addition, booleans
limit future extensions.
Provide a special kind of runAfter
- runAfterWhenSkipped
- that users can use instead of runAfter
to allow for the
ordering-dependent Task
to execute even when the Task
has been skipped. Related ideas discussed
in tektoncd/pipeline#2653 as runAfterUnconditionally
and tektoncd/pipeline#1684 as runOn
.
tasks:
- name: task1
when:
- input: foo
operator: in
values: [ bar ]
taskRef:
- name: task1
- name: task2
runAfterWhenSkipped:
- task1
taskRef:
- name: task2
However, it won't be clear that the skipping policies are related to WhenExpressions
specifically and can be confusing
to reason about when they are specified separately.
- Implementation in Tekton Pipelines Pull Request #4085
- Tekton Pipeline release v0.27.0 "Tonkinese Talos"
- Migration announcement in Tekton Pipelines Discussion #4185
- Related Designs:
- Selected alternatives considered so far:
- Some alternatives had proof of concepts in this pull request
- Related Issues: