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:
@@ -0,0 +1,93 @@
|
||||
## Context
|
||||
|
||||
Currently the project uses a single version number shared between client and engine, managed by `release.sh`. Both `client/VERSION` and `engine/VERSION` are always bumped to the same value. A single git tag `vX.Y.Z` is created, and a single Gitea release bundles Go client binaries and Docker engine image references. This means any change to either component forces a full release of both.
|
||||
|
||||
The client is a Go binary distributed as platform-specific downloads. The engine is a Python FastAPI server distributed as Docker images. They communicate over HTTP via `/api/v1/` endpoints. The engine already exposes its version via `GET /api/v1/status` → `{"version": "X.Y.Z", ...}`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Allow client and engine to have independent version numbers and release cadences
|
||||
- Provide a runtime compatibility check so users get a clear error when their client is too new for their engine
|
||||
- Split release tooling so each component can be released without touching the other
|
||||
|
||||
**Non-Goals:**
|
||||
- API versioning beyond the existing `/api/v1/` path prefix
|
||||
- Backward-compatible negotiation or feature detection (client either works or fails)
|
||||
- Automatic upgrades or update notifications
|
||||
- Version checking in the other direction (engine requiring minimum client)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Tag naming: `client-vX.Y.Z` and `engine-vX.Y.Z`
|
||||
|
||||
Prefix-style tags clearly identify which component a release belongs to and sort well in git tag listings.
|
||||
|
||||
**Why over path-style (`client/vX.Y.Z`):** Slashes in git tags can cause issues with some tooling and are less conventional. Prefix-style is simpler and widely used in monorepos.
|
||||
|
||||
**Why over separate repos:** The project is small and tightly coupled at the API level. A monorepo with prefixed tags keeps everything together while allowing independent releases.
|
||||
|
||||
### 2. Two release scripts: `release-client.sh` and `release-engine.sh`
|
||||
|
||||
Each script handles its own component end-to-end: version bump, build, tag, release, push.
|
||||
|
||||
**Why over a single script with flags:** Two simple scripts are easier to understand and maintain than one script with component-selection logic. Each script is ~100 lines instead of one ~200-line script with branching. The shared logic (version helpers, pre-flight checks) is minimal and acceptable to duplicate.
|
||||
|
||||
**Shared structure for both scripts:**
|
||||
1. Pre-flight checks (on main branch, tag doesn't exist)
|
||||
2. Version bump (reads/writes component's VERSION file only)
|
||||
3. Build artifacts (Go binaries or Docker images)
|
||||
4. Commit version bump, create prefixed tag, push
|
||||
5. Create Gitea release with assets
|
||||
6. (Engine only) Push Docker images
|
||||
|
||||
### 3. `MinEngineVersion` as a build-time constant in the Go client
|
||||
|
||||
The client embeds a `MinEngineVersion` string constant alongside the existing `Version` constant. It is set via `-ldflags` at build time, sourced from a `client/MIN_ENGINE_VERSION` file.
|
||||
|
||||
**Why a separate file over embedding in `VERSION`:** The two values have different lifecycles. `VERSION` changes every release; `MIN_ENGINE_VERSION` changes only when the client starts using a new engine feature. A separate file makes the intent clear.
|
||||
|
||||
**Why ldflags over hardcoding in Go source:** Consistent with how `Version` is already injected. The value lives in a plain text file that's easy to bump manually.
|
||||
|
||||
### 4. Compatibility check on every API call via the `Client` struct
|
||||
|
||||
The `api.Client` checks engine compatibility on its first HTTP call by hitting `GET /api/v1/status` and comparing the `version` field against `MinEngineVersion`. The result is cached on the `Client` instance — subsequent calls skip the check.
|
||||
|
||||
**Flow:**
|
||||
1. First call to any `Client` method (Get/Post/Delete/Put)
|
||||
2. Before the actual request, call `GET /api/v1/status`
|
||||
3. Parse `version` from response
|
||||
4. Compare against `MinEngineVersion` using semver major.minor.patch comparison
|
||||
5. If engine version < min: print error to stderr, `os.Exit(1)`
|
||||
6. If check passes: set `versionChecked = true`, proceed with original request
|
||||
7. If status endpoint unreachable: proceed with original request (connectivity error will surface on the actual call)
|
||||
|
||||
**Why hard fail, no skip flag:** This is a personal tool. If the client needs a newer engine, the user needs to update. A skip flag adds complexity for a scenario where the outcome (broken behavior) is worse than the error.
|
||||
|
||||
**Why check on first API call, not at startup:** The `PersistentPreRunE` in cobra runs before every command, but some future commands might not need the engine (e.g. `kb version`, `kb help`). Checking in the `Client` ensures we only check when actually contacting the engine.
|
||||
|
||||
**Why proceed when status endpoint is unreachable:** If we can't reach `/status`, the actual API call will also fail with a connection error. No point in double-failing. The compatibility check is for version mismatch, not connectivity.
|
||||
|
||||
### 5. Compose files: use `build:` context, not pinned image tags
|
||||
|
||||
The compose files currently use `build:` directives, not pre-built image references. Users who build locally don't need pinned tags — they're building from source. Users pulling pre-built images will reference the image tag directly in their own compose file or `docker run` command.
|
||||
|
||||
**Decision:** Leave compose files as-is. Release notes for engine releases will include the exact `docker pull` command with the versioned tag.
|
||||
|
||||
### 6. Semver comparison: major.minor.patch, no pre-release
|
||||
|
||||
Compare versions as three integers. No support for pre-release suffixes (`-rc1`, `-beta`) — the project doesn't use them. If `MinEngineVersion` is `2.1.0` and engine reports `2.1.5`, the check passes. If engine reports `2.0.9`, it fails.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Extra HTTP round-trip on first command** — One additional `GET /api/v1/status` call per client invocation. Negligible for a local-network tool.
|
||||
→ Mitigation: Cached after first check within the Client instance.
|
||||
|
||||
- **Developer must remember to bump `MIN_ENGINE_VERSION`** — When adding client code that depends on a new engine endpoint/field, the developer must manually update the file.
|
||||
→ Mitigation: This is a conscious decision point. The file's existence serves as a reminder. Could add a CI check later if needed.
|
||||
|
||||
- **Breaking change to git tag format** — Existing `v2.0.x` tags won't match the new `client-v*` / `engine-v*` convention. Old tags remain in history.
|
||||
→ Mitigation: No migration needed. Old tags stay as historical artifacts. New convention starts from the first independent release.
|
||||
|
||||
- **Two Gitea releases per coordinated release** — When both components change, two releases are created instead of one.
|
||||
→ Mitigation: Acceptable trade-off. Each release is self-contained with its own assets and notes.
|
||||
Reference in New Issue
Block a user