Skip to content

Commit 72e36de

Browse files
committed
Add make release for release automation
1 parent 49d6ca5 commit 72e36de

File tree

7 files changed

+279
-3
lines changed

7 files changed

+279
-3
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
uses: goreleaser/goreleaser-action@v6
2929
with:
3030
version: v2.3.2
31-
args: release
31+
args: release --release-notes tools/release/release-note.md
3232
env:
3333
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3434
- uses: actions/attest-build-provenance@v2

.goreleaser.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ archives:
2323
format: zip
2424
files:
2525
- none*
26-
changelog:
27-
disable: true
2826
checksum:
2927
name_template: 'checksums.txt'
3028
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-terraform/releases for later releases.
2+
13
## 0.10.0 (2024-11-04)
24

35
### Enhancements

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ build:
99
install: build
1010
mkdir -p ~/.tflint.d/plugins
1111
mv ./tflint-ruleset-terraform ~/.tflint.d/plugins
12+
13+
release:
14+
cd tools/release; go run main.go

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

0 commit comments

Comments
 (0)