1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80 package main
81
82 import (
83 "bytes"
84 "context"
85 "encoding/json"
86 "errors"
87 "flag"
88 "fmt"
89 "go/build"
90 "io"
91 "log"
92 "os"
93 "os/exec"
94 "path"
95 "path/filepath"
96 "sort"
97 "strings"
98 "unicode"
99
100 "golang.org/x/exp/apidiff"
101 "golang.org/x/mod/modfile"
102 "golang.org/x/mod/module"
103 "golang.org/x/mod/semver"
104 "golang.org/x/mod/zip"
105 "golang.org/x/tools/go/packages"
106 )
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138 func main() {
139 log.SetFlags(0)
140 log.SetPrefix("gorelease: ")
141 wd, err := os.Getwd()
142 if err != nil {
143 log.Fatal(err)
144 }
145 ctx := context.WithValue(context.Background(), "env", append(os.Environ(), "GO111MODULE=on"))
146 success, err := runRelease(ctx, os.Stdout, wd, os.Args[1:])
147 if err != nil {
148 if _, ok := err.(*usageError); ok {
149 fmt.Fprintln(os.Stderr, err)
150 os.Exit(2)
151 } else {
152 log.Fatal(err)
153 }
154 }
155 if !success {
156 os.Exit(1)
157 }
158 }
159
160
161
162
163 func runRelease(ctx context.Context, w io.Writer, dir string, args []string) (success bool, err error) {
164
165
166 fs := flag.NewFlagSet("gorelease", flag.ContinueOnError)
167 fs.Usage = func() {}
168 fs.SetOutput(io.Discard)
169 var baseOpt, releaseVersion string
170 fs.StringVar(&baseOpt, "base", "", "previous version to compare against")
171 fs.StringVar(&releaseVersion, "version", "", "proposed version to be released")
172 if err := fs.Parse(args); err != nil {
173 return false, &usageError{err: err}
174 }
175
176 if len(fs.Args()) > 0 {
177 return false, usageErrorf("no arguments allowed")
178 }
179
180 if releaseVersion != "" {
181 if semver.Build(releaseVersion) != "" {
182 return false, usageErrorf("release version %q is not a canonical semantic version: build metadata is not supported", releaseVersion)
183 }
184 if c := semver.Canonical(releaseVersion); c != releaseVersion {
185 return false, usageErrorf("release version %q is not a canonical semantic version", releaseVersion)
186 }
187 }
188
189 var baseModPath, baseVersion string
190 if at := strings.Index(baseOpt, "@"); at >= 0 {
191 baseModPath = baseOpt[:at]
192 baseVersion = baseOpt[at+1:]
193 } else if dot, slash := strings.Index(baseOpt, "."), strings.Index(baseOpt, "/"); dot >= 0 && slash >= 0 && dot < slash {
194 baseModPath = baseOpt
195 } else {
196 baseVersion = baseOpt
197 }
198 if baseModPath == "" {
199 if baseVersion != "" && semver.Canonical(baseVersion) == baseVersion && releaseVersion != "" {
200 if cmp := semver.Compare(baseOpt, releaseVersion); cmp == 0 {
201 return false, usageErrorf("-base and -version must be different")
202 } else if cmp > 0 {
203 return false, usageErrorf("base version (%q) must be lower than release version (%q)", baseVersion, releaseVersion)
204 }
205 }
206 } else if baseModPath != "" && baseVersion == "none" {
207 return false, usageErrorf(`base version (%q) cannot have version "none" with explicit module path`, baseOpt)
208 }
209
210
211 modRoot, err := findModuleRoot(dir)
212 if err != nil {
213 return false, err
214 }
215 repoRoot := findRepoRoot(modRoot)
216
217
218 release, err := loadLocalModule(ctx, modRoot, repoRoot, releaseVersion)
219 if err != nil {
220 return false, err
221 }
222
223
224
225 var max string
226 if baseModPath == "" {
227 if baseVersion != "" && semver.Canonical(baseVersion) == baseVersion && module.Check(release.modPath, baseVersion) != nil {
228
229
230
231
232 prefix, _, _ := module.SplitPathVersion(release.modPath)
233 major := semver.Major(baseVersion)
234 if strings.HasPrefix(prefix, "gopkg.in/") {
235 baseModPath = prefix + "." + semver.Major(baseVersion)
236 } else if major >= "v2" {
237 baseModPath = prefix + "/" + major
238 } else {
239 baseModPath = prefix
240 }
241 } else {
242 baseModPath = release.modPath
243 max = releaseVersion
244 }
245 }
246 base, err := loadDownloadedModule(ctx, baseModPath, baseVersion, max)
247 if err != nil {
248 return false, err
249 }
250
251
252 report, err := makeReleaseReport(ctx, base, release)
253 if err != nil {
254 return false, err
255 }
256 if _, err := fmt.Fprint(w, report.String()); err != nil {
257 return false, err
258 }
259 return report.isSuccessful(), nil
260 }
261
262 type moduleInfo struct {
263 modRoot string
264 repoRoot string
265 modPath string
266 version string
267 versionQuery string
268 versionInferred bool
269 highestTransitiveVersion string
270 modPathMajor string
271 tagPrefix string
272
273 goModPath string
274 goModData []byte
275 goSumData []byte
276 goModFile *modfile.File
277
278 diagnostics []string
279 pkgs []*packages.Package
280
281
282
283 existingVersions []string
284 }
285
286
287
288
289
290
291
292
293
294 func loadLocalModule(ctx context.Context, modRoot, repoRoot, version string) (m moduleInfo, err error) {
295 if repoRoot != "" && !hasFilePathPrefix(modRoot, repoRoot) {
296 return moduleInfo{}, fmt.Errorf("module root %q is not in repository root %q", modRoot, repoRoot)
297 }
298
299
300 m = moduleInfo{
301 modRoot: modRoot,
302 repoRoot: repoRoot,
303 version: version,
304 goModPath: filepath.Join(modRoot, "go.mod"),
305 }
306
307 if version != "" && semver.Compare(version, "v0.0.0-99999999999999-zzzzzzzzzzzz") < 0 {
308 m.diagnostics = append(m.diagnostics, fmt.Sprintf("Version %s is lower than most pseudo-versions. Consider releasing v0.1.0-0 instead.", version))
309 }
310
311 m.goModData, err = os.ReadFile(m.goModPath)
312 if err != nil {
313 return moduleInfo{}, err
314 }
315 m.goModFile, err = modfile.ParseLax(m.goModPath, m.goModData, nil)
316 if err != nil {
317 return moduleInfo{}, err
318 }
319 if m.goModFile.Module == nil {
320 return moduleInfo{}, fmt.Errorf("%s: module directive is missing", m.goModPath)
321 }
322 m.modPath = m.goModFile.Module.Mod.Path
323 if err := checkModPath(m.modPath); err != nil {
324 return moduleInfo{}, err
325 }
326 var ok bool
327 _, m.modPathMajor, ok = module.SplitPathVersion(m.modPath)
328 if !ok {
329
330 panic(fmt.Sprintf("could not find version suffix in module path %q", m.modPath))
331 }
332 if m.goModFile.Go == nil {
333 m.diagnostics = append(m.diagnostics, "go.mod: go directive is missing")
334 }
335
336
337 if repoRoot != "" && modRoot != repoRoot {
338 if strings.HasPrefix(m.modPathMajor, ".") {
339 m.diagnostics = append(m.diagnostics, fmt.Sprintf("%s: module path starts with gopkg.in and must be declared in the root directory of the repository", m.modPath))
340 } else {
341 codeDir := filepath.ToSlash(modRoot[len(repoRoot)+1:])
342 var altGoModPath string
343 if m.modPathMajor == "" {
344
345
346
347 if strings.HasSuffix(m.modPath, "/"+codeDir) {
348 m.tagPrefix = codeDir + "/"
349 } else {
350 m.diagnostics = append(m.diagnostics, fmt.Sprintf("%s: module path must end with %[2]q, since it is in subdirectory %[2]q", m.modPath, codeDir))
351 }
352 } else {
353 if strings.HasSuffix(m.modPath, "/"+codeDir) {
354
355
356
357 m.tagPrefix = codeDir[:len(codeDir)-len(m.modPathMajor)+1]
358 altGoModPath = modRoot[:len(modRoot)-len(m.modPathMajor)+1] + "go.mod"
359 } else if strings.HasSuffix(m.modPath, "/"+codeDir+m.modPathMajor) {
360
361
362
363 m.tagPrefix = codeDir + "/"
364 altGoModPath = filepath.Join(modRoot, m.modPathMajor[1:], "go.mod")
365 } else {
366 m.diagnostics = append(m.diagnostics, fmt.Sprintf("%s: module path must end with %[2]q or %q, since it is in subdirectory %[2]q", m.modPath, codeDir, codeDir+m.modPathMajor))
367 }
368 }
369
370
371
372 if altGoModPath != "" {
373 if data, err := os.ReadFile(altGoModPath); err == nil {
374 if altModPath := modfile.ModulePath(data); m.modPath == altModPath {
375 goModRel, _ := filepath.Rel(repoRoot, m.goModPath)
376 altGoModRel, _ := filepath.Rel(repoRoot, altGoModPath)
377 m.diagnostics = append(m.diagnostics, fmt.Sprintf("module is defined in two locations:\n\t%s\n\t%s", goModRel, altGoModRel))
378 }
379 }
380 }
381 }
382 }
383
384
385
386
387
388
389 tmpModRoot, err := copyModuleToTempDir(repoRoot, m.modPath, m.modRoot)
390 if err != nil {
391 return moduleInfo{}, err
392 }
393 defer func() {
394 if rerr := os.RemoveAll(tmpModRoot); err == nil && rerr != nil {
395 err = fmt.Errorf("removing temporary module directory: %v", rerr)
396 }
397 }()
398 tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, prepareDiagnostics, err := prepareLoadDir(ctx, m.goModFile, m.modPath, tmpModRoot, version, false)
399 if err != nil {
400 return moduleInfo{}, err
401 }
402 defer func() {
403 if rerr := os.RemoveAll(tmpLoadDir); err == nil && rerr != nil {
404 err = fmt.Errorf("removing temporary load directory: %v", rerr)
405 }
406 }()
407
408 var loadDiagnostics []string
409 m.pkgs, loadDiagnostics, err = loadPackages(ctx, m.modPath, tmpModRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths)
410 if err != nil {
411 return moduleInfo{}, err
412 }
413
414 m.diagnostics = append(m.diagnostics, prepareDiagnostics...)
415 m.diagnostics = append(m.diagnostics, loadDiagnostics...)
416
417 highestVersion, err := findSelectedVersion(ctx, tmpLoadDir, m.modPath)
418 if err != nil {
419 return moduleInfo{}, err
420 }
421
422 if highestVersion != "" {
423
424
425
426
427 m.highestTransitiveVersion = highestVersion
428 }
429
430 retracted, err := loadRetractions(ctx, tmpLoadDir)
431 if err != nil {
432 return moduleInfo{}, err
433 }
434 m.diagnostics = append(m.diagnostics, retracted...)
435
436 return m, nil
437 }
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453 func loadDownloadedModule(ctx context.Context, modPath, version, max string) (m moduleInfo, err error) {
454
455
456 m = moduleInfo{modPath: modPath}
457 if err := checkModPath(modPath); err != nil {
458 return moduleInfo{}, err
459 }
460
461 var ok bool
462 _, m.modPathMajor, ok = module.SplitPathVersion(modPath)
463 if !ok {
464
465 panic(fmt.Sprintf("could not find version suffix in module path %q", modPath))
466 }
467
468 if version == "none" {
469
470 m.version = "none"
471 return m, nil
472 }
473 if version == "" {
474
475 m.versionInferred = true
476 if m.version, err = inferBaseVersion(ctx, modPath, max); err != nil {
477 return moduleInfo{}, err
478 }
479 if m.version == "none" {
480 return m, nil
481 }
482 } else if version != module.CanonicalVersion(version) {
483
484 m.versionQuery = version
485 if m.version, err = queryVersion(ctx, modPath, version); err != nil {
486 return moduleInfo{}, err
487 }
488 if m.version != "none" && max != "" && semver.Compare(m.version, max) >= 0 {
489
490
491
492
493 return moduleInfo{}, fmt.Errorf("base version %s (%s) must be lower than release version %s", m.version, m.versionQuery, max)
494 }
495 } else {
496
497 if err := module.CheckPathMajor(version, m.modPathMajor); err != nil {
498
499
500 return moduleInfo{}, fmt.Errorf("can't compare major versions: base version %s does not belong to module %s", version, modPath)
501 }
502 m.version = version
503 }
504
505
506
507
508
509 v := module.Version{Path: modPath, Version: m.version}
510 if m.modRoot, m.goModPath, err = downloadModule(ctx, v); err != nil {
511 return moduleInfo{}, err
512 }
513 if m.goModData, err = os.ReadFile(m.goModPath); err != nil {
514 return moduleInfo{}, err
515 }
516 if m.goModFile, err = modfile.ParseLax(m.goModPath, m.goModData, nil); err != nil {
517 return moduleInfo{}, err
518 }
519 if m.goModFile.Module == nil {
520 return moduleInfo{}, fmt.Errorf("%s: missing module directive", m.goModPath)
521 }
522 m.modPath = m.goModFile.Module.Mod.Path
523
524
525 tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, _, err := prepareLoadDir(ctx, nil, m.modPath, m.modRoot, m.version, true)
526 if err != nil {
527 return moduleInfo{}, err
528 }
529 defer func() {
530 if rerr := os.RemoveAll(tmpLoadDir); err == nil && rerr != nil {
531 err = fmt.Errorf("removing temporary load directory: %v", err)
532 }
533 }()
534
535 if m.pkgs, _, err = loadPackages(ctx, m.modPath, m.modRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths); err != nil {
536 return moduleInfo{}, err
537 }
538
539
540 ev, err := existingVersions(ctx, m.modPath, tmpLoadDir)
541 if err != nil {
542 return moduleInfo{}, err
543 }
544 m.existingVersions = ev
545
546 return m, nil
547 }
548
549
550
551
552
553
554
555
556 func makeReleaseReport(ctx context.Context, base, release moduleInfo) (report, error) {
557
558
559
560
561
562 shouldCompare := base.version != "none"
563 isInternal := func(modPath, pkgPath string) bool {
564 if !hasPathPrefix(pkgPath, modPath) {
565 panic(fmt.Sprintf("package %s not in module %s", pkgPath, modPath))
566 }
567 for pkgPath != modPath {
568 if path.Base(pkgPath) == "internal" {
569 return true
570 }
571 pkgPath = path.Dir(pkgPath)
572 }
573 return false
574 }
575 r := report{
576 base: base,
577 release: release,
578 }
579 for _, pair := range zipPackages(base.modPath, base.pkgs, release.modPath, release.pkgs) {
580 basePkg, releasePkg := pair.base, pair.release
581 switch {
582 case releasePkg == nil:
583
584 if internal := isInternal(base.modPath, basePkg.PkgPath); !internal || len(basePkg.Errors) > 0 {
585 pr := packageReport{
586 path: basePkg.PkgPath,
587 baseErrors: basePkg.Errors,
588 }
589 if !internal {
590 pr.Report = apidiff.Report{
591 Changes: []apidiff.Change{{
592 Message: "package removed",
593 Compatible: false,
594 }},
595 }
596 }
597 r.addPackage(pr)
598 }
599
600 case basePkg == nil:
601
602 if internal := isInternal(release.modPath, releasePkg.PkgPath); !internal && shouldCompare || len(releasePkg.Errors) > 0 {
603 pr := packageReport{
604 path: releasePkg.PkgPath,
605 releaseErrors: releasePkg.Errors,
606 }
607 if !internal && shouldCompare {
608
609
610 pr.Report = apidiff.Report{
611 Changes: []apidiff.Change{{
612 Message: "package added",
613 Compatible: true,
614 }},
615 }
616 }
617 r.addPackage(pr)
618 }
619
620 default:
621
622
623
624 internal := isInternal(release.modPath, releasePkg.PkgPath)
625 if !internal && basePkg.Name != "main" && releasePkg.Name != "main" {
626 pr := packageReport{
627 path: basePkg.PkgPath,
628 baseErrors: basePkg.Errors,
629 releaseErrors: releasePkg.Errors,
630 Report: apidiff.Changes(basePkg.Types, releasePkg.Types),
631 }
632 r.addPackage(pr)
633 }
634 }
635 }
636
637 if r.canVerifyReleaseVersion() {
638 if release.version == "" {
639 r.suggestReleaseVersion()
640 } else {
641 r.validateReleaseVersion()
642 }
643 }
644
645 return r, nil
646 }
647
648
649
650 func existingVersions(ctx context.Context, modPath, modRoot string) (versions []string, err error) {
651 defer func() {
652 if err != nil {
653 err = fmt.Errorf("listing versions of %s: %w", modPath, err)
654 }
655 }()
656
657 type listVersions struct {
658 Versions []string
659 }
660 cmd := exec.CommandContext(ctx, "go", "list", "-json", "-m", "-versions", modPath)
661 cmd.Env = copyEnv(ctx, cmd.Env)
662 cmd.Dir = modRoot
663 out, err := cmd.Output()
664 if err != nil {
665 return nil, cleanCmdError(err)
666 }
667 if len(out) == 0 {
668 return nil, nil
669 }
670
671 var lv listVersions
672 if err := json.Unmarshal(out, &lv); err != nil {
673 return nil, err
674 }
675 return lv.Versions, nil
676 }
677
678
679
680 func findRepoRoot(dir string) string {
681 vcsDirs := []string{".git", ".hg", ".svn", ".bzr"}
682 d := filepath.Clean(dir)
683 for {
684 for _, vcsDir := range vcsDirs {
685 if _, err := os.Stat(filepath.Join(d, vcsDir)); err == nil {
686 return d
687 }
688 }
689 parent := filepath.Dir(d)
690 if parent == d {
691 return ""
692 }
693 d = parent
694 }
695 }
696
697
698 func findModuleRoot(dir string) (string, error) {
699 d := filepath.Clean(dir)
700 for {
701 if fi, err := os.Stat(filepath.Join(d, "go.mod")); err == nil && !fi.IsDir() {
702 return dir, nil
703 }
704 parent := filepath.Dir(d)
705 if parent == d {
706 break
707 }
708 d = parent
709 }
710 return "", fmt.Errorf("%s: cannot find go.mod file", dir)
711 }
712
713
714
715
716
717 func checkModPath(modPath string) error {
718 if path.IsAbs(modPath) || filepath.IsAbs(modPath) {
719
720 return fmt.Errorf("module path %q must not be an absolute path.\nIt must be an address where your module may be found.", modPath)
721 }
722 if suffix := dirMajorSuffix(modPath); suffix == "v0" || suffix == "v1" {
723 return fmt.Errorf("module path %q has major version suffix %q.\nA major version suffix is only allowed for v2 or later.", modPath, suffix)
724 } else if strings.HasPrefix(suffix, "v0") {
725 return fmt.Errorf("module path %q has major version suffix %q.\nA major version may not have a leading zero.", modPath, suffix)
726 } else if strings.ContainsRune(suffix, '.') {
727 return fmt.Errorf("module path %q has major version suffix %q.\nA major version may not contain dots.", modPath, suffix)
728 }
729 return module.CheckPath(modPath)
730 }
731
732
733
734
735
736
737
738
739
740 func inferBaseVersion(ctx context.Context, modPath, max string) (baseVersion string, err error) {
741 defer func() {
742 if err != nil {
743 err = &baseVersionError{err: err, modPath: modPath}
744 }
745 }()
746
747 versions, err := loadVersions(ctx, modPath)
748 if err != nil {
749 return "", err
750 }
751
752 for i := len(versions) - 1; i >= 0; i-- {
753 v := versions[i]
754 if semver.Prerelease(v) == "" &&
755 (max == "" || semver.Compare(v, max) < 0) {
756 return v, nil
757 }
758 }
759
760 if max == "" || maybeFirstVersion(max) {
761 return "none", nil
762 }
763 return "", fmt.Errorf("no versions found lower than %s", max)
764 }
765
766
767 func queryVersion(ctx context.Context, modPath, query string) (resolved string, err error) {
768 defer func() {
769 if err != nil {
770 err = fmt.Errorf("could not resolve version %s@%s: %w", modPath, query, err)
771 }
772 }()
773 if query == "upgrade" || query == "patch" {
774 return "", errors.New("query is based on requirements in main go.mod file")
775 }
776
777 tmpDir, err := os.MkdirTemp("", "")
778 if err != nil {
779 return "", err
780 }
781 defer func() {
782 if rerr := os.Remove(tmpDir); rerr != nil && err == nil {
783 err = rerr
784 }
785 }()
786 arg := modPath + "@" + query
787 cmd := exec.CommandContext(ctx, "go", "list", "-m", "-f", "{{.Version}}", "--", arg)
788 cmd.Env = copyEnv(ctx, cmd.Env)
789 cmd.Dir = tmpDir
790 cmd.Env = append(cmd.Env, "GO111MODULE=on")
791 out, err := cmd.Output()
792 if err != nil {
793 return "", cleanCmdError(err)
794 }
795 return strings.TrimSpace(string(out)), nil
796 }
797
798
799
800
801 func loadVersions(ctx context.Context, modPath string) (versions []string, err error) {
802 defer func() {
803 if err != nil {
804 err = fmt.Errorf("could not load versions for %s: %v", modPath, err)
805 }
806 }()
807
808 tmpDir, err := os.MkdirTemp("", "")
809 if err != nil {
810 return nil, err
811 }
812 defer func() {
813 if rerr := os.Remove(tmpDir); rerr != nil && err == nil {
814 err = rerr
815 }
816 }()
817 cmd := exec.CommandContext(ctx, "go", "list", "-m", "-versions", "--", modPath)
818 cmd.Env = copyEnv(ctx, cmd.Env)
819 cmd.Dir = tmpDir
820 out, err := cmd.Output()
821 if err != nil {
822 return nil, cleanCmdError(err)
823 }
824 versions = strings.Fields(string(out))
825 if len(versions) > 0 {
826 versions = versions[1:]
827 }
828
829
830
831 sort.Slice(versions, func(i, j int) bool {
832 return semver.Compare(versions[i], versions[j]) < 0
833 })
834 return versions, nil
835 }
836
837
838
839 func maybeFirstVersion(v string) bool {
840 major, minor, patch, _, _, err := parseVersion(v)
841 if err != nil {
842 return false
843 }
844 if major == "0" {
845 return minor == "0" && patch == "0" ||
846 minor == "0" && patch == "1" ||
847 minor == "1" && patch == "0"
848 }
849 return minor == "0" && patch == "0"
850 }
851
852
853
854
855
856
857
858
859 func dirMajorSuffix(path string) string {
860 i := len(path)
861 for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') || path[i-1] == '.' {
862 i--
863 }
864 if i <= 1 || i == len(path) || path[i-1] != 'v' || (i > 1 && path[i-2] != '/') {
865 return ""
866 }
867 return path[i-1:]
868 }
869
870
871
872
873
874
875 func copyModuleToTempDir(repoRoot, modPath, modRoot string) (dir string, err error) {
876
877
878 version := "v0.0.0-gorelease"
879 _, majorPathSuffix, _ := module.SplitPathVersion(modPath)
880 if majorPathSuffix != "" {
881 version = majorPathSuffix[1:] + ".0.0-gorelease"
882 }
883 m := module.Version{Path: modPath, Version: version}
884
885 zipFile, err := os.CreateTemp("", "gorelease-*.zip")
886 if err != nil {
887 return "", err
888 }
889 defer func() {
890 zipFile.Close()
891 os.Remove(zipFile.Name())
892 }()
893
894 dir, err = os.MkdirTemp("", "gorelease")
895 if err != nil {
896 return "", err
897 }
898 defer func() {
899 if err != nil {
900 os.RemoveAll(dir)
901 dir = ""
902 }
903 }()
904
905 var fallbackToDir bool
906 if repoRoot != "" {
907 var err error
908 fallbackToDir, err = tryCreateFromVCS(zipFile, m, modRoot, repoRoot)
909 if err != nil {
910 return "", err
911 }
912 }
913
914 if repoRoot == "" || fallbackToDir {
915
916 if err := zip.CreateFromDir(zipFile, m, modRoot); err != nil {
917 var e zip.FileErrorList
918 if errors.As(err, &e) {
919 return "", e
920 }
921 return "", err
922 }
923 }
924
925 if err := zipFile.Close(); err != nil {
926 return "", err
927 }
928 if err := zip.Unzip(dir, m, zipFile.Name()); err != nil {
929 return "", err
930 }
931 return dir, nil
932 }
933
934
935
936
937
938 func tryCreateFromVCS(zipFile io.Writer, m module.Version, modRoot, repoRoot string) (fallbackToDir bool, _ error) {
939
940 if !hasFilePathPrefix(modRoot, repoRoot) {
941 panic(fmt.Sprintf("repo root %q is not a prefix of mod root %q", repoRoot, modRoot))
942 }
943 hasUncommitted, err := hasGitUncommittedChanges(repoRoot)
944 if err != nil {
945
946 return true, nil
947 }
948 if hasUncommitted {
949 return false, fmt.Errorf("repo %s has uncommitted changes", repoRoot)
950 }
951 modRel := filepath.ToSlash(trimFilePathPrefix(modRoot, repoRoot))
952 if err := zip.CreateFromVCS(zipFile, m, repoRoot, "HEAD", modRel); err != nil {
953 var fel zip.FileErrorList
954 if errors.As(err, &fel) {
955 return false, fel
956 }
957 var uve *zip.UnrecognizedVCSError
958 if errors.As(err, &uve) {
959
960 return true, nil
961 }
962 return false, err
963 }
964
965 return false, nil
966 }
967
968
969
970 func downloadModule(ctx context.Context, m module.Version) (modRoot, goModPath string, err error) {
971 defer func() {
972 if err != nil {
973 err = &downloadError{m: m, err: cleanCmdError(err)}
974 }
975 }()
976
977
978
979
980
981
982
983 tmpDir, err := os.MkdirTemp("", "gorelease-download")
984 if err != nil {
985 return "", "", err
986 }
987 defer os.Remove(tmpDir)
988 cmd := exec.CommandContext(ctx, "go", "mod", "download", "-json", "--", m.Path+"@"+m.Version)
989 cmd.Env = copyEnv(ctx, cmd.Env)
990 cmd.Dir = tmpDir
991 out, err := cmd.Output()
992 var xerr *exec.ExitError
993 if err != nil {
994 var ok bool
995 if xerr, ok = err.(*exec.ExitError); !ok {
996 return "", "", err
997 }
998 }
999
1000
1001
1002 parsed := struct{ Dir, GoMod, Error string }{}
1003 if jsonErr := json.Unmarshal(out, &parsed); jsonErr != nil {
1004 if xerr != nil {
1005 return "", "", cleanCmdError(xerr)
1006 }
1007 return "", "", jsonErr
1008 }
1009 if parsed.Error != "" {
1010 return "", "", errors.New(parsed.Error)
1011 }
1012 if xerr != nil {
1013 return "", "", cleanCmdError(xerr)
1014 }
1015 return parsed.Dir, parsed.GoMod, nil
1016 }
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055 func prepareLoadDir(ctx context.Context, modFile *modfile.File, modPath, modRoot, version string, cached bool) (dir string, goModData, goSumData []byte, pkgPaths []string, diagnostics []string, err error) {
1056 defer func() {
1057 if err != nil {
1058 if cached {
1059 err = fmt.Errorf("preparing to load packages for %s@%s: %w", modPath, version, err)
1060 } else {
1061 err = fmt.Errorf("preparing to load packages for %s: %w", modPath, err)
1062 }
1063 }
1064 }()
1065
1066 if module.Check(modPath, version) != nil {
1067
1068
1069
1070 version = "v0.0.0-gorelease"
1071 if _, pathMajor, _ := module.SplitPathVersion(modPath); pathMajor != "" {
1072 version = pathMajor[1:] + ".0.0-gorelease"
1073 }
1074 }
1075
1076 dir, err = os.MkdirTemp("", "gorelease-load")
1077 if err != nil {
1078 return "", nil, nil, nil, nil, err
1079 }
1080
1081 f := &modfile.File{}
1082 f.AddModuleStmt("gorelease-load-module")
1083 f.AddRequire(modPath, version)
1084 if !cached {
1085 f.AddReplace(modPath, version, modRoot, "")
1086 }
1087 if modFile != nil {
1088 if modFile.Go != nil {
1089 f.AddGoStmt(modFile.Go.Version)
1090 }
1091 for _, r := range modFile.Require {
1092 f.AddRequire(r.Mod.Path, r.Mod.Version)
1093 }
1094 }
1095 goModData, err = f.Format()
1096 if err != nil {
1097 return "", nil, nil, nil, nil, err
1098 }
1099 if err := os.WriteFile(filepath.Join(dir, "go.mod"), goModData, 0666); err != nil {
1100 return "", nil, nil, nil, nil, err
1101 }
1102
1103 goSumData, err = os.ReadFile(filepath.Join(modRoot, "go.sum"))
1104 if err != nil && !os.IsNotExist(err) {
1105 return "", nil, nil, nil, nil, err
1106 }
1107 if err := os.WriteFile(filepath.Join(dir, "go.sum"), goSumData, 0666); err != nil {
1108 return "", nil, nil, nil, nil, err
1109 }
1110
1111
1112
1113 fakeImports := &strings.Builder{}
1114 fmt.Fprint(fakeImports, "package tmp\n")
1115 imps, err := collectImportPaths(modPath, modRoot)
1116 if err != nil {
1117 return "", nil, nil, nil, nil, err
1118 }
1119 for _, imp := range imps {
1120 fmt.Fprintf(fakeImports, "import _ %q\n", imp)
1121 }
1122 if err := os.WriteFile(filepath.Join(dir, "tmp.go"), []byte(fakeImports.String()), 0666); err != nil {
1123 return "", nil, nil, nil, nil, err
1124 }
1125
1126
1127 cmd := exec.CommandContext(ctx, "go", "get", "-d", ".")
1128 cmd.Env = copyEnv(ctx, cmd.Env)
1129 cmd.Dir = dir
1130 if _, err := cmd.Output(); err != nil {
1131 return "", nil, nil, nil, nil, fmt.Errorf("looking for missing dependencies: %w", cleanCmdError(err))
1132 }
1133
1134
1135 goModPath := filepath.Join(dir, "go.mod")
1136 loadReqs := func(data []byte) (reqs []module.Version, err error) {
1137 modFile, err := modfile.ParseLax(goModPath, data, nil)
1138 if err != nil {
1139 return nil, err
1140 }
1141 for _, r := range modFile.Require {
1142 reqs = append(reqs, r.Mod)
1143 }
1144 return reqs, nil
1145 }
1146
1147 oldReqs, err := loadReqs(goModData)
1148 if err != nil {
1149 return "", nil, nil, nil, nil, err
1150 }
1151 newGoModData, err := os.ReadFile(goModPath)
1152 if err != nil {
1153 return "", nil, nil, nil, nil, err
1154 }
1155 newReqs, err := loadReqs(newGoModData)
1156 if err != nil {
1157 return "", nil, nil, nil, nil, err
1158 }
1159
1160 oldMap := make(map[module.Version]bool)
1161 for _, req := range oldReqs {
1162 oldMap[req] = true
1163 }
1164 var missing []module.Version
1165 for _, req := range newReqs {
1166
1167 if req.Path == modPath {
1168 continue
1169 }
1170 if !oldMap[req] {
1171 missing = append(missing, req)
1172 }
1173 }
1174
1175 if len(missing) > 0 {
1176 var missingReqs []string
1177 for _, m := range missing {
1178 missingReqs = append(missingReqs, m.String())
1179 }
1180 diagnostics = append(diagnostics, fmt.Sprintf("go.mod: the following requirements are needed\n\t%s\nRun 'go mod tidy' to add missing requirements.", strings.Join(missingReqs, "\n\t")))
1181 return dir, goModData, goSumData, imps, diagnostics, nil
1182 }
1183
1184
1185
1186
1187 if !cached {
1188
1189 goSumPath := filepath.Join(dir, "go.sum")
1190 newGoSumData, err := os.ReadFile(goSumPath)
1191 if err != nil {
1192 if !os.IsNotExist(err) {
1193 return "", nil, nil, nil, nil, err
1194 }
1195
1196
1197 }
1198
1199 if !sumsMatchIgnoringPath(string(goSumData), string(newGoSumData), modPath) {
1200 diagnostics = append(diagnostics, "go.sum: one or more sums are missing. Run 'go mod tidy' to add missing sums.")
1201 }
1202 }
1203
1204 return dir, goModData, goSumData, imps, diagnostics, nil
1205 }
1206
1207
1208
1209 func sumsMatchIgnoringPath(sum1, sum2, modPathToIgnore string) bool {
1210 lines1 := make(map[string]bool)
1211 for _, line := range strings.Split(string(sum1), "\n") {
1212 if line == "" {
1213 continue
1214 }
1215 lines1[line] = true
1216 }
1217 for _, line := range strings.Split(string(sum2), "\n") {
1218 if line == "" {
1219 continue
1220 }
1221 parts := strings.Fields(line)
1222 if len(parts) < 1 {
1223 panic(fmt.Sprintf("go.sum malformed: unexpected line %s", line))
1224 }
1225 if parts[0] == modPathToIgnore {
1226 continue
1227 }
1228
1229 if !lines1[line] {
1230 return false
1231 }
1232 }
1233
1234 lines2 := make(map[string]bool)
1235 for _, line := range strings.Split(string(sum2), "\n") {
1236 if line == "" {
1237 continue
1238 }
1239 lines2[line] = true
1240 }
1241 for _, line := range strings.Split(string(sum1), "\n") {
1242 if line == "" {
1243 continue
1244 }
1245 parts := strings.Fields(line)
1246 if len(parts) < 1 {
1247 panic(fmt.Sprintf("go.sum malformed: unexpected line %s", line))
1248 }
1249 if parts[0] == modPathToIgnore {
1250 continue
1251 }
1252
1253 if !lines2[line] {
1254 return false
1255 }
1256 }
1257
1258 return true
1259 }
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270 func collectImportPaths(modPath, root string) (importPaths []string, _ error) {
1271 err := filepath.Walk(root, func(walkPath string, fi os.FileInfo, err error) error {
1272 if err != nil {
1273 return err
1274 }
1275
1276
1277 if !fi.IsDir() {
1278 return nil
1279 }
1280 base := filepath.Base(walkPath)
1281 if strings.HasPrefix(base, ".") || strings.HasPrefix(base, "_") || base == "testdata" || base == "internal" {
1282 return filepath.SkipDir
1283 }
1284
1285 p, err := build.Default.ImportDir(walkPath, 0)
1286 if err != nil {
1287 if nogoErr := (*build.NoGoError)(nil); errors.As(err, &nogoErr) {
1288
1289
1290 return nil
1291 }
1292 return err
1293 }
1294
1295
1296 importPath := path.Join(modPath, filepath.ToSlash(trimFilePathPrefix(p.Dir, root)))
1297 importPaths = append(importPaths, importPath)
1298
1299 return nil
1300 })
1301 if err != nil {
1302 return nil, fmt.Errorf("listing packages in %s: %v", root, err)
1303 }
1304
1305 return importPaths, nil
1306 }
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322 func loadPackages(ctx context.Context, modPath, modRoot, loadDir string, goModData, goSumData []byte, pkgPaths []string) (pkgs []*packages.Package, diagnostics []string, err error) {
1323
1324
1325
1326
1327 cfg := &packages.Config{
1328 Mode: packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps,
1329 Dir: loadDir,
1330 Context: ctx,
1331 }
1332 cfg.Env = copyEnv(ctx, cfg.Env)
1333 if len(pkgPaths) > 0 {
1334 pkgs, err = packages.Load(cfg, pkgPaths...)
1335 if err != nil {
1336 return nil, nil, err
1337 }
1338 }
1339
1340
1341
1342 sort.Slice(pkgs, func(i, j int) bool {
1343 return pkgs[i].PkgPath < pkgs[j].PkgPath
1344 })
1345
1346
1347 prefix := modRoot + string(os.PathSeparator)
1348 for _, pkg := range pkgs {
1349 for i := range pkg.Errors {
1350 pkg.Errors[i].Pos = strings.TrimPrefix(pkg.Errors[i].Pos, prefix)
1351 }
1352 }
1353
1354 return pkgs, diagnostics, nil
1355 }
1356
1357 type packagePair struct {
1358 base, release *packages.Package
1359 }
1360
1361
1362
1363
1364
1365
1366 func zipPackages(baseModPath string, basePkgs []*packages.Package, releaseModPath string, releasePkgs []*packages.Package) []packagePair {
1367 baseIndex, releaseIndex := 0, 0
1368 var pairs []packagePair
1369 for baseIndex < len(basePkgs) || releaseIndex < len(releasePkgs) {
1370 var basePkg, releasePkg *packages.Package
1371 var baseSuffix, releaseSuffix string
1372 if baseIndex < len(basePkgs) {
1373 basePkg = basePkgs[baseIndex]
1374 baseSuffix = trimPathPrefix(basePkg.PkgPath, baseModPath)
1375 }
1376 if releaseIndex < len(releasePkgs) {
1377 releasePkg = releasePkgs[releaseIndex]
1378 releaseSuffix = trimPathPrefix(releasePkg.PkgPath, releaseModPath)
1379 }
1380
1381 var pair packagePair
1382 if basePkg != nil && (releasePkg == nil || baseSuffix < releaseSuffix) {
1383
1384 pair = packagePair{basePkg, nil}
1385 baseIndex++
1386 } else if releasePkg != nil && (basePkg == nil || releaseSuffix < baseSuffix) {
1387
1388 pair = packagePair{nil, releasePkg}
1389 releaseIndex++
1390 } else {
1391
1392 pair = packagePair{basePkg, releasePkg}
1393 baseIndex++
1394 releaseIndex++
1395 }
1396 pairs = append(pairs, pair)
1397 }
1398 return pairs
1399 }
1400
1401
1402
1403
1404
1405
1406 func findSelectedVersion(ctx context.Context, modDir, modPath string) (latestVersion string, err error) {
1407 defer func() {
1408 if err != nil {
1409 err = fmt.Errorf("could not find selected version for %s: %v", modPath, err)
1410 }
1411 }()
1412
1413 cmd := exec.CommandContext(ctx, "go", "list", "-m", "-f", "{{.Version}}", "--", modPath)
1414 cmd.Env = copyEnv(ctx, cmd.Env)
1415 cmd.Dir = modDir
1416 out, err := cmd.Output()
1417 if err != nil {
1418 return "", cleanCmdError(err)
1419 }
1420 return strings.TrimSpace(string(out)), nil
1421 }
1422
1423 func copyEnv(ctx context.Context, current []string) []string {
1424 env, ok := ctx.Value("env").([]string)
1425 if !ok {
1426 return current
1427 }
1428 clone := make([]string, len(env))
1429 copy(clone, env)
1430 return clone
1431 }
1432
1433
1434 func loadRetractions(ctx context.Context, modRoot string) ([]string, error) {
1435 cmd := exec.CommandContext(ctx, "go", "list", "-json", "-m", "-u", "all")
1436 if env, ok := ctx.Value("env").([]string); ok {
1437 cmd.Env = env
1438 }
1439 cmd.Dir = modRoot
1440 out, err := cmd.Output()
1441 if err != nil {
1442 return nil, cleanCmdError(err)
1443 }
1444
1445 var retracted []string
1446 type message struct {
1447 Path string
1448 Version string
1449 Retracted []string
1450 }
1451
1452 dec := json.NewDecoder(bytes.NewBuffer(out))
1453 for {
1454 var m message
1455 if err := dec.Decode(&m); err == io.EOF {
1456 break
1457 } else if err != nil {
1458 return nil, err
1459 }
1460 if len(m.Retracted) == 0 {
1461 continue
1462 }
1463 rationale, ok := shortRetractionRationale(m.Retracted)
1464 if ok {
1465 retracted = append(retracted, fmt.Sprintf("required module %s@%s retracted by module author: %s", m.Path, m.Version, rationale))
1466 } else {
1467 retracted = append(retracted, fmt.Sprintf("required module %s@%s retracted by module author", m.Path, m.Version))
1468 }
1469 }
1470
1471 return retracted, nil
1472 }
1473
1474
1475
1476
1477
1478
1479
1480 func shortRetractionRationale(rationales []string) (string, bool) {
1481 if len(rationales) == 0 {
1482 return "", false
1483 }
1484 rationale := rationales[0]
1485
1486 const maxRationaleBytes = 500
1487 if i := strings.Index(rationale, "\n"); i >= 0 {
1488 rationale = rationale[:i]
1489 }
1490 rationale = strings.TrimSpace(rationale)
1491 if rationale == "" || rationale == "retracted by module author" {
1492 return "", false
1493 }
1494 if len(rationale) > maxRationaleBytes {
1495 return "", false
1496 }
1497 for _, r := range rationale {
1498 if !unicode.IsGraphic(r) && !unicode.IsSpace(r) {
1499 return "", false
1500 }
1501 }
1502
1503 return rationale, true
1504 }
1505
1506
1507
1508 func hasGitUncommittedChanges(dir string) (bool, error) {
1509 stdout := &bytes.Buffer{}
1510 cmd := exec.Command("git", "status", "--porcelain")
1511 cmd.Dir = dir
1512 cmd.Stdout = stdout
1513 if err := cmd.Run(); err != nil {
1514 return false, cleanCmdError(err)
1515 }
1516 return stdout.Len() != 0, nil
1517 }
1518
View as plain text