Skip to content

feat(update): install + self-update from prebuilt wheel asset (0.60.0) - #408

Merged
padak merged 4 commits into
mainfrom
fix/353-prebuilt-wheel
Jun 11, 2026
Merged

feat(update): install + self-update from prebuilt wheel asset (0.60.0)#408
padak merged 4 commits into
mainfrom
fix/353-prebuilt-wheel

Conversation

@padak

@padak padak commented Jun 11, 2026

Copy link
Copy Markdown
Member

Closes #353.

Problem

uv tool install git+https://github.com/keboola/cli builds the package from source on every install. The wheel build recompiles the bundled React SPA via npm ci + vite build (the uv cache never covers the npm step), which takes 2-4 minutes on WSL2 and trips the hardcoded 120s auto-update timeout -- so kbagent doctor / kbagent update print a false "Auto-update failed".

Measured locally (fast Mac): a CLI-only wheel builds in 0.49s, the full UI build in 7.39s -- ~93% of build time is the npm step. On WSL2 that 7s balloons to minutes. Raising the timeout to 300s is not enough (the reporter measured 4m09s warm-cache).

Vrstva 1 -- eliminate the user-side build

  • release.yml builds the universal py3-none-any wheel once on Linux CI and uploads it as a Release asset (release: published; workflow_dispatch backfills older tags like v0.59.0). CI already builds + verifies the wheel via the build-windows job.
  • install.sh bootstrap (curl … | sh) resolves the latest release and installs the prebuilt wheel -- no source build, no gh CLI, just curl + uv. Mirrors the pattern the install guide already uses for uv / Claude Code. [server] extras by default; KBAGENT_NO_SERVER=1 opts out.
  • resolve_kbagent_wheel_url HEAD-probes the asset; build_kbagent_upgrade_command installs it via a PEP 508 direct reference (keboola-agent-cli[server] @ <wheel-url>), falling back to git+ when the asset is absent (older releases). Both the startup hook and kbagent update use it.

Vrstva 2 -- update UX

  • UPDATE_TIMEOUT_SECONDS (300) + KBAGENT_UPDATE_TIMEOUT env override replace the two hardcoded 120s timeouts (startup hook + kbagent update).
  • UpdateOutcome enum distinguishes a build TIMEOUT (slow git+ build, finishes on the next run) from a genuine FAILED, so the startup hook stops printing a false "Auto-update failed".
  • _should_skip_all scans all argv, so kbagent --json update skips the startup hook -- fixing the reporter's Bug 3 (startup banner disagreeing with the explicit command's JSON output).

Net effect

Install and update drop from minutes-of-build to a seconds-long download, on WSL and everywhere else. The timeout stops mattering on the happy path.

Tests

  • resolve_kbagent_wheel_url: 200 → URL, 404 / HTTP error → None, empty version → None
  • wheel install path with / without [server], pip fallback, precedence over prerelease, no-tools → None
  • _perform_update: wheel asset present vs git+ fallback
  • get_update_timeout: default / env override / invalid value
  • UpdateOutcome.TIMEOUT is not a failure (no re-exec, no "failed" banner)
  • Bug 3: kbagent --json update skips the startup hook
  • conftest defaults the HEAD probe to 404 so no test reaches the network

Full gate green -- make check: 3934 passed, 8 skipped.

Follow-ups (not in this PR)

  • The reporter's Bug 2 ("kbagent update reports already-up-to-date when it isn't") looked like a consequence of the timeout race; the wheel fast path + TIMEOUT handling should remove the trigger. Worth confirming live before closing it out separately rather than changing _is_up_to_date blind.
  • Update the Windows/WSL install guide to use install.sh (the verified client guide currently documents the git+ workaround).

Open in Devin Review

Closes #353. git-installing kbagent rebuilds the bundled React SPA via
npm on every install (the uv cache misses it), taking 2-4 min on WSL2
and tripping the hardcoded 120s auto-update timeout.

Vrstva 1 -- eliminate the user-side build:
- release.yml publishes the universal py3-none-any wheel as a Release
  asset on every (pre)release; workflow_dispatch backfills older tags.
- install.sh bootstrap (curl|sh) installs the prebuilt wheel: no build,
  no gh CLI; [server] extras by default (KBAGENT_NO_SERVER=1 opts out).
