Skip to content

Commit cada63b

Browse files
ciroautuoriclaude
andcommitted
fix(packaging): ship the dashboard, gate the deploy, repair the desktop entry
The wheel did not contain the UI. web/ lived at the repo root where setuptools' packages.find could not see it, so every non-editable install — the Arch package, `pip install openvidia`, any real deployment — got a proxy with no dashboard and no /health, because /health is registered inside attach_webui and that was skipped when web_dir did not exist. It only ever worked because install.sh uses `uv sync` / `pip install -e`, which run from the source tree. web/ now lives in the package with package-data, and CI builds the wheel, installs it into a clean venv, and asserts the bundle is there — an editable install structurally cannot catch this. deploy.yml ran on `push`, in parallel with CI, so a commit with failing tests was deployed anyway; `needs:` cannot cross workflow files, which is presumably why the gate was missing. It now triggers on workflow_run and checks the conclusion. `sudo systemctl restart openvidia || true` swallowed every failure, making the job green by construction: a missing unit is now reported and skipped, and a restart that fails, fails. The desktop entry was broken in both installation paths. install.sh patched Exec= with sed using | as the delimiter and left the value unquoted, so a path containing |, & or a space produced a mangled entry. The launcher it pointed at hardcoded `uv`, so a pip install got an entry that could not start what it had just installed — and `uv` is usually not on the PATH a desktop environment hands to a launcher anyway. install.sh now generates the launcher with fully resolved absolute paths and writes the entry outright. The PKGBUILD shipped the unpatched entry pointing at a script it never installed, so the menu item did nothing at all; it now relies on Exec=openvidia, which the wheel's entry point provides at /usr/bin. Also in the PKGBUILD: python-tomlkit, python-pyqt6 and python-pyqt6-webengine were missing from depends (installed clean, failed at runtime), python-build and python-installer were missing from makedepends so package() fell back to `pip install --root`, which downloads during the build, and pkgver() read pyproject.toml — returning the same string for every commit, so a -git package never offered an update. CI now runs through uv sync against uv.lock instead of a hand-written pip list that omitted uvicorn and tomlkit and passed only because nothing imported them at module scope. install.sh: curl -f, so a 404 no longer counts as a healthy proxy; and the closing banner no longer advertises `openvidia` as the command, which with uv lives in .venv/bin and is never on the user's PATH. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q6bBMoEC4Fjb2k2JTvUFWg
1 parent cbaad71 commit cada63b

20 files changed

Lines changed: 288 additions & 196 deletions

‎.github/workflows/ci.yml‎

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v4
1616

17-
- name: Install dependencies
18-
run: |
19-
python3 -m pip install --break-system-packages --quiet pytest pytest-asyncio httpx fastapi psutil
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v5
19+
20+
# `uv sync` resolves from pyproject + uv.lock, so CI runs the same
21+
# versions as a developer's machine. The previous job pip-installed a
22+
# hand-written list (pytest pytest-asyncio httpx fastapi psutil) which
23+
# omitted uvicorn and tomlkit and passed only because nothing imported
24+
# them at module scope — the first test to touch server_manager would
25+
# have failed for a reason that had nothing to do with the change.
26+
- name: Sync dependencies
27+
run: uv sync --frozen
2028

2129
- name: Run tests
22-
run: PYTHONPATH=. python3 -m pytest tests/ -v --tb=short
30+
run: uv run pytest tests/ -v --tb=short
2331

2432
lint:
2533
name: Ruff
@@ -28,11 +36,70 @@ jobs:
2836
steps:
2937
- uses: actions/checkout@v4
3038

31-
- name: Install ruff
32-
run: python3 -m pip install --break-system-packages --quiet ruff
39+
- name: Install uv
40+
uses: astral-sh/setup-uv@v5
41+
42+
- name: Sync dependencies
43+
run: uv sync --frozen
3344

3445
- name: Lint
35-
run: python3 -m ruff check openvidia/ tests/
46+
run: uv run ruff check openvidia/ tests/
3647

