Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions api/v1alpha2/taskspawner_tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package v1alpha2

// TrackerSource is the provider-neutral view of a code-host source on a
// TaskSpawner. Polling sources (githubIssues, githubPullRequests, gitlab) and
// webhook sources (githubWebhook, gitlabWebhook) all project onto it, so code
// that only needs the provider, the repository, or the reporting
// configuration does not branch on the concrete source.
type TrackerSource struct {
// Provider is the code host, using the Workspace provider values.
Provider string
// Webhook is true for event-driven sources and false for polling sources.
Webhook bool
// Repo scopes the source to a repository other than the Workspace's: the
// GitHub "owner/repo" override or restriction, or the GitLab project path.
// Empty means the Workspace repository.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: For webhook sources, an empty Repository or Project means unrestricted delivery, not the Workspace repository. Clarify this contract so future callers do not treat an empty webhook restriction as a workspace fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/v1alpha2/taskspawner_tracker.go, line 15:

<comment>For webhook sources, an empty `Repository` or `Project` means unrestricted delivery, not the Workspace repository. Clarify this contract so future callers do not treat an empty webhook restriction as a workspace fallback.</comment>

<file context>
@@ -0,0 +1,90 @@
+	Webhook bool
+	// Repo scopes the source to a repository other than the Workspace's: the
+	// GitHub "owner/repo" override or restriction, or the GitLab project path.
+	// Empty means the Workspace repository.
+	Repo string
+	// PriorityLabels orders discovered items for polling sources that support it.
</file context>

Repo string
// PriorityLabels orders discovered items for polling sources that support it.
PriorityLabels []string
// Comments is the status-comment configuration, or nil when comment
// reporting is off. The deprecated GitHub reporting.enabled flag maps to
// PerTask comments.
Comments *CommentsReporting
// Checks is the GitHub Check Run configuration, or nil.
Checks *GitHubChecksReporting
}

// CommentMode returns the effective comment mode: the configured one, or
// PerTask when comments are enabled without an explicit mode.
func (t TrackerSource) CommentMode() CommentMode {
if t.Comments != nil && t.Comments.Mode != "" {
return t.Comments.Mode
}
return CommentModePerTask
}

