Skip to content

Commit 945bab2

Browse files
authored
Add make release for release automation (#401)
1 parent 1a60b78 commit 945bab2

File tree

9 files changed

+290
-4
lines changed

9 files changed

+290
-4
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
uses: goreleaser/goreleaser-action@v6
3131
with:
3232
version: v2.7.0
33-
args: release
33+
args: release --release-notes tools/release/release-note.md
3434
env:
3535
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3636
- uses: actions/attest-build-provenance@v2

.goreleaser.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ archives:
2424
- 'zip'
2525
files:
2626
- none*
27-
changelog:
28-
disable: true
2927
checksum:
3028
name_template: 'checksums.txt'
3129
extra_files:

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
See https://github.com/terraform-linters/tflint-ruleset-google/releases for later releases.
2+
13
## 0.31.0 (2025-02-23)
24

35
### Breaking Changes

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ build:
1212
install: build
1313
mkdir -p ~/.tflint.d/plugins
1414
mv ./tflint-ruleset-google ~/.tflint.d/plugins
15+
16+
release:
17+
cd tools/release; go run main.go

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
193193
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
194194
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
195195
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
196+
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
196197
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M=
197198
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
198199
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=

tools/go.mod

Lines changed: 0 additions & 1 deletion
This file was deleted.

tools/release/go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/terraform-linters/tflint-ruleset-google/tools/release
2+
3+
go 1.24.0
4+
5+
require (
6+
github.com/google/go-github/v69 v69.2.0
7+
github.com/hashicorp/go-version v1.7.0
8+
golang.org/x/oauth2 v0.28.0
9+
)
10+
11+
require github.com/google/go-querystring v1.1.0 // indirect

tools/release/go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
2+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
3+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
4+
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
5+
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
6+
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
7+
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
8+
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
9+
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
10+
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
11+
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
12+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

tools/release/main.go

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

0 commit comments

Comments
 (0)