Skip to content

Commit

Permalink
Merge pull request #27 from mumoshu/go-ver-sync
Browse files Browse the repository at this point in the history
Automate updating go.mod and workflow go versions according to Dockerfile
  • Loading branch information
cw-atkhry authored Nov 5, 2024
2 parents 4ba16f6 + 7a47e5d commit 2e7b6e6
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
jobs:
test:
runs-on: ubuntu-latest
permissions:
# This is required to push changes back to the repository
contents: write
steps:
- uses: actions/checkout@v4
- name: Set up Go
Expand All @@ -20,6 +23,12 @@ jobs:
with:
image-tag: 'latest'
install-awslocal: 'true'
- name: Set up Git user
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "aws-checker@@users.noreply.github.com"
- name: Bump Go versions in go.mod and workflows if necessary
run: go run ./tools/syncgover
- name: Run tests
run: go test -v ./...
env:
Expand All @@ -39,6 +48,11 @@ jobs:
DYNAMODB_TABLE: mytable
# This one is for setupSQSQueue in main_test
SQS_QUEUE_URL: https://sqs.ap-northeast-1.amazonaws.com/123456789012/myqueue
- name: Push code changes back to the repository
if: github.event_name == 'pull_request'
run: |
echo Pushing changes to ${GITHUB_HEAD_REF}
git push origin HEAD:${GITHUB_HEAD_REF} || echo "Unable to push changes"
goreleaser:
runs-on: ubuntu-latest
steps:
Expand Down
174 changes: 174 additions & 0 deletions tools/syncgover/syncgover.go
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
}
136 changes: 136 additions & 0 deletions tools/syncgover/syncgover_test.go
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")
}
19 changes: 19 additions & 0 deletions tools/syncgover/testdata/input/.github/workflows/test.yml
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 ./...
1 change: 1 addition & 0 deletions tools/syncgover/testdata/input/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM golang:1.23.2
3 changes: 3 additions & 0 deletions tools/syncgover/testdata/input/go.mod
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 tools/syncgover/testdata/output/.github/workflows/test.yml
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 ./...
1 change: 1 addition & 0 deletions tools/syncgover/testdata/output/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM golang:1.23.2

0 comments on commit 2e7b6e6

Please sign in to comment.