Independent client/engine versioning with compatibility check
Split release.sh into release-client.sh and release-engine.sh for independent release cadences. Client checks engine version on first API call and hard-fails if engine is below MinEngineVersion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,9 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kb-search/kb/internal/config"
|
||||
)
|
||||
@@ -18,11 +21,25 @@ type FileUpload struct {
|
||||
Reader io.Reader
|
||||
}
|
||||
|
||||
// Package-level version info, set once by cmd.init via SetVersionInfo.
|
||||
var (
|
||||
clientVersion string
|
||||
minEngineVersion string
|
||||
)
|
||||
|
||||
// SetVersionInfo configures the client and minimum engine version for compatibility checking.
|
||||
// Called once from cmd package initialization.
|
||||
func SetVersionInfo(cv, minEV string) {
|
||||
clientVersion = cv
|
||||
minEngineVersion = minEV
|
||||
}
|
||||
|
||||
// Client is an HTTP client for the kb-search engine API.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
apiKey string
|
||||
httpClient *http.Client
|
||||
versionChecked bool
|
||||
}
|
||||
|
||||
// NewClient creates a Client from the current configuration.
|
||||
@@ -48,6 +65,7 @@ func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request,
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request) (*http.Response, error) {
|
||||
c.checkEngineVersion()
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot reach engine at %s: %v", c.baseURL, err)
|
||||
@@ -55,6 +73,71 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) checkEngineVersion() {
|
||||
if c.versionChecked {
|
||||
return
|
||||
}
|
||||
c.versionChecked = true
|
||||
|
||||
minVer := minEngineVersion
|
||||
if minVer == "" || minVer == "dev" {
|
||||
return
|
||||
}
|
||||
|
||||
statusReq, err := c.newRequest(http.MethodGet, "/api/v1/status", nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := c.httpClient.Do(statusReq)
|
||||
if err != nil {
|
||||
return // unreachable — let the actual request surface the error
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var status struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !semverAtLeast(status.Version, minVer) {
|
||||
fmt.Fprintf(os.Stderr, "Error: kb client v%s requires engine v%s+ (connected engine is v%s)\nUpdate your engine image to engine-v%s or later.\n",
|
||||
clientVersion, minVer, status.Version, minVer)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// semverAtLeast returns true if version >= minimum, comparing major.minor.patch.
|
||||
func semverAtLeast(version, minimum string) bool {
|
||||
parse := func(s string) (int, int, int) {
|
||||
s = strings.TrimPrefix(s, "v")
|
||||
parts := strings.SplitN(s, ".", 3)
|
||||
var major, minor, patch int
|
||||
if len(parts) >= 1 {
|
||||
major, _ = strconv.Atoi(parts[0])
|
||||
}
|
||||
if len(parts) >= 2 {
|
||||
minor, _ = strconv.Atoi(parts[1])
|
||||
}
|
||||
if len(parts) >= 3 {
|
||||
patch, _ = strconv.Atoi(parts[2])
|
||||
}
|
||||
return major, minor, patch
|
||||
}
|
||||
|
||||
vMaj, vMin, vPat := parse(version)
|
||||
mMaj, mMin, mPat := parse(minimum)
|
||||
|
||||
if vMaj != mMaj {
|
||||
return vMaj > mMaj
|
||||
}
|
||||
if vMin != mMin {
|
||||
return vMin > mMin
|
||||
}
|
||||
return vPat >= mPat
|
||||
}
|
||||
|
||||
// Get performs a GET request to the given path.
|
||||
func (c *Client) Get(path string) (*http.Response, error) {
|
||||
req, err := c.newRequest(http.MethodGet, path, nil)
|
||||
|
||||
Reference in New Issue
Block a user