- resolve_kbagent_wheel_url HEAD-probes the asset; build_kbagent_upgrade_command
  installs it via a PEP 508 direct ref, falling back to git+ when absent.
  Both the startup hook and `kbagent update` use it.

Vrstva 2 -- update UX:
- UPDATE_TIMEOUT_SECONDS (300) + KBAGENT_UPDATE_TIMEOUT env override
  replace the two hardcoded 120s timeouts.
- UpdateOutcome enum distinguishes TIMEOUT (slow build, retried next run)
  from FAILED, so the startup hook stops printing a false "Auto-update
  failed" banner.
- _should_skip_all scans all argv so `kbagent --json update` skips the
  startup hook (Bug 3: startup banner vs explicit-command JSON output).

Tests: wheel-URL resolver, wheel install path + git+ fallback, timeout
resolver, TIMEOUT outcome, Bug 3. conftest defaults the HEAD probe to
404 so no test reaches the network.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread src/keboola_agent_cli/auto_update.py Outdated
The argv scan in _should_skip_all matched "update"/"version" ANYWHERE in
argv, so `kbagent config update` / `flow update` / `agent update` wrongly
skipped the startup auto-update check (Devin Review finding on PR #408).

Replace it with _top_level_subcommand_is_versioning, which walks past
global flags (and --config-dir's value) to the first positional token --
the real subcommand -- so `kbagent --json update` still skips (Bug 3)
while nested *-update subcommands do not. Parametrized tests cover both.

@padak padak left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of #408 — feat(update): install + self-update from prebuilt wheel asset (0.60.0)

Generated by kbagent-pr-reviewer subagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed via make check, not duplicated here.

Summary

This PR eliminates the WSL2 build-timeout problem (issue #353) by publishing a prebuilt py3-none-any wheel as a GitHub Release asset and wiring both the startup auto-update hook and kbagent update to prefer that wheel over the git+ source build. Supporting infrastructure includes a install.sh bootstrap script, the release.yml CI workflow that builds and uploads the wheel, an UpdateOutcome enum that distinguishes slow builds from genuine failures, and the KBAGENT_UPDATE_TIMEOUT env-var escape hatch. The implementation is clean, test coverage is solid, and make check passes (3947 passed, 8 skipped). Two non-blocking gaps were found: (1) kbagent version's upgrade_command field still shows the git+ command even when a wheel asset is available, because the check_for_updates path does not call resolve_kbagent_wheel_url; (2) the PR description says unit tests for get_update_timeout (default / env override / invalid value) were added, but they are absent from the diff. No blocking findings. Verdict: COMMENT.

Verdict

  • Verdict: COMMENT
  • Blocking findings: 0
  • Non-blocking findings: 3
  • Nits: 2

Blocking findings

(none)

Non-blocking findings

[NB-1] src/keboola_agent_cli/services/version_service.py:743kbagent version upgrade_command field still shows git+ when a wheel asset is available

_check_for_updates (called by kbagent version) builds the upgrade_command string shown in JSON output via build_kbagent_upgrade_command(prerelease=..., target_version=...) without resolving wheel_url first (line 743). The parallel _update_kbagent path (called by kbagent update) does call resolve_kbagent_wheel_url(kbagent_latest) and passes the result in. So kbagent --json version will always advertise a git+ install command even when the prebuilt wheel is available, which means a user or AI agent copy-pasting upgrade_command from the JSON output will get the slow source build. Fix: add the same wheel_url = resolve_kbagent_wheel_url(kbagent_latest) call before line 743 and pass it in.

[NB-2] tests/test_version_service.py and tests/test_auto_update.pyget_update_timeout unit tests missing despite PR description claiming otherwise

The PR description explicitly lists "get_update_timeout: default / env override / invalid value" as added tests under "Tests". A grep over both test files confirms no TestGetUpdateTimeout class or test.*get_update_timeout functions appear in the diff. The function has meaningful env-var parsing logic with integer validation and fallback: KBAGENT_UPDATE_TIMEOUT with non-numeric or non-positive values must fall back silently to the constant. Without explicit tests the env-var fallback paths (invalid string, negative value, zero) are untested. Fix: add three tests: test_default_returns_300, test_env_override_returns_custom, test_invalid_env_falls_back_to_default.

[NB-3] src/keboola_agent_cli/commands/context.py and CLAUDE.mdKBAGENT_UPDATE_TIMEOUT env var not documented in AGENT_CONTEXT or the env-vars table

context.py has a documented table of environment variables (KBAGENT_INCLUDE_PRERELEASE, KBAGENT_SKIP_UPDATE, etc.) at the bottom of the AGENT_CONTEXT string. KBAGENT_UPDATE_TIMEOUT is not listed. Per the plugin synchronization map rule, AGENT_CONTEXT is the primary reference an AI agent consults at session start; an undocumented env var means the agent can't tell users about it when they report slow WSL updates. The CLAUDE.md ## All CLI Commands section also has the kbagent update [--beta] entry with an env-var comment block that does not mention KBAGENT_UPDATE_TIMEOUT. Fix: add a one-line entry to the env-vars section in context.py (KBAGENT_UPDATE_TIMEOUT: integer seconds, overrides the 300s self-update subprocess timeout; raise for slow WSL git+ builds) and mirror it in the CLAUDE.md update command block.

Nits

  • [NIT-1] install.sh:200uv tool install --force "${spec} @ ${wheel_url}" passes the entire PEP 508 spec as a single shell-quoted string. When spec or wheel_url contains spaces (unlikely but possible in user-provided envs), the word-splitting may silently fail. The pattern already works on current inputs, but a -- separator or double-quoting each component individually would be more defensive.

  • [NIT-2] .github/workflows/release.yml:73tag="${{ github.event.release.tag_name || inputs.tag }}" inlines the GitHub expression directly into the shell command. For workflow_dispatch the inputs.tag value is maintainer-supplied and the checked-out ref already constrains what the tag can be, so this is not a practical injection path, but the GitHub security hardening guide recommends routing user-controlled inputs through a step env: variable (env: TAG: ... then "$TAG") to make the trust boundary explicit. Low severity; flag for awareness.

Verification log

  • gh pr view 408 --json title,body,files,additions,deletions,baseRefName,headRefName,labels,state → 14 files, +650/-38, state=OPEN, fix/353-prebuilt-wheelmain. Conventional feat(update): prefix for infra-only change is acceptable (adds user-visible behavior: prebuilt wheel, new install.sh, UpdateOutcome.TIMEOUT message change). ✓
  • git rev-parse --abbrev-ref HEADfix/353-prebuilt-wheel ✓ matches PR branch
  • gh auth status → authenticated as padak to github.com
  • Read CONTRIBUTING.md, CLAUDE.md, plugins/kbagent/agents/keboola-expert.md
  • grep typer src/keboola_agent_cli/services/version_service.py → empty ✓ (no layer violation)
  • grep httpx src/keboola_agent_cli/services/version_service.pyhttpx.head + httpx.get (bare function calls, no context manager) -- this is a pre-existing pattern in the file; the new resolve_kbagent_wheel_url matches the existing style. Not a new violation.
  • Magic numbers check (grep '\b(timeout|retries|interval)\s*=\s*[0-9]+' on + lines) → empty ✓ (120 removed; 300 lives in constants.UPDATE_TIMEOUT_SECONDS)
  • Raw error_code strings, bare except, print() in src/ → empty ✓
  • Token/secret exposure scan → empty ✓ (only GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} in workflow, which is the standard pattern)
  • grep 'update\|version' permissions.py"version": "read" and "update": "admin" present ✓ (no new command needing registration)
  • Plugin synchronization map check: this PR adds NO new CLI commands; it modifies internal update infrastructure. The release-version checklist items are satisfied: pyproject.toml bumped to 0.60.0 ✓, changelog.py has 0.60.0 entry ✓, plugin.json and marketplace.json bumped ✓. The AGENT_CONTEXT / CLAUDE.md gaps for KBAGENT_UPDATE_TIMEOUT are flagged as NB-3 above.
  • grep 'KBAGENT_UPDATE_TIMEOUT' tests/test_version_service.py tests/test_auto_update.py → no output; get_update_timeout tests absent from diff → NB-2 above.
  • kbagent version upgrade_command gap in _check_for_updates at line 743 → NB-1 above.
  • make check3947 passed, 8 skipped, 124 deselected, 14 warnings in 129.78s exit 0 ✓
  • Behavior verification: could not run kbagent update against the live wheel path because v0.60.0 is not yet released and no asset exists at the expected URL. The conftest _no_wheel_asset_probe fixture correctly defaults the HEAD probe to 404 to prevent tests from hitting the network. The test_installs_wheel_when_asset_present in TestPerformUpdateWheel covers the wheel path at the unit level.
  • install.sh sed pattern: echo 'https://github.com/.../releases/tag/v0.59.0' | sed -n 's#.*/releases/tag/v\{0,1\}##p'0.59.0 ✓ (macOS BSD sed interprets \{0,1\} correctly in BRE mode)
  • GitHub Actions versions in release.yml vs ci.yml: both use checkout@v5, setup-uv@v7, setup-node@v6, setup-python@v6 ✓ (consistent)
  • scripts/check_wheel_ui.py referenced in release.yml → file exists ✓

Open questions for the author

  • [NB-1] was the kbagent version upgrade_command omission intentional? It is a small overhead (one extra HEAD probe during kbagent version) but keeping the two paths consistent would make the JSON output more useful for programmatic consumers who read upgrade_command to decide what to run.

…ests)

kbagent-pr-reviewer findings on PR #408 (all non-blocking):
- NB-1: kbagent version's JSON `upgrade_command` now resolves the wheel
  asset too (it was advertising git+ even when the prebuilt wheel exists),
  matching the `kbagent update` path.
- NB-2: add TestGetUpdateTimeout (default / env override / invalid-value
  fallback) -- the PR description claimed these but the diff lacked them.
- NB-3: document KBAGENT_UPDATE_TIMEOUT in AGENT_CONTEXT (context.py) and
  the CLAUDE.md update block (plugin-sync silent-drift surface).
- NIT-2: route the release tag through a job env var in release.yml
  instead of inlining the GitHub expression into shell run steps.
@padak

padak commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review -- addressed all actionable findings in f001ff5:

  • NB-1: kbagent version's JSON upgrade_command now resolves the wheel asset too (one extra HEAD probe), matching the kbagent update path -- programmatic consumers reading upgrade_command get the fast path.
  • NB-2: added TestGetUpdateTimeout (default 300 / env override / invalid-value fallback for empty, whitespace, non-numeric, negative, zero).
  • NB-3: documented KBAGENT_UPDATE_TIMEOUT in AGENT_CONTEXT (context.py env-vars table) and the CLAUDE.md update block.
  • NIT-2: routed the release tag through a job env: TAG var in release.yml; shell steps now use "$TAG" instead of the inlined expression.

NIT-1 left as-is by design: "${spec} @ ${wheel_url}" is intentionally a single double-quoted argument -- one PEP 508 direct-reference spec (keboola-agent-cli[server] @ <url>) whose literal space around @ is part of the spec. The double-quoting is exactly what prevents word-splitting; spec and wheel_url are internally constructed, not user input.

make check green (3955 passed, 8 skipped).

…review)

