Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Daniel's tooling for local module replacements #535

Merged
merged 3 commits into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/check-consistency.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Check consistency

on:
pull_request:
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.21
- name: Build tooling
run: (cd tooling/go && go build ./cmd/local-overrides-enforcer)
- name: Check consistency
run: ./tooling/go/local-overrides-enforcer
17 changes: 17 additions & 0 deletions .github/workflows/test-tooling.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Test tooling

on:
pull_request:
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.21
- name: Run tooling tests
run: cd tooling/go && go test ./...
1 change: 1 addition & 0 deletions tooling/go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/local-overrides-enforcer
74 changes: 74 additions & 0 deletions tooling/go/cmd/local-overrides-enforcer/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"flag"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"strings"

"github.com/CodeYourFuture/curriculum/tooling/go/internal/local-overrides-enforcer/checker"
)

func main() {
var rootDirectory string
flag.StringVar(&rootDirectory, "root-dir", ".", "Root directory to search for go.mod files in")
excludeDirectoriesFlag := flag.String("exclude", filepath.Join("tooling", "go"), "Directories to exclude from searches (comma-delimited)")
var parentModule string
flag.StringVar(&parentModule, "parent-module", "github.com/CodeYourFuture/curriculum", "Parent module to search for missing overrides within")

flag.Parse()

var err error
rootDirectory, err = filepath.Abs(rootDirectory)
if err != nil {
log.Fatalf("Failed to get absolute path of root directory: %v", err)
}

var excludeDirectories []string
for _, excludeDirectory := range strings.Split(*excludeDirectoriesFlag, ",") {
excludeDirectory, err = filepath.Abs(excludeDirectory)
if err != nil {
log.Fatalf("Failed to get absolute path of exclude directory: %v", err)
}
excludeDirectories = append(excludeDirectories, excludeDirectory)
}

sawBadFile := false

err = filepath.WalkDir(rootDirectory, func(path string, d fs.DirEntry, err error) error {
for _, excluded := range excludeDirectories {
if path == excluded {
return fs.SkipDir
}
}
if err != nil {
return err
}

if d.Name() != "go.mod" {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read %s: %w", path, err)
}
expectedContents, ok, err := checker.CheckFile(path, content, parentModule)
if err != nil {
return fmt.Errorf("failed to check %s: %w", path, err)
}
if !ok {
sawBadFile = true
fmt.Printf("⚠️ File at path %s didn't have some local overrides - its contents should be:\n%s\n", path, expectedContents)
}
return nil
})
if err != nil {
log.Fatalf("Error walking filesystem: %v", err)
}
if sawBadFile {
os.Exit(1)
}
}
14 changes: 14 additions & 0 deletions tooling/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module github.com/CodeYourFuture/curriculum/tooling/go

go 1.21.5

