Skip to content

Commit 4188b73

Browse files
authored
chore(internal/kokoro): port check_incompat_changes (#3769)
1 parent 6e49f21 commit 4188b73

File tree

2 files changed

+256
-31
lines changed

2 files changed

+256
-31
lines changed

internal/apidiff/apidiff.go

+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// +build linux darwin
16+
17+
package main
18+
19+
import (
20+
"encoding/json"
21+
"flag"
22+
"fmt"
23+
"io/ioutil"
24+
"log"
25+
"os"
26+
osexec "os/exec"
27+
"path"
28+
"strings"
29+
)
30+
31+
// TODO(noahdietz): remove this once the fix in golang.org/x/tools is released.
32+
// https://github.com/golang/go/issues/44796
33+
const ignored = "- MaxPublishRequestBytes: value changed from 0.000582077 to 10000000"
34+
const rootMod = "cloud.google.com/go"
35+
36+
var repoMetadataPath string
37+
var verbose bool
38+
39+
func init() {
40+
flag.StringVar(&repoMetadataPath, "repo-metadata", "", "path to a repo-metadata-full JSON file [required]")
41+
flag.BoolVar(&verbose, "verbose", false, "enable verbose command logging")
42+
}
43+
44+
func main() {
45+
flag.Parse()
46+
if repoMetadataPath == "" {
47+
log.Fatalln("Missing required flag: -repo-metadata")
48+
}
49+
50+
head, err := exec("git", "log", "-1")
51+
if err != nil {
52+
log.Fatalln(err)
53+
}
54+
if strings.Contains(head, "BREAKING_CHANGE") {
55+
log.Println("Not running apidiff because description contained tag BREAKING_CHANGE.")
56+
return
57+
}
58+
59+
root, err := os.Getwd()
60+
if err != nil {
61+
log.Fatalln(err)
62+
}
63+
64+
f, err := os.Open(repoMetadataPath)
65+
if err != nil {
66+
log.Fatalln(err)
67+
}
68+
defer f.Close()
69+
70+
var m manifest
71+
if err := json.NewDecoder(f).Decode(&m); err != nil {
72+
log.Fatalln(err)
73+
}
74+
75+
_, err = exec("go", "install", "golang.org/x/exp/cmd/apidiff@latest")
76+
if err != nil {
77+
log.Fatalln(err)
78+
}
79+
80+
temp, err := ioutil.TempDir("/tmp", "google-cloud-go-*")
81+
if err != nil {
82+
log.Fatalln(err)
83+
}
84+
defer os.RemoveAll(temp)
85+
86+
_, err = exec("git", "clone", "https://github.com/googleapis/google-cloud-go", temp)
87+
if err != nil {
88+
log.Fatalln(err)
89+
}
90+
91+
diffs, diffingErrs, err := diffModules(root, temp, m)
92+
if err != nil {
93+
log.Fatalln(err)
94+
}
95+
96+
if len(diffingErrs) > 0 {
97+
fmt.Fprintln(os.Stderr, "The following packages encountered errors:")
98+
for imp, err := range diffingErrs {
99+
fmt.Fprintf(os.Stderr, "%s: %s\n", imp, err)
100+
}
101+
}
102+
103+
if len(diffs) > 0 {
104+
fmt.Fprintln(os.Stderr, "The following breaking changes were found:")
105+
for imp, d := range diffs {
106+
fmt.Fprintf(os.Stderr, "%s:\n%s\n", imp, d)
107+
}
108+
os.Exit(1)
109+
}
110+
}
111+
112+
// manifestEntry is used for JSON marshaling in manifest.
113+
// Copied from internal/gapicgen/generator/gapics.go.
114+
type manifestEntry struct {
115+
DistributionName string `json:"distribution_name"`
116+
Description string `json:"description"`
117+
Language string `json:"language"`
118+
ClientLibraryType string `json:"client_library_type"`
119+
DocsURL string `json:"docs_url"`
120+
ReleaseLevel string `json:"release_level"`
121+
}
122+
123+
type manifest map[string]manifestEntry
124+
125+
func diffModules(root, baseDir string, m manifest) (map[string]string, map[string]error, error) {
126+
diffs := map[string]string{}
127+
issues := map[string]error{}
128+
129+
for imp, entry := range m {
130+
// Only diff stable clients.
131+
if entry.ReleaseLevel != "ga" {
132+
continue
133+
}
134+
135+
// Prepare module directory paths relative to the repo root.
136+
pkg := strings.TrimPrefix(imp, rootMod+"/")
137+
baseModDir := baseDir
138+
modDir := root
139+
140+
// Manual clients are also submodules, so we need to run apidiff in the
141+
// submodule.
142+
if entry.ClientLibraryType == "manual" {
143+
baseModDir = path.Join(baseModDir, pkg)
144+
modDir = path.Join(modDir, pkg)
145+
}
146+
147+
// Create apidiff base from repo remote HEAD.
148+
base, err := writeBase(m, baseModDir, imp, pkg)
149+
if err != nil {
150+
issues[imp] = err
151+
continue
152+
}
153+
154+
// Diff the current checked out change against remote HEAD base.
155+
out, err := diff(m, modDir, imp, pkg, base)
156+
if err != nil {
157+
issues[imp] = err
158+
continue
159+
}
160+
161+
if out != "" && out != ignored {
162+
diffs[imp] = out
163+
}
164+
}
165+
166+
return diffs, issues, nil
167+
}
168+
169+
func writeBase(m manifest, baseModDir, imp, pkg string) (string, error) {
170+
if err := cd(baseModDir); err != nil {
171+
return "", err
172+
}
173+
174+
base := path.Join(baseModDir, "pkg.master")
175+
out, err := exec("apidiff", "-w", base, imp)
176+
if err != nil && !isSubModErr(out) {
177+
return "", err
178+
}
179+
180+
// If there was an issue with loading a submodule, change into that
181+
// submodule directory and try again.
182+
if isSubModErr(out) {
183+
parent := manualParent(m, imp)
184+
if parent == pkg {
185+
return "", fmt.Errorf("unable to find parent module for %q", imp)
186+
}
187+
if err := cd(parent); err != nil {
188+
return "", err
189+
}
190+
out, err := exec("apidiff", "-w", base, imp)
191+
if err != nil {
192+
return "", fmt.Errorf("%s: %s", err, out)
193+
}
194+
}
195+
return base, nil
196+
}
197+
198+
func diff(m manifest, modDir, imp, pkg, base string) (string, error) {
199+
if err := cd(modDir); err != nil {
200+
return "", err
201+
}
202+
out, err := exec("apidiff", "-incompatible", base, imp)
203+
if err != nil && !isSubModErr(out) {
204+
return "", err
205+
}
206+
if isSubModErr(out) {
207+
parent := manualParent(m, imp)
208+
if parent == pkg {
209+
return "", fmt.Errorf("unable to find parent module for %q", imp)
210+
}
211+
if err := cd(parent); err != nil {
212+
return "", err
213+
}
214+
out, err = exec("apidiff", "-w", base, imp)
215+
if err != nil {
216+
return "", fmt.Errorf("%s: %s", err, out)
217+
}
218+
}
219+
220+
return out, err
221+
}
222+
223+
func manualParent(m manifest, imp string) string {
224+
pkg := strings.TrimPrefix(imp, rootMod)
225+
split := strings.Split(pkg, "/")
226+
227+
mod := rootMod
228+
for _, seg := range split {
229+
mod = path.Join(mod, seg)
230+
if parent, ok := m[mod]; ok && parent.ClientLibraryType == "manual" {
231+
return strings.TrimPrefix(mod, rootMod+"/")
232+
}
233+
}
234+
235+
return pkg
236+
}
237+
238+
func isSubModErr(msg string) bool {
239+
return strings.Contains(msg, "missing") || strings.Contains(msg, "required")
240+
}
241+
242+
func cd(dir string) error {
243+
if verbose {
244+
log.Printf("+ cd %s\n", dir)
245+
}
246+
return os.Chdir(dir)
247+
}
248+
249+
func exec(cmd string, args ...string) (string, error) {
250+
if verbose {
251+
log.Printf("+ %s %s\n", cmd, strings.Join(args, " "))
252+
}
253+
out, err := osexec.Command(cmd, args...).CombinedOutput()
254+
return strings.TrimSpace(string(out)), err
255+
}

internal/kokoro/check_incompat_changes.sh

+1-31
Original file line numberDiff line numberDiff line change
@@ -21,34 +21,4 @@ if [[ `go version` != *"go1.16"* ]]; then
2121
exit 0
2222
fi
2323

24-
if git log -1 | grep BREAKING_CHANGE_ACCEPTABLE; then
25-
exit 0
26-
fi
27-
28-
go mod download golang.org/x/exp
29-
go install golang.org/x/exp/cmd/apidiff
30-
31-
# We compare against master@HEAD. This is unfortunate in some cases: if you're
32-
# working on an out-of-date branch, and master gets some new feature (that has
33-
# nothing to do with your work on your branch), you'll get an error message.
34-
# Thankfully the fix is quite simple: rebase your branch.
35-
git clone https://github.com/googleapis/google-cloud-go /tmp/gocloud
36-
37-
MANUALS="bigquery bigtable datastore firestore pubsub spanner storage logging"
38-
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"
39-
for dir in $MANUALS $STABLE_GAPICS; do
40-
pkg="cloud.google.com/go/$dir"
41-
echo "Testing $pkg"
42-
43-
cd /tmp/gocloud
44-
apidiff -w /tmp/pkg.master $pkg
45-
cd - > /dev/null
46-
47-
apidiff -incompatible /tmp/pkg.master $pkg > diff.txt
48-
rm /tmp/pkg.master
49-
if [ -s diff.txt ]; then
50-
echo "Detected incompatible API changes between master@HEAD and current state:"
51-
cat diff.txt
52-
exit 1
53-
fi
54-
done
24+
go run ./internal/apidiff -verbose -repo-metadata=./internal/.repo-metadata-full.json

0 commit comments

Comments
 (0)