blob: 2338c249719f8170ef3d1501a51d9d3c9bf1f30f [file] [edit]
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"flag"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"cloud.google.com/go/internal/actions/logg"
)
var (
quiet = flag.Bool("q", false, "optional, quiet mode, minimal logging, defaults to false")
dep = flag.String("dep", "", "required, the module dependency to update")
version = flag.String("version", "latest", "optional, the version for the go get command, defaults to 'latest'")
noIndirect = flag.Bool("no-indirect", false, "optional, ignores indirect deps, defaults to false")
commitLvl = flag.String("commit-level", "fix", "optional, nested commit conventional commits type for output, defaults to 'fix'")
msg = flag.String("msg", "", "optional, nested commit message for output, defaults to 'update <dep> to <version>'")
indirectDep *regexp.Regexp
directDep *regexp.Regexp
commitLevel string
commitMsg string
nestedCommits strings.Builder
)
func main() {
logg.Quiet = *quiet
flag.Parse()
if *dep == "" {
logg.Fatalf("Missing required option: -dep=[module]")
}
if *version != "latest" {
directDep = regexp.MustCompile(fmt.Sprintf(`%s %s`, *dep, *version))
}
if *noIndirect {
indirectDep = regexp.MustCompile(fmt.Sprintf(`%s [\-\/\.a-zA-Z0-9]+ \/\/ indirect`, *dep))
}
commitLevel = *commitLvl
commitMsg := *msg
if commitMsg == "" {
commitMsg = fmt.Sprintf("update %s to %s", *dep, *version)
}
rootDir, err := os.Getwd()
if err != nil {
logg.Fatal(err)
}
logg.Printf("Root dir: %s", rootDir)
modDirs, err := modDirs(rootDir)
if err != nil {
logg.Fatalf("error listing submodules: %v", err)
}
for _, m := range modDirs {
modFile := filepath.Join(m, "go.mod")
depends, err := dependson(modFile, *dep, *version)
if err != nil {
logg.Fatalf("error checking for dep: %v", err)
}
if !depends {
continue
}
if err := update(m, *dep, *version, rootDir); err != nil {
logg.Printf("(non-fatal) failed to update %s: %s", m, err)
}
}
if !*quiet {
fmt.Println(nestedCommits.String())
}
}
func modDirs(dir string) (submodules []string, err error) {
// Find all external modules
err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.Name() == "go.mod" {
submodules = append(submodules, filepath.Dir(path))
}
return nil
})
return submodules, err
}
func dependson(mod, dep, version string) (bool, error) {
b, err := os.ReadFile(mod)
if err != nil {
return false, err
}
content := string(b)
target := fmt.Sprintf("%s ", dep)
has := strings.Contains(content, target)
eligible := version == "latest" || !directDep.MatchString(content)
if *noIndirect {
eligible = eligible && !indirectDep.MatchString(content)
}
return has && eligible, nil
}
func update(modDir, dep, version, rootDir string) error {
c := exec.Command("go", "get", fmt.Sprintf("%s@%s", dep, version))
c.Dir = modDir
_, err := c.Output()
if err != nil {
return err
}
c = exec.Command("go", "mod", "tidy")
c.Dir = modDir
_, err = c.Output()
if err != nil {
return err
}
if !*quiet {
modDir = strings.TrimSpace(modDir)
scope := strings.TrimPrefix(modDir, rootDir+"/")
if scope == modDir || scope == "" || scope == "main" || strings.Contains(scope, "internal/") {
return nil
}
nestedCommits.WriteString("BEGIN_NESTED_COMMIT\n")
nestedCommits.WriteString(fmt.Sprintf("%s(%s): %s\n", commitLevel, scope, commitMsg))
nestedCommits.WriteString("END_NESTED_COMMIT\n")
}
return nil
}