Skip to content

Commit

Permalink
Add make release for release automation (#836)
Browse files Browse the repository at this point in the history
  • Loading branch information
wata727 authored Mar 2, 2025
1 parent 672a511 commit 41d5527
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
uses: goreleaser/goreleaser-action@v6
with:
version: v2.3.2
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 @@ -23,8 +23,6 @@ archives:
format: 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-aws/releases for later releases.

## 0.37.0 (2024-12-31)

### 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-aws ~/.tflint.d/plugins

release:
cd tools/release; go run main.go
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-aws/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.27.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.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.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-aws") {
return fmt.Errorf("remote.origin is not terraform-linters/tflint-ruleset-aws, 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-aws",
&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
}

0 comments on commit 41d5527

Please sign in to comment.