github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/generic-autobumper/main.go (about) 1 /* 2 Copyright 2019 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 main 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "net/http" 26 "os" 27 "path/filepath" 28 "regexp" 29 "sort" 30 "strings" 31 32 flag "github.com/spf13/pflag" 33 "golang.org/x/oauth2/google" 34 35 "github.com/sirupsen/logrus" 36 37 "k8s.io/apimachinery/pkg/util/sets" 38 "sigs.k8s.io/prow/cmd/generic-autobumper/bumper" 39 "sigs.k8s.io/prow/cmd/generic-autobumper/imagebumper" 40 41 "sigs.k8s.io/yaml" 42 ) 43 44 const ( 45 latestVersion = "latest" 46 upstreamVersion = "upstream" 47 upstreamStagingVersion = "upstream-staging" 48 tagVersion = "vYYYYMMDD-deadbeef" 49 defaultUpstreamURLBase = "https://raw.githubusercontent.com/kubernetes/test-infra/master" 50 googleImageRegistryAuth = "google" 51 cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform" 52 53 defaultOncallGroup = "testinfra" 54 errOncallMsgTempl = "An error occurred while finding an assignee: `%s`.\nFalling back to Blunderbuss." 55 noOncallMsg = "Nobody is currently oncall, so falling back to Blunderbuss." 56 ) 57 58 var ( 59 tagRegexp = regexp.MustCompile("v[0-9]{8}-[a-f0-9]{6,9}") 60 imageMatcher = regexp.MustCompile(`(?s)^.+image:(.+):(v[a-zA-Z0-9_.-]+)`) 61 ) 62 63 var _ bumper.PRHandler = (*client)(nil) 64 65 type client struct { 66 o *options 67 images map[string]string 68 versions map[string][]string 69 } 70 71 // Changes returns a slice of functions, each one does some stuff, and 72 // returns commit message for the changes 73 func (c *client) Changes() []func(context.Context) (string, error) { 74 return []func(context.Context) (string, error){ 75 func(ctx context.Context) (string, error) { 76 var err error 77 if c.images, err = updateReferencesWrapper(ctx, c.o); err != nil { 78 return "", fmt.Errorf("failed to update image references: %w", err) 79 } 80 81 if c.versions, err = getVersionsAndCheckConsistency(c.o.Prefixes, c.images); err != nil { 82 return "", err 83 } 84 85 var body string 86 var prefixNames []string 87 for _, prefix := range c.o.Prefixes { 88 prefixNames = append(prefixNames, prefix.Name) 89 body = body + generateSummary(prefix.Name, prefix.Repo, prefix.Prefix, prefix.Summarise, c.images) + "\n\n" 90 } 91 92 return fmt.Sprintf("Bumping %s\n\n%s", strings.Join(prefixNames, " and "), body), nil 93 }, 94 } 95 } 96 97 // PRTitleBody returns the body of the PR, this function runs after each commit 98 func (c *client) PRTitleBody() (string, string) { 99 body := generatePRBody(c.images, c.o.Prefixes) + 100 getAssignment(c.o.OncallAddress, c.o.OncallGroup, c.o.SkipOncallAssignment, c.o.SelfAssign) + "\n" 101 if c.o.AdditionalPRBody != "" { 102 body += c.o.AdditionalPRBody + "\n" 103 } 104 return makeCommitSummary(c.o.Prefixes, c.versions), body 105 } 106 107 func generatePRBody(images map[string]string, prefixes []prefix) (body string) { 108 body = "" 109 for _, prefix := range prefixes { 110 body = body + generateSummary(prefix.Name, prefix.Repo, prefix.Prefix, prefix.Summarise, images) + "\n\n" 111 } 112 return body + "\n" 113 } 114 115 // options is the options for autobumper operations. 116 type options struct { 117 // The URL where upstream image references are located. Only required if Target Version is "upstream" or "upstreamStaging". Use "https://raw.githubusercontent.com/{ORG}/{REPO}" 118 // Images will be bumped based off images located at the address using this URL and the refConfigFile or stagingRefConigFile for each Prefix. 119 UpstreamURLBase string `yaml:"upstreamURLBase"` 120 // The config paths to be included in this bump, in which only .yaml files will be considered. By default all files are included. 121 IncludedConfigPaths []string `yaml:"includedConfigPaths"` 122 // The config paths to be excluded in this bump, in which only .yaml files will be considered. 123 ExcludedConfigPaths []string `yaml:"excludedConfigPaths"` 124 // The extra non-yaml file to be considered in this bump. 125 ExtraFiles []string `yaml:"extraFiles"` 126 // The target version to bump images version to, which can be one of latest, upstream, upstream-staging and vYYYYMMDD-deadbeef. 127 TargetVersion string `yaml:"targetVersion"` 128 // List of prefixes that the autobumped is looking for, and other information needed to bump them. Must have at least 1 prefix. 129 Prefixes []prefix `yaml:"prefixes"` 130 // The oncall address where we can get the JSON file that stores the current oncall information. 131 OncallAddress string `json:"onCallAddress"` 132 // The oncall group that is responsible for reviewing the change, i.e. "test-infra". 133 OncallGroup string `json:"onCallGroup"` 134 // Whether skip if no oncall is discovered 135 SkipIfNoOncall bool `yaml:"skipIfNoOncall"` 136 // SkipOncallAssignment skips assigning to oncall. 137 // The OncallAddress and OncallGroup are required for auto-bumper to figure out whether there are active oncall, 138 // which is used to avoid bumping when there is no active oncall. 139 SkipOncallAssignment bool `yaml:"skipOncallAssignment"` 140 // SelfAssign is used to comment `/assign` and `/cc` so that blunderbuss wouldn't assign 141 // bump PR to someone else. 142 SelfAssign bool `yaml:"selfAssign"` 143 // ImageRegistryAuth determines a way the autobumper with authenticate when talking to image registry. 144 // Allowed values: 145 // * "" (empty) -- uses no auth token 146 // * "google" -- uses Google's "Application Default Credentials" as defined on https://pkg.go.dev/golang.org/x/oauth2/google#hdr-Credentials. 147 ImageRegistryAuth string `yaml:"imageRegistryAuth"` 148 // AdditionalPRBody allows for generic, additional content in the body of the PR 149 AdditionalPRBody string `yaml:"additionalPRBody"` 150 } 151 152 // prefix is the information needed for each prefix being bumped. 153 type prefix struct { 154 // Name of the tool being bumped 155 Name string `yaml:"name"` 156 // The image prefix that the autobumper should look for 157 Prefix string `yaml:"prefix"` 158 // File that is looked at to determine current upstream image when bumping to upstream. Required only if targetVersion is "upstream" 159 RefConfigFile string `yaml:"refConfigFile"` 160 // File that is looked at to determine current upstream staging image when bumping to upstream staging. Required only if targetVersion is "upstream-staging" 161 StagingRefConfigFile string `yaml:"stagingRefConfigFile"` 162 // The repo where the image source resides for the images with this prefix. Used to create the links to see comparisons between images in the PR summary. 163 Repo string `yaml:"repo"` 164 // Whether or not the format of the PR summary for this prefix should be summarised. 165 Summarise bool `yaml:"summarise"` 166 // Whether the prefix tags should be consistent after the bump 167 ConsistentImages bool `yaml:"consistentImages"` 168 // A list of images whose tags are not required to be consistent after the bump. Requires `consistentImages: true`. 169 ConsistentImageExceptions []string `yaml:"consistentImageExceptions"` 170 } 171 172 func parseOptions() (*options, *bumper.Options, error) { 173 var config string 174 var labelsOverride []string 175 var skipPullRequest bool 176 var signoff bool 177 178 var o options 179 flag.StringVar(&config, "config", "", "The path to the config file for the autobumber.") 180 flag.StringSliceVar(&labelsOverride, "labels-override", nil, "Override labels to be added to PR.") 181 flag.BoolVar(&skipPullRequest, "skip-pullrequest", false, "") 182 flag.BoolVar(&signoff, "signoff", false, "Signoff the commits.") 183 flag.BoolVar(&o.SkipIfNoOncall, "skip-if-no-oncall", false, "Don't run anything if no oncall is discovered") 184 flag.Parse() 185 186 var pro bumper.Options 187 data, err := os.ReadFile(config) 188 if err != nil { 189 return nil, nil, fmt.Errorf("read %q: %w", config, err) 190 } 191 192 if err = yaml.Unmarshal(data, &o); err != nil { 193 return nil, nil, fmt.Errorf("unmarshal %q: %w", config, err) 194 } 195 196 if err := yaml.Unmarshal(data, &pro); err != nil { 197 return nil, nil, fmt.Errorf("unmarshal %q: %w", config, err) 198 } 199 200 if labelsOverride != nil { 201 pro.Labels = labelsOverride 202 } 203 if o.OncallGroup == "" { 204 o.OncallGroup = defaultOncallGroup 205 } 206 pro.SkipPullRequest = skipPullRequest 207 pro.Signoff = signoff 208 return &o, &pro, nil 209 } 210 211 func validateOptions(o *options) error { 212 if len(o.Prefixes) == 0 { 213 return errors.New("must have at least one Prefix specified") 214 } 215 for _, prefix := range o.Prefixes { 216 if len(prefix.ConsistentImageExceptions) > 0 && !prefix.ConsistentImages { 217 return fmt.Errorf("consistentImageExceptions requires consistentImages to be true, found in prefix %q", prefix.Name) 218 } 219 } 220 if len(o.IncludedConfigPaths) == 0 { 221 return errors.New("includedConfigPaths is mandatory") 222 } 223 if o.TargetVersion != latestVersion && o.TargetVersion != upstreamVersion && 224 o.TargetVersion != upstreamStagingVersion && !tagRegexp.MatchString(o.TargetVersion) { 225 logrus.WithField("allowed", []string{latestVersion, upstreamVersion, upstreamStagingVersion, tagVersion}).Warn( 226 "Warning: targetVersion mot in allowed so it might not work properly.") 227 } 228 if o.TargetVersion == upstreamVersion { 229 for _, prefix := range o.Prefixes { 230 if prefix.RefConfigFile == "" { 231 return fmt.Errorf("targetVersion can't be %q without refConfigFile for each prefix. %q is missing one", upstreamVersion, prefix.Name) 232 } 233 } 234 } 235 if o.TargetVersion == upstreamStagingVersion { 236 for _, prefix := range o.Prefixes { 237 if prefix.StagingRefConfigFile == "" { 238 return fmt.Errorf("targetVersion can't be %q without stagingRefConfigFile for each prefix. %q is missing one", upstreamStagingVersion, prefix.Name) 239 } 240 } 241 } 242 if (o.TargetVersion == upstreamVersion || o.TargetVersion == upstreamStagingVersion) && o.UpstreamURLBase == "" { 243 o.UpstreamURLBase = defaultUpstreamURLBase 244 logrus.Warnf("targetVersion can't be 'upstream' or 'upstreamStaging` without upstreamURLBase set. Default upstreamURLBase is %q", defaultUpstreamURLBase) 245 } 246 247 if o.ImageRegistryAuth != "" && o.ImageRegistryAuth != googleImageRegistryAuth { 248 return fmt.Errorf("imageRegistryAuth has incorrect value: %q. Only \"\" and %q are allowed", o.ImageRegistryAuth, googleImageRegistryAuth) 249 } 250 251 return nil 252 } 253 254 func isOncallActive(oncallAddress, oncallGroup string) bool { 255 _, oncallActive, _ := getOncallInfo(oncallAddress, oncallGroup) 256 return oncallActive 257 } 258 259 func getAssignment(oncallAddress, oncallGroup string, skipOncallAssignment, selfAssign bool) string { 260 // No reason to self assign if wants to assign to oncall 261 if selfAssign { 262 return "/cc" 263 } 264 if skipOncallAssignment { 265 return "" 266 } 267 // Processing oncall info now 268 curtOncall, _, err := getOncallInfo(oncallAddress, oncallGroup) 269 if err != nil { 270 return fmt.Sprintf(errOncallMsgTempl, err.Error()) 271 } 272 if curtOncall == "" { 273 return noOncallMsg 274 } 275 return curtOncall 276 } 277 278 func getOncallInfo(oncallAddress, oncallGroup string) (string, bool, error) { 279 if oncallAddress == "" { 280 return "", false, nil 281 } 282 283 req, err := http.Get(oncallAddress) 284 if err != nil { 285 return "", false, err 286 } 287 defer req.Body.Close() 288 if req.StatusCode != http.StatusOK { 289 return "", false, fmt.Errorf("requesting oncall address: HTTP error %d: %q", req.StatusCode, req.Status) 290 } 291 oncall := struct { 292 Oncall map[string]string `json:"Oncall"` 293 Active map[string]bool `json:"Active"` 294 }{} 295 if err := json.NewDecoder(req.Body).Decode(&oncall); err != nil { 296 return "", false, err 297 } 298 curtOncall, ok := oncall.Oncall[oncallGroup] 299 if !ok { 300 return "", false, fmt.Errorf("oncall map doesn't contain group '%s'", oncallGroup) 301 } 302 oncallActive, ok := oncall.Active[oncallGroup] 303 if !ok { 304 return "", false, fmt.Errorf("oncall map doesn't contain group '%s'", oncallGroup) 305 } 306 if curtOncall != "" { 307 return "/cc @" + curtOncall, oncallActive, nil 308 } 309 return "", false, nil 310 } 311 312 // updateReferencesWrapper update the references of prow-images and/or boskos-images and/or testimages 313 // in the files in any of "subfolders" of the includeConfigPaths but not in excludeConfigPaths 314 // if the file is a yaml file (*.yaml) or extraFiles[file]=true 315 func updateReferencesWrapper(ctx context.Context, o *options) (map[string]string, error) { 316 logrus.Info("Bumping image references...") 317 var allPrefixes []string 318 for _, prefix := range o.Prefixes { 319 allPrefixes = append(allPrefixes, prefix.Prefix) 320 } 321 filterRegexp, err := regexp.Compile(strings.Join(allPrefixes, "|")) 322 if err != nil { 323 return nil, fmt.Errorf("bad regexp %q: %w", strings.Join(allPrefixes, "|"), err) 324 } 325 var client *http.Client = http.DefaultClient 326 if o.ImageRegistryAuth == googleImageRegistryAuth { 327 var err error 328 client, err = google.DefaultClient(ctx, cloudPlatformScope) 329 if err != nil { 330 return nil, fmt.Errorf("failed to create authed client: %v", err) 331 } 332 } 333 imageBumperCli := imagebumper.NewClient(client) 334 return updateReferences(imageBumperCli, filterRegexp, o) 335 } 336 337 type imageBumper interface { 338 FindLatestTag(imageHost, imageName, currentTag string) (string, error) 339 UpdateFile(tagPicker func(imageHost, imageName, currentTag string) (string, error), path string, imageFilter *regexp.Regexp) error 340 GetReplacements() map[string]string 341 AddToCache(image, newTag string) 342 TagExists(imageHost, imageName, currentTag string) (bool, error) 343 } 344 345 func updateReferences(imageBumperCli imageBumper, filterRegexp *regexp.Regexp, o *options) (map[string]string, error) { 346 var tagPicker func(string, string, string) (string, error) 347 348 switch o.TargetVersion { 349 case latestVersion: 350 tagPicker = imageBumperCli.FindLatestTag 351 case upstreamVersion, upstreamStagingVersion: 352 var err error 353 if tagPicker, err = upstreamImageVersionResolver(o, o.TargetVersion, parseUpstreamImageVersion, imageBumperCli); err != nil { 354 return nil, fmt.Errorf("failed to resolve the %s image version: %w", o.TargetVersion, err) 355 } 356 default: 357 tagPicker = func(imageHost, imageName, currentTag string) (string, error) { return o.TargetVersion, nil } 358 } 359 360 updateFile := func(name string) error { 361 logrus.WithField("file", name).Info("Updating file") 362 if err := imageBumperCli.UpdateFile(tagPicker, name, filterRegexp); err != nil { 363 return fmt.Errorf("failed to update the file: %w", err) 364 } 365 return nil 366 } 367 updateYAMLFile := func(name string) error { 368 if strings.HasSuffix(name, ".yaml") && !isUnderPath(name, o.ExcludedConfigPaths) { 369 return updateFile(name) 370 } 371 return nil 372 } 373 374 // Updated all .yaml files under the included config paths but not under excluded config paths. 375 for _, path := range o.IncludedConfigPaths { 376 info, err := os.Stat(path) 377 if err != nil { 378 return nil, fmt.Errorf("failed to get the file info for %q: %w", path, err) 379 } 380 if info.IsDir() { 381 err := filepath.Walk(path, func(subpath string, info os.FileInfo, err error) error { 382 return updateYAMLFile(subpath) 383 }) 384 if err != nil { 385 return nil, fmt.Errorf("failed to update yaml files under %q: %w", path, err) 386 } 387 } else { 388 if err := updateYAMLFile(path); err != nil { 389 return nil, fmt.Errorf("failed to update the yaml file %q: %w", path, err) 390 } 391 } 392 } 393 394 // Update the extra files in any case. 395 for _, file := range o.ExtraFiles { 396 if err := updateFile(file); err != nil { 397 return nil, fmt.Errorf("failed to update the extra file %q: %w", file, err) 398 } 399 } 400 401 return imageBumperCli.GetReplacements(), nil 402 } 403 404 // used by updateReferences 405 func upstreamImageVersionResolver( 406 o *options, upstreamVersionType string, parse func(upstreamAddress, prefix string) (string, error), imageBumperCli imageBumper) (func(imageHost, imageName, currentTag string) (string, error), error) { 407 upstreamVersions, err := upstreamConfigVersions(upstreamVersionType, o, parse) 408 if err != nil { 409 return nil, err 410 } 411 412 return func(imageHost, imageName, currentTag string) (string, error) { 413 imageFullPath := imageHost + "/" + imageName + ":" + currentTag 414 for prefix, version := range upstreamVersions { 415 if !strings.HasPrefix(imageFullPath, prefix) { 416 continue 417 } 418 if exists, err := imageBumperCli.TagExists(imageHost, imageName, version); err != nil { 419 return "", err 420 } else if exists { 421 imageBumperCli.AddToCache(imageFullPath, version) 422 return version, nil 423 } 424 imageBumperCli.AddToCache(imageFullPath, currentTag) 425 return "", fmt.Errorf("Unable to bump to %s, image tag %s does not exist for %s", imageFullPath, version, imageName) 426 } 427 return currentTag, nil 428 }, nil 429 } 430 431 // used by upstreamImageVersionResolver 432 func upstreamConfigVersions(upstreamVersionType string, o *options, parse func(upstreamAddress, prefix string) (string, error)) (versions map[string]string, err error) { 433 versions = make(map[string]string) 434 var upstreamAddress string 435 for _, prefix := range o.Prefixes { 436 if upstreamVersionType == upstreamVersion { 437 upstreamAddress = o.UpstreamURLBase + "/" + prefix.RefConfigFile 438 } else if upstreamVersionType == upstreamStagingVersion { 439 upstreamAddress = o.UpstreamURLBase + "/" + prefix.StagingRefConfigFile 440 } else { 441 return nil, fmt.Errorf("unsupported upstream version type: %s, must be one of %v", 442 upstreamVersionType, []string{upstreamVersion, upstreamStagingVersion}) 443 } 444 version, err := parse(upstreamAddress, prefix.Prefix) 445 if err != nil { 446 return nil, err 447 } 448 versions[prefix.Prefix] = version 449 } 450 451 return versions, nil 452 } 453 454 // used by updateReferences 455 func parseUpstreamImageVersion(upstreamAddress, prefix string) (string, error) { 456 resp, err := http.Get(upstreamAddress) 457 if err != nil { 458 return "", fmt.Errorf("error sending GET request to %q: %w", upstreamAddress, err) 459 } 460 defer resp.Body.Close() 461 if resp.StatusCode != http.StatusOK { 462 return "", fmt.Errorf("HTTP error %d (%q) fetching upstream config file", resp.StatusCode, resp.Status) 463 } 464 body, err := io.ReadAll(resp.Body) 465 if err != nil { 466 return "", fmt.Errorf("error reading the response body: %w", err) 467 } 468 for _, line := range strings.Split(strings.TrimSuffix(string(body), "\n"), "\n") { 469 res := imageMatcher.FindStringSubmatch(string(line)) 470 if len(res) > 2 && strings.Contains(res[1], prefix) { 471 return res[2], nil 472 } 473 } 474 return "", fmt.Errorf("unable to find match for %s in upstream refConfigFile", prefix) 475 } 476 477 // getVersionsAndCheckConisistency takes a list of Prefixes and a map of 478 // all the images found in the code before the bump : their versions after the bump 479 // For example {"gcr.io/k8s-prow/test1:tag": "newtag", "gcr.io/k8s-prow/test2:tag": "newtag"}, 480 // and returns a map of new versions resulted from bumping : the images using those versions. 481 // It will error if one of the Prefixes was bumped inconsistently when it was not supposed to 482 func getVersionsAndCheckConsistency(prefixes []prefix, images map[string]string) (map[string][]string, error) { 483 // Key is tag, value is full image. 484 versions := map[string][]string{} 485 for _, prefix := range prefixes { 486 exceptions := sets.NewString(prefix.ConsistentImageExceptions...) 487 var consistencyVersion, consistencySourceImage string 488 for k, v := range images { 489 if strings.HasPrefix(k, prefix.Prefix) { 490 image := imageFromName(k) 491 if prefix.ConsistentImages && !exceptions.Has(image) { 492 if consistencySourceImage != "" && (consistencyVersion != v) { 493 return nil, fmt.Errorf("%s -> %s not bumped consistently for prefix %s (%s), expected version %s based on bump of %s", k, v, prefix.Prefix, prefix.Name, consistencyVersion, consistencySourceImage) 494 } 495 if consistencySourceImage == "" { 496 consistencyVersion = v 497 consistencySourceImage = k 498 } 499 } 500 501 //Only add bumped images to the new versions map 502 if !strings.Contains(k, v) { 503 versions[v] = append(versions[v], k) 504 } 505 506 } 507 } 508 } 509 return versions, nil 510 } 511 512 // makeCommitSummary takes a list of Prefixes and a map of new tags resulted 513 // from bumping : the images using those tags and returns a summary of what was 514 // bumped for use in the commit message 515 func makeCommitSummary(prefixes []prefix, versions map[string][]string) string { 516 var allPrefixes []string 517 for _, prefix := range prefixes { 518 allPrefixes = append(allPrefixes, prefix.Name) 519 } 520 if len(versions) == 0 { 521 return fmt.Sprintf("Update %s images as necessary", strings.Join(allPrefixes, ", ")) 522 } 523 var inconsistentBumps []string 524 var consistentBumps []string 525 for _, prefix := range prefixes { 526 tag, bumped := isBumpedPrefix(prefix, versions) 527 if !prefix.ConsistentImages && bumped { 528 inconsistentBumps = append(inconsistentBumps, prefix.Name) 529 } else if prefix.ConsistentImages && bumped { 530 consistentBumps = append(consistentBumps, fmt.Sprintf("%s to %s", prefix.Name, tag)) 531 } 532 } 533 var msgs []string 534 if len(consistentBumps) != 0 { 535 msgs = append(msgs, strings.Join(consistentBumps, ", ")) 536 } 537 if len(inconsistentBumps) != 0 { 538 msgs = append(msgs, fmt.Sprintf("%s as needed", strings.Join(inconsistentBumps, ", "))) 539 } 540 return fmt.Sprintf("Update %s", strings.Join(msgs, " and ")) 541 542 } 543 544 // Generate PR summary for github 545 func generateSummary(name, repo, prefix string, summarise bool, images map[string]string) string { 546 type delta struct { 547 oldCommit string 548 newCommit string 549 oldDate string 550 newDate string 551 variant string 552 component string 553 } 554 versions := map[string][]delta{} 555 for image, newTag := range images { 556 if !strings.HasPrefix(image, prefix) { 557 continue 558 } 559 if strings.HasSuffix(image, ":"+newTag) { 560 continue 561 } 562 oldDate, oldCommit, oldVariant := imagebumper.DeconstructTag(tagFromName(image)) 563 newDate, newCommit, _ := imagebumper.DeconstructTag(newTag) 564 oldCommit = commitToRef(oldCommit) 565 newCommit = commitToRef(newCommit) 566 k := oldCommit + ":" + newCommit 567 d := delta{ 568 oldCommit: oldCommit, 569 newCommit: newCommit, 570 oldDate: oldDate, 571 newDate: newDate, 572 variant: formatVariant(oldVariant), 573 component: componentFromName(image), 574 } 575 versions[k] = append(versions[k], d) 576 } 577 578 switch { 579 case len(versions) == 0: 580 return fmt.Sprintf("No %s changes.", prefix) 581 case len(versions) == 1 && summarise: 582 for k, v := range versions { 583 s := strings.Split(k, ":") 584 return fmt.Sprintf("%s changes: %s/compare/%s...%s (%s → %s)", prefix, repo, s[0], s[1], formatTagDate(v[0].oldDate), formatTagDate(v[0].newDate)) 585 } 586 default: 587 changes := make([]string, 0, len(versions)) 588 for k, v := range versions { 589 s := strings.Split(k, ":") 590 names := make([]string, 0, len(v)) 591 for _, d := range v { 592 names = append(names, d.component+d.variant) 593 } 594 sort.Strings(names) 595 changes = append(changes, fmt.Sprintf("%s/compare/%s...%s | %s → %s | %s", 596 repo, s[0], s[1], formatTagDate(v[0].oldDate), formatTagDate(v[0].newDate), strings.Join(names, ", "))) 597 } 598 sort.Slice(changes, func(i, j int) bool { return strings.Split(changes[i], "|")[1] < strings.Split(changes[j], "|")[1] }) 599 return fmt.Sprintf("Multiple distinct %s changes:\n\nCommits | Dates | Images\n--- | --- | ---\n%s\n", prefix, strings.Join(changes, "\n")) 600 } 601 panic("unreachable!") 602 } 603 604 func main() { 605 ctx := context.Background() 606 logrus.SetLevel(logrus.DebugLevel) 607 o, pro, err := parseOptions() 608 if err != nil { 609 logrus.WithError(err).Fatalf("Failed to run the bumper tool") 610 } 611 612 if o.SkipIfNoOncall { 613 if !isOncallActive(o.OncallAddress, o.OncallGroup) { 614 615 logrus.Info("`skip-if-no-oncall` is configured and there is no active oncall. Skip bumping.") 616 return 617 } 618 } 619 if err := validateOptions(o); err != nil { 620 logrus.WithError(err).Fatalf("Failed validating flags") 621 } 622 623 if err := bumper.Run(ctx, pro, &client{o: o}); err != nil { 624 logrus.WithError(err).Fatalf("failed to run the bumper tool") 625 } 626 }