Skip to content

Commit

Permalink
chore(internal/kokoro): port check_incompat_changes (#3769)
Browse files Browse the repository at this point in the history
  • Loading branch information
noahdietz committed Mar 16, 2021
1 parent 6e49f21 commit 4188b73
Show file tree
Hide file tree
Showing 2 changed files with 256 additions and 31 deletions.
255 changes: 255 additions & 0 deletions internal/apidiff/apidiff.go
@@ -0,0 +1,255 @@
// Copyright 2021 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.

// +build linux darwin

package main

import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
osexec "os/exec"
"path"
"strings"
)

// TODO(noahdietz): remove this once the fix in golang.org/x/tools is released.
// https://github.com/golang/go/issues/44796
const ignored = "- MaxPublishRequestBytes: value changed from 0.000582077 to 10000000"
const rootMod = "cloud.google.com/go"

var repoMetadataPath string
var verbose bool

func init() {
flag.StringVar(&repoMetadataPath, "repo-metadata", "", "path to a repo-metadata-full JSON file [required]")
flag.BoolVar(&verbose, "verbose", false, "enable verbose command logging")
}

func main() {
flag.Parse()
if repoMetadataPath == "" {
log.Fatalln("Missing required flag: -repo-metadata")
}

head, err := exec("git", "log", "-1")
if err != nil {
log.Fatalln(err)
}
if strings.Contains(head, "BREAKING_CHANGE") {
log.Println("Not running apidiff because description contained tag BREAKING_CHANGE.")
return
}

root, err := os.Getwd()
if err != nil {
log.Fatalln(err)
}

f, err := os.Open(repoMetadataPath)
if err != nil {
log.Fatalln(err)
}
defer f.Close()

var m manifest
if err := json.NewDecoder(f).Decode(&m); err != nil {
log.Fatalln(err)
}

_, err = exec("go", "install", "golang.org/x/exp/cmd/apidiff@latest")
if err != nil {
log.Fatalln(err)
}

temp, err := ioutil.TempDir("/tmp", "google-cloud-go-*")
if err != nil {
log.Fatalln(err)
}
defer os.RemoveAll(temp)

_, err = exec("git", "clone", "https://github.com/googleapis/google-cloud-go", temp)
if err != nil {
log.Fatalln(err)
}

diffs, diffingErrs, err := diffModules(root, temp, m)
if err != nil {
log.Fatalln(err)
}

if len(diffingErrs) > 0 {
fmt.Fprintln(os.Stderr, "The following packages encountered errors:")
for imp, err := range diffingErrs {
fmt.Fprintf(os.Stderr, "%s: %s\n", imp, err)
}
}

if len(diffs) > 0 {
fmt.Fprintln(os.Stderr, "The following breaking changes were found:")
for imp, d := range diffs {
fmt.Fprintf(os.Stderr, "%s:\n%s\n", imp, d)
}
os.Exit(1)
}
}

// manifestEntry is used for JSON marshaling in manifest.
// Copied from internal/gapicgen/generator/gapics.go.
type manifestEntry struct {
DistributionName string `json:"distribution_name"`
Description string `json:"description"`
Language string `json:"language"`
ClientLibraryType string `json:"client_library_type"`
DocsURL string `json:"docs_url"`
ReleaseLevel string `json:"release_level"`
}

type manifest map[string]manifestEntry

func diffModules(root, baseDir string, m manifest) (map[string]string, map[string]error, error) {
diffs := map[string]string{}
issues := map[string]error{}

for imp, entry := range m {
// Only diff stable clients.
if entry.ReleaseLevel != "ga" {
continue
}

// Prepare module directory paths relative to the repo root.
pkg := strings.TrimPrefix(imp, rootMod+"/")
baseModDir := baseDir
modDir := root

// Manual clients are also submodules, so we need to run apidiff in the
// submodule.
if entry.ClientLibraryType == "manual" {
baseModDir = path.Join(baseModDir, pkg)
modDir = path.Join(modDir, pkg)
}

// Create apidiff base from repo remote HEAD.
base, err := writeBase(m, baseModDir, imp, pkg)
if err != nil {
issues[imp] = err
continue
}

// Diff the current checked out change against remote HEAD base.
out, err := diff(m, modDir, imp, pkg, base)
if err != nil {
issues[imp] = err
continue
}

if out != "" && out != ignored {
diffs[imp] = out
}
}

return diffs, issues, nil
}

