go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/usertext/cl_error.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package usertext 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "strings" 22 "text/template" 23 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/retry/transient" 26 "go.chromium.org/luci/gae/service/datastore" 27 28 "go.chromium.org/luci/cv/internal/changelist" 29 "go.chromium.org/luci/cv/internal/common" 30 "go.chromium.org/luci/cv/internal/run" 31 ) 32 33 // SFormatCLError is the same as FormatCLError but returns a string. 34 func SFormatCLError(ctx context.Context, reason *changelist.CLError, cl *changelist.CL, mode run.Mode) (string, error) { 35 var sb strings.Builder 36 if err := FormatCLError(ctx, reason, cl, mode, &sb); err != nil { 37 return "", err 38 } 39 return sb.String(), nil 40 } 41 42 // FormatCLError formats 1 CL error by writing it into strings.Builder. 43 // 44 // mode should be specified if known. 45 func FormatCLError(ctx context.Context, reason *changelist.CLError, cl *changelist.CL, mode run.Mode, sb *strings.Builder) error { 46 switch v := reason.GetKind().(type) { 47 case *changelist.CLError_OwnerLacksEmail: 48 if !v.OwnerLacksEmail { 49 return errors.New("owner_lacks_email must be set to true") 50 } 51 return tmplCLOwnerLacksEmails.Execute(sb, map[string]string{ 52 "GerritHost": cl.Snapshot.GetGerrit().GetHost(), 53 }) 54 55 case *changelist.CLError_UnsupportedMode: 56 if v.UnsupportedMode == "" { 57 return errors.New("unsupported_mode must be set") 58 } 59 return tmplUnsupportedMode.Execute(sb, v) 60 61 case *changelist.CLError_SelfCqDepend: 62 if !v.SelfCqDepend { 63 return errors.New("self_cq_depend must be set") 64 } 65 return tmplSelfCQDepend.Execute(sb, nil) 66 67 case *changelist.CLError_CorruptGerritMetadata: 68 if v.CorruptGerritMetadata == "" { 69 return errors.New("corrupt_gerrit_metadata must be set") 70 } 71 return tmplCorruptGerritCLMetadata.Execute(sb, v) 72 73 case *changelist.CLError_WatchedByManyConfigGroups_: 74 cgs := v.WatchedByManyConfigGroups.GetConfigGroups() 75 if len(cgs) < 2 { 76 return errors.New("at least 2 config_groups required") 77 } 78 return tmplWatchedByManyConfigGroups.Execute(sb, map[string]any{ 79 "ConfigGroups": cgs, 80 "TargetRef": cl.Snapshot.GetGerrit().GetInfo().GetRef(), 81 }) 82 83 case *changelist.CLError_WatchedByManyProjects_: 84 projects := v.WatchedByManyProjects.GetProjects() 85 if len(projects) < 2 { 86 return errors.New("at least 2 projects required") 87 } 88 return tmplWatchedByManyProjects.Execute(sb, map[string]any{ 89 "Projects": projects, 90 }) 91 92 case *changelist.CLError_InvalidDeps_: 93 // Although it's possible for a CL to have several kinds of wrong deps, 94 // it's rare in practice, so simply error out on the most important kind. 95 var bad []*changelist.Dep 96 args := make(map[string]any, 2) 97 var t *template.Template 98 switch d := v.InvalidDeps; { 99 case len(d.GetUnwatched()) > 0: 100 bad, t = d.GetUnwatched(), tmplUnwatchedDeps 101 case len(d.GetWrongConfigGroup()) > 0: 102 bad, t = d.GetWrongConfigGroup(), tmplWrongDepsConfigGroup 103 case len(d.GetSingleFullDeps()) > 0: 104 bad, t = d.GetSingleFullDeps(), tmplSingleFullOpenDeps 105 args["mode"] = string(mode) 106 case len(d.GetCombinableUntriggered()) > 0: 107 bad, t = d.GetCombinableUntriggered(), tmplCombinableUntriggered 108 case len(d.GetCombinableMismatchedMode()) > 0: 109 bad, t = d.GetCombinableMismatchedMode(), tmplCombinableMismatchedMode 110 args["mode"] = string(mode) 111 case d.GetTooMany() != nil: 112 args["max"] = d.GetTooMany().GetMaxAllowed() 113 args["actual"] = d.GetTooMany().GetActual() 114 t = tmplTooManyDeps 115 bad = nil // there is no point listing all of them. 116 default: 117 return errors.Reason("unsupported InvalidDeps reason %s", d).Err() 118 } 119 urls, err := depsURLs(ctx, bad) 120 if err != nil { 121 return err 122 } 123 sort.Strings(urls) 124 args["deps"] = urls 125 return t.Execute(sb, args) 126 127 case *changelist.CLError_ReusedTrigger_: 128 if v.ReusedTrigger == nil { 129 return errors.New("reused_trigger must be set") 130 } 131 return tmplReusedTrigger.Execute(sb, v.ReusedTrigger) 132 133 case *changelist.CLError_CommitBlocked: 134 return tmplCommitBlocked.Execute(sb, nil) 135 136 case *changelist.CLError_TriggerDeps_: 137 if v.TriggerDeps == nil { 138 return errors.New("trigger_deps must be set") 139 } 140 var msgs []string 141 var deps []*changelist.Dep 142 for _, denied := range v.TriggerDeps.GetPermissionDenied() { 143 var mb strings.Builder 144 fmt.Fprintf(&mb, "no permission to vote") 145 if denied.GetEmail() != "" { 146 fmt.Fprintf(&mb, " on behalf of %s", denied.GetEmail()) 147 } 148 msgs = append(msgs, mb.String()) 149 deps = append(deps, &changelist.Dep{Clid: denied.GetClid()}) 150 } 151 for _, clid := range v.TriggerDeps.GetNotFound() { 152 msgs = append(msgs, "the CL no longer exists in Gerrit") 153 deps = append(deps, &changelist.Dep{Clid: clid}) 154 } 155 for _, clid := range v.TriggerDeps.GetInternalGerritError() { 156 msgs = append(msgs, "internal Gerrit error") 157 deps = append(deps, &changelist.Dep{Clid: clid}) 158 } 159 urls, err := depsURLs(ctx, deps) 160 if err != nil { 161 return err 162 } 163 return tmplTriggerDeps.Execute(sb, map[string]any{"urls": urls, "messages": msgs}) 164 165 case *changelist.CLError_DepRunFailed: 166 if v.DepRunFailed == 0 { 167 return errors.New("dep_run_failed must be > 0") 168 } 169 urls, err := depsURLs(ctx, []*changelist.Dep{{Clid: v.DepRunFailed}}) 170 if err != nil { 171 return err 172 } 173 return tmplDepRunFailed.Execute(sb, map[string]any{"url": urls[0]}) 174 175 default: 176 return errors.Reason("unsupported purge reason %t: %s", v, reason).Err() 177 } 178 } 179 180 func depsURLs(ctx context.Context, deps []*changelist.Dep) ([]string, error) { 181 cls := make([]*changelist.CL, len(deps)) 182 for i, d := range deps { 183 cls[i] = &changelist.CL{ID: common.CLID(d.GetClid())} 184 } 185 if err := datastore.Get(ctx, cls); err != nil { 186 return nil, errors.Annotate(err, "failed to load deps as CLs").Tag(transient.Tag).Err() 187 } 188 urls := make([]string, len(deps)) 189 for i, cl := range cls { 190 var err error 191 if urls[i], err = cl.URL(); err != nil { 192 return nil, err 193 } 194 } 195 return urls, nil 196 } 197 198 var tmplCLOwnerLacksEmails = tmplMust(` 199 {{CQ_OR_CV}} can't process the CL because its owner doesn't have a preferred email set in Gerrit settings. 200 201 You can set preferred email at https://{{.GerritHost}}/settings/#EmailAddresses 202 `) 203 204 var tmplUnsupportedMode = tmplMust(` 205 {{CQ_OR_CV}} can't process the CL because its mode {{.UnsupportedMode | printf "%q"}} is not supported. 206 {{CONTACT_YOUR_INFRA}} 207 `) 208 209 var tmplSelfCQDepend = tmplMust(` 210 {{CQ_OR_CV}} can't process the CL because it depends on itself. 211 212 Please check Cq-Depend: in CL description (commit message). If you think this is a mistake, {{CONTACT_YOUR_INFRA}}. 213 `) 214 215 var tmplCorruptGerritCLMetadata = tmplMust(` 216 {{CQ_OR_CV}} can't process the CL because its Gerrit metadata looks corrupted. 217 218 {{.CorruptGerritMetadata}} 219 220 Consider filing a Gerrit bug or {{CONTACT_YOUR_INFRA}}. 221 In the meantime, consider re-uploading your CL(s). 222 `) 223 224 var tmplWatchedByManyConfigGroups = tmplMust(` 225 {{CQ_OR_CV}} can't process the CL because it is watched by more than 1 config group: 226 {{range $cg := .ConfigGroups}} * {{$cg}} 227 {{end}} 228 {{CONTACT_YOUR_INFRA}}. For their info: 229 * current CL target ref is {{.TargetRef | printf "%q"}}, 230 * relevant doc https://chromium.googlesource.com/infra/luci/luci-go/+/HEAD/lucicfg/doc/#luci.cq_group 231 `) 232 233 var tmplWatchedByManyProjects = tmplMust(` 234 {{CQ_OR_CV}} can't process the CL because it is watched by more than 1 LUCI project: 235 {{range $p := .Projects}} * {{$p}} 236 {{end}} 237 {{CONTACT_YOUR_INFRA}}. Relevant doc for their info: 238 https://chromium.googlesource.com/infra/luci/luci-go/+/HEAD/lucicfg/doc/#luci.cq_group 239 `) 240 241 var tmplUnwatchedDeps = tmplMust(` 242 {{CQ_OR_CV}} can't process the CL because its deps are not watched by the same LUCI project: 243 {{range $url := .deps}} * {{$url}} 244 {{end}} 245 Please check Cq-Depend: in CL description (commit message). If you think this is a mistake, {{CONTACT_YOUR_INFRA}}. 246 `) 247 248 var tmplWrongDepsConfigGroup = tmplMust(` 249 {{CQ_OR_CV}} can't process the CL because its deps do not belong to the same config group: 250 {{range $url := .deps}} * {{$url}} 251 {{end}} 252 `) 253 254 var tmplSingleFullOpenDeps = tmplMust(` 255 {{CQ_OR_CV}} can't process the CL in {{.mode | printf "%q"}} mode because it has not yet submitted dependencies: 256 {{range $url := .deps}} * {{$url}} 257 {{end}} 258 Please submit directly or via CQ the dependencies first. 259 `) 260 261 var tmplCombinableUntriggered = tmplMust(` 262 {{CQ_OR_CV}} can't process the CL because its dependencies weren't CQ-ed at all: 263 {{range $url := .deps}} * {{$url}} 264 {{end}} 265 Please trigger this CL and its dependencies at the same time. 266 `) 267 268 var tmplCombinableMismatchedMode = tmplMust(` 269 {{CQ_OR_CV}} can't process the CL because its mode {{.mode | printf "%q"}} does not match mode on its dependencies: 270 {{range $url := .deps}} * {{$url}} 271 {{end}} 272 `) 273 274 var tmplTooManyDeps = tmplMust(` 275 {{CQ_OR_CV}} can't process the CL because it has too many deps: {{.actual}} (max supported: {{.max}}) 276 `) 277 278 // TODO(crbug/1223350): once we have a way to reference prior Runs, add a link 279 // to the Run. 280 var tmplReusedTrigger = tmplMust(` 281 {{CQ_OR_CV}} can't process the CL because it has previously completed a Run ({{.Run | printf "%q"}}) triggered by the same vote(s). 282 283 This may happen in rare circumstances such as moving a Gerrit Change to a new branch, abandoning & restoring the CL during an ongoing CQ Run, or when different users vote differently on a CQ label. 284 285 Please re-trigger the CQ if necessary. 286 `) 287 288 var tmplCommitBlocked = tmplMust(` 289 {{CQ_OR_CV}} won't start a full run for the CL because it has a "Commit: false" footer. 290 291 The "Commit: false" footer is used to prevent accidental submission of a CL. You may try a dry run; if you want to submit the CL, you must first remove the "Commit: false" footer from the CL description. 292 `) 293 294 var tmplTriggerDeps = tmplMust(` 295 {{CQ_OR_CV}} failed to vote the CQ label on the following dependencies. 296 {{range $i, $url := .urls}} * {{$url}} - {{index $.messages $i}} 297 {{end}} 298 `) 299 300 var tmplDepRunFailed = tmplMust(` 301 Revoking the CQ vote, because a Run failed on [CL]({{.url}}) that this CL depends on. 302 `)