3748
- name: Format check
38-
run: python3 -m ruff format --check openvidia/ tests/
49+
run: uv run ruff format --check openvidia/ tests/
50+
51+
package:
52+
name: Wheel installs and serves
53+
runs-on: [self-hosted, soliso, openvidia]
54+
55+
steps:
56+
- uses: actions/checkout@v4
57+
58+
- name: Install uv
59+
uses: astral-sh/setup-uv@v5
60+
61+
# An editable install runs from the source tree, so it cannot tell you
62+
# whether the wheel is complete. It was not: the dashboard bundle lived
63+
# outside the package for two releases, and every real install shipped a
64+
# proxy with no UI and no /health. This job builds the artifact users
65+
# actually get, installs it elsewhere, and checks it.
66+
- name: Build the wheel
67+
run: uv build --wheel
68+
69+
- name: Install it into a clean environment
70+
run: |
71+
uv venv /tmp/ov-pkg-check
72+
uv pip install --python /tmp/ov-pkg-check/bin/python dist/*.whl
73+
74+
- name: The dashboard bundle shipped
75+
run: |
76+
/tmp/ov-pkg-check/bin/python - <<'PY'
77+
import pathlib, openvidia
78+
web = pathlib.Path(openvidia.__file__).parent / "web"
79+
assert web.is_dir(), f"{web} is not in the wheel"
80+
missing = [n for n in ("index.html", "main.js", "styles.css") if not (web / n).is_file()]
81+
assert not missing, f"missing from the wheel: {missing}"
82+
print("dashboard bundle OK:", web)
83+
PY
84+
85+
- name: The entry point runs
86+
run: /tmp/ov-pkg-check/bin/openvidia --help
87+
88+
- name: Cleanup
89+
if: always()
90+
run: rm -rf /tmp/ov-pkg-check
91+
92+
shell:
93+
name: Shellcheck
94+
runs-on: [self-hosted, soliso, openvidia]
95+
96+
steps:
97+
- uses: actions/checkout@v4
98+
99+
- name: Check the installer and launcher
100+
run: |
101+
if ! command -v shellcheck >/dev/null; then
102+
echo "shellcheck is not installed on this runner — skipping"
103+
exit 0
104+
fi
105+
shellcheck install.sh openvidia-launcher.sh

‎.github/workflows/deploy.yml‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,42 @@
11
name: Deploy to Soliso
22

3+
# Triggered by CI finishing, not by the push itself. As a `push` workflow this
4+
# ran in PARALLEL with CI, so a commit with failing tests was deployed anyway —
5+
# `needs:` cannot reference jobs in another workflow file, which is presumably
6+
# why the gate was missing. The `conclusion == success` check below is what
7+
# actually enforces it: workflow_run fires on failure too.
38
on:
4-
push:
5-
branches: [ main ]
9+
workflow_run:
10+
workflows: [CI]
11+
types: [completed]
12+
branches: [main]
613

714
jobs:
815
deploy:
916
name: Install & Restart
1017
runs-on: [self-hosted, soliso, openvidia]
18+
if: github.event.workflow_run.conclusion == 'success'
1119

1220
steps:
1321
- uses: actions/checkout@v4
22+
with:
23+
ref: ${{ github.event.workflow_run.head_sha }}
24+
25+
- name: Install uv
26+
uses: astral-sh/setup-uv@v5
1427

1528
- name: Install / update package
16-
run: python3 -m pip install --break-system-packages -e . --quiet
29+
run: uv pip install --system --break-system-packages .
1730

31+
# `|| true` used to swallow every failure here, so this job was green by
32+
# construction and told you nothing about whether the deploy worked. A
33+
# unit that is not installed on this host is reported and skipped; a
34+
# restart that is attempted and fails is a real failure.
1835
- name: Restart service
19-
run: sudo systemctl restart openvidia || true
36+
run: |
37+
if ! systemctl list-unit-files openvidia.service >/dev/null 2>&1; then
38+
echo "::notice::openvidia.service is not installed here — nothing to restart"
39+
exit 0
40+
fi
41+
sudo systemctl restart openvidia.service
42+
sudo systemctl is-active --quiet openvidia.service

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
**Multi-key proxy for NVIDIA NIM with a native desktop dashboard.**
1212

1313
<p align="center">
14-
<img src="web/assets/dashboard.gif" alt="OpenVidia dashboard — pooled keys, ★ Starred model shortlist, live activity log" width="400">
14+
<img src="openvidia/web/assets/dashboard.gif" alt="OpenVidia dashboard — pooled keys, ★ Starred model shortlist, live activity log" width="400">
1515
</p>
1616

1717
Pool multiple free-tier API keys behind one endpoint. Automatic rotation, per-key cooldown, sliding-window RPM limiting, and a compact desktop app — no browser needed.
@@ -410,7 +410,7 @@ pywebview auto-detects the best available backend.
410410
# .desktop file (auto-installed by install.sh)
411411
cp openvidia.desktop ~/.local/share/applications/
412412
# Icon
413-
cp web/assets/logo.png ~/.local/share/icons/hicolor/256x256/apps/openvidia.png
413+
cp openvidia/web/assets/logo.png ~/.local/share/icons/hicolor/256x256/apps/openvidia.png
414414
update-desktop-database ~/.local/share/applications/
415415
```
416416

‎install.sh‎

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@ echo "▶ Installing Python dependencies..."
1515
# local venv, so the system python3 would start with nothing importable.
1616
if command -v uv >/dev/null 2>&1; then
1717
uv sync --quiet
18-
RUN=(uv run openvidia)
18+
# Absolute paths, resolved now. A desktop entry is launched by the DE with
19+
# a minimal PATH that usually excludes ~/.local/bin, where uv installs
20+
# itself — so a launcher that says `uv` finds nothing, exits, and the user
21+
# sees an icon that does nothing at all.
22+
UV_BIN="$(command -v uv)"
23+
RUN=("$UV_BIN" run openvidia)
1924
echo " ✓ Dependencies installed (uv)"
2025
elif command -v pip >/dev/null 2>&1; then
2126
pip install -e . --quiet
22-
RUN=(python3 -m openvidia)
27+
PY_BIN="$(command -v python3)"
28+
RUN=("$PY_BIN" -m openvidia)
2329
echo " ✓ Dependencies installed (pip)"
2430
else
2531
echo " ✗ You need to install uv (recommended) or pip before continuing"
@@ -28,21 +34,52 @@ else
2834
fi
2935
echo ""
3036

31-
# ── 2. Auto-configure detected CLIs ─────────────
32-
echo "▶ Auto-configuring CLIs (opencode, Codex, Claude Code, Grok)..."
33-
"${RUN[@]}" setup 2>/dev/null || true
37+
# ── 2. Configure opencode ───────────────────────
38+
# Only opencode. Every other CLI runs on demand with `openvidia run <cli>`,
39+
# which sets its variables for one process and writes nothing.
40+
echo "▶ Configuring opencode..."
41+
"${RUN[@]}" setup || echo " ⚠ setup reported a problem — the proxy will still start"
3442
echo ""
3543

3644
# ── 3. Desktop integration (Linux only) ────────
3745
if [[ "$(uname -s)" == "Linux" ]]; then
3846
echo "▶ Installing desktop entry..."
3947
DESKTOP_DIR="$HOME/.local/share/applications"
4048
ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
41-
mkdir -p "$DESKTOP_DIR" "$ICON_DIR"
42-
cp openvidia.desktop "$DESKTOP_DIR/" 2>/dev/null || true
43-
cp web/assets/logo.png "$ICON_DIR/openvidia.png" 2>/dev/null || true
49+
BIN_DIR="$HOME/.local/bin"
50+
mkdir -p "$DESKTOP_DIR" "$ICON_DIR" "$BIN_DIR"
51+
52+
# Generate the launcher rather than patching the checked-in one. That file
53+
# hardcoded `uv`, so a pip install produced a desktop entry pointing at a
54+
# launcher that could not start the app it had just installed.
55+
LAUNCHER="$BIN_DIR/openvidia-desktop"
56+
{
57+
echo '#!/usr/bin/env bash'
58+
echo '# Generated by install.sh — every path is absolute so the entry'
59+
echo '# does not depend on the desktop environment PATH.'
60+
echo 'set -e'
61+
printf 'cd %q\n' "$DIR"
62+
printf 'exec'
63+
printf ' %q' "${RUN[@]}"
64+
printf ' "$@"\n'
65+
} > "$LAUNCHER"
66+
chmod 755 "$LAUNCHER"
67+
68+
# Write the entry outright instead of sed-patching Exec=. The old
69+
# substitution used | as its delimiter and left the value unquoted, so an
70+
# install path containing |, & or a space produced a broken or mangled
71+
# entry — "~/My Projects" was enough to break it.
72+
grep -v '^Exec=' openvidia.desktop > "$DESKTOP_DIR/openvidia.desktop"
73+
printf 'Exec="%s"\n' "$LAUNCHER" >> "$DESKTOP_DIR/openvidia.desktop"
74+
chmod 644 "$DESKTOP_DIR/openvidia.desktop"
75+
76+
cp openvidia/web/assets/logo.png "$ICON_DIR/openvidia.png" 2>/dev/null || true
4477
update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
45-
echo " ✓ Desktop entry installed"
78+
if command -v desktop-file-validate >/dev/null 2>&1; then
79+
desktop-file-validate "$DESKTOP_DIR/openvidia.desktop" || \
80+
echo " ⚠ desktop entry failed validation (see above)"
81+
fi
82+
echo " ✓ Desktop entry installed → $LAUNCHER"
4683
echo ""
4784
fi
4885

@@ -61,7 +98,9 @@ LAUNCHER=$!
6198
DEADLINE=$((SECONDS + 60))
6299
READY=""
63100
while [ $SECONDS -lt $DEADLINE ]; do
64-
if curl -s --max-time 3 http://localhost:1919/health >/dev/null 2>&1; then
101+
# -f matters: without it curl exits 0 on a 404, so READY could be set by a
102+
# server that was up but serving nothing.
103+
if curl -fsS --max-time 3 http://localhost:1919/health >/dev/null 2>&1; then
65104
READY=1
66105
break
67106
fi
@@ -72,7 +111,7 @@ while [ $SECONDS -lt $DEADLINE ]; do
72111
done
73112

74113
if [ -n "$READY" ]; then
75-
KEYS=$(curl -s --max-time 5 http://localhost:1919/health | python3 -c "import sys,json; print(json.load(sys.stdin).get('keys','?'))" 2>/dev/null || echo "?")
114+
KEYS=$(curl -fsS --max-time 5 http://localhost:1919/health | python3 -c "import sys,json; print(json.load(sys.stdin).get('keys','?'))" 2>/dev/null || echo "?")
76115
echo " ✓ Proxy active — $KEYS keys on http://localhost:1919"
77116
echo " ✓ Desktop app opened"
78117
else
@@ -82,14 +121,17 @@ else
82121
fi
83122
echo ""
84123

85-
echo "╔════════════════════════════════════════════╗"
86-
echo "║ Installation complete! ║"
87-
echo "╠════════════════════════════════════════════╣"
88-
echo "║ Command: openvidia ║"
89-
echo "║ Proxy: http://localhost:1919/v1 ║"
90-
echo "║ Dashboard: http://localhost:1919 ║"
91-
echo "╠════════════════════════════════════════════╣"
92-
echo "║ opencode → /model openvidia ║"
93-
echo "║ codex → codex --model openvidia ║"
94-
echo "║ grok → grok --model openvidia ║"
95-
echo "╚════════════════════════════════════════════╝"
124+
# The old banner said "Command: openvidia". With uv the entry point lives in
125+
# .venv/bin and is never on the user's PATH, so that line named a command that
126+
# did not exist. Print what actually works on this install.
127+
echo "Installation complete."
128+
echo ""
129+
echo " Start: ${RUN[*]}"
130+
echo " Proxy: http://localhost:1919/v1"
131+
echo " Dashboard: http://localhost:1919"
132+
echo ""
133+
echo " opencode is configured — select it with /model openvidia"
134+
echo " Other CLIs run on demand, no config files touched:"
135+
echo " ${RUN[*]} run codex"
136+
echo " ${RUN[*]} run claude"
137+
echo " ${RUN[*]} run grok"

‎openvidia.desktop‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#!/usr/bin/env xdg-open
21
[Desktop Entry]
32
Type=Application
43
Name=OpenVidia
@@ -8,6 +7,6 @@ Comment[it]=Proxy multi-chiave per NVIDIA NIM con dashboard desktop
87
Exec=openvidia
98
Icon=openvidia
109
Terminal=false
11-
Categories=Development;Network;Utility;
10+
Categories=Development;
1211
Keywords=nvidia;nim;proxy;api;llm;development;
1312
StartupNotify=true

‎openvidia/__main__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ async def main_async():
392392
def log(msg: str):
393393
print(msg, flush=True)
394394

395-
web_dir = Path(__file__).resolve().parent.parent / "web"
395+
web_dir = Path(__file__).resolve().parent / "web"
396396
srv = await start(
397397
PORT,
398398
keys,
@@ -577,7 +577,7 @@ def open_desk(port: int) -> None:
577577
return
578578

579579
url = f"http://localhost:{port}"
580-
assets = Path(__file__).resolve().parent.parent / "web" / "assets"
580+
assets = Path(__file__).resolve().parent / "web" / "assets"
581581
icon_path = str(assets / "logo.png")
582582
print(f"● Desktop window → {url}", flush=True)
583583

0 commit comments

Comments
 (0)