-
Notifications
You must be signed in to change notification settings - Fork 336
pipeline-controller: deduplicate protected second-stage tests on /lgtm #5365
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/sirupsen/logrus" | ||
|
|
||
| ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" | ||
| prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" | ||
| prowconfig "sigs.k8s.io/prow/pkg/config" | ||
| "sigs.k8s.io/prow/pkg/kube" | ||
| ) | ||
|
|
||
| // commentCreator abstracts the GitHub comment API for testing. | ||
| type commentCreator interface { | ||
| CreateComment(org, repo string, number int, comment string) error | ||
| } | ||
|
|
||
| // pjLister abstracts Kubernetes ProwJob listing for testing. | ||
| type pjLister interface { | ||
| List(ctx context.Context, list ctrlruntimeclient.ObjectList, opts ...ctrlruntimeclient.ListOption) error | ||
| } | ||
|
|
||
| // sendCommentWithMode posts /test commands for presubmits that need | ||
| // to be triggered in the second pipeline stage. The presubmits are | ||
| // split into two categories: | ||
| // | ||
| // - protected: non-optional, always-run jobs that must be present | ||
| // in the second stage. | ||
| // - conditionally-required: jobs whose pipeline_run_if_changed or | ||
| // pipeline_skip_if_only_changed annotations matched the changed files. | ||
| // | ||
| // For conditionally-required presubmits, acquireConditionalContexts | ||
| // already de-duplicates by checking if a ProwJob exists at the same SHA. | ||
| // For protected presubmits, we apply the same de-duplication check here | ||
| // before generating /test commands. | ||
| // | ||
| // If isExplicitCommand is true (i.e. triggered via /pipeline required), | ||
| // all matching tests are triggered unconditionally regardless of whether | ||
| // ProwJobs already exist. | ||
| func sendCommentWithMode( | ||
| ctx context.Context, | ||
| logger *logrus.Entry, | ||
| ghc commentCreator, | ||
| lister pjLister, | ||
| prowJob *prowapi.ProwJob, | ||
| protectedPresubmits []prowconfig.Presubmit, | ||
| conditionalPresubmits []prowconfig.Presubmit, | ||
| isExplicitCommand bool, | ||
| namespace string, | ||
| ) (string, error) { | ||
| if prowJob.Spec.Refs == nil || len(prowJob.Spec.Refs.Pulls) == 0 { | ||
| return "", fmt.Errorf("prowjob %s has no pull request refs", prowJob.Name) | ||
| } | ||
|
|
||
| org := prowJob.Spec.Refs.Org | ||
| repo := prowJob.Spec.Refs.Repo | ||
| prNumber := prowJob.Spec.Refs.Pulls[0].Number | ||
| headSHA := prowJob.Spec.Refs.Pulls[0].SHA | ||
|
|
||
| // Gather /test commands for protected and conditionally-required presubmits. | ||
| // When this is an explicit /pipeline required command, trigger ALL tests | ||
| // unconditionally. Otherwise, deduplicate against existing ProwJobs at the same SHA. | ||
| var protectedCommands []string | ||
| var conditionalCommands []string | ||
| var protectedAlreadyExist []string | ||
| var conditionalMsg string | ||
|
|
||
| if isExplicitCommand { | ||
| for _, ps := range protectedPresubmits { | ||
| protectedCommands = append(protectedCommands, fmt.Sprintf("/test %s", ps.Name)) | ||
| } | ||
| for _, ps := range conditionalPresubmits { | ||
| conditionalCommands = append(conditionalCommands, fmt.Sprintf("/test %s", ps.Name)) | ||
| } | ||
| } else { | ||
| conditionalCommands, conditionalMsg = acquireConditionalContexts( | ||
| ctx, logger, lister, conditionalPresubmits, org, repo, prNumber, headSHA, namespace, | ||
| ) | ||
| for _, ps := range protectedPresubmits { | ||
| exists, err := prowJobExistsForSHA(ctx, lister, ps.Name, org, repo, prNumber, headSHA, namespace) | ||
| if err != nil { | ||
| logger.WithError(err).WithField("job", ps.Name).Warn("failed to check for existing ProwJob, will trigger to be safe") | ||
| protectedCommands = append(protectedCommands, fmt.Sprintf("/test %s", ps.Name)) | ||
| continue | ||
| } | ||
| if exists { | ||
| logger.WithField("job", ps.Name).WithField("sha", headSHA).Info("protected ProwJob already exists at HEAD, skipping re-trigger") | ||
| protectedAlreadyExist = append(protectedAlreadyExist, ps.Name) | ||
| } else { | ||
| protectedCommands = append(protectedCommands, fmt.Sprintf("/test %s", ps.Name)) | ||
| } | ||
|
Comment on lines
+82
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- helpers.go outline ---'
ast-grep outline cmd/pipeline-controller/helpers.go --view expanded || true
printf '%s\n' '--- helpers.go relevant sections ---'
sed -n '1,180p' cmd/pipeline-controller/helpers.go
printf '%s\n' '--- related symbols and call sites ---'
rg -n -C 4 'prowJobExistsForSHA|protectedPresubmits|protectedCommands|protectedAlreadyExist|create.*comment|CreateComment|/test' cmd/pipeline-controller
printf '%s\n' '--- tests ---'
rg -n -C 3 'prowJobExistsForSHA|protected|re-trigger|already exists|/test' cmd/pipeline-controller --glob '*_test.go'Repository: openshift/ci-tools Length of output: 28145 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- helpers.go remainder ---'
sed -n '160,230p' cmd/pipeline-controller/helpers.go
printf '%s\n' '--- all sendCommentWithMode call sites ---'
rg -n -C 8 'sendCommentWithMode\s*\(' .
printf '%s\n' '--- pipeline-controller files ---'
git ls-files 'cmd/pipeline-controller/*'
printf '%s\n' '--- relevant test setup and fake implementations ---'
sed -n '1,230p' cmd/pipeline-controller/helpers_test.go
sed -n '230,380p' cmd/pipeline-controller/helpers_test.go
printf '%s\n' '--- package and dependency context ---'
rg -n -C 3 'pipeline-controller|controller-runtime|ProwJob' cmd/pipeline-controller go.mod go.sum | head -240Repository: openshift/ci-tools Length of output: 33184 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- change scope ---'
git status --short
git diff --stat
git diff -- cmd/pipeline-controller/helpers.go cmd/pipeline-controller/helpers_test.go cmd/pipeline-controller/main.go | sed -n '1,260p'
printf '%s\n' '--- controller/event infrastructure references ---'
rg -n -C 5 'CreateComment|AddComment|IssueComment|PullRequest|event handler|handler|pipeline required|sendCommentWithMode' --glob '*.go' --glob '!cmd/pipeline-controller/helpers_test.go' .
printf '%s\n' '--- ProwJob creation and comment processing references ---'
rg -n -C 4 'Create.*ProwJob|ProwJob.*Create|/test|handle.*comment|Handle.*Comment|IssueComment' pkg cmd --glob '*.go' | head -300
printf '%s\n' '--- commit summary ---'
git log -1 --oneline --decorateRepository: openshift/ci-tools Length of output: 50375 🏁 Script executed (no clone): Length of output: 198 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- change scope only ---'
git status --short
git diff --stat
git log -1 --format='%H%n%s%n%b'
printf '%s\n' '--- pipeline-controller references only ---'
rg -n -C 3 'pipeline-controller|sendCommentWithMode|CreateComment|/test' \
cmd/pipeline-controller pkg --glob '*.go' --glob '!**/*_test.go' || true
printf '%s\n' '--- main.go ---'
cat -n cmd/pipeline-controller/main.go
printf '%s\n' '--- helper function boundaries ---'
cat -n cmd/pipeline-controller/helpers.go | sed -n '26,122p'Repository: openshift/ci-tools Length of output: 29678 🌐 Web query:
💡 Result: When a user posts a /test comment on a GitHub pull request, Prow processes this interaction through several components in an asynchronous manner, but the prow-controller-manager is not the component that interprets the command [1][2]. The flow operates as follows: 1. GitHub sends the comment event to Prow's Hook component via a webhook [1][2]. 2. Hook translates the webhook payload into a generic event and delivers it to the appropriate plugin—in this case, the Trigger plugin [1][2]. 3. The Trigger plugin performs the logic of parsing the /test command, validating the user's permissions, and determining which jobs should be run [1][2]. 4. If valid, the Trigger plugin directly communicates with the Kubernetes API server to create the ProwJob custom resource [1][2][3]. 5. The prow-controller-manager then observes the creation of this new ProwJob object, identifies that it needs to be executed, and subsequently creates the corresponding Kubernetes pod to run the actual test [1][2][3]. Therefore, the prow-controller-manager does not create the ProwJob in response to the comment; rather, it acts on the ProwJob resource after it has been created by the Trigger plugin [1][2]. Citations:
Make non-explicit deduplication atomic. Concurrent handlers can both observe no ProwJob before 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
| } | ||
|
|
||
| allCommands := append(protectedCommands, conditionalCommands...) | ||
| if len(allCommands) == 0 { | ||
| // All tests already exist at this SHA; return an informational message | ||
| // rather than posting an empty comment. | ||
| var parts []string | ||
| if len(protectedAlreadyExist) > 0 { | ||
| parts = append(parts, fmt.Sprintf("protected tests already triggered at SHA %s: %s", headSHA, strings.Join(protectedAlreadyExist, ", "))) | ||
| } | ||
| if conditionalMsg != "" { | ||
| parts = append(parts, conditionalMsg) | ||
| } | ||
| msg := fmt.Sprintf("All pipeline tests already exist at the current HEAD. %s", strings.Join(parts, "; ")) | ||
| logger.Info(msg) | ||
| return msg, nil | ||
| } | ||
|
|
||
| comment := strings.Join(allCommands, "\n") | ||
| if err := ghc.CreateComment(org, repo, prNumber, comment); err != nil { | ||
| return "", fmt.Errorf("failed to create comment on %s/%s#%d: %w", org, repo, prNumber, err) | ||
| } | ||
|
|
||
| return fmt.Sprintf("triggered %d test(s) for %s/%s#%d at SHA %s", len(allCommands), org, repo, prNumber, headSHA), nil | ||
| } | ||
|
|
||
| // acquireConditionalContexts checks which conditionally-required presubmits | ||
| // already have ProwJobs at the given SHA and returns /test commands only for | ||
| // those that do not. It also returns an informational message about any tests | ||
| // that were skipped because they already exist. | ||
| func acquireConditionalContexts( | ||
| ctx context.Context, | ||
| logger *logrus.Entry, | ||
| lister pjLister, | ||
| presubmits []prowconfig.Presubmit, | ||
| org, repo string, | ||
| prNumber int, | ||
| headSHA string, | ||
| namespace string, | ||
| ) ([]string, string) { | ||
| var commands []string | ||
| var alreadyExist []string | ||
|
|
||
| for _, ps := range presubmits { | ||
| exists, err := prowJobExistsForSHA(ctx, lister, ps.Name, org, repo, prNumber, headSHA, namespace) | ||
| if err != nil { | ||
| logger.WithError(err).WithField("job", ps.Name).Warn("failed to check for existing ProwJob, will trigger to be safe") | ||
| commands = append(commands, fmt.Sprintf("/test %s", ps.Name)) | ||
| continue | ||
| } | ||
| if exists { | ||
| logger.WithField("job", ps.Name).WithField("sha", headSHA).Info("conditional ProwJob already exists at HEAD, skipping re-trigger") | ||
| alreadyExist = append(alreadyExist, ps.Name) | ||
| } else { | ||
| commands = append(commands, fmt.Sprintf("/test %s", ps.Name)) | ||
| } | ||
| } | ||
|
|
||
| var msg string | ||
| if len(alreadyExist) > 0 { | ||
| msg = fmt.Sprintf("conditional tests already triggered at SHA %s: %s", headSHA, strings.Join(alreadyExist, ", ")) | ||
| } | ||
| return commands, msg | ||
| } | ||
|
|
||
| // prowJobExistsForSHA checks if a ProwJob with the given job name already exists | ||
| // for the specified PR at the given HEAD SHA. It uses label-based filtering | ||
| // following the standard Prow labeling convention. | ||
| func prowJobExistsForSHA( | ||
| ctx context.Context, | ||
| lister pjLister, | ||
| jobName string, | ||
| org, repo string, | ||
| prNumber int, | ||
| headSHA string, | ||
| namespace string, | ||
| ) (bool, error) { | ||
| var pjList prowapi.ProwJobList | ||
| matchLabels := ctrlruntimeclient.MatchingLabels{ | ||
| kube.OrgLabel: org, | ||
| kube.RepoLabel: repo, | ||
| kube.PullLabel: fmt.Sprintf("%d", prNumber), | ||
| kube.ProwJobTypeLabel: string(prowapi.PresubmitJob), | ||
| kube.ProwJobAnnotation: jobName, | ||
| } | ||
| opts := []ctrlruntimeclient.ListOption{ | ||
| matchLabels, | ||
| ctrlruntimeclient.InNamespace(namespace), | ||
| } | ||
|
|
||
| if err := lister.List(ctx, &pjList, opts...); err != nil { | ||
| return false, fmt.Errorf("listing ProwJobs for %s: %w", jobName, err) | ||
| } | ||
|
|
||
| for i := range pjList.Items { | ||
| pj := &pjList.Items[i] | ||
| if pj.Spec.Refs != nil && len(pj.Spec.Refs.Pulls) > 0 && pj.Spec.Refs.Pulls[0].SHA == headSHA { | ||
| return true, nil | ||
| } | ||
| } | ||
| return false, nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Reject a nil
prowJobbefore field access.Line 54 dereferences
prowJob. A nil caller input causes a panic instead of returning an actionable error.Proposed fix
) (string, error) { + if prowJob == nil { + return "", fmt.Errorf("prowjob is nil") + } if prowJob.Spec.Refs == nil || len(prowJob.Spec.Refs.Pulls) == 0 {As per coding guidelines, “check for nil before dereferencing pointers.”
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines