summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2022-03-23 13:32:20 +0100
committerMartin Polden <mpolden@mpolden.no>2022-03-23 16:18:35 +0100
commitb2abf9e4ee27548a162f95c012de09b133308742 (patch)
tree46a1e95acf9328d452d51f891216ee2fda2e30ee /client
parent54439bff77bbb97f981e88cc316f3de2fc7a7850 (diff)
Implement conditional make wrapper
Diffstat (limited to 'client')
-rw-r--r--client/go/Makefile30
-rw-r--r--client/go/cond_make.go248
2 files changed, 267 insertions, 11 deletions
diff --git a/client/go/Makefile b/client/go/Makefile
index 0bd7325ef33..63922acb518 100644
--- a/client/go/Makefile
+++ b/client/go/Makefile
@@ -31,10 +31,13 @@ all: test checkfmt install
#
# Example:
#
-# $ git checkout vX.Y.Z
-# $ make dist-homebrew
-dist-homebrew: dist-version
- brew bump-formula-pr --version $(VERSION) --no-browse vespa-cli
+# $ make maybe-dist-homebrew
+--dist-homebrew: dist-version
+# TODO(mpolden): Remove --dry-run when this is ready
+ brew bump-formula-pr --dry-run --version $(VERSION) --no-browse vespa-cli
+
+dist-homebrew:
+ go run cond_make.go --dist-homebrew
# Create a GitHub release draft for all platforms. Note that this only creates a
# draft, which is not publicly visible until it's explicitly published.
@@ -45,13 +48,16 @@ dist-homebrew: dist-version
#
# Example:
#
-# $ git checkout vX.Y.Z
-# $ make dist-github
-dist-github: dist
- gh release create v$(VERSION) --repo vespa-engine/vespa --notes-file $(CURDIR)/README.md --draft --title "Vespa CLI $(VERSION)" \
+# $ make maybe-dist-github
+--dist-github: dist
+# TODO(mpolden): Remove @echo when this is ready
+ @echo gh release create v$(VERSION) --repo vespa-engine/vespa --notes-file $(CURDIR)/README.md --title "Vespa CLI $(VERSION)" \
$(DIST)/vespa-cli_$(VERSION)_sha256sums.txt \
$(DIST)/vespa-cli_$(VERSION)_*.{zip,tar.gz}
+dist-github:
+ go run cond_make.go --dist-github
+
#
# Cross-platform build targets
#
@@ -80,7 +86,7 @@ $(DIST_TARGETS): DIST_NAME=vespa-cli_$(VERSION)_$(GOOS)_$(GOARCH)
$(DIST_TARGETS): dist-version ci manpages
$(DIST_TARGETS):
mkdir -p $(DIST)/$(DIST_NAME)/bin
- env GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOPROXY_OVERRIDE) go build -o $(DIST)/$(DIST_NAME)/bin $(GO_FLAGS) ./...
+ env GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(DIST)/$(DIST_NAME)/bin $(GO_FLAGS) ./...
cp -a $(GIT_ROOT)/LICENSE $(DIST)/$(DIST_NAME)
if [ "$(GOOS)" = "windows" ]; then \
cd $(DIST) && zip -r $(DIST)/$(DIST_NAME).zip $(DIST_NAME); \
@@ -110,6 +116,9 @@ ifdef CI
go env
endif
+install-brew:
+ brew install vespa-cli
+
install: ci
env GOBIN=$(BIN) go install $(GO_FLAGS) ./...
@@ -120,8 +129,7 @@ manpages: install
clean:
rm -rf $(DIST)
rm -f $(BIN)/vespa $(SHARE)/man/man1/vespa.1 $(SHARE)/man/man1/vespa-*.1
- rmdir -p $(BIN) &> /dev/null || true
- rmdir -p $(SHARE)/man/man1 &> /dev/null || true
+ rmdir -p $(BIN) $(SHARE)/man/man1 &> /dev/null || true
test: ci
go test ./...
diff --git a/client/go/cond_make.go b/client/go/cond_make.go
new file mode 100644
index 00000000000..af551ff3236
--- /dev/null
+++ b/client/go/cond_make.go
@@ -0,0 +1,248 @@
+// This is a wrapper around make that runs the given target conditionally, i.e. only when considered necessary.
+//
+// For example, the Homebrew target only bumps the formula for vespa-cli if no pull request has previously been made
+// for the latest release.
+//
+// This source file is not part of the standard Vespa CLI build and is only used from the Makefile in this directory.
+
+//go:build ignore
+
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+func init() {
+ log.SetPrefix("cond-make: ")
+ log.SetFlags(0) // No timestamps
+}
+
+func requireEnv(variable string) (string, error) {
+ value := os.Getenv(variable)
+ if value == "" {
+ return "", fmt.Errorf("environment variable %s is not set", variable)
+ }
+ return value, nil
+}
+
+func quote(args []string) string {
+ var sb strings.Builder
+ for i, arg := range args {
+ if strings.Contains(arg, " ") {
+ sb.WriteString(fmt.Sprintf("%q", arg))
+ } else {
+ sb.WriteString(arg)
+ }
+ if i < len(args)-1 {
+ sb.WriteString(" ")
+ }
+ }
+ return sb.String()
+}
+
+func newCmd(name string, arg ...string) (*exec.Cmd, *bytes.Buffer, *bytes.Buffer) {
+ cmd := exec.Command(name, arg...)
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ cmd.Stdout = io.MultiWriter(os.Stdout, &stdout)
+ cmd.Stderr = io.MultiWriter(os.Stderr, &stderr)
+ log.Printf("$ %s", quote(cmd.Args))
+ return cmd, &stdout, &stderr
+}
+
+func runCmd(name string, arg ...string) (string, string, error) {
+ cmd, stdout, stderr := newCmd(name, arg...)
+ err := cmd.Run()
+ return stdout.String(), stderr.String(), err
+}
+
+// latestTag returns the most recent tag as determined by sorting local git tags as version numbers.
+func latestTag() (string, error) {
+ stdout, _, err := runCmd("sh", "-c", "git tag -l 'v[0-9]*' | sort -V | tail -1")
+ if err != nil {
+ return "", err
+ }
+ version := strings.TrimSpace(stdout)
+ if version == "" {
+ return "", fmt.Errorf("no tag found")
+ }
+ return version, nil
+}
+
+// latestReleasedTag returns the tag of the most recent release available on given mirror.
+func latestReleasedTag(mirror string) (string, error) {
+ switch mirror {
+ case "github":
+ resp, err := http.Get("https://api.github.com/repos/vespa-engine/vespa/releases/latest")
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ var release gitHubRelease
+ dec := json.NewDecoder(resp.Body)
+ if err := dec.Decode(&release); err != nil {
+ return "", err
+ }
+ return release.TagName, nil
+ case "homebrew":
+ cmd, stdout, _ := newCmd("brew", "info", "--json", "--formula", "vespa-cli")
+ cmd.Stdout = stdout // skip printing output to os.Stdout
+ if err := cmd.Run(); err != nil {
+ return "", err
+ }
+ var brewInfo []brewFormula
+ if err := json.Unmarshal(stdout.Bytes(), &brewInfo); err != nil {
+ return "", err
+ }
+ if len(brewInfo) == 0 {
+ return "", fmt.Errorf("vespa-cli formula not found")
+ }
+ return "v" + brewInfo[0].Versions.Stable, nil
+ }
+ return "", fmt.Errorf("invalid mirror: %q", mirror)
+}
+
+// hasChanges returns true if there are changes to Vespa CLI code between tag1 and tag2.
+func hasChanges(tag1, tag2 string) (bool, error) {
+ _, _, err := runCmd("git", "diff", "--quiet", tag1, tag2, ".")
+ if err != nil {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
+ switch exitErr.ExitCode() {
+ case 0:
+ return false, nil
+ case 1:
+ return true, nil
+ }
+ }
+ }
+ return false, err
+}
+
+// candidateTag returns the latest tag that should be released to mirror. If there is nothing to release, the returned
+// tag is empty.
+func candidateTag(mirror string) (string, error) {
+ latestTag, err := latestTag()
+ if err != nil {
+ return "", err
+ }
+ releasedTag, err := latestReleasedTag(mirror)
+ if err != nil {
+ return "", err
+ }
+ changes, err := hasChanges(releasedTag, latestTag)
+ if err != nil {
+ return "", err
+ }
+ if !changes {
+ log.Printf("no changes found between %s and %s: skipping release", releasedTag, latestTag)
+ return "", nil
+ }
+ log.Printf("found changes between %s and %s: creating release", releasedTag, latestTag)
+ return latestTag, nil
+}
+
+// switchToTag checks out the given tag in git and returns the current branch name. The Makefile and this file always
+// preserved from current branch after checking out tag.
+func switchToTag(tag string) (string, error) {
+ stdout, _, err := runCmd("git", "rev-parse", "--abbrev-ref", "HEAD")
+ if err != nil {
+ return "", err
+ }
+ prevBranch := strings.TrimSpace(stdout)
+ if err := checkoutRef(tag); err != nil {
+ return "", err
+ }
+ _, _, err = runCmd("git", "checkout", prevBranch, "Makefile", "cond_make.go")
+ if err != nil {
+ return "", err
+ }
+ return prevBranch, err
+}
+
+func checkoutRef(ref string) error {
+ _, _, err := runCmd("git", "checkout", ref)
+ return err
+}
+
+// releaseToHomebrew releases Vespa CLI to GitHub by calling the given make target, if necessary.
+func releaseToHomebrew(target string) error {
+ if _, err := requireEnv("HOMEBREW_GITHUB_API_TOKEN"); err != nil {
+ return err
+ }
+ tag, err := candidateTag("homebrew")
+ if tag == "" || err != nil {
+ return err
+ }
+ prevBranch, err := switchToTag(tag)
+ if err != nil {
+ return err
+ }
+ defer checkoutRef(prevBranch)
+ _, stderr, err := runCmd("make", "--", target)
+ if err != nil {
+ if strings.Contains(stderr, "Error: These pull requests may be duplicates:") {
+ return nil // fine, pull request already created
+ }
+ }
+ return err
+}
+
+// releaseToGitHub releases Vespa CLI to GitHub by calling the given make target, if necessary.
+func releaseToGitHub(target string) error {
+ if _, err := requireEnv("GH_TOKEN"); err != nil {
+ return err
+ }
+ tag, err := candidateTag("github")
+ if tag == "" || err != nil {
+ return err
+ }
+ prevBranch, err := switchToTag(tag)
+ if err != nil {
+ return err
+ }
+ defer checkoutRef(prevBranch)
+ _, _, err = runCmd("make", "--", target)
+ return err
+}
+
+func main() {
+ if len(os.Args) != 2 {
+ log.Fatalf("usage: %s TARGET", os.Args[0])
+ }
+ target := os.Args[1]
+ switch target {
+ case "--dist-homebrew":
+ if err := releaseToHomebrew(target); err != nil {
+ log.Fatal(err)
+ }
+ case "--dist-github":
+ if err := releaseToGitHub(target); err != nil {
+ log.Fatal(err)
+ }
+ default:
+ log.Fatalf("unsupported target: %s", target)
+ }
+}
+
+type gitHubRelease struct {
+ TagName string `json:"tag_name"`
+}
+
+type brewFormula struct {
+ Versions brewVersions `json:"versions"`
+}
+
+type brewVersions struct {
+ Stable string `json:"stable"`
+}