Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TEP-0007: Conditions Beta #159

Merged
merged 1 commit into from
Aug 4, 2020
Merged

Conversation

jerop
Copy link
Member

@jerop jerop commented Jul 22, 2020

Conditions is a CRD used to specify a criteria to determine whether or
not a Task executes. When other Tekton resources were migrated to beta,
it remained in alpha because it was missing features needed by users.
After analyzing the feature requests and discussing with users, we have
identified that the most critical gaps in Conditions are simplicity,
efficiency and skipping. We want to address these gaps so that it can
work well with the other Pipeline resources and users can count on its
stability.

@tekton-robot tekton-robot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 22, 2020
@tekton-robot tekton-robot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Jul 22, 2020
@jerop jerop force-pushed the conditions branch 4 times, most recently from 688d3d3 to 7ca16c5 Compare July 22, 2020 20:06
Copy link
Contributor

@bobcatfish bobcatfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great to me! I'd like to hear if folks who have strong feelings about this have any issues / suggestions ( @jlpettersson @afrittoli ) and I left some thoughts, but I think we could merge this as "proposed" asap (maybe next day or two unless folks raise large issues with it as is) and then open another PR to get it to "implementable" after that

teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
@jlpettersson
Copy link
Member

jlpettersson commented Jul 28, 2020

This is looking great to me! I'd like to hear if folks who have strong feelings about this have any issues / suggestions ( @jlpettersson @afrittoli ) and I left some thoughts, but I think we could merge this as "proposed" asap (maybe next day or two unless folks raise large issues with it as is) and then open another PR to get it to "implementable" after that

It is currently a draft and wip in state PR for proposed - I am a bit new to TEP, so I don't really know when to provide what kind of feedback. Maybe I should add suggestions as PRs after this is merged but before it is changed to "implementable" ?

In large - this looks good to me - good job @jerop

I have some minor comments/suggestions - but they can all be changed/addressed in a later state.

@jerop jerop marked this pull request as ready for review July 28, 2020 20:20
@tekton-robot tekton-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 28, 2020

`Key` is the input for the `Guard` checking - can be from `Parameters` or `Results`. `Values` is an array of string values. `Operator` represents a key's relationship to a set of values.

