github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/tools/release/main.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "fmt" 8 "io" 9 "log" 10 "os" 11 "os/exec" 12 "regexp" 13 "strings" 14 15 "github.com/google/go-github/v53/github" 16 "github.com/hashicorp/go-version" 17 "golang.org/x/oauth2" 18 ) 19 20 var token = os.Getenv("GITHUB_TOKEN") 21 var versionRegexp = regexp.MustCompile(`^\d+\.\d+\.\d+$`) 22 var goModRequireSDKRegexp = regexp.MustCompile(`github.com/terraform-linters/tflint-plugin-sdk v(.+)`) 23 var goModRequireBundledPluginRegexp = regexp.MustCompile(`github.com/terraform-linters/tflint-ruleset-terraform v(.+)`) 24 25 func main() { 26 currentVersion := getCurrentVersion() 27 log.Printf("current version: %s", currentVersion) 28 29 newVersion := getNewVersion() 30 log.Printf("new version: %s", newVersion) 31 32 releaseNotePath := "tools/release/release-note.md" 33 34 log.Println("checking requirements...") 35 if err := checkRequirements(currentVersion, newVersion); err != nil { 36 log.Fatal(err) 37 } 38 39 log.Println("rewriting files with new version...") 40 if err := rewriteFileWithNewVersion("tflint/meta.go", currentVersion, newVersion); err != nil { 41 log.Fatal(err) 42 } 43 if err := rewriteFileWithNewVersion(".github/ISSUE_TEMPLATE/bug.yml", currentVersion, newVersion); err != nil { 44 log.Fatal(err) 45 } 46 47 log.Println("generating release notes...") 48 if err := generateReleaseNote(currentVersion, newVersion, releaseNotePath); err != nil { 49 log.Fatal(err) 50 } 51 if err := editFileInteractive(releaseNotePath); err != nil { 52 log.Fatal(err) 53 } 54 55 log.Println("running tests...") 56 if err := execCommand(os.Stdout, "make", "test"); err != nil { 57 log.Fatal(err) 58 } 59 if err := execCommand(os.Stdout, "make", "e2e"); err != nil { 60 log.Fatal(err) 61 } 62 63 log.Println("commiting and tagging...") 64 if err := execCommand(os.Stdout, "git", "add", "."); err != nil { 65 log.Fatal(err) 66 } 67 if err := execCommand(os.Stdout, "git", "commit", "-m", fmt.Sprintf("Bump up version to v%s", newVersion)); err != nil { 68 log.Fatal(err) 69 } 70 if err := execCommand(os.Stdout, "git", "tag", fmt.Sprintf("v%s", newVersion)); err != nil { 71 log.Fatal(err) 72 } 73 74 if err := execCommand(os.Stdout, "git", "push", "origin", "master", "--tags"); err != nil { 75 log.Fatal(err) 76 } 77 log.Printf("pushed v%s", newVersion) 78 } 79 80 func getCurrentVersion() string { 81 stdout := &bytes.Buffer{} 82 if err := execCommand(stdout, "git", "describe", "--tags", "--abbrev=0"); err != nil { 83 log.Fatal(err) 84 } 85 return strings.TrimPrefix(strings.TrimSpace(stdout.String()), "v") 86 } 87 88 func getNewVersion() string { 89 reader := bufio.NewReader(os.Stdin) 90 fmt.Print(`Enter new version (without leading "v"): `) 91 input, err := reader.ReadString('\n') 92 if err != nil { 93 log.Fatal(fmt.Errorf("failed to read user input: %w", err)) 94 } 95 version := strings.TrimSpace(input) 96 97 if !versionRegexp.MatchString(version) { 98 log.Fatal(fmt.Errorf("invalid version: %s", version)) 99 } 100 return version 101 } 102 103 func checkRequirements(old string, new string) error { 104 if token == "" { 105 return fmt.Errorf("GITHUB_TOKEN is not set. Required to generate release notes") 106 } 107 108 oldVersion, err := version.NewVersion(old) 109 if err != nil { 110 return fmt.Errorf("failed to parse current version: %w", err) 111 } 112 newVersion, err := version.NewVersion(new) 113 if err != nil { 114 return fmt.Errorf("failed to parse new version: %w", err) 115 } 116 if !newVersion.GreaterThan(oldVersion) { 117 return fmt.Errorf("new version must be greater than current version") 118 } 119 120 if err := checkGitStatus(); err != nil { 121 return fmt.Errorf("failed to check Git status: %w", err) 122 } 123 124 if err := checkGoModules(); err != nil { 125 return fmt.Errorf("failed to check Go modules: %w", err) 126 } 127 return nil 128 } 129 130 func checkGitStatus() error { 131 stdout := &bytes.Buffer{} 132 if err := execCommand(stdout, "git", "status", "--porcelain"); err != nil { 133 return err 134 } 135 if strings.TrimSpace(stdout.String()) != "" { 136 return fmt.Errorf("the current working tree is dirty. Please commit or stash changes") 137 } 138 139 stdout = &bytes.Buffer{} 140 if err := execCommand(stdout, "git", "rev-parse", "--abbrev-ref", "HEAD"); err != nil { 141 return err 142 } 143 if strings.TrimSpace(stdout.String()) != "master" { 144 return fmt.Errorf("the current branch is not master, got %s", strings.TrimSpace(stdout.String())) 145 } 146 147 stdout = &bytes.Buffer{} 148 if err := execCommand(stdout, "git", "config", "--get", "remote.origin.url"); err != nil { 149 return err 150 } 151 if !strings.Contains(strings.TrimSpace(stdout.String()), "terraform-linters/tflint") { 152 return fmt.Errorf("remote.origin is not terraform-linters/tflint, got %s", strings.TrimSpace(stdout.String())) 153 } 154 return nil 155 } 156 157 func checkGoModules() error { 158 bytes, err := os.ReadFile("go.mod") 159 if err != nil { 160 return fmt.Errorf("failed to read go.mod: %w", err) 161 } 162 content := string(bytes) 163 164 matches := goModRequireSDKRegexp.FindStringSubmatch(content) 165 if len(matches) != 2 { 166 return fmt.Errorf(`failed to parse go.mod: did not match "%s"`, goModRequireSDKRegexp.String()) 167 } 168 if !versionRegexp.MatchString(matches[1]) { 169 return fmt.Errorf(`failed to parse go.mod: SDK version "%s" is not stable`, matches[1]) 170 } 171 172 matches = goModRequireBundledPluginRegexp.FindStringSubmatch(content) 173 if len(matches) != 2 { 174 return fmt.Errorf(`failed to parse go.mod: did not match "%s"`, goModRequireBundledPluginRegexp.String()) 175 } 176 if !versionRegexp.MatchString(matches[1]) { 177 return fmt.Errorf(`failed to parse go.mod: bundled plugin version "%s" is not stable`, matches[1]) 178 } 179 return nil 180 } 181 182 func rewriteFileWithNewVersion(path string, old string, new string) error { 183 log.Printf("rewrite %s", path) 184 185 bytes, err := os.ReadFile(path) 186 if err != nil { 187 return fmt.Errorf("failed to read %s: %w", path, err) 188 } 189 content := string(bytes) 190 191 replaced := strings.ReplaceAll(content, old, new) 192 if replaced == content { 193 return fmt.Errorf("%s is not changed", path) 194 } 195 196 if err := os.WriteFile(path, []byte(replaced), 0644); err != nil { 197 return fmt.Errorf("failed to write %s: %w", path, err) 198 } 199 return nil 200 } 201 202 func generateReleaseNote(old string, new string, savedPath string) error { 203 tagName := fmt.Sprintf("v%s", new) 204 previousTagName := fmt.Sprintf("v%s", old) 205 targetCommitish := "master" 206 207 client := github.NewClient(oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{ 208 AccessToken: token, 209 }))) 210 211 note, _, err := client.Repositories.GenerateReleaseNotes( 212 context.Background(), 213 "terraform-linters", 214 "tflint", 215 &github.GenerateNotesOptions{ 216 TagName: tagName, 217 PreviousTagName: &previousTagName, 218 TargetCommitish: &targetCommitish, 219 }, 220 ) 221 if err != nil { 222 return fmt.Errorf("failed to generate release notes: %w", err) 223 } 224 225 if err := os.WriteFile(savedPath, []byte(note.Body), 0644); err != nil { 226 return fmt.Errorf("failed to write %s: %w", savedPath, err) 227 } 228 return err 229 } 230 231 func editFileInteractive(path string) error { 232 editor := "vi" 233 if e := os.Getenv("EDITOR"); e != "" { 234 editor = e 235 } 236 return execShellCommand(os.Stdout, fmt.Sprintf("%s %s", editor, path)) 237 } 238 239 func execShellCommand(stdout io.Writer, command string) error { 240 shell := "sh" 241 if s := os.Getenv("SHELL"); s != "" { 242 shell = s 243 } 244 245 return execCommand(stdout, shell, "-c", command) 246 } 247 248 func execCommand(stdout io.Writer, name string, args ...string) error { 249 cmd := exec.Command(name, args...) 250 cmd.Stdin = os.Stdin 251 cmd.Stdout = stdout 252 cmd.Stderr = os.Stderr 253 254 if err := cmd.Run(); err != nil { 255 commands := append([]string{name}, args...) 256 return fmt.Errorf(`failed to exec "%s": %w`, strings.Join(commands, " "), err) 257 } 258 return nil 259 }