feat(package): ship the web UI in the wheel and serve it from the API

pip install docsgpt now brings the web UI with it: `docsgpt api` serves the
API and the UI on one port.

- scripts/build_frontend.sh builds the frontend into docsgpt/static
  (gitignored) the way the frontend image does: .env.development as the
  production baseline, and index.html loading /config.js ahead of the
  bundle. hatch admits the directory into the wheel and the sdist through
  `artifacts`; the package workflows run the script before `uv build` and
  fail if the wheel lacks the UI. The backend image keeps ignoring it.
- docsgpt/ui.py serves the build in front of Flask: files as they are,
  hashed assets immutable, Flask's own path prefixes (taken from its URL map,
  so new blueprints need no registration) passed through, every other GET
  rendered as index.html for the client-side router. /config.js is generated
  per request with VITE_API_HOST and VITE_BASE_URL set to the page's origin,
  VITE_* environment variables winning. SERVE_UI=false leaves the API alone.
- docsgpt api configures gunicorn in code (gunicorn.app.base.Application)
  instead of rewriting sys.argv, so the SIGUSR2 re-exec that gunicorn uses
  for zero-downtime upgrades runs the docsgpt console script again and
  works; verified with a live handover.
- Docs: the pip page says the UI is included, that DOCSGPT_HOME and
  DOCSGPT_ENV_FILE are process environment variables rather than .env
  entries, and the settings page describes SERVE_UI.
This commit is contained in:
Alex committed 2026-09-09 11:09:09 +01:00
1 parent 0c348917b1
commit 43f493b926
15 files changed
+419 -39

No files matched your search

+31
View File
@@ -12,6 +12,8 @@ on:
- 'docsgpt/**'
- 'README.md'
- 'LICENSE'
- 'frontend/**'
- 'scripts/build_frontend.sh'
- '.github/workflows/package-build.yml'
push:
branches: [main]
@@ -21,6 +23,8 @@ on:
- 'docsgpt/**'
- 'README.md'
- 'LICENSE'
- 'frontend/**'
- 'scripts/build_frontend.sh'
- '.github/workflows/package-build.yml'
workflow_dispatch:
@@ -37,6 +41,17 @@ jobs:
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Build the web UI into the package
env:
CI: "true"
run: bash scripts/build_frontend.sh
- name: Build the sdist and wheel
run: uv build
@@ -73,6 +88,21 @@ jobs:
print(f"wheel ok: {len(names)} files")
PY
- name: The wheel carries the web UI and no generated runtime config
run: |
python3 - <<'PY'
import glob
import zipfile
wheel = zipfile.ZipFile(glob.glob("dist/*.whl")[0])
names = set(wheel.namelist())
assert "docsgpt/static/index.html" in names, "the UI build is missing from the wheel"
assert any(n.startswith("docsgpt/static/assets/") for n in names), "no bundle under docsgpt/static/assets/"
assert "docsgpt/static/config.js" not in names, "config.js is generated by the API at runtime"
assert b'src="/config.js"' in wheel.read("docsgpt/static/index.html"), "index.html must load /config.js"
print("ui ok:", sum(n.startswith("docsgpt/static/") for n in names), "files")
PY
- name: Install the wheel into a clean environment and run the command
run: |
uv venv "$RUNNER_TEMP/docsgpt-venv"
@@ -81,6 +111,7 @@ jobs:
"$RUNNER_TEMP/docsgpt-venv/bin/docsgpt" --help
"$RUNNER_TEMP/docsgpt-venv/bin/docsgpt" api --help
"$RUNNER_TEMP/docsgpt-venv/bin/python" -c "from docsgpt.core import paths; print('home:', paths.home_dir())"
"$RUNNER_TEMP/docsgpt-venv/bin/python" -c "from docsgpt.core.paths import package_dir; assert (package_dir() / 'static' / 'index.html').is_file(); print('ui installed')"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
+26
View File
@@ -59,9 +59,35 @@ jobs:
echo "::error::unknown target '$TARGET': expected pypi or testpypi"
exit 1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
# A publishing workflow must not restore a cache another job could have poisoned.
package-manager-cache: false
- name: Build the web UI into the package
env:
CI: "true"
run: bash scripts/build_frontend.sh
- name: Build the sdist and wheel
run: uv build
- name: The wheel carries the web UI and no generated runtime config
run: |
python3 - <<'PY'
import glob
import zipfile
wheel = zipfile.ZipFile(glob.glob("dist/*.whl")[0])
names = set(wheel.namelist())
assert "docsgpt/static/index.html" in names, "the UI build is missing from the wheel"
assert any(n.startswith("docsgpt/static/assets/") for n in names), "no bundle under docsgpt/static/assets/"
assert "docsgpt/static/config.js" not in names, "config.js is generated by the API at runtime"
assert b'src="/config.js"' in wheel.read("docsgpt/static/index.html"), "index.html must load /config.js"
print("ui ok:", sum(n.startswith("docsgpt/static/") for n in names), "files")
PY
- name: The archives carry the released version
if: env.RELEASE_VERSION != ''
run: |