Devin Review on PR #408: the _no_wheel_asset_probe autouse fixture patched
httpx.head on the shared module object, so any future httpx.head caller would
silently get a 404. Now it returns 404 only for the kbagent release-asset URL
and raises loudly on any other URL, so an accidental reliance fails visibly
instead of getting a surprise 404.
@padak

padak commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Re: the additional findings on the Devin web view (not posted as GitHub inline comments):

  • conftest httpx global patch — fixed in eccacd2. The _no_wheel_asset_probe fixture now returns 404 only for the kbagent release-asset URL and raises loudly on any other httpx.head call, so a future accidental caller fails visibly instead of getting a surprise 404.
  • auto_update.py:228 (config/flow/agent update) — already fixed in bfb40cf; verified live (config update/flow update/agent update -> skip=False, --json update -> skip=True). Inline thread resolved.
  • install.sh PEP 508 vs Python --with for git+ — acknowledged; both forms work with uv. install.sh is internally consistent (PEP 508 for both wheel and git+ paths); the Python path keeps the battle-tested --with form from Windows installation completely broken — two independent bugs in build hook #320. Left as-is by design.
  • HEAD probe latency / wheel --force — acknowledged as intentional (your own analysis agrees: probe only fires on stale-cache + newer-version, GitHub CDN <200ms; --force is correct for an exact-version wheel). No change.

make check green (3955 passed, 8 skipped).

@padak
padak merged commit 759131a into main Jun 11, 2026
4 checks passed
@padak
padak deleted the fix/353-prebuilt-wheel branch June 11, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-update falsely reports failure on WSL Ubuntu - cold package prepare (~2m 27s) exceeds hardcoded 120s timeout

1 participant