// Tracker returns the code-host view of the configured source. ok is false
// when the source is not backed by a code host (cron, jira, linear, generic
// webhook, slack) or when no source is set.
func (w When) Tracker() (TrackerSource, bool) {
switch {
case w.GitHubIssues != nil:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When a When object contains both jira and gitlab, this switch returns GitLab while the spawner constructs the Jira source, causing provider validation and reporting to target the wrong system. Reject multiple non-nil source fields before selecting a tracker, or add one-of validation to When.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/v1alpha2/taskspawner_tracker.go, line 41:

<comment>When a `When` object contains both `jira` and `gitlab`, this switch returns GitLab while the spawner constructs the Jira source, causing provider validation and reporting to target the wrong system. Reject multiple non-nil source fields before selecting a tracker, or add one-of validation to `When`.</comment>

<file context>
@@ -0,0 +1,90 @@
+// webhook, slack) or when no source is set.
+func (w When) Tracker() (TrackerSource, bool) {
+	switch {
+	case w.GitHubIssues != nil:
+		return githubTracker(w.GitHubIssues.Repo, w.GitHubIssues.PriorityLabels, w.GitHubIssues.Reporting, false), true
+	case w.GitHubPullRequests != nil:
</file context>

return githubTracker(w.GitHubIssues.Repo, w.GitHubIssues.PriorityLabels, w.GitHubIssues.Reporting, false), true
case w.GitHubPullRequests != nil:
return githubTracker(w.GitHubPullRequests.Repo, w.GitHubPullRequests.PriorityLabels, w.GitHubPullRequests.Reporting, false), true
case w.GitHubWebhook != nil:
return githubTracker(w.GitHubWebhook.Repository, nil, w.GitHubWebhook.Reporting, true), true
case w.GitLab != nil:
return gitlabTracker(w.GitLab.Project, w.GitLab.Reporting, false), true
case w.GitLabWebhook != nil:
return gitlabTracker(w.GitLabWebhook.Project, w.GitLabWebhook.Reporting, true), true
}
return TrackerSource{}, false
}

// PollInterval returns the configured poll interval of the polling source, or
// "" when the source uses the default or is not polled.
func (w When) PollInterval() string {
switch {
case w.GitHubIssues != nil:
return w.GitHubIssues.PollInterval
case w.GitHubPullRequests != nil:
return w.GitHubPullRequests.PollInterval
case w.Jira != nil:
return w.Jira.PollInterval
case w.GitLab != nil:
return w.GitLab.PollInterval
}
return ""
}

func githubTracker(repo string, priorityLabels []string, reporting *GitHubReporting, webhook bool) TrackerSource {
t := TrackerSource{Provider: WorkspaceProviderGitHub, Webhook: webhook, Repo: repo, PriorityLabels: priorityLabels}
if reporting == nil {
return t
}
t.Comments = reporting.Comments
if t.Comments == nil && reporting.Enabled {
t.Comments = &CommentsReporting{Mode: CommentModePerTask}
}
t.Checks = reporting.Checks
return t
}

func gitlabTracker(project string, reporting *GitLabReporting, webhook bool) TrackerSource {
t := TrackerSource{Provider: WorkspaceProviderGitLab, Webhook: webhook, Repo: project}
if reporting != nil {
t.Comments = reporting.Comments
}
return t
}
116 changes: 116 additions & 0 deletions api/v1alpha2/taskspawner_tracker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package v1alpha2

import (
"reflect"
"testing"
)

// whenFieldTrackers records, for every When field, whether it is a code-host
// source. Adding a field to When without listing it here fails the test, so a
// new provider cannot be forgotten in Tracker().
var whenFieldTrackers = map[string]bool{
"GitHubIssues": true,
"GitHubPullRequests": true,
"GitHubWebhook": true,
"GitLab": true,
"GitLabWebhook": true,
"Cron": false,
"Jira": false,
"LinearWebhook": false,
"GenericWebhook": false,
"Slack": false,
}

func TestWhenTrackerCoversEveryField(t *testing.T) {
whenType := reflect.TypeOf(When{})
for i := 0; i < whenType.NumField(); i++ {
field := whenType.Field(i)
wantTracker, listed := whenFieldTrackers[field.Name]
if !listed {
t.Fatalf("When.%s is not classified in whenFieldTrackers; decide whether Tracker() must cover it", field.Name)
}
var when When
reflect.ValueOf(&when).Elem().Field(i).Set(reflect.New(field.Type.Elem()))
if _, ok := when.Tracker(); ok != wantTracker {
t.Errorf("When{%s}.Tracker() ok = %v, want %v", field.Name, ok, wantTracker)
}
}
if len(whenFieldTrackers) != whenType.NumField() {
t.Errorf("whenFieldTrackers lists %d fields, When has %d", len(whenFieldTrackers), whenType.NumField())
}
}

func TestWhenTracker(t *testing.T) {
tests := []struct {
name string
when When
want TrackerSource
}{
{
name: "github issues with deprecated enabled flag",
when: When{GitHubIssues: &GitHubIssues{Repo: "org/upstream", PriorityLabels: []string{"p0"}, Reporting: &GitHubReporting{Enabled: true}}},
want: TrackerSource{Provider: WorkspaceProviderGitHub, Repo: "org/upstream", PriorityLabels: []string{"p0"}, Comments: &CommentsReporting{Mode: CommentModePerTask}},
},
{
name: "github pull requests with sticky comments and checks",
when: When{GitHubPullRequests: &GitHubPullRequests{Reporting: &GitHubReporting{
Comments: &CommentsReporting{Mode: CommentModeSticky},
Checks: &GitHubChecksReporting{Name: "kelos"},
}}},
want: TrackerSource{Provider: WorkspaceProviderGitHub, Comments: &CommentsReporting{Mode: CommentModeSticky}, Checks: &GitHubChecksReporting{Name: "kelos"}},
},
{
name: "github webhook restriction",
when: When{GitHubWebhook: &GitHubWebhook{Repository: "org/repo"}},
want: TrackerSource{Provider: WorkspaceProviderGitHub, Webhook: true, Repo: "org/repo"},
},
{
name: "gitlab project with comments",
when: When{GitLab: &GitLab{Project: "group/repo", Reporting: &GitLabReporting{Comments: &CommentsReporting{}}}},
want: TrackerSource{Provider: WorkspaceProviderGitLab, Repo: "group/repo", Comments: &CommentsReporting{}},
},
{
name: "gitlab webhook without reporting",
when: When{GitLabWebhook: &GitLabWebhook{Project: "group/repo"}},
want: TrackerSource{Provider: WorkspaceProviderGitLab, Webhook: true, Repo: "group/repo"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := tt.when.Tracker()
if !ok {
t.Fatal("Tracker() ok = false, want true")
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Tracker() = %+v, want %+v", got, tt.want)
}
})
}
}

func TestTrackerSourceCommentMode(t *testing.T) {
if got := (TrackerSource{}).CommentMode(); got != CommentModePerTask {
t.Errorf("CommentMode() without comments = %q, want PerTask", got)
}
if got := (TrackerSource{Comments: &CommentsReporting{}}).CommentMode(); got != CommentModePerTask {
t.Errorf("CommentMode() with empty mode = %q, want PerTask", got)
}
if got := (TrackerSource{Comments: &CommentsReporting{Mode: CommentModeSticky}}).CommentMode(); got != CommentModeSticky {
t.Errorf("CommentMode() = %q, want Sticky", got)
}
}

func TestWhenPollInterval(t *testing.T) {
tests := map[string]When{
"2m": {GitHubIssues: &GitHubIssues{PollInterval: "2m"}},
"3m": {GitHubPullRequests: &GitHubPullRequests{PollInterval: "3m"}},
"4m": {Jira: &Jira{PollInterval: "4m"}},
"30s": {GitLab: &GitLab{PollInterval: "30s"}},
"": {GitHubWebhook: &GitHubWebhook{}},
}
for want, when := range tests {
if got := when.PollInterval(); got != want {
t.Errorf("PollInterval() = %q, want %q", got, want)
}
}
}
Loading
Loading