Skip to content

Add make release for release automation #401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ archives:
- 'zip'
files:
- none*
changelog:
disable: true
checksum:
name_template: 'checksums.txt'
extra_files:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 0 additions & 1 deletion tools/go.mod

This file was deleted.

11 changes: 11 additions & 0 deletions tools/release/go.mod
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions tools/release/go.sum
Original file line number Diff line number Diff line change
@@ -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=
260 changes: 260 additions & 0 deletions tools/release/main.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading