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>
6.6 KiB
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:
- Pre-flight checks (on main branch, tag doesn't exist)
- Version bump (reads/writes component's VERSION file only)
- Build artifacts (Go binaries or Docker images)
- Commit version bump, create prefixed tag, push
- Create Gitea release with assets
- (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:
- First call to any
Clientmethod (Get/Post/Delete/Put) - Before the actual request, call
GET /api/v1/status - Parse
versionfrom response - Compare against
MinEngineVersionusing semver major.minor.patch comparison - If engine version < min: print error to stderr,
os.Exit(1) - If check passes: set
versionChecked = true, proceed with original request - 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/statuscall 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.xtags won't match the newclient-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.