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  }