chore: add script to analyze which releases have migrations (#10823)

* chore: add script to analyze which releases have migrations
This commit is contained in:
Steven Masley 2023-11-27 10:53:32 -06:00 committed by GitHub
parent abb2c7656a
commit 20525c8b2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 352 additions and 0 deletions

View File

@ -0,0 +1,86 @@
# Migration Releases
The `main.go` is a program that lists all releases and which migrations are contained with each upgrade.
# Usage
```bash
releasemigrations [--patches] [--minors] [--majors]
-after-v2
Only include releases after v2.0.0
-dir string
Migration directory (default "coderd/database/migrations")
-list
List migrations
-majors
Include major releases
-minors
Include minor releases
-patches
Include patches releases
-versions string
Comma separated list of versions to use. This skips uses git tag to find tags.
```
# Examples
## Find all migrations between 2 versions
Going from 2.3.0 to 2.4.0
```bash
$ go run scripts/releasemigrations/main.go --list --versions=v2.3.0,v2.4.0 11:47:00 AM
2023/11/21 11:47:09 [minor] 4 migrations added between v2.3.0 and v2.4.0
2023/11/21 11:47:09 coderd/database/migrations/000165_prevent_autostart_days.up.sql
2023/11/21 11:47:09 coderd/database/migrations/000166_template_active_version.up.sql
2023/11/21 11:47:09 coderd/database/migrations/000167_workspace_agent_api_version.up.sql
2023/11/21 11:47:09 coderd/database/migrations/000168_pg_coord_tailnet_v2_api.up.sql
2023/11/21 11:47:09 Patches: 0 (0 with migrations)
2023/11/21 11:47:09 Minors: 1 (1 with migrations)
2023/11/21 11:47:09 Majors: 0 (0 with migrations)
```
## Looking at all patch releases after v2
```bash
$ go run scripts/releasemigrations/main.go --patches --after-v2 11:47:09 AM
2023/11/21 11:48:00 [patch] No migrations added between v2.0.0 and v2.0.1
2023/11/21 11:48:00 [patch] 2 migrations added between v2.0.1 and v2.0.2
2023/11/21 11:48:00 [patch] No migrations added between v2.1.0 and v2.1.1
2023/11/21 11:48:00 [patch] No migrations added between v2.1.1 and v2.1.2
2023/11/21 11:48:00 [patch] No migrations added between v2.1.2 and v2.1.3
2023/11/21 11:48:00 [patch] 1 migrations added between v2.1.3 and v2.1.4
2023/11/21 11:48:00 [patch] 2 migrations added between v2.1.4 and v2.1.5
2023/11/21 11:48:00 [patch] 1 migrations added between v2.3.0 and v2.3.1
2023/11/21 11:48:00 [patch] 1 migrations added between v2.3.1 and v2.3.2
2023/11/21 11:48:00 [patch] 1 migrations added between v2.3.2 and v2.3.3
2023/11/21 11:48:00 Patches: 10 (6 with migrations)
2023/11/21 11:48:00 Minors: 4 (4 with migrations)
2023/11/21 11:48:00 Majors: 0 (0 with migrations)
```
## Seeing all the noise this thing can make
This shows when every migration was introduced.
```bash
$ go run scripts/releasemigrations/main.go --patches --minors --majors --list
# ...
2023/11/21 11:48:31 [minor] 5 migrations added between v2.2.1 and v2.3.0
2023/11/21 11:48:31 coderd/database/migrations/000160_provisioner_job_status.up.sql
2023/11/21 11:48:31 coderd/database/migrations/000161_workspace_agent_stats_template_id_created_at_user_id_include_sessions.up.sql
2023/11/21 11:48:31 coderd/database/migrations/000162_workspace_automatic_updates.up.sql
2023/11/21 11:48:31 coderd/database/migrations/000163_external_auth_extra.up.sql
2023/11/21 11:48:31 coderd/database/migrations/000164_archive_template_versions.up.sql
2023/11/21 11:48:31 [patch] 1 migrations added between v2.3.0 and v2.3.1
2023/11/21 11:48:31 coderd/database/migrations/000165_prevent_autostart_days.up.sql
2023/11/21 11:48:31 [patch] 1 migrations added between v2.3.1 and v2.3.2
2023/11/21 11:48:31 coderd/database/migrations/000166_template_active_version.up.sql
2023/11/21 11:48:31 [patch] 1 migrations added between v2.3.2 and v2.3.3
2023/11/21 11:48:31 coderd/database/migrations/000167_workspace_agent_api_version.up.sql
2023/11/21 11:48:31 [minor] 1 migrations added between v2.3.3 and v2.4.0
2023/11/21 11:48:31 coderd/database/migrations/000168_pg_coord_tailnet_v2_api.up.sql
2023/11/21 11:48:31 Patches: 122 (55 with migrations)
2023/11/21 11:48:31 Minors: 31 (26 with migrations)
2023/11/21 11:48:31 Majors: 1 (1 with migrations)
```

View File

@ -0,0 +1,266 @@
package main
import (
"flag"
"fmt"
"log"
"os/exec"
"strings"
"golang.org/x/xerrors"
"golang.org/x/mod/semver"
)
// main will print out the number of migrations added between each release.
// All upgrades are categorized as either major, minor, or patch based on semver.
//
// This isn't an exact science and is opinionated. Upgrade paths are not
// always strictly linear from release to release. Users can skip patches for
// example.
func main() {
var includePatches bool
var includeMinors bool
var includeMajors bool
var afterV2 bool
var listMigs bool
var migrationDirectory string
var versionList string
// If you only run with --patches, the upgrades that are minors are excluded.
// Example being 1.0.0 -> 1.1.0 is a minor upgrade, so it's not included.
flag.BoolVar(&includePatches, "patches", false, "Include patches releases")
flag.BoolVar(&includeMinors, "minors", false, "Include minor releases")
flag.BoolVar(&includeMajors, "majors", false, "Include major releases")
flag.StringVar(&versionList, "versions", "", "Comma separated list of versions to use. This skips uses git tag to find tags.")
flag.BoolVar(&afterV2, "after-v2", false, "Only include releases after v2.0.0")
flag.BoolVar(&listMigs, "list", false, "List migrations")
flag.StringVar(&migrationDirectory, "dir", "coderd/database/migrations", "Migration directory")
flag.Parse()
if !includePatches && !includeMinors && !includeMajors && versionList == "" {
usage()
return
}
var vList []string
if versionList != "" {
// Include all for printing purposes.
includeMajors = true
includeMinors = true
includePatches = true
vList = strings.Split(versionList, ",")
}
err := run(Options{
VersionList: vList,
IncludePatches: includePatches,
IncludeMinors: includeMinors,
IncludeMajors: includeMajors,
AfterV2: afterV2,
ListMigrations: listMigs,
MigrationDirectory: migrationDirectory,
})
if err != nil {
log.Fatal(err)
}
}
func usage() {
_, _ = fmt.Println("Usage: releasemigrations [--patches] [--minors] [--majors] [--list]")
_, _ = fmt.Println("Choose at lease one of --patches, --minors, or --majors. You can choose all!")
_, _ = fmt.Println("Must be run from the coder repo at the root.")
}
type Options struct {
VersionList []string
IncludePatches bool
IncludeMinors bool
IncludeMajors bool
AfterV2 bool
ListMigrations bool
MigrationDirectory string
}
func (o Options) Filter(tags []string) []string {
if o.AfterV2 {
for i, tag := range tags {
if tag == "v2.0.0" {
tags = tags[i:]
break
}
}
}
if o.IncludeMajors && o.IncludeMinors && o.IncludePatches {
return tags
}
filtered := make([]string, 0, len(tags))
current := tags[0]
filtered = append(filtered, current)
for i := 1; i < len(tags); i++ {
a := current
current = tags[i]
vDiffType := versionDiff(a, tags[i])
if !o.IncludeMajors && vDiffType == "major" {
continue
}
if !o.IncludeMinors && vDiffType == "minor" {
// This isn't perfect, but we need to include
// the first minor release for the first patch to work.
// Eg: 1.0.0 -> 1.1.0 -> 1.1.1
// If we didn't include 1.1.0, then the 1.1.1 patch would
// apply to 1.0.0
if !o.IncludePatches {
continue
}
}
if !o.IncludePatches && vDiffType == "patch" {
continue
}
filtered = append(filtered, tags[i])
}
return filtered
}
func run(opts Options) error {
var tags []string
if len(opts.VersionList) > 0 {
tags = opts.VersionList
} else {
var err error
tags, err = gitTags()
if err != nil {
return xerrors.Errorf("gitTags: %w", err)
}
tags = opts.Filter(tags)
}
patches := make([]string, 0)
minors := make([]string, 0)
majors := make([]string, 0)
patchesHasMig := 0
minorsHasMig := 0
majorsHasMig := 0
for i := 0; i < len(tags)-1; i++ {
a := tags[i]
b := tags[i+1]
migrations, err := hasMigrationDiff(opts.MigrationDirectory, a, b)
if err != nil {
return xerrors.Errorf("hasMigrationDiff %q->%q: %w", a, b, err)
}
vDiff := fmt.Sprintf("%s->%s", a, b)
vDiffType := versionDiff(a, b)
skipPrint := true
switch vDiffType {
case "major":
majors = append(majors, vDiff)
if len(migrations) > 0 {
majorsHasMig++
}
skipPrint = !opts.IncludeMajors
case "minor":
minors = append(minors, vDiff)
if len(migrations) > 0 {
minorsHasMig++
}
skipPrint = !opts.IncludeMinors
case "patch":
patches = append(patches, vDiff)
if len(migrations) > 0 {
patchesHasMig++
}
skipPrint = !opts.IncludePatches
}
if skipPrint {
continue
}
if migrations != nil {
log.Printf("[%s] %d migrations added between %s and %s\n", vDiffType, len(migrations), a, b)
if opts.ListMigrations {
for _, migration := range migrations {
log.Printf("\t%s", migration)
}
}
} else {
log.Printf("[%s] No migrations added between %s and %s\n", vDiffType, a, b)
}
}
log.Printf("Patches: %d (%d with migrations)\n", len(patches), patchesHasMig)
log.Printf("Minors: %d (%d with migrations)\n", len(minors), minorsHasMig)
log.Printf("Majors: %d (%d with migrations)\n", len(majors), majorsHasMig)
return nil
}
func versionDiff(a, b string) string {
ac, bc := semver.Canonical(a), semver.Canonical(b)
if semver.Major(ac) != semver.Major(bc) {
return "major"
}
if semver.MajorMinor(ac) != semver.MajorMinor(bc) {
return "minor"
}
return "patch"
}
func hasMigrationDiff(dir string, a, b string) ([]string, error) {
cmd := exec.Command("git", "diff",
// Only added files
"--diff-filter=A",
"--name-only",
a, b, dir)
output, err := cmd.Output()
if err != nil {
return nil, xerrors.Errorf("%s\n%s", strings.Join(cmd.Args, " "), err)
}
if len(output) == 0 {
return nil, nil
}
migrations := strings.Split(strings.TrimSpace(string(output)), "\n")
filtered := make([]string, 0, len(migrations))
for _, migration := range migrations {
migration := migration
if strings.Contains(migration, "fixtures") {
continue
}
// Only show the ups
if strings.HasSuffix(migration, ".down.sql") {
continue
}
filtered = append(filtered, migration)
}
return filtered, nil
}
func gitTags() ([]string, error) {
cmd := exec.Command("git", "tag")
output, err := cmd.Output()
if err != nil {
return nil, err
}
tags := strings.Split(string(output), "\n")
// Sort by semver
semver.Sort(tags)
filtered := make([]string, 0, len(tags))
for _, tag := range tags {
if tag != "" && semver.IsValid(tag) {
filtered = append(filtered, tag)
}
}
return filtered, nil
}