Skip to content

Commit 9f8d16e

Browse files
authored
Add make release for release automation (#366)
1 parent fc5053a commit 9f8d16e

File tree

7 files changed

+280
-5
lines changed

7 files changed

+280
-5
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@v1

.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-azurerm/releases for later releases.
2+
13
## 0.27.0 (2024-07-22)
24

35
### Enhancements

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ updateSubmodule:
2525
git submodule update --init --recursive
2626
cd ../../..
2727

28+
release:
29+
cd tools; go run ./release
2830

29-
30-
.PHONY: test e2e build install lint tools updateSubmodule
31+
.PHONY: test e2e build install lint tools updateSubmodule release

tools/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ module github.com/terraform-linters/tflint-ruleset-azurerm/tools
33
go 1.24.1
44

55
require (
6+
github.com/google/go-github/v69 v69.2.0
7+
github.com/hashicorp/go-version v1.7.0
68
github.com/hashicorp/hcl/v2 v2.23.0
79
github.com/zclconf/go-cty v1.15.0
10+
golang.org/x/oauth2 v0.28.0
811
)
912

1013
require (
1114
github.com/agext/levenshtein v1.2.1 // indirect
1215
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
1316
github.com/google/go-cmp v0.6.0 // indirect
17+
github.com/google/go-querystring v1.1.0 // indirect
1418
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
1519
golang.org/x/mod v0.8.0 // indirect
1620
golang.org/x/sys v0.5.0 // indirect

tools/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
66
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
77
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
88
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
9+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
910
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
1011
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
12+
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
13+
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
14+
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
15+
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
16+
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
17+
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
1118
github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
1219
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
1320
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
@@ -18,6 +25,8 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6
1825
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
1926
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
2027
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
28+
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
29+
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
2130
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
2231
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
2332
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
@@ -26,3 +35,4 @@ golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
2635
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
2736
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
2837
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
38+
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-azurerm") {
161+
return fmt.Errorf("remote.origin is not terraform-linters/tflint-ruleset-azurerm, 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-azurerm",
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)