golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/cmd/gorelease/report.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "fmt" 9 "strings" 10 11 "golang.org/x/exp/apidiff" 12 "golang.org/x/mod/module" 13 "golang.org/x/mod/semver" 14 "golang.org/x/tools/go/packages" 15 ) 16 17 // report describes the differences in the public API between two versions 18 // of a module. 19 type report struct { 20 // base contains information about the "old" module version being compared 21 // against. base.version may be "none", indicating there is no base version 22 // (for example, if this is the first release). base.version may not be "". 23 base moduleInfo 24 25 // release contains information about the version of the module to release. 26 // The version may be set explicitly with -version or suggested using 27 // suggestVersion, in which case release.versionInferred is true. 28 release moduleInfo 29 30 // packages is a list of package reports, describing the differences 31 // for individual packages, sorted by package path. 32 packages []packageReport 33 34 // versionInvalid explains why the proposed or suggested version is not valid. 35 versionInvalid *versionMessage 36 37 // haveCompatibleChanges is true if there are any backward-compatible 38 // changes in non-internal packages. 39 haveCompatibleChanges bool 40 41 // haveIncompatibleChanges is true if there are any backward-incompatible 42 // changes in non-internal packages. 43 haveIncompatibleChanges bool 44 45 // haveBaseErrors is true if there were errors loading packages 46 // in the base version. 47 haveBaseErrors bool 48 49 // haveReleaseErrors is true if there were errors loading packages 50 // in the release version. 51 haveReleaseErrors bool 52 } 53 54 // String returns a human-readable report that lists errors, compatible changes, 55 // and incompatible changes in each package. If releaseVersion is set, the 56 // report states whether releaseVersion is valid (and why). If releaseVersion is 57 // not set, it suggests a new version. 58 func (r *report) String() string { 59 buf := &strings.Builder{} 60 for _, p := range r.packages { 61 buf.WriteString(p.String()) 62 } 63 64 if !r.canVerifyReleaseVersion() { 65 return buf.String() 66 } 67 68 if len(r.release.diagnostics) > 0 { 69 buf.WriteString("# diagnostics\n") 70 for _, d := range r.release.diagnostics { 71 fmt.Fprintln(buf, d) 72 } 73 buf.WriteByte('\n') 74 } 75 76 buf.WriteString("# summary\n") 77 baseVersion := r.base.version 78 if r.base.modPath != r.release.modPath { 79 baseVersion = r.base.modPath + "@" + baseVersion 80 } 81 if r.base.versionInferred { 82 fmt.Fprintf(buf, "Inferred base version: %s\n", baseVersion) 83 } else if r.base.versionQuery != "" { 84 fmt.Fprintf(buf, "Base version: %s (%s)\n", baseVersion, r.base.versionQuery) 85 } 86 87 if r.versionInvalid != nil { 88 fmt.Fprintln(buf, r.versionInvalid) 89 } else if r.release.versionInferred { 90 if r.release.tagPrefix == "" { 91 fmt.Fprintf(buf, "Suggested version: %s\n", r.release.version) 92 } else { 93 fmt.Fprintf(buf, "Suggested version: %[1]s (with tag %[2]s%[1]s)\n", r.release.version, r.release.tagPrefix) 94 } 95 } else if r.release.version != "" { 96 if r.release.tagPrefix == "" { 97 fmt.Fprintf(buf, "%s is a valid semantic version for this release.\n", r.release.version) 98 99 if semver.Compare(r.release.version, "v0.0.0-99999999999999-zzzzzzzzzzzz") < 0 { 100 fmt.Fprintf(buf, `Note: %s sorts lower in MVS than pseudo-versions, which may be 101 unexpected for users. So, it may be better to choose a different suffix.`, r.release.version) 102 } 103 } else { 104 fmt.Fprintf(buf, "%[1]s (with tag %[2]s%[1]s) is a valid semantic version for this release\n", r.release.version, r.release.tagPrefix) 105 } 106 } 107 108 if r.versionInvalid == nil && r.haveBaseErrors { 109 fmt.Fprintln(buf, "Errors were found in the base version. Some API changes may be omitted.") 110 } 111 112 return buf.String() 113 } 114 115 func (r *report) addPackage(p packageReport) { 116 r.packages = append(r.packages, p) 117 if len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 { 118 // Only count compatible and incompatible changes if there were no errors. 119 // When there are errors, definitions may be missing, and fixes may appear 120 // incompatible when they are not. Changes will still be reported, but 121 // they won't affect version validation or suggestions. 122 for _, c := range p.Changes { 123 if !c.Compatible && len(p.releaseErrors) == 0 { 124 r.haveIncompatibleChanges = true 125 } else if c.Compatible && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 { 126 r.haveCompatibleChanges = true 127 } 128 } 129 } 130 if len(p.baseErrors) > 0 { 131 r.haveBaseErrors = true 132 } 133 if len(p.releaseErrors) > 0 { 134 r.haveReleaseErrors = true 135 } 136 } 137 138 // validateReleaseVersion checks whether r.release.version is valid. 139 // If r.release.version is not valid, an error is returned explaining why. 140 // r.release.version must be set. 141 func (r *report) validateReleaseVersion() { 142 if r.release.version == "" { 143 panic("validateVersion called without version") 144 } 145 setNotValid := func(format string, args ...interface{}) { 146 r.versionInvalid = &versionMessage{ 147 message: fmt.Sprintf("%s is not a valid semantic version for this release.", r.release.version), 148 reason: fmt.Sprintf(format, args...), 149 } 150 } 151 152 if r.haveReleaseErrors { 153 if r.haveReleaseErrors { 154 setNotValid("Errors were found in one or more packages.") 155 return 156 } 157 } 158 159 // TODO(jayconrod): link to documentation for all of these errors. 160 161 // Check that the major version matches the module path. 162 _, suffix, ok := module.SplitPathVersion(r.release.modPath) 163 if !ok { 164 setNotValid("%s: could not find version suffix in module path", r.release.modPath) 165 return 166 } 167 if suffix != "" { 168 if suffix[0] != '/' && suffix[0] != '.' { 169 setNotValid("%s: unknown module path version suffix: %q", r.release.modPath, suffix) 170 return 171 } 172 pathMajor := suffix[1:] 173 major := semver.Major(r.release.version) 174 if pathMajor != major { 175 setNotValid(`The major version %s does not match the major version suffix 176 in the module path: %s`, major, r.release.modPath) 177 return 178 } 179 } else if major := semver.Major(r.release.version); major != "v0" && major != "v1" { 180 setNotValid(`The module path does not end with the major version suffix /%s, 181 which is required for major versions v2 or greater.`, major) 182 return 183 } 184 185 for _, v := range r.base.existingVersions { 186 if semver.Compare(v, r.release.version) == 0 { 187 setNotValid("version %s already exists", v) 188 } 189 } 190 191 // Check that compatible / incompatible changes are consistent. 192 if semver.Major(r.base.version) == "v0" || r.base.modPath != r.release.modPath { 193 return 194 } 195 if r.haveIncompatibleChanges { 196 setNotValid("There are incompatible changes.") 197 return 198 } 199 if r.haveCompatibleChanges && semver.MajorMinor(r.base.version) == semver.MajorMinor(r.release.version) { 200 setNotValid(`There are compatible changes, but the minor version is not incremented 201 over the base version (%s).`, r.base.version) 202 return 203 } 204 205 if r.release.highestTransitiveVersion != "" && semver.Compare(r.release.highestTransitiveVersion, r.release.version) > 0 { 206 setNotValid(`Module indirectly depends on a higher version of itself (%s). 207 `, r.release.highestTransitiveVersion) 208 } 209 } 210 211 // suggestReleaseVersion suggests a new version consistent with observed 212 // changes. 213 func (r *report) suggestReleaseVersion() { 214 setNotValid := func(format string, args ...interface{}) { 215 r.versionInvalid = &versionMessage{ 216 message: "Cannot suggest a release version.", 217 reason: fmt.Sprintf(format, args...), 218 } 219 } 220 setVersion := func(v string) { 221 r.release.version = v 222 r.release.versionInferred = true 223 } 224 225 if r.base.modPath != r.release.modPath { 226 setNotValid("Base module path is different from release.") 227 return 228 } 229 230 if r.haveReleaseErrors || r.haveBaseErrors { 231 setNotValid("Errors were found.") 232 return 233 } 234 235 var major, minor, patch, pre string 236 if r.base.version != "none" { 237 minVersion := r.base.version 238 if r.release.highestTransitiveVersion != "" && semver.Compare(r.release.highestTransitiveVersion, minVersion) > 0 { 239 setNotValid("Module indirectly depends on a higher version of itself (%s) than the base version (%s).", r.release.highestTransitiveVersion, r.base.version) 240 return 241 } 242 243 var err error 244 major, minor, patch, pre, _, err = parseVersion(minVersion) 245 if err != nil { 246 panic(fmt.Sprintf("could not parse base version: %v", err)) 247 } 248 } 249 250 if r.haveIncompatibleChanges && r.base.version != "none" && pre == "" && major != "0" { 251 setNotValid("Incompatible changes were detected.") 252 return 253 // TODO(jayconrod): briefly explain how to prepare major version releases 254 // and link to documentation. 255 } 256 257 // Check whether we're comparing to the latest version of base. 258 // 259 // This could happen further up, but we want the more pressing errors above 260 // to take precedence. 261 var latestForBaseMajor string 262 for _, v := range r.base.existingVersions { 263 if semver.Major(v) != semver.Major(r.base.version) { 264 continue 265 } 266 if latestForBaseMajor == "" || semver.Compare(latestForBaseMajor, v) < 0 { 267 latestForBaseMajor = v 268 } 269 } 270 if latestForBaseMajor != "" && latestForBaseMajor != r.base.version { 271 setNotValid(fmt.Sprintf("Can only suggest a release version when compared against the most recent version of this major: %s.", latestForBaseMajor)) 272 return 273 } 274 275 if r.base.version == "none" { 276 if _, pathMajor, ok := module.SplitPathVersion(r.release.modPath); !ok { 277 panic(fmt.Sprintf("could not parse module path %q", r.release.modPath)) 278 } else if pathMajor == "" { 279 setVersion("v0.1.0") 280 } else { 281 setVersion(pathMajor[1:] + ".0.0") 282 } 283 return 284 } 285 286 if pre != "" { 287 // suggest non-prerelease version 288 } else if r.haveCompatibleChanges || (r.haveIncompatibleChanges && major == "0") || r.requirementsChanged() { 289 minor = incDecimal(minor) 290 patch = "0" 291 } else { 292 patch = incDecimal(patch) 293 } 294 setVersion(fmt.Sprintf("v%s.%s.%s", major, minor, patch)) 295 return 296 } 297 298 // canVerifyReleaseVersion returns true if we can safely suggest a new version 299 // or if we can verify the version passed in with -version is safe to tag. 300 func (r *report) canVerifyReleaseVersion() bool { 301 // For now, return true if the base and release module paths are the same, 302 // ignoring the major version suffix. 303 // TODO(#37562, #39192, #39666, #40267): there are many more situations when 304 // we can't verify a new version. 305 basePath := strings.TrimSuffix(r.base.modPath, r.base.modPathMajor) 306 releasePath := strings.TrimSuffix(r.release.modPath, r.release.modPathMajor) 307 return basePath == releasePath 308 } 309 310 // requirementsChanged reports whether requirements have changed from base to 311 // version. 312 // 313 // requirementsChanged reports true for, 314 // - A requirement was upgraded to a higher minor version. 315 // - A requirement was added. 316 // - The version of Go was incremented. 317 // 318 // It does not report true when, for example, a requirement was downgraded or 319 // remove. We care more about the former since that might force dependent 320 // modules that have the same dependency to upgrade. 321 func (r *report) requirementsChanged() bool { 322 if r.base.goModFile == nil { 323 // There wasn't a modfile before, and now there is. 324 return true 325 } 326 327 // baseReqs is a map of module path to MajorMinor of the base module 328 // requirements. 329 baseReqs := make(map[string]string) 330 for _, r := range r.base.goModFile.Require { 331 baseReqs[r.Mod.Path] = r.Mod.Version 332 } 333 334 for _, r := range r.release.goModFile.Require { 335 if _, ok := baseReqs[r.Mod.Path]; !ok { 336 // A module@version was added to the "require" block between base 337 // and release. 338 return true 339 } 340 if semver.Compare(semver.MajorMinor(r.Mod.Version), semver.MajorMinor(baseReqs[r.Mod.Path])) > 0 { 341 // The version of r.Mod.Path increased from base to release. 342 return true 343 } 344 } 345 346 if r.release.goModFile.Go != nil && r.base.goModFile.Go != nil { 347 if r.release.goModFile.Go.Version > r.base.goModFile.Go.Version { 348 // The Go version increased from base to release. 349 return true 350 } 351 } 352 353 return false 354 } 355 356 // isSuccessful returns true the module appears to be safe to release at the 357 // proposed or suggested version. 358 func (r *report) isSuccessful() bool { 359 return len(r.release.diagnostics) == 0 && r.versionInvalid == nil 360 } 361 362 type versionMessage struct { 363 message, reason string 364 } 365 366 func (m versionMessage) String() string { 367 return m.message + "\n" + m.reason + "\n" 368 } 369 370 // incDecimal returns the decimal string incremented by 1. 371 func incDecimal(decimal string) string { 372 // Scan right to left turning 9s to 0s until you find a digit to increment. 373 digits := []byte(decimal) 374 i := len(digits) - 1 375 for ; i >= 0 && digits[i] == '9'; i-- { 376 digits[i] = '0' 377 } 378 if i >= 0 { 379 digits[i]++ 380 } else { 381 // digits is all zeros 382 digits[0] = '1' 383 digits = append(digits, '0') 384 } 385 return string(digits) 386 } 387 388 type packageReport struct { 389 apidiff.Report 390 path string 391 baseErrors, releaseErrors []packages.Error 392 } 393 394 func (p *packageReport) String() string { 395 if len(p.Changes) == 0 && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 { 396 return "" 397 } 398 buf := &strings.Builder{} 399 fmt.Fprintf(buf, "# %s\n", p.path) 400 if len(p.baseErrors) > 0 { 401 fmt.Fprintf(buf, "## errors in base version:\n") 402 for _, e := range p.baseErrors { 403 fmt.Fprintln(buf, e) 404 } 405 buf.WriteByte('\n') 406 } 407 if len(p.releaseErrors) > 0 { 408 fmt.Fprintf(buf, "## errors in release version:\n") 409 for _, e := range p.releaseErrors { 410 fmt.Fprintln(buf, e) 411 } 412 buf.WriteByte('\n') 413 } 414 if len(p.Changes) > 0 { 415 var compatible, incompatible []apidiff.Change 416 for _, c := range p.Changes { 417 if c.Compatible { 418 compatible = append(compatible, c) 419 } else { 420 incompatible = append(incompatible, c) 421 } 422 } 423 if len(incompatible) > 0 { 424 fmt.Fprintf(buf, "## incompatible changes\n") 425 for _, c := range incompatible { 426 fmt.Fprintln(buf, c.Message) 427 } 428 } 429 if len(compatible) > 0 { 430 fmt.Fprintf(buf, "## compatible changes\n") 431 for _, c := range compatible { 432 fmt.Fprintln(buf, c.Message) 433 } 434 } 435 buf.WriteByte('\n') 436 } 437 return buf.String() 438 } 439 440 // parseVersion returns the major, minor, and patch numbers, prerelease text, 441 // and metadata for a given version. 442 // 443 // TODO(jayconrod): extend semver to do this and delete this function. 444 func parseVersion(vers string) (major, minor, patch, pre, meta string, err error) { 445 if !strings.HasPrefix(vers, "v") { 446 return "", "", "", "", "", fmt.Errorf("version %q does not start with 'v'", vers) 447 } 448 base := vers[1:] 449 if i := strings.IndexByte(base, '+'); i >= 0 { 450 meta = base[i+1:] 451 base = base[:i] 452 } 453 if i := strings.IndexByte(base, '-'); i >= 0 { 454 pre = base[i+1:] 455 base = base[:i] 456 } 457 parts := strings.Split(base, ".") 458 if len(parts) != 3 { 459 return "", "", "", "", "", fmt.Errorf("version %q should have three numbers", vers) 460 } 461 major, minor, patch = parts[0], parts[1], parts[2] 462 return major, minor, patch, pre, meta, nil 463 }