diff --git a/api/v1alpha1/taskspawner_types.go b/api/v1alpha1/taskspawner_types.go index 7dc07719..a9d438bb 100644 --- a/api/v1alpha1/taskspawner_types.go +++ b/api/v1alpha1/taskspawner_types.go @@ -36,6 +36,18 @@ type When struct { // Jira discovers issues from a Jira project. // +optional Jira *Jira `json:"jira,omitempty"` + + // GitHubWebhook configures webhook-driven task spawning from GitHub events. + // A global webhook server watches TaskSpawners with this field set and + // creates Tasks when incoming webhooks match the configured filters. + // +optional + GitHubWebhook *GitHubWebhook `json:"githubWebhook,omitempty"` + + // LinearWebhook configures webhook-driven task spawning from Linear events. + // A global webhook server watches TaskSpawners with this field set and + // creates Tasks when incoming webhooks match the configured filters. + // +optional + LinearWebhook *LinearWebhook `json:"linearWebhook,omitempty"` } // Cron triggers task spawning on a cron schedule. @@ -295,6 +307,93 @@ type Jira struct { PollInterval string `json:"pollInterval,omitempty"` } +// GitHubWebhook configures webhook-driven task spawning from GitHub events. +type GitHubWebhook struct { + // Events is the list of GitHub event types to listen for. + // e.g., "issue_comment", "pull_request_review", "push", "issues", "pull_request" + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + Events []string `json:"events"` + + // Filters refine which events trigger tasks. If multiple filters are + // defined, any match triggers a task (OR semantics). If empty, all + // events in the Events list trigger tasks. + // +optional + Filters []GitHubWebhookFilter `json:"filters,omitempty"` +} + +// GitHubWebhookFilter defines criteria for matching a GitHub webhook event. +type GitHubWebhookFilter struct { + // Event is the GitHub event type this filter applies to. + // +kubebuilder:validation:Required + Event string `json:"event"` + + // Action filters by webhook action (e.g., "created", "opened", "submitted"). + // +optional + Action string `json:"action,omitempty"` + + // BodyContains filters by substring match on the comment or review body. + // +optional + BodyContains string `json:"bodyContains,omitempty"` + + // Labels requires the issue or PR to have all of these labels. + // +optional + Labels []string `json:"labels,omitempty"` + + // State filters by issue or PR state ("open", "closed"). + // +optional + State string `json:"state,omitempty"` + + // Branch filters push events by branch name (supports glob patterns). + // +optional + Branch string `json:"branch,omitempty"` + + // Draft filters pull requests by draft status. nil means no filtering. + // +optional + Draft *bool `json:"draft,omitempty"` + + // Author filters by the event sender's username. + // +optional + Author string `json:"author,omitempty"` +} + +// LinearWebhook configures webhook-driven task spawning from Linear events. +type LinearWebhook struct { + // Types is the list of Linear resource types to listen for. + // e.g., "Issue", "Comment", "Project" + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + Types []string `json:"types"` + + // Filters refine which events trigger tasks (OR semantics). + // If empty, all events matching the Types list trigger tasks. + // +optional + Filters []LinearWebhookFilter `json:"filters,omitempty"` +} + +// LinearWebhookFilter defines criteria for matching a Linear webhook event. +type LinearWebhookFilter struct { + // Type is the Linear resource type this filter applies to. + // +kubebuilder:validation:Required + Type string `json:"type"` + + // Action filters by webhook action ("create", "update", "remove"). + // +optional + Action string `json:"action,omitempty"` + + // States filters by Linear workflow state names (e.g., "Todo", "In Progress"). + // +optional + States []string `json:"states,omitempty"` + + // Labels requires the issue to have all of these labels. + // +optional + Labels []string `json:"labels,omitempty"` + + // ExcludeLabels excludes issues with any of these labels. + // +optional + ExcludeLabels []string `json:"excludeLabels,omitempty"` +} + // TaskTemplateMetadata holds optional labels and annotations for spawned Tasks. type TaskTemplateMetadata struct { // Labels are merged into the spawned Task's labels. Values support Go diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 536ad3b2..215bb123 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -328,6 +328,58 @@ func (in *GitHubReporting) DeepCopy() *GitHubReporting { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubWebhook) DeepCopyInto(out *GitHubWebhook) { + *out = *in + if in.Events != nil { + in, out := &in.Events, &out.Events + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make([]GitHubWebhookFilter, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubWebhook. +func (in *GitHubWebhook) DeepCopy() *GitHubWebhook { + if in == nil { + return nil + } + out := new(GitHubWebhook) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubWebhookFilter) DeepCopyInto(out *GitHubWebhookFilter) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Draft != nil { + in, out := &in.Draft, &out.Draft + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubWebhookFilter. +func (in *GitHubWebhookFilter) DeepCopy() *GitHubWebhookFilter { + if in == nil { + return nil + } + out := new(GitHubWebhookFilter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitRemote) DeepCopyInto(out *GitRemote) { *out = *in @@ -359,6 +411,63 @@ func (in *Jira) DeepCopy() *Jira { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LinearWebhook) DeepCopyInto(out *LinearWebhook) { + *out = *in + if in.Types != nil { + in, out := &in.Types, &out.Types + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make([]LinearWebhookFilter, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LinearWebhook. +func (in *LinearWebhook) DeepCopy() *LinearWebhook { + if in == nil { + return nil + } + out := new(LinearWebhook) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LinearWebhookFilter) DeepCopyInto(out *LinearWebhookFilter) { + *out = *in + if in.States != nil { + in, out := &in.States, &out.States + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludeLabels != nil { + in, out := &in.ExcludeLabels, &out.ExcludeLabels + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LinearWebhookFilter. +func (in *LinearWebhookFilter) DeepCopy() *LinearWebhookFilter { + if in == nil { + return nil + } + out := new(LinearWebhookFilter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MCPServerSpec) DeepCopyInto(out *MCPServerSpec) { *out = *in @@ -878,6 +987,16 @@ func (in *When) DeepCopyInto(out *When) { *out = new(Jira) **out = **in } + if in.GitHubWebhook != nil { + in, out := &in.GitHubWebhook, &out.GitHubWebhook + *out = new(GitHubWebhook) + (*in).DeepCopyInto(*out) + } + if in.LinearWebhook != nil { + in, out := &in.LinearWebhook, &out.LinearWebhook + *out = new(LinearWebhook) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new When. diff --git a/go.mod b/go.mod index ffce2050..2dd39bac 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/go-logr/logr v1.4.3 + github.com/google/go-github/v66 v66.0.0 github.com/google/uuid v1.6.0 github.com/google/yamlfmt v0.21.0 github.com/onsi/ginkgo/v2 v2.27.2 @@ -54,6 +55,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect diff --git a/go.sum b/go.sum index e03035e3..7bd5416c 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,13 @@ github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= +github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -274,6 +279,7 @@ golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnps golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= diff --git a/internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml b/internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml index 25d9875a..0c138edd 100644 --- a/internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml +++ b/internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml @@ -792,6 +792,69 @@ spec: rule: '!(has(self.commentPolicy) && ((has(self.triggerComment) && size(self.triggerComment) > 0) || (has(self.excludeComments) && size(self.excludeComments) > 0)))' + githubWebhook: + description: |- + GitHubWebhook configures webhook-driven task spawning from GitHub events. + A global webhook server watches TaskSpawners with this field set and + creates Tasks when incoming webhooks match the configured filters. + properties: + events: + description: |- + Events is the list of GitHub event types to listen for. + e.g., "issue_comment", "pull_request_review", "push", "issues", "pull_request" + items: + type: string + minItems: 1 + type: array + filters: + description: |- + Filters refine which events trigger tasks. If multiple filters are + defined, any match triggers a task (OR semantics). If empty, all + events in the Events list trigger tasks. + items: + description: GitHubWebhookFilter defines criteria for matching + a GitHub webhook event. + properties: + action: + description: Action filters by webhook action (e.g., + "created", "opened", "submitted"). + type: string + author: + description: Author filters by the event sender's username. + type: string + bodyContains: + description: BodyContains filters by substring match + on the comment or review body. + type: string + branch: + description: Branch filters push events by branch name + (supports glob patterns). + type: string + draft: + description: Draft filters pull requests by draft status. + nil means no filtering. + type: boolean + event: + description: Event is the GitHub event type this filter + applies to. + type: string + labels: + description: Labels requires the issue or PR to have + all of these labels. + items: + type: string + type: array + state: + description: State filters by issue or PR state ("open", + "closed"). + type: string + required: + - event + type: object + type: array + required: + - events + type: object jira: description: Jira discovers issues from a Jira project. properties: @@ -831,6 +894,61 @@ spec: - project - secretRef type: object + linearWebhook: + description: |- + LinearWebhook configures webhook-driven task spawning from Linear events. + A global webhook server watches TaskSpawners with this field set and + creates Tasks when incoming webhooks match the configured filters. + properties: + filters: + description: |- + Filters refine which events trigger tasks (OR semantics). + If empty, all events matching the Types list trigger tasks. + items: + description: LinearWebhookFilter defines criteria for matching + a Linear webhook event. + properties: + action: + description: Action filters by webhook action ("create", + "update", "remove"). + type: string + excludeLabels: + description: ExcludeLabels excludes issues with any + of these labels. + items: + type: string + type: array + labels: + description: Labels requires the issue to have all of + these labels. + items: + type: string + type: array + states: + description: States filters by Linear workflow state + names (e.g., "Todo", "In Progress"). + items: + type: string + type: array + type: + description: Type is the Linear resource type this filter + applies to. + type: string + required: + - type + type: object + type: array + types: + description: |- + Types is the list of Linear resource types to listen for. + e.g., "Issue", "Comment", "Project" + items: + type: string + minItems: 1 + type: array + required: + - types + type: object type: object required: - taskTemplate diff --git a/internal/manifests/install-crd.yaml b/internal/manifests/install-crd.yaml index 96f24feb..a97d3873 100644 --- a/internal/manifests/install-crd.yaml +++ b/internal/manifests/install-crd.yaml @@ -1471,6 +1471,69 @@ spec: rule: '!(has(self.commentPolicy) && ((has(self.triggerComment) && size(self.triggerComment) > 0) || (has(self.excludeComments) && size(self.excludeComments) > 0)))' + githubWebhook: + description: |- + GitHubWebhook configures webhook-driven task spawning from GitHub events. + A global webhook server watches TaskSpawners with this field set and + creates Tasks when incoming webhooks match the configured filters. + properties: + events: + description: |- + Events is the list of GitHub event types to listen for. + e.g., "issue_comment", "pull_request_review", "push", "issues", "pull_request" + items: + type: string + minItems: 1 + type: array + filters: + description: |- + Filters refine which events trigger tasks. If multiple filters are + defined, any match triggers a task (OR semantics). If empty, all + events in the Events list trigger tasks. + items: + description: GitHubWebhookFilter defines criteria for matching + a GitHub webhook event. + properties: + action: + description: Action filters by webhook action (e.g., + "created", "opened", "submitted"). + type: string + author: + description: Author filters by the event sender's username. + type: string + bodyContains: + description: BodyContains filters by substring match + on the comment or review body. + type: string + branch: + description: Branch filters push events by branch name + (supports glob patterns). + type: string + draft: + description: Draft filters pull requests by draft status. + nil means no filtering. + type: boolean + event: + description: Event is the GitHub event type this filter + applies to. + type: string + labels: + description: Labels requires the issue or PR to have + all of these labels. + items: + type: string + type: array + state: + description: State filters by issue or PR state ("open", + "closed"). + type: string + required: + - event + type: object + type: array + required: + - events + type: object jira: description: Jira discovers issues from a Jira project. properties: @@ -1510,6 +1573,61 @@ spec: - project - secretRef type: object + linearWebhook: + description: |- + LinearWebhook configures webhook-driven task spawning from Linear events. + A global webhook server watches TaskSpawners with this field set and + creates Tasks when incoming webhooks match the configured filters. + properties: + filters: + description: |- + Filters refine which events trigger tasks (OR semantics). + If empty, all events matching the Types list trigger tasks. + items: + description: LinearWebhookFilter defines criteria for matching + a Linear webhook event. + properties: + action: + description: Action filters by webhook action ("create", + "update", "remove"). + type: string + excludeLabels: + description: ExcludeLabels excludes issues with any + of these labels. + items: + type: string + type: array + labels: + description: Labels requires the issue to have all of + these labels. + items: + type: string + type: array + states: + description: States filters by Linear workflow state + names (e.g., "Todo", "In Progress"). + items: + type: string + type: array + type: + description: Type is the Linear resource type this filter + applies to. + type: string + required: + - type + type: object + type: array + types: + description: |- + Types is the list of Linear resource types to listen for. + e.g., "Issue", "Comment", "Project" + items: + type: string + minItems: 1 + type: array + required: + - types + type: object type: object required: - taskTemplate