sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/crier/reporters/github/reporter.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package reporter implements a reporter interface for github 18 // TODO(krzyzacy): move logic from report.go here 19 package github 20 21 import ( 22 "context" 23 "errors" 24 "fmt" 25 "strings" 26 "time" 27 28 "github.com/sirupsen/logrus" 29 ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" 30 "sigs.k8s.io/controller-runtime/pkg/reconcile" 31 32 v1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 33 "sigs.k8s.io/prow/pkg/config" 34 "sigs.k8s.io/prow/pkg/crier/reporters/criercommonlib" 35 "sigs.k8s.io/prow/pkg/github/report" 36 "sigs.k8s.io/prow/pkg/kube" 37 ) 38 39 const ( 40 // GitHubReporterName is the name for github reporter 41 GitHubReporterName = "github-reporter" 42 ) 43 44 // Client is a github reporter client 45 type Client struct { 46 gc report.GitHubClient 47 config config.Getter 48 reportAgent v1.ProwJobAgent 49 prLocks *criercommonlib.ShardedLock 50 lister ctrlruntimeclient.Reader 51 } 52 53 // NewReporter returns a reporter client 54 func NewReporter(gc report.GitHubClient, cfg config.Getter, reportAgent v1.ProwJobAgent, lister ctrlruntimeclient.Reader) *Client { 55 c := &Client{ 56 gc: gc, 57 config: cfg, 58 reportAgent: reportAgent, 59 prLocks: criercommonlib.NewShardedLock(), 60 lister: lister, 61 } 62 c.prLocks.RunCleanup() 63 return c 64 } 65 66 // GetName returns the name of the reporter 67 func (c *Client) GetName() string { 68 return GitHubReporterName 69 } 70 71 // ShouldReport returns if this prowjob should be reported by the github reporter 72 func (c *Client) ShouldReport(_ context.Context, _ *logrus.Entry, pj *v1.ProwJob) bool { 73 if !pj.Spec.Report { 74 return false 75 } 76 77 switch { 78 case pj.Labels[kube.GerritReportLabel] != "": 79 return false // TODO(fejta): opt-in to github reporting 80 case pj.Spec.Type != v1.PresubmitJob && pj.Spec.Type != v1.PostsubmitJob: 81 return false // Report presubmit and postsubmit github jobs for github reporter 82 case c.reportAgent != "" && pj.Spec.Agent != c.reportAgent: 83 return false // Only report for specified agent 84 } 85 86 return true 87 } 88 89 // Report will report via reportlib 90 func (c *Client) Report(ctx context.Context, log *logrus.Entry, pj *v1.ProwJob) ([]*v1.ProwJob, *reconcile.Result, error) { 91 ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) 92 defer cancel() 93 94 // TODO(krzyzacy): ditch ReportTemplate, and we can drop reference to config.Getter 95 err := report.ReportStatusContext(ctx, c.gc, *pj, c.config().GitHubReporter) 96 if err != nil { 97 if strings.Contains(err.Error(), "This SHA and context has reached the maximum number of statuses") { 98 // This is completely unrecoverable, so just swallow the error to make sure we wont retry, even when crier gets restarted. 99 log.WithError(err).Debug("Encountered an error, skipping retries") 100 err = nil 101 } else if strings.Contains(err.Error(), "\"message\":\"Not Found\"") || strings.Contains(err.Error(), "\"message\":\"No commit found for SHA:") { 102 // "message":"Not Found" error occurs when someone force push, which is not a crier error 103 log.WithError(err).Debug("Could not find PR commit, skipping retries") 104 err = nil 105 } 106 // Always return when there is any error reporting status context. 107 return []*v1.ProwJob{pj}, nil, err 108 } 109 110 // The github comment create/update/delete done for presubmits 111 // needs pr-level locking to avoid racing when reporting multiple 112 // jobs in parallel. 113 if pj.Spec.Type == v1.PresubmitJob { 114 key, err := lockKeyForPJ(pj) 115 if err != nil { 116 return nil, nil, fmt.Errorf("failed to get lockkey for job: %w", err) 117 } 118 lock, err := c.prLocks.GetLock(ctx, *key) 119 if err != nil { 120 return nil, nil, err 121 } 122 if err := lock.Acquire(ctx, 1); err != nil { 123 return nil, nil, err 124 } 125 defer lock.Release(1) 126 } 127 128 // Check if this org or repo has opted out of failure report comments. 129 // This check has to be here and not in ShouldReport as we always need to report 130 // the status context, just potentially not creating a comment. 131 refs := pj.Spec.Refs 132 fullRepo := fmt.Sprintf("%s/%s", refs.Org, refs.Repo) 133 for _, ident := range c.config().GitHubReporter.NoCommentRepos { 134 if refs.Org == ident || fullRepo == ident { 135 return []*v1.ProwJob{pj}, nil, nil 136 } 137 } 138 // Check if this org or repo has opted out of failure report comments 139 toReport := []v1.ProwJob{*pj} 140 var mustCreateComment bool 141 for _, ident := range c.config().GitHubReporter.SummaryCommentRepos { 142 if pj.Spec.Refs.Org == ident || fullRepo == ident { 143 mustCreateComment = true 144 toReport, err = pjsToReport(ctx, log, c.lister, pj) 145 if err != nil { 146 return []*v1.ProwJob{pj}, nil, err 147 } 148 } 149 } 150 err = report.ReportComment(ctx, c.gc, c.config().Plank.ReportTemplateForRepo(pj.Spec.Refs), toReport, c.config().GitHubReporter, mustCreateComment) 151 152 return []*v1.ProwJob{pj}, nil, err 153 } 154 155 func pjsToReport(ctx context.Context, log *logrus.Entry, lister ctrlruntimeclient.Reader, pj *v1.ProwJob) ([]v1.ProwJob, error) { 156 if len(pj.Spec.Refs.Pulls) != 1 { 157 return nil, nil 158 } 159 // find all prowjobs from this PR 160 selector := map[string]string{} 161 for _, l := range []string{kube.OrgLabel, kube.RepoLabel, kube.PullLabel} { 162 selector[l] = pj.ObjectMeta.Labels[l] 163 } 164 var pjs v1.ProwJobList 165 if err := lister.List(ctx, &pjs, ctrlruntimeclient.MatchingLabels(selector)); err != nil { 166 return nil, fmt.Errorf("Cannot list prowjob with selector %v", selector) 167 } 168 169 latestBatch := make(map[string]v1.ProwJob) 170 for _, pjob := range pjs.Items { 171 if !pjob.Complete() { // Any job still running should prevent from comments 172 return nil, nil 173 } 174 if !pjob.Spec.Report { // Filtering out non-reporting jobs 175 continue 176 } 177 // Now you have convinced me that you are the same job from my revision, 178 // continue convince me that you are the last one of your kind 179 if existing, ok := latestBatch[pjob.Spec.Job]; !ok { 180 latestBatch[pjob.Spec.Job] = pjob 181 } else if pjob.CreationTimestamp.After(existing.CreationTimestamp.Time) { 182 latestBatch[pjob.Spec.Job] = pjob 183 } 184 } 185 186 var toReport []v1.ProwJob 187 for _, pjob := range latestBatch { 188 toReport = append(toReport, pjob) 189 } 190 191 return toReport, nil 192 } 193 194 func lockKeyForPJ(pj *v1.ProwJob) (*criercommonlib.SimplePull, error) { 195 if pj.Spec.Type != v1.PresubmitJob { 196 return nil, fmt.Errorf("can only get lock key for presubmit jobs, was %q", pj.Spec.Type) 197 } 198 if pj.Spec.Refs == nil { 199 return nil, errors.New("pj.Spec.Refs is nil") 200 } 201 if n := len(pj.Spec.Refs.Pulls); n != 1 { 202 return nil, fmt.Errorf("prowjob doesn't have one but %d pulls", n) 203 } 204 return criercommonlib.NewSimplePull(pj.Spec.Refs.Org, pj.Spec.Refs.Repo, pj.Spec.Refs.Pulls[0].Number), nil 205 }