require (
github.com/stretchr/testify v1.8.4
golang.org/x/mod v0.14.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
12 changes: 12 additions & 0 deletions tooling/go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
56 changes: 56 additions & 0 deletions tooling/go/internal/local-overrides-enforcer/checker/checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package checker

import (
"fmt"
"strings"

"golang.org/x/mod/modfile"
)

// CheckFile checks that a go.mod file has local overrides for all children of the passed parent module.
// It returns one of:
// - If the contents was correct: "", true, nil
// - If the contents was not correct: the expected contents, false, nil
// - If an error occurred: "", false, error
func CheckFile(goModPath string, contents []byte, parentModule string) (string, bool, error) {
gomodFile, err := modfile.Parse(goModPath, contents, nil)
if err != nil {
return "", false, fmt.Errorf("failed to parse %s as go.mod file: %w", goModPath, err)
}

parentModuleWithTrailingSlash := parentModule + "/"

if !strings.HasPrefix(gomodFile.Module.Mod.Path, parentModuleWithTrailingSlash) {
return "", false, fmt.Errorf("module at path %s was named %s which isn't a child of %s", goModPath, gomodFile.Module.Mod.Path, parentModule)
}
slashCount := strings.Count(gomodFile.Module.Mod.Path[len(parentModule):], "/")

replaces := make(map[string]struct{})
for _, replace := range gomodFile.Replace {
replaces[replace.Old.Path] = struct{}{}
}

missingReplaces := false
for _, require := range gomodFile.Require {
modPath := require.Mod.Path
if !strings.HasPrefix(modPath, parentModuleWithTrailingSlash) {
continue
}
if _, isReplaced := replaces[modPath]; isReplaced {
continue
}
missingReplaces = true
rel := modPath[len(parentModuleWithTrailingSlash):]
if err := gomodFile.AddReplace(modPath, "", strings.Repeat("../", slashCount)+rel, ""); err != nil {
return "", false, fmt.Errorf("failed to add replace: %w", err)
}
}
if missingReplaces {
formatted, err := gomodFile.Format()
if err != nil {
return "", false, fmt.Errorf("failed to serialize go.mod file: %w", err)
}
return string(formatted), false, nil
}
return "", true, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package checker_test

import (
"testing"

"github.com/CodeYourFuture/curriculum/tooling/go/internal/local-overrides-enforcer/checker"
"github.com/stretchr/testify/require"
)

func TestCorrect(t *testing.T) {
goModContent := `module github.com/CodeYourFuture/curriculum/org-cyf

go 1.21.3

replace github.com/CodeYourFuture/curriculum/common-content => ../common-content
replace github.com/CodeYourFuture/curriculum/common-theme => ../common-theme

require (
github.com/CodeYourFuture/curriculum/common-content v0.0.0-20240103071042-5b2177342232 // indirect
github.com/CodeYourFuture/curriculum/common-theme v0.0.0-20240103071042-5b2177342232 // indirect
)
`

newContent, ok, err := checker.CheckFile("/some/go.mod", []byte(goModContent), "github.com/CodeYourFuture/curriculum")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, "", newContent)
}

func TestMissingReplace(t *testing.T) {
goModContent := `module github.com/CodeYourFuture/curriculum/org-cyf

go 1.21.3

replace github.com/CodeYourFuture/curriculum/common-theme => ../common-theme

require (
github.com/CodeYourFuture/curriculum/common-content v0.0.0-20240103071042-5b2177342232 // indirect
github.com/CodeYourFuture/curriculum/common-theme v0.0.0-20240103071042-5b2177342232 // indirect
)
`

newContent, ok, err := checker.CheckFile("/some/go.mod", []byte(goModContent), "github.com/CodeYourFuture/curriculum")
require.NoError(t, err)
require.False(t, ok)
require.Contains(t, newContent, "replace github.com/CodeYourFuture/curriculum/common-content => ../common-content")
}

func TestModuleNotChildOfParent(t *testing.T) {
goModContent := `module github.com/CodeYourFuture/wrong

go 1.21.3

replace github.com/CodeYourFuture/curriculum/common-content => ../common-content
replace github.com/CodeYourFuture/curriculum/common-theme => ../common-theme

require (
github.com/CodeYourFuture/curriculum/common-content v0.0.0-20240103071042-5b2177342232 // indirect
github.com/CodeYourFuture/curriculum/common-theme v0.0.0-20240103071042-5b2177342232 // indirect
)
`

_, _, err := checker.CheckFile("/some/go.mod", []byte(goModContent), "github.com/CodeYourFuture/curriculum")
require.ErrorContains(t, err, "module at path /some/go.mod was named github.com/CodeYourFuture/wrong which isn't a child of github.com/CodeYourFuture/curriculum")
}

func TestInvalidGoModFile(t *testing.T) {
_, _, err := checker.CheckFile("/some/go.mod", []byte("hello"), "github.com/CodeYourFuture/curriculum")
require.ErrorContains(t, err, "failed to parse /some/go.mod as go.mod file: ")
}
Loading