Skip to content

Commit 7bc91f2

Browse files
committed
chore: add script to analyze which releases have migrations
1 parent 7fa70ce commit 7bc91f2

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed

scripts/releasemigrations/main.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"os/exec"
8+
"strings"
9+
10+
"golang.org/x/xerrors"
11+
12+
"golang.org/x/mod/semver"
13+
)
14+
15+
// main will print out the number of migrations added between each release.
16+
// All upgrades are categorized as either major, minor, or patch based on semver.
17+
//
18+
// This isn't an exact science and is opinionated. Upgrade paths are not
19+
// always strictly linear from release to release. Users can skip patches for
20+
// example.
21+
func main() {
22+
var includePatches bool
23+
var includeMinors bool
24+
var includeMajors bool
25+
// If you only run with --patches, the upgrades that are minors are excluded.
26+
// Example being 1.0.0 -> 1.1.0 is a minor upgrade, so it's not included.
27+
flag.BoolVar(&includePatches, "patches", false, "Include patches releases")
28+
flag.BoolVar(&includeMinors, "minors", false, "Include minor releases")
29+
flag.BoolVar(&includeMajors, "majors", false, "Include major releases")
30+
flag.Parse()
31+
32+
if !includePatches && !includeMinors && !includeMajors {
33+
usage()
34+
return
35+
}
36+
37+
err := run(Options{
38+
IncludePatches: includePatches,
39+
IncludeMinors: includeMinors,
40+
IncludeMajors: includeMajors,
41+
})
42+
if err != nil {
43+
log.Fatal(err)
44+
}
45+
}
46+
47+
func usage() {
48+
fmt.Println("Usage: releasemigrations [--patches] [--minors] [--majors]")
49+
fmt.Println("Choose at lease one of --patches, --minors, or --majors. You can choose all!")
50+
}
51+
52+
type Options struct {
53+
IncludePatches bool
54+
IncludeMinors bool
55+
IncludeMajors bool
56+
}
57+
58+
func (o Options) Filter(tags []string) []string {
59+
if o.IncludeMajors && o.IncludeMinors && o.IncludePatches {
60+
return tags
61+
}
62+
63+
filtered := make([]string, 0, len(tags))
64+
current := tags[0]
65+
filtered = append(filtered, current)
66+
for i := 1; i < len(tags); i++ {
67+
a := current
68+
current = tags[i]
69+
70+
vDiffType := versionDiff(a, tags[i])
71+
if !o.IncludeMajors && vDiffType == "major" {
72+
continue
73+
}
74+
if !o.IncludeMinors && vDiffType == "minor" {
75+
// This isn't perfect, but we need to include
76+
// the first minor release for the first patch to work.
77+
// Eg: 1.0.0 -> 1.1.0 -> 1.1.1
78+
// If we didn't include 1.1.0, then the 1.1.1 patch would
79+
// apply to 1.0.0
80+
if !o.IncludePatches {
81+
continue
82+
}
83+
}
84+
if !o.IncludePatches && vDiffType == "patch" {
85+
continue
86+
}
87+
filtered = append(filtered, tags[i])
88+
}
89+
90+
return filtered
91+
}
92+
93+
func run(opts Options) error {
94+
tags, err := gitTags()
95+
if err != nil {
96+
return xerrors.Errorf("gitTags: %w", err)
97+
}
98+
tags = opts.Filter(tags)
99+
100+
patches := make([]string, 0)
101+
minors := make([]string, 0)
102+
majors := make([]string, 0)
103+
patchesHasMig := 0
104+
minorsHasMig := 0
105+
majorsHasMig := 0
106+
107+
for i := 0; i < len(tags)-1; i++ {
108+
a := tags[i]
109+
b := tags[i+1]
110+
111+
migrations, err := hasMigrationDiff(a, b)
112+
if err != nil {
113+
return xerrors.Errorf("hasMigrationDiff %q->%q: %w", a, b, err)
114+
}
115+
116+
vDiff := fmt.Sprintf("%s->%s", a, b)
117+
vDiffType := versionDiff(a, b)
118+
skipPrint := true
119+
switch vDiffType {
120+
case "major":
121+
majors = append(majors, vDiff)
122+
if len(migrations) > 0 {
123+
majorsHasMig++
124+
}
125+
skipPrint = !opts.IncludeMajors
126+
case "minor":
127+
minors = append(minors, vDiff)
128+
if len(migrations) > 0 {
129+
minorsHasMig++
130+
}
131+
skipPrint = !opts.IncludeMinors
132+
case "patch":
133+
patches = append(patches, vDiff)
134+
if len(migrations) > 0 {
135+
patchesHasMig++
136+
}
137+
skipPrint = !opts.IncludePatches
138+
}
139+
140+
if skipPrint {
141+
continue
142+
}
143+
144+
if migrations != nil {
145+
log.Printf("[%s] %d migrations added between %s and %s\n", vDiffType, len(migrations)/2, a, b)
146+
//for _, migration := range migrations {
147+
// log.Printf(" %s\n", migration)
148+
//}
149+
} else {
150+
log.Printf("[%s] No migrations added between %s and %s\n", vDiffType, a, b)
151+
}
152+
}
153+
154+
log.Printf("Patches: %d (%d with migrations)\n", len(patches), patchesHasMig)
155+
log.Printf("Minors: %d (%d with migrations)\n", len(minors), minorsHasMig)
156+
log.Printf("Majors: %d (%d with migrations)\n", len(majors), majorsHasMig)
157+
158+
return nil
159+
}
160+
161+
func versionDiff(a, b string) string {
162+
ac, bc := semver.Canonical(a), semver.Canonical(b)
163+
if semver.Major(ac) != semver.Major(bc) {
164+
return "major"
165+
}
166+
if semver.MajorMinor(ac) != semver.MajorMinor(bc) {
167+
return "minor"
168+
}
169+
return "patch"
170+
}
171+
172+
func hasMigrationDiff(a, b string) ([]string, error) {
173+
cmd := exec.Command("git", "diff",
174+
// Only added files
175+
"--diff-filter=A",
176+
"--name-only",
177+
a, b, "coderd/database/migrations")
178+
output, err := cmd.Output()
179+
if err != nil {
180+
return nil, xerrors.Errorf("%s\n%s", strings.Join(cmd.Args, " "), err)
181+
return nil, err
182+
}
183+
if len(output) == 0 {
184+
return nil, nil
185+
}
186+
187+
migrations := strings.Split(string(output), "\n")
188+
return migrations, nil
189+
}
190+
191+
func gitTags() ([]string, error) {
192+
cmd := exec.Command("git", "tag")
193+
output, err := cmd.Output()
194+
if err != nil {
195+
return nil, err
196+
}
197+
198+
tags := strings.Split(string(output), "\n")
199+
200+
// Sort by semver
201+
semver.Sort(tags)
202+
203+
filtered := make([]string, 0, len(tags))
204+
for _, tag := range tags {
205+
if tag != "" && semver.IsValid(tag) {
206+
filtered = append(filtered, tag)
207+
}
208+
}
209+
210+
return filtered, nil
211+
}

0 commit comments

Comments
 (0)