Skip to content

Commit

Permalink
chore(internal/actions): add updateall command (#7901)
Browse files Browse the repository at this point in the history
Adds an `updateall` command that effectively implements a tool for what was done in #7885. 

This also refactors the logging helpers from `changefinder` into a shared package as `cloud.google.com/go/internal/actions/logg` so both action commands can use it.
  • Loading branch information
noahdietz committed May 18, 2023
1 parent 3560582 commit d118e63
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 36 deletions.
48 changes: 12 additions & 36 deletions internal/actions/cmd/changefinder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,41 +19,40 @@ import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/exec"
"strings"

"cloud.google.com/go/internal/actions/logg"
)

var (
dir = flag.String("dir", "", "the root directory to evaluate")
format = flag.String("format", "plain", "output format, one of [plain|github], defaults to 'plain'")
ghVarName = flag.String("gh-var", "submodules", "github format's variable name to set output for, defaults to 'submodules'.")
base = flag.String("base", "origin/main", "the base ref to compare to, defaults to 'origin/main'")
quiet = flag.Bool("q", false, "quiet mode, minimal logging")
// Only used in quiet mode, printed in the event of an error.
logBuffer []string
)

func main() {
flag.BoolVar(&logg.Quiet, "q", false, "quiet mode, minimal logging")
flag.Parse()
rootDir, err := os.Getwd()
if err != nil {
fatalE(err)
logg.Fatal(err)
}
if *dir != "" {
rootDir = *dir
}
logg("Root dir: %q", rootDir)
logg.Printf("Root dir: %q", rootDir)

submodules, err := mods(rootDir)
if err != nil {
fatalE(err)
logg.Fatal(err)
}

changes, err := gitFilesChanges(rootDir)
if err != nil {
fatal("unable to get files changed: %v", err)
logg.Fatalf("unable to get files changed: %v", err)
}

modulesSeen := map[string]bool{}
Expand All @@ -64,11 +63,11 @@ func main() {
}
submod, ok := owner(change, submodules)
if !ok {
logg("no module for: %s", change)
logg.Printf("no module for: %s", change)
continue
}
if _, seen := modulesSeen[submod]; !seen {
logg("changes in submodule: %s", submod)
logg.Printf("changes in submodule: %s", submod)
updatedSubmodules = append(updatedSubmodules, submod)
modulesSeen[submod] = true
}
Expand All @@ -82,7 +81,7 @@ func output(s []string) error {
case "github":
b, err := json.Marshal(s)
if err != nil {
fatal("unable to marshal submodules: %v", err)
logg.Fatalf("unable to marshal submodules: %v", err)
}
fmt.Printf("::set-output name=%s::%s", *ghVarName, b)
case "plain":
Expand Down Expand Up @@ -119,7 +118,7 @@ func mods(dir string) (submodules []string, err error) {
if mod == "cloud.google.com/go" || strings.Contains(mod, "internal") {
continue
}
logg("found module: %s", mod)
logg.Printf("found module: %s", mod)
mod = strings.TrimPrefix(mod, "cloud.google.com/go/")
submodules = append(submodules, mod)
}
Expand All @@ -135,29 +134,6 @@ func gitFilesChanges(dir string) ([]string, error) {
return nil, err
}
b = bytes.TrimSpace(b)
logg("Files changed:\n%s", b)
logg.Printf("Files changed:\n%s", b)
return strings.Split(string(b), "\n"), nil
}

// logg is a potentially quiet log.Printf.
func logg(format string, values ...interface{}) {
if *quiet {
logBuffer = append(logBuffer, fmt.Sprintf(format, values...))
return
}
log.Printf(format, values...)
}

func fatalE(err error) {
if *quiet {
log.Print(strings.Join(logBuffer, "\n"))
}
log.Fatal(err)
}

func fatal(format string, values ...interface{}) {
if *quiet {
log.Print(strings.Join(logBuffer, "\n"))
}
log.Fatalf(format, values...)
}
27 changes: 27 additions & 0 deletions internal/actions/cmd/updateall/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# updateall

`updateall` will update all submodules that depend on the target dep to the
target version, and tidy the module afterwards.

The available flags are as follows:
* `-q`: Optional. Enables quiet mode with no logging. In the event of an error
while in quiet mode, all logs that were surpressed are dumped with the error,
defaults to `false` (i.e. "verbose").
* `-dep=[module]`: Required. The module dependency to be updated
* `-version=[version]`: Optional. The module version to update to, defaults to
`latest`.
* `-no-indirect`: Optional. Exclude updating submodules with only an indirect
dependency on the target, defaults to false.

Example usages from this repo root:

```sh
# update the google.golang.org/api dependency to latest including indirect deps
go run ./internal/actions/cmd/updateall -dep google.golang.org/api

# quiet mode, update google.golang.org/api dependency to v0.122.0 including indirect deps
go run ./internal/actions/cmd/updateall -q -dep google.golang.org/api -version=v0.122.0

# quiet mode, update google.golang.org/api dependency to v0.122.0 excluding indirect deps
go run ./internal/actions/cmd/updateall -q -dep google.golang.org/api -version=v0.122.0 -no-indirect
```
122 changes: 122 additions & 0 deletions internal/actions/cmd/updateall/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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 (
dep = flag.String("dep", "", "required, the module dependency to update")
version = flag.String("version", "latest", "optional, the verison to update to, defaults to 'latest'")
noIndirect = flag.Bool("no-indirect", false, "optional, ignores indirect deps, defaults to false")
indirectDep *regexp.Regexp
directDep *regexp.Regexp
)

func main() {
flag.BoolVar(&logg.Quiet, "q", false, "quiet mode, minimal logging")
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))
}
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); err != nil {
logg.Printf("(non-fatal) failed to update %s: %s", m, err)
}
}
}

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(mod, dep, version string) error {
c := exec.Command("go", "get", fmt.Sprintf("%s@%s", dep, version))
c.Dir = mod
_, err := c.Output()
if err != nil {
return err
}

c = exec.Command("go", "mod", "tidy")
c.Dir = mod
_, err = c.Output()
if err != nil {
return err
}

return nil
}
54 changes: 54 additions & 0 deletions internal/actions/logg/logg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 logg

import (
"fmt"
"log"
"strings"
)

var (
// Quiet is the global variable toggled by -q flags.
Quiet bool
logBuffer []string
)

// Printf is a potentially quiet log.Printf.
func Printf(format string, values ...interface{}) {
if Quiet {
logBuffer = append(logBuffer, fmt.Sprintf(format, values...))
return
}
log.Printf(format, values...)
}

// Fatal is a potentially really loud log.Fatal.
// It dumps the log buffer if run in quiet mode.
func Fatal(err error) {
if Quiet && len(logBuffer) > 0 {
log.Print(strings.Join(logBuffer, "\n"))
}
log.Fatal(err)
}

// Fatalf is a potentially really loud log.Fatalf.
// It dumps the log buffer if run in quiet mode.
func Fatalf(format string, values ...interface{}) {
if Quiet && len(logBuffer) > 0 {
log.Print(strings.Join(logBuffer, "\n"))
}
log.Fatalf(format, values...)
}

0 comments on commit d118e63

Please sign in to comment.