Valid [Operators](https://github.com/kubernetes/kubernetes/blob/7f23a743e8c23ac6489340bbb34fa6f1d392db9d/staging/src/k8s.io/apimachinery/pkg/selection/operator.go#L24-L32) are `In`, `NotIn`, `Exists` and `DoesNotExist`. If the `Operator` is `In` or `NotIn`, the `Values` array must be non-empty and if the operator is `Exists` or `DoesNotExist`, the `Values` array must be empty.
Copy link
Member

@jlpettersson jlpettersson Jul 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all - I really like the introduction of a lightweight way to write condition-expressions - like with matchExpressions.

matchExpressions is designed for Kubernetes Labels. Labels consist of pairs of a Key and a Value. Therefor the first thing is a key: in the syntax and the operators Exists and DoesNotExists means that the key exists - independent of what Value it has.

Does it make sense for us to adapt the wording and possible the operators? For us, we want to use Param or a TaskResult as input instead of a Label-key. Should we use e.g. param: and taskResult: or something similar that fits us better - instead of key:? e.g. is more intuitive for end users of Tekton?

I think the In and NotIn operators fits us good, and users understand them. But if we want to use Exists and DoesNotExists - we should at least explain what it means in our context - or maybe abandon them - since they are not directly transferrable to our context?

We may also be interested in other operators, e.g. IsTrue and IsNotTrue / IsFalse - and maybe IsEmpty and IsNotEmpty - but that may also be added later.
These would just be shortcuts for common things that always can be used with In in combination of values: [""] or values: ["true"] / values: ["false"]

E.g.

name: worskspace-contains-dockerfile
key: $(tasks.dockerfile-check.results.skip)
operator: In
values: ["true"]

could be optimized to:

name: worskspace-contains-dockerfile
key: $(tasks.dockerfile-check.results.skip)
operator: IsTrue

Copy link
Member

@jlpettersson jlpettersson Jul 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLDR; we might want to start with the operators In and NotIn
and wait or potentially abandon Exists and DoesNotExists - in our context.

key: might not be the field name that fits best in our context.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking we can keep the generic name (maybe rename it to input) so that it's flexible to accept different string inputs from different tekton resources using the string reference '$(x.y.z)', so that even as Tekton evolves, we won't have to modify to support them here. For example:

Agreed that we should proceed with In and NotIn, then we can explore other Operators later if needed -- mentioned the examples you provided.

name: check-file-exists
input: '$(tasks.file-exists.results.exists)'
operator: In
values: ['true']
---
name: branch-is-main
input: `$(params.branch)`
operator: In
values: [‘main’]
---
name: always-false
input: ‘false’
operator: In
values: [‘’]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, input: sounds better than key: in our context.

@tekton-robot tekton-robot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jul 29, 2020
Copy link
Member Author

@jerop jerop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @jlpettersson and @bobcatfish for the feedback!

teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved

`Key` is the input for the `Guard` checking - can be from `Parameters` or `Results`. `Values` is an array of string values. `Operator` represents a key's relationship to a set of values.

Valid [Operators](https://github.com/kubernetes/kubernetes/blob/7f23a743e8c23ac6489340bbb34fa6f1d392db9d/staging/src/k8s.io/apimachinery/pkg/selection/operator.go#L24-L32) are `In`, `NotIn`, `Exists` and `DoesNotExist`. If the `Operator` is `In` or `NotIn`, the `Values` array must be non-empty and if the operator is `Exists` or `DoesNotExist`, the `Values` array must be empty.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking we can keep the generic name (maybe rename it to input) so that it's flexible to accept different string inputs from different tekton resources using the string reference '$(x.y.z)', so that even as Tekton evolves, we won't have to modify to support them here. For example:

Agreed that we should proceed with In and NotIn, then we can explore other Operators later if needed -- mentioned the examples you provided.

name: check-file-exists
input: '$(tasks.file-exists.results.exists)'
operator: In
values: ['true']
---
name: branch-is-main
input: `$(params.branch)`
operator: In
values: [‘main’]
---
name: always-false
input: ‘false’
operator: In
values: [‘’]

teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
teps/0007-conditions-beta.md Outdated Show resolved Hide resolved
@jerop jerop force-pushed the conditions branch 3 times, most recently from 997c5e0 to 056ccf4 Compare July 29, 2020 22:24
Copy link
Member

@jlpettersson jlpettersson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it a lot, as written now.
Thank you for all the work and discussions!
It would be good to get more eyes on it as well.
/lgtm

teps/0007-conditions-beta.md Show resolved Hide resolved
input: '$(tasks.file-exists.results.exists)'
operator: In
values: ['true']
taskRef:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this example easy to read, easy to reason about, easy to understand. As this example is written now, this is also not too hard to implement.

@tekton-robot tekton-robot added the lgtm Indicates that a PR is ready to be merged. label Jul 29, 2020
@tekton-robot tekton-robot removed lgtm Indicates that a PR is ready to be merged. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jul 30, 2020
1. Users need a way to specify logic - `Guards` - to determine if a `Task` should execute or not.
1. Users need a way to specify whether to execute dependent `Tasks` when `Guards` of a parent `Task` evaluate to `False`.
1. Users should be able to specify multiple `Guards` for a given `Task`.
1. Users should be able to use `Outputs` from parent `Tasks` and static input to specify `Guards`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • What happens when one of the Guards fail? Do we exit the pipeline just like its done today?

  • How are we addressing Guard not producing any task result? The way its works with DAG is: A DAG task could exit successfully without instantiating a task result /tekton/results/foo. The pipeline exits with failure if this result is referenced by any other task since the result file does not exist. We could follow the same behavior. 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when one of the Guards fail? Do we exit the pipeline just like its done today?

That is not how it works today. If a guarded tasks condition evaluates to false that task is omitted, and the tasks that depend on it is omitted - but the Pipeline continue to run to completion.

This was a bit unclear from docs before, but I tried to expand on that in tektoncd/pipeline#2636

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • What happens when one of the Guards fail? Do we exit the pipeline just like its done today?

Building on @jlpettersson's response, note that in this proposal, the Guards are the When Expressions (with input, operator, and values) -- not the Tasks that produce the Results that the When Expressions operate on.

As it is today with any other Task, if that parent Task fails, the Pipeline will fail. However, if the Guard (When Expression) evaluates to False, the guarded Task will be skipped and it's dependents too (except when continueAfterSkip is specified)

  • How are we addressing Guard not producing any task result? The way its works with DAG is: A DAG task could exit successfully without instantiating a task result /tekton/results/foo. The pipeline exits with failure if this result is referenced by any other task since the result file does not exist. We could follow the same behavior. 🤔

So it's not a Guard that produces a Result, the Guard operates on a Result from a parent Task. I agree that, when the Guard is set to operate on a missing resource (Param, Result, etc), that it should exit the Pipeline since it doesn't exist. Added it to the TEP, thank you 🙏

Copy link
Member Author

@jerop jerop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you @pritidesai for the review 🙇

1. Users need a way to specify logic - `Guards` - to determine if a `Task` should execute or not.
1. Users need a way to specify whether to execute dependent `Tasks` when `Guards` of a parent `Task` evaluate to `False`.
1. Users should be able to specify multiple `Guards` for a given `Task`.
1. Users should be able to use `Outputs` from parent `Tasks` and static input to specify `Guards`.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • What happens when one of the Guards fail? Do we exit the pipeline just like its done today?

Building on @jlpettersson's response, note that in this proposal, the Guards are the When Expressions (with input, operator, and values) -- not the Tasks that produce the Results that the When Expressions operate on.

As it is today with any other Task, if that parent Task fails, the Pipeline will fail. However, if the Guard (When Expression) evaluates to False, the guarded Task will be skipped and it's dependents too (except when continueAfterSkip is specified)

  • How are we addressing Guard not producing any task result? The way its works with DAG is: A DAG task could exit successfully without instantiating a task result /tekton/results/foo. The pipeline exits with failure if this result is referenced by any other task since the result file does not exist. We could follow the same behavior. 🤔

So it's not a Guard that produces a Result, the Guard operates on a Result from a parent Task. I agree that, when the Guard is set to operate on a missing resource (Param, Result, etc), that it should exit the Pipeline since it doesn't exist. Added it to the TEP, thank you 🙏


```yaml
name: branch-is-main
input: $(params.branch)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right


### Skipping

Provide more flexibility when a `Guard` executes as `False` by adding a field, `continueAfterSkip`, used to specify whether execute the `Tasks` that are ordering-dependent on the skipped guarded `Task`. The `continueAfterSkip` field defaults to `false`/`no` and users can set it to `true`/`yes` (case insensitive) to allow for execution of the rest of the branch.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, added the clarification

A `Task` branch is made up of dependent `Tasks`, where there are two types of dependencies:
- _Resource dependency_: based on resources needed from parent `Task`, which includes Workspaces, `Results` and Resources.
- _Ordering dependency_: based on runAfter which provides sequencing of `Tasks` when there may not be resource dependencies.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that it adds an implicit resource dependency, added a note about it to the TEP

Comment on lines +412 to +429
- name: check-name-matches
input: "$(params.githubCommand)"
key: In
values: ["", "/test $(params.checkName)"]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for looking into this @jlpettersson!

will test it with the when expression I translated it to when we have a poc and yes, in the worst case it can be in a script

Copy link
Contributor

@bobcatfish bobcatfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great! Lemme know when you want to merge as "proposed" @jerop , you've got the 👍 from me at least :)

I'm so glad we were able to find a simple solution that gives us so much with so little!!!!!!! ❤️ thanks @jerop and @jlpettersson 🙇‍♀️

```

### Efficiency
To improve the efficiency of guarded execution of `Tasks`, we need to avoid spinning up new `Pods` to check `Guards`. We can use string expressions for `Guard` checking, but we want to avoid adding an opinionated and complex expression language to the Tekton API to ensure Tekton can be supported by as many systems as possible. We propose using a simple expression syntax `When Expressions` (similar to Kubernetes' `When Expressions`) to evaluate `Guards` efficiently. The components of `When Expressions` are `Input`, `Operator` and `Values`. `Input` is the input for the `Guard` checking which can be static inputs or outputs from parent `Tasks`, such as `Parameters` or `Results`. `Values` is an array of string values. `Operator` represents an `Input`'s relationship to a set of `Values`. `Operators` we will use in `Guards` are `In` and `NotIn`. The `Values` array must be non-empty. When we have more than one `Guard`, the guarded `Task` will be executed when all the `Guards` evaluate to `True`. Note that when a `Guard` uses a resource from another `Task`, such as a `Result`, it introduces an implicit resource dependency that makes the guarded `Task` dependent on the resource-producing `Task`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you include a link to the kubernetes docs and/or code on their match expressions?

(also nitpick but could you add some newlines in this paragraph when the lines get long - makes it easier to comment on specific parts of the paragraph at least :D). it also might be easier to read as several paragraphs, e.g. current looks like:

image

might be easier to read as something like:

The components of When Expressions are Input, Operator and Values:

  • Input is the input for the Guard checking which can be static inputs or outputs from parent Tasks, such as Parameters or Results.
  • ...

- name: check-file-exists
input: '$(tasks.file-exists.results.exists)'
operator: In
values: ['true']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double checking: you can do variable replacement in the "values" field as well as "input" is that right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think it's more useful when you can do replacement in both of them -- will add a note on that too

values: [‘’]
```

We can explore adding more `Operators` later if needed, such as `IsTrue`, `IsFalse`, `IsEmpty` and `IsNotEmpty`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

links to the k8s equivalents of for all of these will help - so folks know why we are mentioning these specific values and not others

(and maybe explicitly mention the fact that < and > are not included here, tho they exist in k8s match expressions?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the links and clarifications, thanks

A `Task` branch is made up of dependent `Tasks`, where there are two types of dependencies:
- _Resource dependency_: based on resources needed from parent `Task`, which includes Workspaces, `Results` and Resources.
- _Ordering dependency_: based on runAfter which provides sequencing of `Tasks` when there may not be resource dependencies.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

es explicit task ordering is specified with runAfter like @jerop classified it as ordering dependency. Task results (causes implicit dependency) classified as resource dependency.

@jlpettersson it is somewhat by design - i say "somewhat" because it didnt become clear to us until maybe the last 6 months but basically it seems useful to be able to distinguish between these two types of dependencies, and it's not so much that they are explicit vs implicit but that they are, like @pritidesai said:

  1. Ordering dependency (runAfter)
  2. Resource dependency (result, pipeline resource, one day workspaces)

Having the controller be able to be aware of the difference between these 2 cases is useful - e.g. specifically in this case we know that (2) + continueAfterSkip is bad; if we only had (1) we wouldn't know that

Back to implicit vs explicit tho: we had some debate about the from syntax that PipelineResources use vs. the variable replacement which implies ordering that we used for results (e.g. $(tasks.foo.results.bar))

Personally I prefer the explicit from - when you see from, you know that ordering is being required as well as a value being used. this is the doc where we discussed it tho im not sure if this makes it clear why we went with variable interpolation instead of from. i think adding support for "from" could still be on the table but id be surprised if we removed the variable interpolation option


#### CelRun Custom Task

If we implement guarded execution of `Tasks` is implemented using [Tasks that produce Skip Result](#tasks-that-produce-skip-result), we can then extend it to use [`CustomTasks`](https://docs.google.com/document/d/10nQSeIse7Ld4fLg4lhfgUmNKtewfaFNET3zlMdRnBuQ/edit#heading=h.nz0qjg4cmzp0) to build and experiment with using string expressions for simple `Guards`. In Triggers, we use [Common Expression Language](https://opensource.google/projects/cel) for filtering using a `CEL` interceptor. We can provide a `CelRun CustomTask`, so that we experiment with `CEL` without putting it into the Tekton API. After experimentation with `CelRun` `CustomTask` for a while, we will revisit whether we want to add it to the Tekton API.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! if this gets merged as "implementable" maybe as a follow up we can at least create an issue about creating a CELRun controller, tho it will no longer block this :D

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good 👍

workspace: source-repo
- name: echo-file-exists
when:
- name: check-file-exists
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if name: field is optional, similar to name: for steps. Now this is mostly a description (as the names of steps are). And like in this example the input: is self-explanatory.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, will make it optional in the implementation

@bobcatfish
Copy link
Contributor

Let's merge this as "proposed" and keep discussing in a PR that updates this to "implementable" :D

/lgtm
/meow

@tekton-robot
Copy link
Contributor

@bobcatfish: cat image

In response to this:

Let's merge this as "proposed" and keep discussing in a PR that updates this to "implementable" :D

/lgtm
/meow

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

@tekton-robot tekton-robot added the lgtm Indicates that a PR is ready to be merged. label Aug 4, 2020
Conditions is a CRD used to specify a criteria to determine whether or
not a Task executes. When other Tekton resources were migrated to beta,
it remained in alpha because it was missing features needed by users.
After analyzing the feature requests and discussing with users, we have
identified that the most critical gaps in Conditions are simplicity,
efficiency and skipping. We want to address these gaps so that it can
work well with the other Pipeline resources and users can count on its
stability.
@tekton-robot tekton-robot removed the lgtm Indicates that a PR is ready to be merged. label Aug 4, 2020
@bobcatfish
Copy link
Contributor

/lgtm

@tekton-robot tekton-robot added the lgtm Indicates that a PR is ready to be merged. label Aug 4, 2020
@bobcatfish
Copy link
Contributor

easycla had me worried for a second there XD

@jlpettersson
Copy link
Member

/lgtm

@tekton-robot tekton-robot merged commit 1eef84f into tektoncd:master Aug 4, 2020
jerop added a commit to jerop/community that referenced this pull request Aug 5, 2020
Added Conditions Beta TEP in tektoncd#159

Let's discuss the TEP further and explore making it implementable as per
the TEP workflow.
jerop added a commit to jerop/community that referenced this pull request Aug 5, 2020
Added Conditions Beta TEP in tektoncd#159

Let's discuss the TEP further and explore making it implementable as per
the TEP workflow.
jerop added a commit to jerop/community that referenced this pull request Aug 5, 2020
Added Conditions Beta TEP in tektoncd#159

Let's discuss the TEP further and explore making it implementable as per
the TEP workflow.
tekton-robot pushed a commit that referenced this pull request Aug 7, 2020
Added Conditions Beta TEP in #159

Let's discuss the TEP further and explore making it implementable as per
the TEP workflow.
jerop added a commit to jerop/community that referenced this pull request Aug 31, 2020
Proposing the design of When Expressions Status and discussing the
alternatives discussed in tektoncd/pipeline#3139

Related TEP: tektoncd#159
jerop added a commit to jerop/community that referenced this pull request Aug 31, 2020
Proposing the design of When Expressions Status and discussing the
alternatives discussed in tektoncd/pipeline#3139

Related TEP: tektoncd#159
jerop added a commit to jerop/community that referenced this pull request Sep 17, 2020
Proposing the design of When Expressions Status and discussing the
alternatives discussed in tektoncd/pipeline#3139

Related TEP: tektoncd#159
tekton-robot pushed a commit that referenced this pull request Sep 17, 2020
Proposing the design of When Expressions Status and discussing the
alternatives discussed in tektoncd/pipeline#3139

Related TEP: #159
@jerop jerop deleted the conditions branch January 6, 2022 15:07
dlorenc pushed a commit to dlorenc/community that referenced this pull request Oct 27, 2022
Signed-off-by: Hector Fernandez <hector@chainguard.dev>

Signed-off-by: Hector Fernandez <hector@chainguard.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. lgtm Indicates that a PR is ready to be merged. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants