Skip to content

Commit 20525c8

Browse files
authored
chore: add script to analyze which releases have migrations (#10823)
* chore: add script to analyze which releases have migrations
1 parent abb2c76 commit 20525c8

File tree

2 files changed

+352
-0
lines changed

2 files changed

+352
-0
lines changed

scripts/releasemigrations/README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Migration Releases
2+
3+
The `main.go` is a program that lists all releases and which migrations are contained with each upgrade.
4+
5+
# Usage
6+
7+
```bash
8+
releasemigrations [--patches] [--minors] [--majors]
9+
-after-v2
10+
Only include releases after v2.0.0
11+
-dir string
12+
Migration directory (default "coderd/database/migrations")
13+
-list
14+
List migrations
15+
-majors
16+
Include major releases
17+
-minors
18+
Include minor releases
19+
-patches
20+
Include patches releases
21+
-versions string
22+
Comma separated list of versions to use. This skips uses git tag to find tags.
23+
```
24+
25+
# Examples
26+
27+
## Find all migrations between 2 versions
28+
29+
Going from 2.3.0 to 2.4.0
30+
31+
```bash
32+
$ go run scripts/releasemigrations/main.go --list --versions=v2.3.0,v2.4.0 11:47:00 AM
33+
2023/11/21 11:47:09 [minor] 4 migrations added between v2.3.0 and v2.4.0
34+
2023/11/21 11:47:09 coderd/database/migrations/000165_prevent_autostart_days.up.sql
35+
2023/11/21 11:47:09 coderd/database/migrations/000166_template_active_version.up.sql
36+
2023/11/21 11:47:09 coderd/database/migrations/000167_workspace_agent_api_version.up.sql
37+
2023/11/21 11:47:09 coderd/database/migrations/000168_pg_coord_tailnet_v2_api.up.sql
38+
2023/11/21 11:47:09 Patches: 0 (0 with migrations)
39+
2023/11/21 11:47:09 Minors: 1 (1 with migrations)
40+
2023/11/21 11:47:09 Majors: 0 (0 with migrations)
41+
```
42+
43+
## Looking at all patch releases after v2
44+
45+
```bash
46+
$ go run scripts/releasemigrations/main.go --patches --after-v2 11:47:09 AM
47+
2023/11/21 11:48:00 [patch] No migrations added between v2.0.0 and v2.0.1
48+
2023/11/21 11:48:00 [patch] 2 migrations added between v2.0.1 and v2.0.2
49+
2023/11/21 11:48:00 [patch] No migrations added between v2.1.0 and v2.1.1
50+
2023/11/21 11:48:00 [patch] No migrations added between v2.1.1 and v2.1.2
51+
2023/11/21 11:48:00 [patch] No migrations added between v2.1.2 and v2.1.3
52+
2023/11/21 11:48:00 [patch] 1 migrations added between v2.1.3 and v2.1.4
53+
2023/11/21 11:48:00 [patch] 2 migrations added between v2.1.4 and v2.1.5
54+
2023/11/21 11:48:00 [patch] 1 migrations added between v2.3.0 and v2.3.1
55+
2023/11/21 11:48:00 [patch] 1 migrations added between v2.3.1 and v2.3.2
56+
2023/11/21 11:48:00 [patch] 1 migrations added between v2.3.2 and v2.3.3
57+
2023/11/21 11:48:00 Patches: 10 (6 with migrations)
58+
2023/11/21 11:48:00 Minors: 4 (4 with migrations)
59+
2023/11/21 11:48:00 Majors: 0 (0 with migrations)
60+
```
61+
62+
## Seeing all the noise this thing can make
63+
64+
This shows when every migration was introduced.
65+
66+
```bash
67+
$ go run scripts/releasemigrations/main.go --patches --minors --majors --list
68+
# ...
69+
2023/11/21 11:48:31 [minor] 5 migrations added between v2.2.1 and v2.3.0
70+
2023/11/21 11:48:31 coderd/database/migrations/000160_provisioner_job_status.up.sql
71+
2023/11/21 11:48:31 coderd/database/migrations/000161_workspace_agent_stats_template_id_created_at_user_id_include_sessions.up.sql
72+
2023/11/21 11:48:31 coderd/database/migrations/000162_workspace_automatic_updates.up.sql
73+
2023/11/21 11:48:31 coderd/database/migrations/000163_external_auth_extra.up.sql
74+
2023/11/21 11:48:31 coderd/database/migrations/000164_archive_template_versions.up.sql
75+
2023/11/21 11:48:31 [patch] 1 migrations added between v2.3.0 and v2.3.1
76+
2023/11/21 11:48:31 coderd/database/migrations/000165_prevent_autostart_days.up.sql
77+
2023/11/21 11:48:31 [patch] 1 migrations added between v2.3.1 and v2.3.2
78+
2023/11/21 11:48:31 coderd/database/migrations/000166_template_active_version.up.sql
79+
2023/11/21 11:48:31 [patch] 1 migrations added between v2.3.2 and v2.3.3
80+
2023/11/21 11:48:31 coderd/database/migrations/000167_workspace_agent_api_version.up.sql
81+
2023/11/21 11:48:31 [minor] 1 migrations added between v2.3.3 and v2.4.0
82+
2023/11/21 11:48:31 coderd/database/migrations/000168_pg_coord_tailnet_v2_api.up.sql
83+
2023/11/21 11:48:31 Patches: 122 (55 with migrations)
84+
2023/11/21 11:48:31 Minors: 31 (26 with migrations)
85+
2023/11/21 11:48:31 Majors: 1 (1 with migrations)
86+
```

scripts/releasemigrations/main.go

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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+
var afterV2 bool
26+
var listMigs bool
27+
var migrationDirectory string
28+
var versionList string
29+
30+
// If you only run with --patches, the upgrades that are minors are excluded.
31+
// Example being 1.0.0 -> 1.1.0 is a minor upgrade, so it's not included.
32+
flag.BoolVar(&includePatches, "patches", false, "Include patches releases")
33+
flag.BoolVar(&includeMinors, "minors", false, "Include minor releases")
34+
flag.BoolVar(&includeMajors, "majors", false, "Include major releases")
35+
flag.StringVar(&versionList, "versions", "", "Comma separated list of versions to use. This skips uses git tag to find tags.")
36+
flag.BoolVar(&afterV2, "after-v2", false, "Only include releases after v2.0.0")
37+
flag.BoolVar(&listMigs, "list", false, "List migrations")
38+
flag.StringVar(&migrationDirectory, "dir", "coderd/database/migrations", "Migration directory")
39+
flag.Parse()
40+
41+
if !includePatches && !includeMinors && !includeMajors && versionList == "" {
42+
usage()
43+
return
44+
}
45+
46+
var vList []string
47+
if versionList != "" {
48+
// Include all for printing purposes.
49+
includeMajors = true
50+
includeMinors = true
51+
includePatches = true
52+
vList = strings.Split(versionList, ",")
53+
}
54+
55+
err := run(Options{
56+
VersionList: vList,
57+
IncludePatches: includePatches,
58+
IncludeMinors: includeMinors,
59+
IncludeMajors: includeMajors,
60+
AfterV2: afterV2,
61+
ListMigrations: listMigs,
62+
MigrationDirectory: migrationDirectory,
63+
})
64+
if err != nil {
65+
log.Fatal(err)
66+
}
67+
}
68+
69+
func usage() {
70+
_, _ = fmt.Println("Usage: releasemigrations [--patches] [--minors] [--majors] [--list]")
71+
_, _ = fmt.Println("Choose at lease one of --patches, --minors, or --majors. You can choose all!")
72+
_, _ = fmt.Println("Must be run from the coder repo at the root.")
73+
}
74+
75+
type Options struct {
76+
VersionList []string
77+
IncludePatches bool
78+
IncludeMinors bool
79+
IncludeMajors bool
80+
AfterV2 bool
81+
ListMigrations bool
82+
MigrationDirectory string
83+
}
84+
85+
func (o Options) Filter(tags []string) []string {
86+
if o.AfterV2 {
87+
for i, tag := range tags {
88+
if tag == "v2.0.0" {
89+
tags = tags[i:]
90+
break
91+
}
92+
}
93+
}
94+
95+
if o.IncludeMajors && o.IncludeMinors && o.IncludePatches {
96+
return tags
97+
}
98+
99+
filtered := make([]string, 0, len(tags))
100+
current := tags[0]
101+
filtered = append(filtered, current)
102+
for i := 1; i < len(tags); i++ {
103+
a := current
104+
current = tags[i]
105+
106+
vDiffType := versionDiff(a, tags[i])
107+
if !o.IncludeMajors && vDiffType == "major" {
108+
continue
109+
}
110+
if !o.IncludeMinors && vDiffType == "minor" {
111+
// This isn't perfect, but we need to include
112+
// the first minor release for the first patch to work.
113+
// Eg: 1.0.0 -> 1.1.0 -> 1.1.1
114+
// If we didn't include 1.1.0, then the 1.1.1 patch would
115+
// apply to 1.0.0
116+
if !o.IncludePatches {
117+
continue
118+
}
119+
}
120+
if !o.IncludePatches && vDiffType == "patch" {
121+
continue
122+
}
123+
filtered = append(filtered, tags[i])
124+
}
125+
126+
return filtered
127+
}
128+
129+
func run(opts Options) error {
130+
var tags []string
131+
if len(opts.VersionList) > 0 {
132+
tags = opts.VersionList
133+
} else {
134+
var err error
135+
tags, err = gitTags()
136+
if err != nil {
137+
return xerrors.Errorf("gitTags: %w", err)
138+
}
139+
tags = opts.Filter(tags)
140+
}
141+
142+
patches := make([]string, 0)
143+
minors := make([]string, 0)
144+
majors := make([]string, 0)
145+
patchesHasMig := 0
146+
minorsHasMig := 0
147+
majorsHasMig := 0
148+
149+
for i := 0; i < len(tags)-1; i++ {
150+
a := tags[i]
151+
b := tags[i+1]
152+
153+
migrations, err := hasMigrationDiff(opts.MigrationDirectory, a, b)
154+
if err != nil {
155+
return xerrors.Errorf("hasMigrationDiff %q->%q: %w", a, b, err)
156+
}
157+
158+
vDiff := fmt.Sprintf("%s->%s", a, b)
159+
vDiffType := versionDiff(a, b)
160+
skipPrint := true
161+
switch vDiffType {
162+
case "major":
163+
majors = append(majors, vDiff)
164+
if len(migrations) > 0 {
165+
majorsHasMig++
166+
}
167+
skipPrint = !opts.IncludeMajors
168+
case "minor":
169+
minors = append(minors, vDiff)
170+
if len(migrations) > 0 {
171+
minorsHasMig++
172+
}
173+
skipPrint = !opts.IncludeMinors
174+
case "patch":
175+
patches = append(patches, vDiff)
176+
if len(migrations) > 0 {
177+
patchesHasMig++
178+
}
179+
skipPrint = !opts.IncludePatches
180+
}
181+
182+
if skipPrint {
183+
continue
184+
}
185+
186+
if migrations != nil {
187+
log.Printf("[%s] %d migrations added between %s and %s\n", vDiffType, len(migrations), a, b)
188+
if opts.ListMigrations {
189+
for _, migration := range migrations {
190+
log.Printf("\t%s", migration)
191+
}
192+
}
193+
} else {
194+
log.Printf("[%s] No migrations added between %s and %s\n", vDiffType, a, b)
195+
}
196+
}
197+
198+
log.Printf("Patches: %d (%d with migrations)\n", len(patches), patchesHasMig)
199+
log.Printf("Minors: %d (%d with migrations)\n", len(minors), minorsHasMig)
200+
log.Printf("Majors: %d (%d with migrations)\n", len(majors), majorsHasMig)
201+
202+
return nil
203+
}
204+
205+
func versionDiff(a, b string) string {
206+
ac, bc := semver.Canonical(a), semver.Canonical(b)
207+
if semver.Major(ac) != semver.Major(bc) {
208+
return "major"
209+
}
210+
if semver.MajorMinor(ac) != semver.MajorMinor(bc) {
211+
return "minor"
212+
}
213+
return "patch"
214+
}
215+
216+
func hasMigrationDiff(dir string, a, b string) ([]string, error) {
217+
cmd := exec.Command("git", "diff",
218+
// Only added files
219+
"--diff-filter=A",
220+
"--name-only",
221+
a, b, dir)
222+
output, err := cmd.Output()
223+
if err != nil {
224+
return nil, xerrors.Errorf("%s\n%s", strings.Join(cmd.Args, " "), err)
225+
}
226+
if len(output) == 0 {
227+
return nil, nil
228+
}
229+
230+
migrations := strings.Split(strings.TrimSpace(string(output)), "\n")
231+
filtered := make([]string, 0, len(migrations))
232+
for _, migration := range migrations {
233+
migration := migration
234+
if strings.Contains(migration, "fixtures") {
235+
continue
236+
}
237+
// Only show the ups
238+
if strings.HasSuffix(migration, ".down.sql") {
239+
continue
240+
}
241+
filtered = append(filtered, migration)
242+
}
243+
return filtered, nil
244+
}
245+
246+
func gitTags() ([]string, error) {
247+
cmd := exec.Command("git", "tag")
248+
output, err := cmd.Output()
249+
if err != nil {
250+
return nil, err
251+
}
252+
253+
tags := strings.Split(string(output), "\n")
254+
255+
// Sort by semver
256+
semver.Sort(tags)
257+
258+
filtered := make([]string, 0, len(tags))
259+
for _, tag := range tags {
260+
if tag != "" && semver.IsValid(tag) {
261+
filtered = append(filtered, tag)
262+
}
263+
}
264+
265+
return filtered, nil
266+
}

0 commit comments

Comments
 (0)