go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/culpritaction/revertculprit/createrevert.go (about) 1 // Copyright 2022 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 revertculprit 16 17 import ( 18 "context" 19 "fmt" 20 "regexp" 21 "strings" 22 23 "go.chromium.org/luci/bisection/internal/config" 24 "go.chromium.org/luci/bisection/internal/gerrit" 25 "go.chromium.org/luci/bisection/model" 26 bisectionpb "go.chromium.org/luci/bisection/proto/v1" 27 "go.chromium.org/luci/bisection/util/datastoreutil" 28 29 "go.chromium.org/luci/common/errors" 30 gerritpb "go.chromium.org/luci/common/proto/gerrit" 31 ) 32 33 // isCulpritRevertible returns: 34 // - whether a revert should be created for the culprit CL; 35 // - the reason it should not be created if applicable; and 36 // - the error if one occurred. 37 func isCulpritRevertible(ctx context.Context, gerritClient *gerrit.Client, 38 culprit *gerritpb.ChangeInfo, culpritModel *model.Suspect, project string) (bool, string, error) { 39 // We only create revert if it belongs to a builder being watched 40 // by sheriffs. 41 if culpritModel.AnalysisType == bisectionpb.AnalysisType_TEST_FAILURE_ANALYSIS { 42 tfa, err := datastoreutil.GetTestFailureAnalysisForSuspect(ctx, culpritModel) 43 if err != nil { 44 return false, "", errors.Annotate(err, "get test failure analysis for suspect").Err() 45 } 46 if len(tfa.SheriffRotations) == 0 { 47 return false, "the builder of the failed test(s) is not being watched by gardeners", nil 48 } 49 } else if culpritModel.AnalysisType == bisectionpb.AnalysisType_COMPILE_FAILURE_ANALYSIS { 50 buildID, err := datastoreutil.GetBuildIDForCompileSuspect(ctx, culpritModel) 51 if err != nil { 52 return false, "", errors.Annotate(err, "get build id for suspect").Err() 53 } 54 build, err := datastoreutil.GetBuild(ctx, buildID) 55 if err != nil { 56 return false, "", errors.Annotate(err, "get build").Err() 57 } 58 if build == nil { 59 return false, "", errors.Reason("no build found: %d", buildID).Err() 60 } 61 if len(build.SheriffRotations) == 0 { 62 return false, "the associated builder is not being watched by gardeners", nil 63 } 64 } 65 66 // Check if the culprit's description has disabled autorevert 67 hasFlag, err := gerrit.HasAutoRevertOffFlagSet(ctx, culprit) 68 if err != nil { 69 return false, "", errors.Annotate(err, "error checking for auto-revert flag").Err() 70 } 71 if hasFlag { 72 return false, "auto-revert has been disabled for this CL by its description", nil 73 } 74 75 // Check if the author of the culprit is irrevertible 76 cannotRevert, err := HasIrrevertibleAuthor(ctx, culprit) 77 if err != nil { 78 return false, "", errors.Annotate(err, "error getting culprit's commit author").Err() 79 } 80 if cannotRevert { 81 return false, "LUCI Bisection cannot revert changes from this CL's author", nil 82 } 83 84 // Check if there are other merged changes depending on the culprit 85 hasDep, err := gerritClient.HasDependency(ctx, culprit) 86 if err != nil { 87 return false, "", errors.Annotate(err, "error checking for dependencies").Err() 88 } 89 if hasDep { 90 return false, "there are merged changes depending on it", nil 91 } 92 // Check if LUCI Bisection's Gerrit config allows revert creation 93 gerritCfg, err := config.GetGerritCfgForSuspect(ctx, culpritModel, project) 94 if err != nil { 95 return false, "", errors.Annotate(err, "error fetching configs").Err() 96 } 97 canCreate, reason, err := config.CanCreateRevert(ctx, gerritCfg, culpritModel.AnalysisType) 98 if err != nil { 99 return false, "", errors.Annotate(err, "error checking Create Revert configs").Err() 100 } 101 if !canCreate { 102 return false, reason, nil 103 } 104 return true, "", nil 105 } 106 107 // createRevert creates a revert for the given culprit. 108 // Returns a revert for the culprit, created by LUCI Bisection. 109 // Note: this should only be called according to the service-wide configuration 110 // data for LUCI Bisection, i.e. 111 // - Gerrit actions are enabled 112 // - creating reverts is enabled 113 // - the daily limit of created reverts has not yet been reached 114 func createRevert(ctx context.Context, gerritClient *gerrit.Client, 115 culpritModel *model.Suspect, culprit *gerritpb.ChangeInfo) (*gerritpb.ChangeInfo, error) { 116 revertDescription, err := generateRevertDescription(ctx, culpritModel, culprit) 117 if err != nil { 118 return nil, err 119 } 120 121 // Create the revert 122 revert, err := gerritClient.CreateRevert(ctx, culprit, revertDescription) 123 if err != nil { 124 return nil, err 125 } 126 127 return revert, nil 128 } 129 130 func generateRevertDescription(ctx context.Context, culpritModel *model.Suspect, 131 culprit *gerritpb.ChangeInfo) (string, error) { 132 paragraphs := []string{} 133 134 if culprit.Subject != "" { 135 paragraphs = append(paragraphs, 136 fmt.Sprintf("Revert \"%s\"", culprit.Subject)) 137 } else { 138 paragraphs = append(paragraphs, 139 fmt.Sprintf("Revert \"%s~%d\"", culprit.Project, culprit.Number)) 140 } 141 142 paragraphs = append(paragraphs, 143 fmt.Sprintf("This reverts commit %s.", culpritModel.GitilesCommit.Id)) 144 145 var message string 146 var err error 147 switch culpritModel.AnalysisType { 148 case bisectionpb.AnalysisType_COMPILE_FAILURE_ANALYSIS: 149 message, err = compileFailureComment(ctx, culpritModel, "", "blameComment") 150 if err != nil { 151 return "", errors.Annotate(err, "compile failure comment").Err() 152 } 153 case bisectionpb.AnalysisType_TEST_FAILURE_ANALYSIS: 154 message, err = testFailureComment(ctx, culpritModel, "", "blameComment") 155 if err != nil { 156 return "", errors.Annotate(err, "test failure comment").Err() 157 } 158 } 159 message = "Reason for revert:\n" + message 160 paragraphs = append(paragraphs, message) 161 162 // Lines in the footer of the description, such as related bugs 163 footerLines := []string{} 164 165 // Add the culprit's description, with each line prefixed with "> "; this is 166 // skipped if the description for the culprit is not available. 167 culpritDescription, err := gerrit.CommitMessage(ctx, culprit) 168 if err == nil { 169 bugPattern := regexp.MustCompile(`(?i)^bug\s*[:=]`) 170 prefixedLines := []string{"Original change's description:"} 171 for _, line := range strings.Split(culpritDescription, "\n") { 172 // Check if the line specifies related bugs, so it can be used in the 173 // footer of the revert 174 if bugPattern.MatchString(line) { 175 footerLines = append(footerLines, line) 176 } 177 178 prefixedLines = append(prefixedLines, 179 strings.TrimSpace(fmt.Sprintf("> %s", line))) 180 } 181 paragraphs = append(paragraphs, strings.Join(prefixedLines, "\n")) 182 } 183 184 // Set CQ flags in the last paragraph, i.e. footer of the CL description 185 cqFlags := []string{ 186 "No-Presubmit: true", 187 "No-Tree-Checks: true", 188 "No-Try: true", 189 } 190 footerLines = append(footerLines, cqFlags...) 191 paragraphs = append(paragraphs, strings.Join(footerLines, "\n")) 192 193 return strings.Join(paragraphs, "\n\n"), nil 194 }