diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e1664b54..03044d7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: uses: goreleaser/goreleaser-action@v6 with: version: v2.7.0 - args: release + args: release --release-notes tools/release/release-note.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/attest-build-provenance@v2 diff --git a/.goreleaser.yml b/.goreleaser.yml index 79c63f9a..378ec1c0 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -24,8 +24,6 @@ archives: - 'zip' files: - none* -changelog: - disable: true checksum: name_template: 'checksums.txt' extra_files: diff --git a/CHANGELOG.md b/CHANGELOG.md index 96caa331..5f8dcf75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +See https://github.com/terraform-linters/tflint-ruleset-google/releases for later releases. + ## 0.31.0 (2025-02-23) ### Breaking Changes diff --git a/Makefile b/Makefile index 9deda836..1ccd890a 100644 --- a/Makefile +++ b/Makefile @@ -12,3 +12,6 @@ build: install: build mkdir -p ~/.tflint.d/plugins mv ./tflint-ruleset-google ~/.tflint.d/plugins + +release: + cd tools/release; go run main.go diff --git a/go.sum b/go.sum index 8ad6992c..fe16c776 100644 --- a/go.sum +++ b/go.sum @@ -193,6 +193,7 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= diff --git a/tools/go.mod b/tools/go.mod deleted file mode 100644 index 392b4c06..00000000 --- a/tools/go.mod +++ /dev/null @@ -1 +0,0 @@ -module github.com/terraform-linters/tflint-ruleset-google/tools diff --git a/tools/release/go.mod b/tools/release/go.mod new file mode 100644 index 00000000..1c34a8e5 --- /dev/null +++ b/tools/release/go.mod @@ -0,0 +1,11 @@ +module github.com/terraform-linters/tflint-ruleset-google/tools/release + +go 1.24.0 + +require ( + github.com/google/go-github/v69 v69.2.0 + github.com/hashicorp/go-version v1.7.0 + golang.org/x/oauth2 v0.28.0 +) + +require github.com/google/go-querystring v1.1.0 // indirect diff --git a/tools/release/go.sum b/tools/release/go.sum new file mode 100644 index 00000000..8dddc44a --- /dev/null +++ b/tools/release/go.sum @@ -0,0 +1,12 @@ +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= +github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tools/release/main.go b/tools/release/main.go new file mode 100644 index 00000000..72b88a5f --- /dev/null +++ b/tools/release/main.go @@ -0,0 +1,260 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "log" + "os" + "os/exec" + "regexp" + "strings" + + "github.com/google/go-github/v69/github" + "github.com/hashicorp/go-version" + "golang.org/x/oauth2" +) + +var token = os.Getenv("GITHUB_TOKEN") +var versionRegexp = regexp.MustCompile(`^\d+\.\d+\.\d+$`) +var goModRequireSDKRegexp = regexp.MustCompile(`github.com/terraform-linters/tflint-plugin-sdk v(.+)`) + +func main() { + if err := os.Chdir("../../"); err != nil { + log.Fatal(err) + } + + currentVersion := getCurrentVersion() + log.Printf("current version: %s", currentVersion) + + newVersion := getNewVersion() + log.Printf("new version: %s", newVersion) + + releaseNotePath := "tools/release/release-note.md" + + log.Println("checking requirements...") + if err := checkRequirements(currentVersion, newVersion); err != nil { + log.Fatal(err) + } + + log.Println("rewriting files with new version...") + if err := rewriteFileWithNewVersion("project/main.go", currentVersion, newVersion); err != nil { + log.Fatal(err) + } + if err := rewriteFileWithNewVersion("README.md", currentVersion, newVersion); err != nil { + log.Fatal(err) + } + + log.Println("generating release notes...") + if err := generateReleaseNote(currentVersion, newVersion, releaseNotePath); err != nil { + log.Fatal(err) + } + if err := editFileInteractive(releaseNotePath); err != nil { + log.Fatal(err) + } + + log.Println("installing and running tests...") + if err := execCommand(os.Stdout, "make", "test"); err != nil { + log.Fatal(err) + } + if err := execCommand(os.Stdout, "make", "install"); err != nil { + log.Fatal(err) + } + if err := execCommand(os.Stdout, "make", "e2e"); err != nil { + log.Fatal(err) + } + + log.Println("committing and tagging...") + if err := execCommand(os.Stdout, "git", "add", "."); err != nil { + log.Fatal(err) + } + if err := execCommand(os.Stdout, "git", "commit", "-m", fmt.Sprintf("Bump up version to v%s", newVersion)); err != nil { + log.Fatal(err) + } + if err := execCommand(os.Stdout, "git", "tag", fmt.Sprintf("v%s", newVersion)); err != nil { + log.Fatal(err) + } + if err := execCommand(os.Stdout, "git", "push", "origin", "master", "--tags"); err != nil { + log.Fatal(err) + } + log.Printf("pushed v%s", newVersion) +} + +func getCurrentVersion() string { + stdout := &bytes.Buffer{} + if err := execCommand(stdout, "git", "describe", "--tags", "--abbrev=0"); err != nil { + log.Fatal(err) + } + return strings.TrimPrefix(strings.TrimSpace(stdout.String()), "v") +} + +func getNewVersion() string { + reader := bufio.NewReader(os.Stdin) + fmt.Print(`Enter new version (without leading "v"): `) + input, err := reader.ReadString('\n') + if err != nil { + log.Fatal(fmt.Errorf("failed to read user input: %w", err)) + } + version := strings.TrimSpace(input) + + if !versionRegexp.MatchString(version) { + log.Fatal(fmt.Errorf("invalid version: %s", version)) + } + return version +} + +func checkRequirements(old string, new string) error { + if token == "" { + return fmt.Errorf("GITHUB_TOKEN is not set. Required to generate release notes") + } + + if _, err := exec.LookPath("tflint"); err != nil { + return fmt.Errorf("TFLint is not installed. Required to run E2E tests") + } + + oldVersion, err := version.NewVersion(old) + if err != nil { + return fmt.Errorf("failed to parse current version: %w", err) + } + newVersion, err := version.NewVersion(new) + if err != nil { + return fmt.Errorf("failed to parse new version: %w", err) + } + if !newVersion.GreaterThan(oldVersion) { + return fmt.Errorf("new version must be greater than current version") + } + + if err := checkGitStatus(); err != nil { + return fmt.Errorf("failed to check Git status: %w", err) + } + + if err := checkGoModules(); err != nil { + return fmt.Errorf("failed to check Go modules: %w", err) + } + return nil +} + +func checkGitStatus() error { + stdout := &bytes.Buffer{} + if err := execCommand(stdout, "git", "status", "--porcelain"); err != nil { + return err + } + if strings.TrimSpace(stdout.String()) != "" { + return fmt.Errorf("the current working tree is dirty. Please commit or stash changes") + } + + stdout = &bytes.Buffer{} + if err := execCommand(stdout, "git", "rev-parse", "--abbrev-ref", "HEAD"); err != nil { + return err + } + if strings.TrimSpace(stdout.String()) != "master" { + return fmt.Errorf("the current branch is not master, got %s", strings.TrimSpace(stdout.String())) + } + + stdout = &bytes.Buffer{} + if err := execCommand(stdout, "git", "config", "--get", "remote.origin.url"); err != nil { + return err + } + if !strings.Contains(strings.TrimSpace(stdout.String()), "terraform-linters/tflint-ruleset-google") { + return fmt.Errorf("remote.origin is not terraform-linters/tflint-ruleset-google, got %s", strings.TrimSpace(stdout.String())) + } + return nil +} + +func checkGoModules() error { + bytes, err := os.ReadFile("go.mod") + if err != nil { + return fmt.Errorf("failed to read go.mod: %w", err) + } + content := string(bytes) + + matches := goModRequireSDKRegexp.FindStringSubmatch(content) + if len(matches) != 2 { + return fmt.Errorf(`failed to parse go.mod: did not match "%s"`, goModRequireSDKRegexp.String()) + } + if !versionRegexp.MatchString(matches[1]) { + return fmt.Errorf(`failed to parse go.mod: SDK version "%s" is not stable`, matches[1]) + } + return nil +} + +func rewriteFileWithNewVersion(path string, old string, new string) error { + log.Printf("rewrite %s", path) + + bytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read %s: %w", path, err) + } + content := string(bytes) + + replaced := strings.ReplaceAll(content, old, new) + if replaced == content { + return fmt.Errorf("%s is not changed", path) + } + + if err := os.WriteFile(path, []byte(replaced), 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", path, err) + } + return nil +} + +func generateReleaseNote(old string, new string, savedPath string) error { + tagName := fmt.Sprintf("v%s", new) + previousTagName := fmt.Sprintf("v%s", old) + targetCommitish := "master" + + client := github.NewClient(oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: token, + }))) + + note, _, err := client.Repositories.GenerateReleaseNotes( + context.Background(), + "terraform-linters", + "tflint-ruleset-google", + &github.GenerateNotesOptions{ + TagName: tagName, + PreviousTagName: &previousTagName, + TargetCommitish: &targetCommitish, + }, + ) + if err != nil { + return fmt.Errorf("failed to generate release notes: %w", err) + } + + if err := os.WriteFile(savedPath, []byte(note.Body), 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", savedPath, err) + } + return err +} + +func editFileInteractive(path string) error { + editor := "vi" + if e := os.Getenv("EDITOR"); e != "" { + editor = e + } + return execShellCommand(os.Stdout, fmt.Sprintf("%s %s", editor, path)) +} + +func execShellCommand(stdout io.Writer, command string) error { + shell := "sh" + if s := os.Getenv("SHELL"); s != "" { + shell = s + } + + return execCommand(stdout, shell, "-c", command) +} + +func execCommand(stdout io.Writer, name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + commands := append([]string{name}, args...) + return fmt.Errorf(`failed to exec "%s": %w`, strings.Join(commands, " "), err) + } + return nil +}