func writeBase(m manifest, baseModDir, imp, pkg string) (string, error) {
if err := cd(baseModDir); err != nil {
return "", err
}

base := path.Join(baseModDir, "pkg.master")
out, err := exec("apidiff", "-w", base, imp)
if err != nil && !isSubModErr(out) {
return "", err
}

// If there was an issue with loading a submodule, change into that
// submodule directory and try again.
if isSubModErr(out) {
parent := manualParent(m, imp)
if parent == pkg {
return "", fmt.Errorf("unable to find parent module for %q", imp)
}
if err := cd(parent); err != nil {
return "", err
}
out, err := exec("apidiff", "-w", base, imp)
if err != nil {
return "", fmt.Errorf("%s: %s", err, out)
}
}
return base, nil
}

func diff(m manifest, modDir, imp, pkg, base string) (string, error) {
if err := cd(modDir); err != nil {
return "", err
}
out, err := exec("apidiff", "-incompatible", base, imp)
if err != nil && !isSubModErr(out) {
return "", err
}
if isSubModErr(out) {
parent := manualParent(m, imp)
if parent == pkg {
return "", fmt.Errorf("unable to find parent module for %q", imp)
}
if err := cd(parent); err != nil {
return "", err
}
out, err = exec("apidiff", "-w", base, imp)
if err != nil {
return "", fmt.Errorf("%s: %s", err, out)
}
}

return out, err
}

func manualParent(m manifest, imp string) string {
pkg := strings.TrimPrefix(imp, rootMod)
split := strings.Split(pkg, "/")

mod := rootMod
for _, seg := range split {
mod = path.Join(mod, seg)
if parent, ok := m[mod]; ok && parent.ClientLibraryType == "manual" {
return strings.TrimPrefix(mod, rootMod+"/")
}
}

return pkg
}

func isSubModErr(msg string) bool {
return strings.Contains(msg, "missing") || strings.Contains(msg, "required")
}

func cd(dir string) error {
if verbose {
log.Printf("+ cd %s\n", dir)
}
return os.Chdir(dir)
}

func exec(cmd string, args ...string) (string, error) {
if verbose {
log.Printf("+ %s %s\n", cmd, strings.Join(args, " "))
}
out, err := osexec.Command(cmd, args...).CombinedOutput()
return strings.TrimSpace(string(out)), err
}
32 changes: 1 addition & 31 deletions internal/kokoro/check_incompat_changes.sh
Expand Up @@ -21,34 +21,4 @@ if [[ `go version` != *"go1.16"* ]]; then
exit 0
fi

if git log -1 | grep BREAKING_CHANGE_ACCEPTABLE; then
exit 0
fi

go mod download golang.org/x/exp
go install golang.org/x/exp/cmd/apidiff

# We compare against master@HEAD. This is unfortunate in some cases: if you're
# working on an out-of-date branch, and master gets some new feature (that has
# nothing to do with your work on your branch), you'll get an error message.
# Thankfully the fix is quite simple: rebase your branch.
git clone https://github.com/googleapis/google-cloud-go /tmp/gocloud

MANUALS="bigquery bigtable datastore firestore pubsub spanner storage logging"
STABLE_GAPICS="container/apiv1 dataproc/apiv1 iam iam/admin/apiv1 iam/credentials/apiv1 kms/apiv1 language/apiv1 logging/apiv2 logging/logadmin pubsub/apiv1 spanner/apiv1 translate/apiv1 vision/apiv1"
for dir in $MANUALS $STABLE_GAPICS; do
pkg="cloud.google.com/go/$dir"
echo "Testing $pkg"

cd /tmp/gocloud
apidiff -w /tmp/pkg.master $pkg
cd - > /dev/null

apidiff -incompatible /tmp/pkg.master $pkg > diff.txt
rm /tmp/pkg.master
if [ -s diff.txt ]; then
echo "Detected incompatible API changes between master@HEAD and current state:"
cat diff.txt
exit 1
fi
done
go run ./internal/apidiff -verbose -repo-metadata=./internal/.repo-metadata-full.json

0 comments on commit 4188b73

Please sign in to comment.