-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #27 from mumoshu/go-ver-sync
Automate updating go.mod and workflow go versions according to Dockerfile
- Loading branch information
Showing
8 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
package main | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"log" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
var ( | ||
Dockerfile = "Dockerfile" | ||
GitHubActionsWorkflowsDir = ".github/workflows" | ||
) | ||
|
||
// syncgover is a tool to read the desired Golang version from the Dockerfile `go` image version tag, | ||
// and update the `go` version in the `go.mod` file and GitHub Actions workflow file(s) accordingly. | ||
// | ||
// This command is intended to be run in a GitHub Actions workflow step before `go test` and image-building steps, | ||
// so that changes made by this tool can be tested and the test results are reflected in the status check. | ||
func main() { | ||
dir, err := os.Getwd() | ||
if err != nil { | ||
fmt.Println(err) | ||
os.Exit(1) | ||
} | ||
|
||
if err := syncGoVer(dir); err != nil { | ||
fmt.Println(err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
// syncGoVer reads the desired Golang version from the Dockerfile `go` image version tag, | ||
// and updates the `go` version in the `go.mod` file and GitHub Actions workflow file(s) accordingly. | ||
func syncGoVer(wd string) error { | ||
var ( | ||
dockerfile = filepath.Join(wd, Dockerfile) | ||
gitHubActionsWorkflowsDir = filepath.Join(wd, GitHubActionsWorkflowsDir) | ||
) | ||
|
||
content, err := os.ReadFile(dockerfile) | ||
if err != nil { | ||
return fmt.Errorf("could not read the Dockerfile: %w", err) | ||
} | ||
|
||
goVersion, err := readGoVersionFromDockerfile(string(content)) | ||
if err != nil { | ||
return fmt.Errorf("could not read the Go version from the Dockerfile: %w", err) | ||
} | ||
|
||
log.Printf("Go image tag in the Dockerfile: %s", goVersion) | ||
|
||
major := strings.Split(goVersion, ".")[0] | ||
minor := strings.Split(goVersion, ".")[1] | ||
minorInt, err := strconv.Atoi(minor) | ||
if err != nil { | ||
return fmt.Errorf("could not convert the minor version to an integer: %w", err) | ||
} | ||
oneMinusMinor := fmt.Sprintf("%s.%d", major, minorInt-1) | ||
|
||
log.Printf("Go version in the go.mod file will be updated to: %s", oneMinusMinor) | ||
|
||
// Update the `go` version in the `go.mod` file. | ||
// Note that we don't pass `"-go", goVersion`, because it can only set up to the version of the go command | ||
// used to build this tool, which should be one minor or patch version older than the "next" version in the Dockerfile. | ||
if err := runCommand(wd, "go", "mod", "tidy", "-go", oneMinusMinor); err != nil { | ||
return fmt.Errorf("could not update the `go` version in the `go.mod` file: %w", err) | ||
} | ||
|
||
// Update the `go` version in the GitHub Actions workflow file(s). | ||
if err := filepath.Walk(gitHubActionsWorkflowsDir, func(path string, info os.FileInfo, err error) error { | ||
if err != nil { | ||
return fmt.Errorf("could not walk the GitHub Actions workflow directory: %w", err) | ||
} | ||
|
||
if info.IsDir() { | ||
return nil | ||
} | ||
|
||
log.Printf("Updating the `go` version in the GitHub Actions workflow file: %s", path) | ||
|
||
// Update the `go` version in the GitHub Actions workflow file. | ||
if err := replaceGoVersioninWorkflow(path, goVersion); err != nil { | ||
return fmt.Errorf("could not update the `go` version in the GitHub Actions workflow file: %w", err) | ||
} | ||
|
||
return nil | ||
}); err != nil { | ||
return err | ||
} | ||
|
||
// Exit with 0 if there are no changes to commit. | ||
if err := runCommand(wd, "git", "diff", "--exit-code"); err == nil { | ||
return nil | ||
} | ||
|
||
// git add and commit the changes | ||
if err := runCommand(wd, "git", "add", "-u"); err != nil { | ||
return fmt.Errorf("could not git add the changes: %w", err) | ||
} | ||
|
||
if err := runCommand(wd, "git", "commit", "-m", "Update Go versions according to Dockerfile"); err != nil { | ||
return fmt.Errorf("could not git commit the changes: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func readGoVersionFromDockerfile(content string) (string, error) { | ||
// Look for the `FROM golang:<version>` line in the Dockerfile, | ||
// and return the version number. | ||
|
||
buf := strings.NewReader(content) | ||
scanner := bufio.NewScanner(buf) | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
if strings.Contains(line, "FROM golang:") { | ||
return strings.TrimPrefix(line, "FROM golang:"), nil | ||
} | ||
} | ||
|
||
return "", fmt.Errorf("could not find the `FROM golang:<version>` line in the Dockerfile") | ||
} | ||
|
||
func runCommand(dir, command string, args ...string) error { | ||
cmd := exec.Command(command, args...) | ||
cmd.Dir = dir | ||
out, err := cmd.CombinedOutput() | ||
if err != nil { | ||
return fmt.Errorf("command %s %s failed: %s: %w", command, strings.Join(args, " "), string(out), err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func replaceGoVersioninWorkflow(file string, goVersion string) error { | ||
content, err := os.ReadFile(file) | ||
if err != nil { | ||
return fmt.Errorf("could not read the GitHub Actions workflow file: %w", err) | ||
} | ||
|
||
var newContent strings.Builder | ||
|
||
buf := strings.NewReader(string(content)) | ||
scanner := bufio.NewScanner(buf) | ||
|
||
for scanner.Scan() { | ||
line := scanner.Text() | ||
if strings.Contains(line, "go-version: ") { | ||
splits := strings.Split(line, ": ") | ||
if len(splits) != 2 { | ||
return fmt.Errorf("could not split the `go-version` line: %s", line) | ||
} | ||
|
||
newContent.WriteString(splits[0]) | ||
newContent.WriteString(": ") | ||
newContent.WriteString(goVersion) | ||
newContent.WriteString("\n") | ||
continue | ||
} | ||
|
||
newContent.WriteString(fmt.Sprintf("%s\n", line)) | ||
} | ||
|
||
if err := os.WriteFile(file, []byte(newContent.String()), 0644); err != nil { | ||
return fmt.Errorf("could not write the GitHub Actions workflow file: %w", err) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package main | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestSyncgover(t *testing.T) { | ||
localRepoPath := filepath.Join(t.TempDir(), "local-repo") | ||
|
||
require.NoError(t, os.MkdirAll(localRepoPath, 0755)) | ||
|
||
recursiveCopy(t, filepath.Join("testdata", "input"), localRepoPath) | ||
gitInitAndCommit(t, localRepoPath) | ||
|
||
require.NoError(t, syncGoVer(localRepoPath)) | ||
|
||
// We compare the output against the snapshot, | ||
// so that Dockerfile is unchanged, while GitHub Actions workflow files are updated. | ||
compareAgainstSnapshot(t, localRepoPath, filepath.Join("testdata", "output")) | ||
// We also ensure that the `go` version in the `go.mod` file is updated. | ||
// This is not covered by the snapshot comparison, because `go mod tidy` | ||
// may change the `go.mod` file in an unpredictable way. | ||
// Example: https://github.com/golang/go/issues/65847 | ||
ensureGoModGoVersion(t, localRepoPath, "1.22") | ||
} | ||
|
||
func recursiveCopy(t *testing.T, src, dst string) { | ||
t.Helper() | ||
|
||
err := filepath.Walk(src, func(path string, info fs.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
|
||
relPath, err := filepath.Rel(src, path) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
dstPath := filepath.Join(dst, relPath) | ||
if info.IsDir() { | ||
return os.MkdirAll(dstPath, 0755) | ||
} | ||
|
||
return copyFile(path, dstPath) | ||
}) | ||
|
||
require.NoError(t, err) | ||
} | ||
|
||
func copyFile(src, dst string) error { | ||
srcFile, err := os.Open(src) | ||
if err != nil { | ||
return err | ||
} | ||
defer srcFile.Close() | ||
|
||
dstFile, err := os.Create(dst) | ||
if err != nil { | ||
return err | ||
} | ||
defer dstFile.Close() | ||
|
||
_, err = io.Copy(dstFile, srcFile) | ||
return err | ||
} | ||
|
||
func gitInitAndCommit(t *testing.T, localRepoPath string) { | ||
t.Helper() | ||
require.NoError(t, runCommand(localRepoPath, "git", "init"), "git init") | ||
require.NoError(t, runCommand(localRepoPath, "git", "add", "."), "git add .") | ||
require.NoError(t, runCommand(localRepoPath, "git", "commit", "-m", "Initial commit"), "git commit") | ||
} | ||
|
||
func compareAgainstSnapshot(t *testing.T, got, want string) { | ||
t.Helper() | ||
|
||
require.NoError(t, filepath.Walk(want, func(path string, info fs.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
|
||
relPath, err := filepath.Rel(want, path) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
gotPath := filepath.Join(got, relPath) | ||
if info.IsDir() { | ||
return nil | ||
} | ||
|
||
wantContent, err := os.ReadFile(path) | ||
require.NoError(t, err) | ||
|
||
gotContent, err := os.ReadFile(gotPath) | ||
require.NoError(t, err) | ||
|
||
require.Equal(t, string(wantContent), string(gotContent)) | ||
return nil | ||
})) | ||
} | ||
|
||
func ensureGoModGoVersion(t *testing.T, localRepoPath, want string) { | ||
goModFile := filepath.Join(localRepoPath, "go.mod") | ||
goModFileContent, err := os.ReadFile(goModFile) | ||
require.NoError(t, err) | ||
goVersion, err := readGoVersionFromGoMod(string(goModFileContent)) | ||
require.NoError(t, err) | ||
require.Equal(t, want, goVersion) | ||
} | ||
|
||
func readGoVersionFromGoMod(content string) (string, error) { | ||
// Look for the `go` version in the `go.mod` file, | ||
// and return the version number. | ||
|
||
buf := strings.NewReader(content) | ||
scanner := bufio.NewScanner(buf) | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
if strings.Contains(line, "go ") { | ||
return strings.TrimPrefix(line, "go "), nil | ||
} | ||
} | ||
|
||
return "", fmt.Errorf("could not find the `go` version in the `go.mod` file") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
on: | ||
pull_request: | ||
branches: | ||
- main | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Set up Go | ||
uses: actions/setup-go@v5 | ||
with: | ||
go-version: 1.22 | ||
- name: Run tests | ||
run: go test -v ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
FROM golang:1.23.2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module example.com/test | ||
|
||
go 1.21.0 |
19 changes: 19 additions & 0 deletions
19
tools/syncgover/testdata/output/.github/workflows/test.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
on: | ||
pull_request: | ||
branches: | ||
- main | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Set up Go | ||
uses: actions/setup-go@v5 | ||
with: | ||
go-version: 1.23.2 | ||
- name: Run tests | ||
run: go test -v ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
FROM golang:1.23.2 |