From 77abb66b238d9de930c98f68ceffe1708e6efca9 Mon Sep 17 00:00:00 2001 From: Viktor Buzin Date: Fri, 9 Oct 2026 19:14:19 +0300 Subject: [PATCH 01/19] Release assets: Pages, GitHub Release, Docker Hub cards, README refresh Publishes the three things that were missing around the 10.5.2 tag. GitHub Release (release.yml). A tag existed but no release page, so nothing announced the release. The body is header.md + docs/WhatsNew-v.md + GitHub's auto-generated PR notes. It refuses to publish when the tag and disagree: publish-nuget packs from the csproj, not the tag, so a mismatch ships a nupkg whose metadata lies about its own version. That is exactly the silent-skip failure 10.5.2 hit. GitHub Pages (pages.yml + site/index.html). Hand-written static landing: header, versions, quick links to the key docs. No docs toolchain -- a landing page does not justify adding Python to a .NET repo. The version is injected from the same csproj that drives `dotnet pack`, so the page cannot advertise an unpublished version, and the build fails loudly on a leftover placeholder. Docker Hub cards (dockerhub-metadata.yml + dockerhub/*.md). Both repos showed empty cards because `docker push` writes no metadata. The overviews now live in the repo and are PATCHed onto both repositories on every v* tag using the existing DOCKERHUB_TOKEN. Deliberately a separate workflow: a metadata failure must never turn a successful image publish into a red run. Description and categories go in separate calls so a taxonomy rejection costs the categories, not the overview. README (EN + RU). Adds Docker Hub, pulls, Release and Pages badges plus a second CI gate. Installation was a single `dotnet add package Delibera.Core` line while three packages and two images are published; it is now a three-way choice, with the localhost-binding warning placed where users will actually hit it. Also fixes the runtime-smoke step in publish-docker.yml, left over from the earlier session: both images declare ENTRYPOINT, so `docker run IMAGE dotnet --version` appended to it and booted the web host instead of printing a version, hanging the job for 20+ minutes. It also asked `dotnet --version`, which needs an SDK that a runtime image does not carry -- the fix would have failed a perfectly good image. Now uses --entrypoint with --list-runtimes, asserts the AspNetCore runtime is present, checks the published payload layout, and is bounded by `timeout` plus a job-level timeout-minutes that did not exist before. Verified locally against the real published 10.5.2 images: `--list-runtimes` prints Microsoft.AspNetCore.App 10.0.12 (exit 0), `node --version` prints v22.23.3 (exit 0), and /app/Delibera.Server.dll and /app/.output/server/index.mjs are both present. Site and release-body assembly were simulated locally: zero unsubstituted placeholders in either output. --- .github/release-notes/header.md | 24 +++ .github/workflows/dockerhub-metadata.yml | 107 ++++++++++++ .github/workflows/pages.yml | 72 ++++++++ .github/workflows/publish-docker.yml | 42 ++++- .github/workflows/release.yml | 118 +++++++++++++ .gitignore | 7 + README-RU.md | 51 +++++- README.md | 48 +++++- dockerhub/delibera-server.md | 106 ++++++++++++ dockerhub/delibera-webui.md | 85 +++++++++ site/index.html | 211 +++++++++++++++++++++++ 11 files changed, 862 insertions(+), 9 deletions(-) create mode 100644 .github/release-notes/header.md create mode 100644 .github/workflows/dockerhub-metadata.yml create mode 100644 .github/workflows/pages.yml create mode 100644 .github/workflows/release.yml create mode 100644 dockerhub/delibera-server.md create mode 100644 dockerhub/delibera-webui.md create mode 100644 site/index.html diff --git a/.github/release-notes/header.md b/.github/release-notes/header.md new file mode 100644 index 0000000..56a1fee --- /dev/null +++ b/.github/release-notes/header.md @@ -0,0 +1,24 @@ +# Delibera {{VERSION}} + +> **⚠️ Before you expose this anywhere:** the API is unauthenticated and unthrottled, and a debate +> spends real LLM credits. Published ports bind to `127.0.0.1`. Put an authenticating reverse proxy +> in front before putting this stack on a network you do not control. + +## Get it + +| | | +| --- | --- | +| NuGet | [`Delibera.Core`](https://www.nuget.org/packages/Delibera.Core) · [`Delibera.Server`](https://www.nuget.org/packages/Delibera.Server) · [`Delibera.Redis`](https://www.nuget.org/packages/Delibera.Redis) | +| Docker | [`techbuzzz/delibera-server`](https://hub.docker.com/r/techbuzzz/delibera-server) · [`techbuzzz/delibera-webui`](https://hub.docker.com/r/techbuzzz/delibera-webui) — `linux/amd64` + `linux/arm64` | +| Docs | [techbuzzz.github.io/Delibera](https://techbuzzz.github.io/Delibera/) | + +```bash +export DELIBERA_VERSION={{VERSION}} +docker compose -f docker-compose.hub.yml up -d +``` + +```bash +dotnet add package Delibera.Core # {{VERSION}} +``` + +--- \ No newline at end of file diff --git a/.github/workflows/dockerhub-metadata.yml b/.github/workflows/dockerhub-metadata.yml new file mode 100644 index 0000000..c36ccde --- /dev/null +++ b/.github/workflows/dockerhub-metadata.yml @@ -0,0 +1,107 @@ +name: dockerhub-metadata + +# Syncs the Docker Hub repository cards (short description, long overview, categories) from the +# markdown files in dockerhub/. +# +# Deliberately a SEPARATE workflow from publish-docker.yml: the images are already pushed by the +# time this runs, so a rejected description or an expired token must never turn a successful image +# publish into a red run. It re-runs on every v* tag and can be re-triggered by hand. +# +# The cards are empty by default when a repository is created through the API — `docker push` does +# not write metadata. That is why the text lives in the repository rather than only in someone's +# browser session. + +on: + workflow_dispatch: + push: + tags: [ 'v*' ] + +permissions: + contents: read + +jobs: + sync: + name: Sync Docker Hub metadata + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Sign in to the Docker Hub API + id: auth + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + run: | + set -euo pipefail + if [ -z "${DOCKERHUB_USERNAME}" ] || [ -z "${DOCKERHUB_TOKEN}" ]; then + echo "DOCKERHUB_USERNAME / DOCKERHUB_TOKEN are not set on this repository." >&2 + echo "Set them under Settings -> Secrets and variables -> Actions." >&2 + exit 1 + fi + # hub.docker.com issues a short-lived JWT for metadata edits; the push token itself is + # scoped to the registry and cannot change repository settings. + token="$(curl -fsS -X POST https://hub.docker.com/v2/users/login/ \ + -H 'Content-Type: application/json' \ + -d "$(jq -nc --arg u "$DOCKERHUB_USERNAME" --arg p "$DOCKERHUB_TOKEN" '{username:$u,password:$p}')" \ + | jq -r '.token // empty')" + if [ -z "$token" ]; then + echo "Docker Hub did not return a token — the username or token is wrong." >&2 + exit 1 + fi + echo "token=$token" >> "$GITHUB_OUTPUT" + + - name: Push descriptions and categories + env: + JWT: ${{ steps.auth.outputs.token }} + run: | + set -euo pipefail + + # repo | short description (the card headline, 100 chars) | categories + while IFS='|' read -r repo short categories; do + [ -z "$repo" ] && continue + echo "── techbuzzz/$repo" + + full="$(jq -Rs . < "dockerhub/$repo.md")" + jq -n --arg d "$short" --arg f "$full" \ + '{description:$d, full_description:$f}' \ + | curl -fsS -X PATCH \ + -H "Authorization: JWT ${JWT}" \ + -H 'Content-Type: application/json' \ + --data-binary @- \ + "https://hub.docker.com/v2/repositories/techbuzzz/$repo/" > /dev/null + echo " description + overview synced" + + # Categories come from a fixed Docker Hub taxonomy and have changed between UI + # revisions, so a rejected value here must not cost us the description above. + if jq -nc --argjson c "$categories" '{categories:$c}' \ + | curl -fsS -X PATCH \ + -H "Authorization: JWT ${JWT}" \ + -H 'Content-Type: application/json' \ + --data-binary @- \ + "https://hub.docker.com/v2/repositories/techbuzzz/$repo/" > /dev/null 2>&1; then + echo " categories synced: $categories" + else + echo " ::warning::categories rejected for $repo ($categories) — description still applied" + fi + done <<'REPOS' + delibera-server|ASP.NET Core host for multi-model AI council debates: REST, SSE streaming, MCP|["web","messaging","ai-machine-learning","network"] + delibera-webui|Nuxt 4 browser front end for Delibera AI council debates|["web","ai-machine-learning"] + REPOS + + - name: Verify the cards are populated + env: + JWT: ${{ steps.auth.outputs.token }} + run: | + set -euo pipefail + for repo in delibera-server delibera-webui; do + body="$(curl -fsS -H "Authorization: JWT ${JWT}" \ + "https://hub.docker.com/v2/repositories/techbuzzz/$repo/")" + desc="$(jq -r '.description // ""' <<< "$body")" + len="$(jq -r '(.full_description // "") | length' <<< "$body")" + cats="$(jq -r '(.categories // []) | join(", ")' <<< "$body")" + printf '%-18s %-72s overview=%s chars categories=[%s]\n' "$repo" "$desc" "$len" "$cats" + if [ "$len" -lt 200 ]; then + echo "::error::overview for $repo is only $len chars — did the PATCH land?" >&2 + exit 1 + fi + done \ No newline at end of file diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..cc707b4 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,72 @@ +name: pages + +# Publishes site/index.html to GitHub Pages on every merge to main. +# +# The site is hand-written HTML, not a generated one: a static landing page does not justify a +# docs toolchain (MkDocs/Docusaurus) in a repo that has no Python. The one thing that must stay +# in sync is the version string, so it is injected here from the csproj that actually drives +# `dotnet pack` — meaning the page can never advertise a version that was not published. + +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# A new push supersedes an in-flight deploy; without this the second run would fail on the +# concurrency lock and leave the site one deploy behind. +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + name: Build and deploy Pages + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + + - name: Resolve version from the packable project + id: version + run: | + set -euo pipefail + # Read the version from the project that publish-nuget.yml packs. Do NOT read it from + # a tag or from CHANGELOG.md: those can disagree with what is on nuget.org, and a + # landing page claiming an unpublished version is worse than a stale one. + version="$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' src/Delibera.Core/Delibera.Core.csproj | head -1)" + if [ -z "$version" ]; then + echo "Could not resolve from src/Delibera.Core/Delibera.Core.csproj" >&2 + exit 1 + fi + echo "Resolved Delibera version: $version" + echo "value=$version" >> "$GITHUB_OUTPUT" + + - name: Assemble site + run: | + set -euo pipefail + mkdir -p _site/img + cp site/index.html _site/index.html + # The banner is the same asset the README uses — one file, two consumers. + cp img/delibera-horizontal-1920x480.png _site/img/ + sed -i "s/{{VERSION}}/${{ steps.version.outputs.value }}/g" _site/index.html + if grep -q '{{VERSION}}' _site/index.html; then + echo "Unsubstituted {{VERSION}} placeholder left in the built site" >&2 + exit 1 + fi + ls -la _site _site/img + + - uses: actions/configure-pages@v5 + + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + + - id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index e830f17..c6c0d1f 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -90,6 +90,9 @@ jobs: needs: [ build, webui-tests ] if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest + # Two multi-arch builds plus the smoke checks finish in ~15 min. 45 leaves room for a + # slow runner without letting a stuck step burn the default 6 hours. + timeout-minutes: 45 permissions: contents: read steps: @@ -161,12 +164,37 @@ jobs: - name: Runtime smoke (amd64) run: | set -euo pipefail - # Runs the actual entrypoint binary from each published image. The WebUI - # container would exit immediately, so --version is the cheapest proof that - # the runtime stage has a working interpreter and the right file layout. - docker run --rm --platform linux/amd64 \ + # Proves the runtime stage has a working interpreter and the right file layout. + # + # --entrypoint is REQUIRED, not decorative: both images declare ENTRYPOINT + # (dotnet Delibera.Server.dll / node .output/server/index.mjs), and `docker run + # IMAGE ` APPENDS to an ENTRYPOINT instead of replacing it. Without + # the override this runs `dotnet Delibera.Server.dll dotnet --version` — which + # boots the web host and never exits, so the step hangs silently instead of + # failing loudly. + # + # --list-runtimes, NOT --version: the server image ships the ASP.NET Core + # RUNTIME, not the SDK, so `dotnet --version` exits 155 with "No .NET SDKs were + # found" and would fail a perfectly good image. Listing runtimes proves the + # interpreter works AND asserts the AspNetCore runtime the host actually needs. + # + # `timeout` bounds it so a future regression costs minutes, not the job timeout. + timeout --kill-after=10 180 docker run --rm --platform linux/amd64 \ + --entrypoint dotnet \ "${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.SERVER_IMG }}:${{ steps.meta.outputs.full }}" \ - dotnet --version - docker run --rm --platform linux/amd64 \ + --list-runtimes | tee /tmp/runtimes + grep -q 'Microsoft.AspNetCore.App' /tmp/runtimes + timeout --kill-after=10 180 docker run --rm --platform linux/amd64 \ + --entrypoint node \ "${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.WEBUI_IMG }}:${{ steps.meta.outputs.full }}" \ - node --version \ No newline at end of file + --version + # The interpreter can be fine while the published payload is missing — a build + # that silently drops /app would sail past the two checks above. + timeout --kill-after=10 180 docker run --rm --platform linux/amd64 \ + --entrypoint ls \ + "${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.SERVER_IMG }}:${{ steps.meta.outputs.full }}" \ + -la /app/Delibera.Server.dll + timeout --kill-after=10 180 docker run --rm --platform linux/amd64 \ + --entrypoint ls \ + "${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.WEBUI_IMG }}:${{ steps.meta.outputs.full }}" \ + -la /app/.output/server/index.mjs \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..189512d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,118 @@ +name: release + +# Publishes a GitHub Release for every v* tag. +# +# The body is assembled from three parts, in this order: +# 1. .github/release-notes/header.md — orientation, registry links, the security warning +# 2. docs/WhatsNew-v.md — the hand-written account of what changed and why +# 3. auto-generated notes — every merged PR, straight from GitHub +# +# (2) is why this is not just `--generate-notes`: an auto-notes-only release lists "updated README" +# and "bump version to 10.5.2" as if they were the release. Part (2) is written by the person who +# knows which of those commits mattered. +# +# Kept separate from publish-nuget.yml so a release-page failure cannot fail a package publish. + +on: + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (defaults to the tag this workflow was dispatched on)' + required: false + type: string + push: + tags: [ 'v*' ] + +permissions: + contents: write + +jobs: + release: + name: Publish GitHub Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Resolve tag and cross-check the version + id: meta + run: | + set -euo pipefail + tag="${TAG:-${GITHUB_REF#refs/tags/}}" + [ -n "$tag" ] || { echo "No tag to release" >&2; exit 1; } + case "$tag" in + v*) version="${tag#v}" ;; + *) echo "Refusing to release a non-version tag: $tag" >&2; exit 1 ;; + esac + + # A tag that disagrees with ships a package whose metadata lies about + # itself — publish-nuget packs from the csproj, not the tag, so the Docker tag would + # read 10.5.2 while the nupkg on nuget.org is 10.5.1. This is the exact mismatch that + # shipped a silent no-op once; fail here instead. + csproj="$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' src/Delibera.Core/Delibera.Core.csproj | head -1)" + if [ "$csproj" != "$version" ]; then + echo "::error::tag $tag says $version but Delibera.Core.csproj says $csproj" >&2 + exit 1 + fi + + echo "Releasing $tag (Delibera.Core $csproj)" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Generate PR notes + id: notes + run: | + set -euo pipefail + gh api --method POST \ + "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ + -f tag_name="${{ steps.meta.outputs.tag }}" > /tmp/auto-notes.md + if [ ! -s /tmp/auto-notes.md ]; then + echo "::warning::GitHub returned no auto notes for this tag" + fi + + - name: Assemble the release body + env: + TAG: ${{ steps.meta.outputs.tag }} + VERSION: ${{ steps.meta.outputs.version }} + run: | + set -euo pipefail + body=/tmp/release-body.md + cp .github/release-notes/header.md "$body" + + sed -i "s/{{VERSION}}/${VERSION}/g" "$body" + + whatsnew="docs/WhatsNew-v${VERSION}.md" + if [ -f "$whatsnew" ]; then + printf '\n## What is new in %s\n\n' "$VERSION" >> "$body" + cat "$whatsnew" >> "$body" + else + echo "::warning::$whatsnew not found — release will carry only the header and PR notes" + fi + + if [ -s /tmp/auto-notes.md ]; then + printf '\n---\n\n## Merged pull requests\n\n' >> "$body" + cat /tmp/auto-notes.md >> "$body" + fi + + if grep -q '{{VERSION}}' "$body"; then + echo "::error::unsubstituted {{VERSION}} left in the release body" >&2 + exit 1 + fi + echo "--- body: $(wc -c < "$body") bytes, $(wc -l < "$body") lines ---" + + - name: Create or update the release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.meta.outputs.tag }} + VERSION: ${{ steps.meta.outputs.version }} + run: | + set -euo pipefail + title="Delibera $VERSION" + if gh release view "$TAG" > /dev/null 2>&1; then + echo "Release for $TAG already exists — updating it." + gh release edit "$TAG" --title "$title" --notes-file /tmp/release-body.md + else + # --verify-tag refuses to create a release whose tag is not on the remote, so a typo + # cannot manufacture a release pointing at a tag that does not exist. + gh release create "$TAG" --title "$title" --notes-file /tmp/release-body.md --verify-tag + fi + gh release view "$TAG" --json url,tagName,isLatest --jq '" -> \(.tagName) latest=\(.isLatest) \(.url)"' \ No newline at end of file diff --git a/.gitignore b/.gitignore index cd9559e..91267ea 100644 --- a/.gitignore +++ b/.gitignore @@ -470,3 +470,10 @@ FodyWeavers.xsd **/.output/ **/.data/ **/dist/ + + +# Social / newsletter drafts (never committed) +# LinkedIn articles and release announcements are written here, reviewed on the +# desktop, and pasted into the platform. Same rationale as .bench/ — personal +# working material, not repository documentation. +.articles/ diff --git a/README-RU.md b/README-RU.md index c3d685a..d2e510c 100644 --- a/README-RU.md +++ b/README-RU.md @@ -9,6 +9,15 @@ **Коллективное принятие решений через структурированное обсуждение ИИ — с RAG, pgvector, Knowledge Keeper, 🛠️ Operator (MCP-инструменты), Chairman, 🔥 сжатием контекста, ✂️ AutoChunking, 💉 Dependency Injection и 📋 журналированием выполнения** [![NuGet](https://img.shields.io/nuget/v/Delibera.Core.svg)](https://www.nuget.org/packages/Delibera.Core) +[![NuGet: Server](https://img.shields.io/nuget/v/Delibera.Server.svg)](https://www.nuget.org/packages/Delibera.Server) +[![NuGet: Redis](https://img.shields.io/nuget/v/Delibera.Redis.svg)](https://www.nuget.org/packages/Delibera.Redis) +[![Release](https://img.shields.io/github/v/release/techbuzzz/Delibera?label=Release)](https://github.com/techbuzzz/Delibera/releases/latest) +[![Docker Hub](https://img.shields.io/badge/Docker%20Hub-2496ED?logo=docker&logoColor=fff)](https://hub.docker.com/r/techbuzzz/delibera-server) +[![Pulls: server](https://img.shields.io/docker/pulls/techbuzzz/delibera-server?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-server) +[![Pulls: webui](https://img.shields.io/docker/pulls/techbuzzz/delibera-webui?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-webui) +[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) +[![CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml) +[![Docker CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](LICENSE) [![.NET 10](https://img.shields.io/badge/.NET-10.0-1F2937.svg)](https://dotnet.microsoft.com) [![C# 15](https://img.shields.io/badge/C%23-15.0--preview-239120.svg)](https://learn.microsoft.com/dotnet/csharp/) @@ -227,10 +236,50 @@ Breaking changes нет — всё опционально, ни один пуб ### Установка +Delibera публикуется как три пакета NuGet и два образа Docker. Выберите подходящий путь. + +#### 📦 NuGet + +```bash +dotnet add package Delibera.Core # сам фреймворк — начните отсюда +dotnet add package Delibera.Server # хост ASP.NET Core (Minimal API, SSE, MCP) +dotnet add package Delibera.Redis # распределённые дебаты + общий кэш результатов +``` + +Все три выходят в каждом релизе с одной версией. `Delibera.Server` подтягивает `Delibera.Redis` +транзитивно, поэтому явная ссылка нужна только если вы используете Redis-интеграцию напрямую. +Redis остаётся **выключенным**, пока не задан `Delibera:Redis:Enabled`. + +#### 🐳 Docker Hub — без сборки + +Образы публикуются на каждом теге `v*` для `linux/amd64` **и** `linux/arm64`: + +| Образ | Что это | +| --- | --- | +| `techbuzzz/delibera-server` | API, SSE-стриминг дебат, MCP-эндпоинты | +| `techbuzzz/delibera-webui` | браузерный интерфейс на Nuxt 4 | + ```bash -dotnet add package Delibera.Core +curl -O https://raw.githubusercontent.com/techbuzzz/Delibera/main/deploy/docker-compose.hub.yml +export DELIBERA_VERSION=10.5.2 # зафиксировать релиз; без переменной тянется :latest +docker compose -f docker-compose.hub.yml up -d +``` + +> ⚠️ **API не аутентифицирован и не ограничен по частоте, а дебата тратит реальные кредиты LLM.** +> Все публикуемые порты слушают `127.0.0.1`. Поставьте аутентифицирующий обратный прокси, прежде чем +> открывать этот стек в сеть, которой не управляете. + +#### 📥 Из исходников + +```bash +git clone https://github.com/techbuzzz/Delibera.git +cd Delibera +dotnet build Delibera.slnx -c Release ``` +Из клона `docker compose up -d` поднимает весь стек — сервер, веб-интерфейс, Ollama, Qdrant, +pgvector и Redis. Пошаговое руководство: [docs/QuickStart-RU.md](docs/QuickStart-RU.md). + ### Минимальный пример ```csharp diff --git a/README.md b/README.md index 2ec7cfd..fedc59c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,13 @@ [![NuGet](https://img.shields.io/nuget/v/Delibera.Core.svg)](https://www.nuget.org/packages/Delibera.Core) [![NuGet: Server](https://img.shields.io/nuget/v/Delibera.Server.svg)](https://www.nuget.org/packages/Delibera.Server) [![NuGet: Redis](https://img.shields.io/nuget/v/Delibera.Redis.svg)](https://www.nuget.org/packages/Delibera.Redis) +[![Release](https://img.shields.io/github/v/release/techbuzzz/Delibera?label=Release)](https://github.com/techbuzzz/Delibera/releases/latest) +[![Docker Hub](https://img.shields.io/badge/Docker%20Hub-2496ED?logo=docker&logoColor=fff)](https://hub.docker.com/r/techbuzzz/delibera-server) +[![Pulls: server](https://img.shields.io/docker/pulls/techbuzzz/delibera-server?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-server) +[![Pulls: webui](https://img.shields.io/docker/pulls/techbuzzz/delibera-webui?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-webui) +[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) [![CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml) +[![Docker CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](LICENSE) [![.NET 10](https://img.shields.io/badge/.NET-10.0-1F2937.svg)](https://dotnet.microsoft.com) [![C# 15](https://img.shields.io/badge/C%23-15.0--preview-239120.svg)](https://learn.microsoft.com/dotnet/csharp/) @@ -229,10 +235,50 @@ Heavier local models, recommended on GPUs with ≥ 24 GB VRAM or on Ollama Cloud ### Installation +Delibera ships as three NuGet packages and two Docker images. Pick the path that fits. + +#### 📦 NuGet + +```bash +dotnet add package Delibera.Core # the framework — start here +dotnet add package Delibera.Server # ASP.NET Core host (Minimal API, SSE, MCP) +dotnet add package Delibera.Redis # distributed debates + shared result cache +``` + +All three publish on every release at the same version. `Delibera.Server` pulls `Delibera.Redis` +transitively, so you only need to reference it explicitly if you use the Redis integration directly. +Redis stays **off** unless you set `Delibera:Redis:Enabled`. + +#### 🐳 Docker Hub — no build required + +Images are published on every `v*` tag for `linux/amd64` **and** `linux/arm64`: + +| Image | What it is | +| --- | --- | +| `techbuzzz/delibera-server` | API, SSE debate streaming, MCP endpoints | +| `techbuzzz/delibera-webui` | Nuxt 4 browser front end for the debate workflow | + ```bash -dotnet add package Delibera.Core +curl -O https://raw.githubusercontent.com/techbuzzz/Delibera/main/deploy/docker-compose.hub.yml +export DELIBERA_VERSION=10.5.2 # pin a release; omit to track :latest +docker compose -f docker-compose.hub.yml up -d +``` + +> ⚠️ **The API is unauthenticated and unthrottled, and a debate spends real LLM credits.** Every +> published port binds to `127.0.0.1`. Put an authenticating reverse proxy in front before exposing +> this stack to any network you do not control. + +#### 📥 From source + +```bash +git clone https://github.com/techbuzzz/Delibera.git +cd Delibera +dotnet build Delibera.slnx -c Release ``` +From a clone, `docker compose up -d` starts the whole stack — server, Web UI, Ollama, Qdrant, +pgvector and Redis. See [docs/QuickStart.md](docs/QuickStart.md) for the walkthrough. + ### Minimal Example ```csharp diff --git a/dockerhub/delibera-server.md b/dockerhub/delibera-server.md new file mode 100644 index 0000000..ce43bce --- /dev/null +++ b/dockerhub/delibera-server.md @@ -0,0 +1,106 @@ +# delibera-server + +ASP.NET Core host for **Delibera** — a framework for collective decision making through structured +AI deliberation. Multiple models reason through a question across rounds, critique each other, and a +**Chairman** weighs the arguments into a final verdict. + +This image is the API surface only. For a browser front end, pair it with +[`delibera-webui`](https://hub.docker.com/r/techbuzzz/delibera-webui). + +--- + +## Quick start + +```bash +docker run -d --name delibera-server \ + -p 127.0.0.1:5200:8080 \ + -e Delibera__Providers__DefaultEndpoint=http://host.docker.internal:11434 \ + -e Delibera__Models__Fast=llama3.2:3b \ + -e Delibera__Models__Strong=qwen2.5:7b \ + techbuzzz/delibera-server:latest +``` + +Or bring up the full stack — server, Web UI, Ollama, Qdrant and Redis: + +```bash +curl -O https://raw.githubusercontent.com/techbuzzz/Delibera/main/deploy/docker-compose.hub.yml +export DELIBERA_VERSION=10.5.2 +docker compose -f docker-compose.hub.yml up -d +``` + +Pin a release with `DELIBERA_VERSION`; omit it to track `:latest`. + +--- + +## ⚠️ Security + +**The API is unauthenticated and unthrottled, and running a debate spends real LLM credits.** +Every published port in the supplied compose file binds to `127.0.0.1` for that reason. + +Put an authenticating reverse proxy in front before exposing this stack to any network you do not +control. Do not widen the port bind back to `0.0.0.0` on the assumption that it is "just local". + +--- + +## What it exposes + +| Endpoint | Purpose | +| --- | --- | +| `/api/v1/debates` | Create, list, inspect and cancel debates | +| `/api/v1/debates/{id}/stream` | Server-Sent Events, one event per round | +| `/api/v1/scenarios`, `/api/v1/corpus` | Templates and the RAG corpus | +| `/api/v1/health` | Liveness and readiness | + +Debates run for 150–200 s and an SSE stream stays open far longer, so read the debate result from +the stream or the record — not from a short client timeout. + +--- + +## Configuration + +Every setting is bound from `Delibera__
__`. + +| Variable | Default | Meaning | +| --- | --- | --- | +| `Delibera__Providers__DefaultType` | `Ollama` | Provider family: `Ollama`, `OpenAI`, … | +| `Delibera__Providers__DefaultEndpoint` | `http://host.docker.internal:11434` | Provider base URL | +| `Delibera__Providers__DefaultModel` | `qwen2.5:14b` | Model used when none is specified | +| `Delibera__Providers__ApiKey` | *(empty)* | Required for hosted providers | +| `Delibera__Providers__EmbeddingModel` | `nomic-embed-text` | Embedding model for RAG | +| `Delibera__Models__Fast` / `__Strong` | `llama3.2:3b` / `qwen2.5:7b` | Council sizing tiers | +| `Delibera__MaxRounds` | `4` | Debate rounds | +| `Delibera__Temperature` | `0.7` | Sampling temperature | +| `Delibera__Strategy` | `Standard` | Debate strategy | +| `Delibera__Rag__Enabled` | `false` | Enable RAG | +| `Delibera__Rag__ProviderType` | `Qdrant` | `Qdrant` or `PgVector` | +| `Delibera__Redis__Enabled` | `false` | Enable the Redis orchestrator | +| `Delibera__Redis__CacheEnabled` | `false` | Enable the Redis result cache | +| `Delibera__Output__Directory` | `/app/debate_results` | Where results are written | + +### Redis + +Off by default — the default path resolves the in-process orchestrator, exactly as before. + +Enabling it publishes round events to a Redis Stream so any API instance can stream them over SSE, +and shares debate state across instances. It does **not** provide distributed turn execution: each +debate still runs in the process that accepted it. A bad connection string fails at startup rather +than silently degrading, because a deployment that quietly fell back would look distributed and not be. + +--- + +## Architectures + +`linux/amd64` and `linux/arm64`, published together under one manifest per tag. + +## Tags + +- `10.5.2`, `10.5` — pinned releases +- `latest` — newest release + +## Links + +- Source: https://github.com/techbuzzz/Delibera +- NuGet: https://www.nuget.org/packages/Delibera.Server +- Release notes: https://github.com/techbuzzz/Delibera/releases + +MIT licensed. \ No newline at end of file diff --git a/dockerhub/delibera-webui.md b/dockerhub/delibera-webui.md new file mode 100644 index 0000000..ed7de71 --- /dev/null +++ b/dockerhub/delibera-webui.md @@ -0,0 +1,85 @@ +# delibera-webui + +Browser front end for **Delibera** — a Nuxt 4 application covering the debate workflow: list debates, +create one from a registered template, watch rounds arrive live, read the verdict with token stats, +export Markdown, cancel a running debate. + +This image is UI only. It talks to the API through +[`delibera-server`](https://hub.docker.com/r/techbuzzz/delibera-server) and holds no state. + +--- + +## Quick start + +```bash +docker run -d --name delibera-webui \ + -p 127.0.0.1:3000:3000 \ + -e NUXT_DELIBERA_API_BASE=http://delibera-server:8080 \ + techbuzzz/delibera-webui:latest +``` + +The API base URL has to be reachable **from inside the container**, so it is the service name on the +Docker network — not `localhost`, and not a host-side address. + +Or bring up the full stack: + +```bash +curl -O https://raw.githubusercontent.com/techbuzzz/Delibera/main/deploy/docker-compose.hub.yml +export DELIBERA_VERSION=10.5.2 +docker compose -f docker-compose.hub.yml up -d +``` + +Then open . + +--- + +## ⚠️ Security + +This UI has **no authentication of its own**. It exposes everything the API exposes, including the +ability to spend LLM credits. + +Every published port in the supplied compose file binds to `127.0.0.1` for that reason. Put an +authenticating reverse proxy in front before exposing this stack to any network you do not control. + +--- + +## How it reaches the API + +Through a Nitro server-side proxy at `/api/delibera/**`, never directly from the browser. Two defects +in the API force this: + +1. The server registers no CORS policy, so a browser calling the API origin directly is blocked on + every request, GETs included. +2. Absolute `streamUrl` / `resultUrl` values are built from `Scheme://Host` with no + forwarded-header handling, so they point at the wrong host behind a proxy or an ingress. + +SSE is forwarded unbuffered, so a round appears the moment it is produced rather than at the end of +the debate. + +--- + +## Configuration + +| Variable | Default | Meaning | +| --- | --- | --- | +| `NUXT_DELIBERA_API_BASE` | `http://delibera-server:8080` | API base URL, resolved inside the container network | +| `NUXT_PUBLIC_TENANT_ID` | `default` | Tenant forwarded to the API | +| `NODE_ENV` | `production` | Standard Node environment | + +--- + +## Architectures + +`linux/amd64` and `linux/arm64`, published together under one manifest per tag. + +## Tags + +- `10.5.2`, `10.5` — pinned releases +- `latest` — newest release + +## Links + +- Source: https://github.com/techbuzzz/Delibera +- Release notes: https://github.com/techbuzzz/Delibera/releases + +MIT licensed. \ No newline at end of file diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..2850a54 --- /dev/null +++ b/site/index.html @@ -0,0 +1,211 @@ + + + + + +Delibera — Thoughtful AI Decisions + + + + + +
+
+ Delibera +

Thoughtful AI Decisions

+

+ Collective decision making through structured AI deliberation — multiple models reason through + a question across rounds, critique each other, and a Chairman weighs the + arguments into a final verdict. RAG via Qdrant or pgvector, MCP tools, live SSE streaming. +

+

+ v{{VERSION}} + Release notes +

+
+ Delibera.Core on NuGet + Docker pulls: server + Docker pulls: webui + Docker Hub + MIT + .NET 10 +
+

+ 🇬🇧 English · + 🇷🇺 Русский +

+
+
+ +
+
+

Install

+

Three NuGet packages and two Docker images, all published on every release.

+ +

NuGet

+
dotnet add package Delibera.Core      # the framework — start here
+dotnet add package Delibera.Server    # ASP.NET Core host (Minimal API, SSE, MCP)
+dotnet add package Delibera.Redis     # distributed debates + shared result cache
+ +

Docker Hub — no build required

+
curl -O https://raw.githubusercontent.com/techbuzzz/Delibera/main/deploy/docker-compose.hub.yml
+export DELIBERA_VERSION={{VERSION}}     # omit to track :latest
+docker compose -f docker-compose.hub.yml up -d
+ +
+ The API is unauthenticated and unthrottled, and a debate spends real LLM credits. + Every published port binds to 127.0.0.1. Put an authenticating reverse proxy in + front before exposing this stack to any network you do not control. +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + + + + \ No newline at end of file From f516b57fa9e50d38f24321cfac9f24483b1dd256 Mon Sep 17 00:00:00 2001 From: Viktor Buzin Date: Fri, 9 Oct 2026 19:20:26 +0300 Subject: [PATCH 02/19] Pages: deploy the real Nuxt app instead of a hand-written landing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the static landing page from the previous commit with what was actually asked for: the Web UI itself, statically exported to GitHub Pages via `nuxt generate`. Two real defects surfaced by running the export rather than reading the docs, both of which would have shipped a broken site behind a green build: - Output location. Nuxt 4 emits to `.output/public`, not `dist`. GitHub's own sample workflow still uploads ./dist because it predates Nuxt 4, and copying it verbatim uploads nothing. - No prerendered routes. With no explicit list, `nuxt generate` emitted only the 200.html and 404.html SPA fallbacks and nothing for the real pages — a deploy that is a blank site. The workflow now asserts index.html exists instead of trusting the exit code. useDebates resolves its API base from NUXT_PUBLIC_DELIBERA_API_BASE instead of a hardcoded `/api/delibera`. Empty by default, so the container path is byte-for-byte unchanged: same-origin BFF, and the browser still never touches the API origin. The variable exists because a static export has no Nitro server and therefore no BFF route. It is read from import.meta.env rather than useRuntimeConfig() because useDebates() is also called from useDebateStream's start() and poll timer, which run after setup has unwound and Vue's current instance is gone. Not changed, deliberately: Delibera.Server still registers no CORS policy. Widening CORS on an unauthenticated API is an owner decision, so the static build deploys with API calls 404ing until that is made deliberately and an origin is set. That state is documented in the workflow, in nuxt.config.ts and on the Docker Hub card. Also adds the missing `generate` script to package.json — the workflow calls it and the manifest did not define it. Verified locally: nuxt typecheck exit 0, vitest 34/34 across 4 files, nuxt generate exit 0 producing index.html (6691 bytes) and debates/new/index.html. --- .github/workflows/pages.yml | 125 +++++++---- README-RU.md | 2 +- README.md | 2 +- dockerhub/delibera-webui.md | 17 ++ site/index.html | 211 ------------------ .../app/composables/useDebates.ts | 48 +++- src/Delibera.WebUI/nuxt.config.ts | 22 +- src/Delibera.WebUI/package.json | 1 + 8 files changed, 162 insertions(+), 266 deletions(-) delete mode 100644 site/index.html diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index cc707b4..b983d32 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,11 +1,17 @@ -name: pages +name: Deploy Nuxt site to Pages -# Publishes site/index.html to GitHub Pages on every merge to main. +# Publishes the real Delibera Web UI to GitHub Pages as a static build. # -# The site is hand-written HTML, not a generated one: a static landing page does not justify a -# docs toolchain (MkDocs/Docusaurus) in a repo that has no Python. The one thing that must stay -# in sync is the version string, so it is injected here from the csproj that actually drives -# `dotnet pack` — meaning the page can never advertise a version that was not published. +# This is a STATIC export of the same app that ships in the container. The important +# consequence, which is not obvious from the workflow: `nuxt generate` produces no Nitro server, +# so the BFF route `/api/delibera/**` does not exist in the deployed site. The browser then needs +# an absolute API origin, supplied at build time by NUXT_PUBLIC_DELIBERA_API_BASE and resolved in +# app/composables/useDebates.ts. That server must allow this origin via CORS — Delibera.Server +# registers no CORS policy by default, because in its normal container deployment the browser +# only ever talks to the same-origin proxy. +# +# Without that env var the site still deploys and still renders, but every request 404s. That is +# a legitimate state for a UI preview; it is not a working install. on: push: @@ -17,56 +23,97 @@ permissions: pages: write id-token: write -# A new push supersedes an in-flight deploy; without this the second run would fail on the -# concurrency lock and leave the site one deploy behind. +# Allow only one concurrent deployment, skipping runs queued between the in-progress and the +# latest queued. Do NOT cancel in-progress runs — let a production deploy finish. concurrency: group: pages - cancel-in-progress: true + cancel-in-progress: false jobs: - deploy: - name: Build and deploy Pages + build: runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - name: Resolve version from the packable project - id: version + - name: Detect package manager + id: detect-package-manager run: | set -euo pipefail - # Read the version from the project that publish-nuget.yml packs. Do NOT read it from - # a tag or from CHANGELOG.md: those can disagree with what is on nuget.org, and a - # landing page claiming an unpublished version is worse than a stale one. - version="$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' src/Delibera.Core/Delibera.Core.csproj | head -1)" - if [ -z "$version" ]; then - echo "Could not resolve from src/Delibera.Core/Delibera.Core.csproj" >&2 + if [ -f "${{ github.workspace }}/src/Delibera.WebUI/pnpm-lock.yaml" ]; then + echo "manager=pnpm" >> "$GITHUB_OUTPUT" + echo "command=install" >> "$GITHUB_OUTPUT" + elif [ -f "${{ github.workspace }}/src/Delibera.WebUI/yarn.lock" ]; then + echo "manager=yarn" >> "$GITHUB_OUTPUT" + echo "command=install" >> "$GITHUB_OUTPUT" + elif [ -f "${{ github.workspace }}/src/Delibera.WebUI/package-lock.json" ]; then + echo "manager=npm" >> "$GITHUB_OUTPUT" + echo "command=ci" >> "$GITHUB_OUTPUT" + else + echo "::error::no lockfile found in src/Delibera.WebUI" >&2 exit 1 fi - echo "Resolved Delibera version: $version" - echo "value=$version" >> "$GITHUB_OUTPUT" - - name: Assemble site + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: ${{ steps.detect-package-manager.outputs.manager }} + cache-dependency-path: src/Delibera.WebUI/package-lock.json + + - name: Setup Pages + uses: actions/configure-pages@v5 + with: + # Injects `router.base` and sets `target: 'static'` in nuxt.config.ts. + static_site_generator: nuxt + + - name: Install dependencies + working-directory: src/Delibera.WebUI + run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} + + - name: Typecheck + working-directory: src/Delibera.WebUI + run: ${{ steps.detect-package-manager.outputs.manager }} run typecheck + + - name: Static HTML export with Nuxt + working-directory: src/Delibera.WebUI + env: + # Empty by default: the site deploys as a UI preview whose API calls 404. + # Set it as a repository variable to point the static build at a live server. + NUXT_PUBLIC_DELIBERA_API_BASE: ${{ vars.NUXT_PUBLIC_DELIBERA_API_BASE }} + run: ${{ steps.detect-package-manager.outputs.manager }} run generate + + - name: Fail if the export produced a real site run: | set -euo pipefail - mkdir -p _site/img - cp site/index.html _site/index.html - # The banner is the same asset the README uses — one file, two consumers. - cp img/delibera-horizontal-1920x480.png _site/img/ - sed -i "s/{{VERSION}}/${{ steps.version.outputs.value }}/g" _site/index.html - if grep -q '{{VERSION}}' _site/index.html; then - echo "Unsubstituted {{VERSION}} placeholder left in the built site" >&2 + # `.output/public`, NOT `dist` — Nuxt 4 moved the static output there. GitHub's own + # sample workflow still uploads ./dist because it predates Nuxt 4, and copying it + # verbatim uploads nothing at all. + # + # The second assertion matters more: without an explicit nitro.prerender.routes list + # the export contains only 200.html + 404.html and NO document for the real pages, + # which deploys as a blank site that still reports a green build. + out=src/Delibera.WebUI/.output/public + if [ ! -f "$out/index.html" ]; then + echo "::error::$out/index.html is missing — the site would deploy blank" >&2 + ls -la "$out" || true exit 1 fi - ls -la _site _site/img - - - uses: actions/configure-pages@v5 + echo "prerendered: $(cd "$out" && find . -name '*.html' | sort | tr '\n' ' ')" + echo "total files: $(find "$out" -type f | wc -l)" - - uses: actions/upload-pages-artifact@v3 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 with: - path: _site + path: src/Delibera.WebUI/.output/public - - id: deployment + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/README-RU.md b/README-RU.md index d2e510c..48bf223 100644 --- a/README-RU.md +++ b/README-RU.md @@ -15,7 +15,7 @@ [![Docker Hub](https://img.shields.io/badge/Docker%20Hub-2496ED?logo=docker&logoColor=fff)](https://hub.docker.com/r/techbuzzz/delibera-server) [![Pulls: server](https://img.shields.io/docker/pulls/techbuzzz/delibera-server?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-server) [![Pulls: webui](https://img.shields.io/docker/pulls/techbuzzz/delibera-webui?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-webui) -[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) +[![Web UI](https://img.shields.io/badge/Web%20UI-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) [![CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml) [![Docker CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](LICENSE) diff --git a/README.md b/README.md index fedc59c..2e4971f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ [![Docker Hub](https://img.shields.io/badge/Docker%20Hub-2496ED?logo=docker&logoColor=fff)](https://hub.docker.com/r/techbuzzz/delibera-server) [![Pulls: server](https://img.shields.io/docker/pulls/techbuzzz/delibera-server?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-server) [![Pulls: webui](https://img.shields.io/docker/pulls/techbuzzz/delibera-webui?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-webui) -[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) +[![Web UI](https://img.shields.io/badge/Web%20UI-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) [![CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml) [![Docker CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](LICENSE) diff --git a/dockerhub/delibera-webui.md b/dockerhub/delibera-webui.md index ed7de71..8ef70e7 100644 --- a/dockerhub/delibera-webui.md +++ b/dockerhub/delibera-webui.md @@ -43,6 +43,23 @@ authenticating reverse proxy in front before exposing this stack to any network --- +## Static build (GitHub Pages) + +The same app is published as a static site at + on every merge to `main`. + +A static export has **no Nitro server**, so the `/api/delibera/**` BFF route does not exist there. +Set `NUXT_PUBLIC_DELIBERA_API_BASE` to a reachable server origin to give the static build an API — +and that server must allow the Pages origin via CORS, which `Delibera.Server` does not do by +default. Without it the page deploys and renders, but its requests 404; that is a UI preview, not +an install. + +Two build details worth knowing if you fork it: Nuxt 4 emits to `.output/public`, not `dist`, and +the prerender routes must be listed explicitly in `nuxt.config.ts` — otherwise the export contains +only the `200.html`/`404.html` SPA fallbacks and deploys as a blank page. + +--- + ## How it reaches the API Through a Nitro server-side proxy at `/api/delibera/**`, never directly from the browser. Two defects diff --git a/site/index.html b/site/index.html deleted file mode 100644 index 2850a54..0000000 --- a/site/index.html +++ /dev/null @@ -1,211 +0,0 @@ - - - - - -Delibera — Thoughtful AI Decisions - - - - - -
-
- Delibera -

Thoughtful AI Decisions

-

- Collective decision making through structured AI deliberation — multiple models reason through - a question across rounds, critique each other, and a Chairman weighs the - arguments into a final verdict. RAG via Qdrant or pgvector, MCP tools, live SSE streaming. -

-

- v{{VERSION}} - Release notes -

-
- Delibera.Core on NuGet - Docker pulls: server - Docker pulls: webui - Docker Hub - MIT - .NET 10 -
-

- 🇬🇧 English · - 🇷🇺 Русский -

-
-
- -
-
-

Install

-

Three NuGet packages and two Docker images, all published on every release.

- -

NuGet

-
dotnet add package Delibera.Core      # the framework — start here
-dotnet add package Delibera.Server    # ASP.NET Core host (Minimal API, SSE, MCP)
-dotnet add package Delibera.Redis     # distributed debates + shared result cache
- -

Docker Hub — no build required

-
curl -O https://raw.githubusercontent.com/techbuzzz/Delibera/main/deploy/docker-compose.hub.yml
-export DELIBERA_VERSION={{VERSION}}     # omit to track :latest
-docker compose -f docker-compose.hub.yml up -d
- -
- The API is unauthenticated and unthrottled, and a debate spends real LLM credits. - Every published port binds to 127.0.0.1. Put an authenticating reverse proxy in - front before exposing this stack to any network you do not control. -
-
-
- -
- -
- -
- -
- -
- -
- - - - - \ No newline at end of file diff --git a/src/Delibera.WebUI/app/composables/useDebates.ts b/src/Delibera.WebUI/app/composables/useDebates.ts index 37fdbd5..94a037d 100644 --- a/src/Delibera.WebUI/app/composables/useDebates.ts +++ b/src/Delibera.WebUI/app/composables/useDebates.ts @@ -3,6 +3,9 @@ * * Every call targets the SAME-ORIGIN proxy route (`/api/delibera/...`), never the API * origin. `tenantId` is sent by the BFF from runtime config, not by the browser. + * + * The one exception is a STATIC build (`nuxt generate`, i.e. GitHub Pages), where no Nitro + * server exists and therefore no BFF route either — see `resolveApiBase()`. */ import type { @@ -13,10 +16,30 @@ import type { } from '#shared/types/delibera' import { parseApiError } from './useApiError' -const BASE = '/api/delibera' +/** + * Resolves the API base, defaulting to the same-origin BFF. + * + * Read from `import.meta.env` rather than `useRuntimeConfig()` on purpose: `useDebates()` is + * called from inside `useDebateStream`'s `start()` and poll timer, which run on mount and on + * a timer — long after setup has unwound and Vue's current instance is gone, so a composable + * context lookup there is unreliable. A build-time constant has no such dependency and is + * exactly what a static export needs anyway. + * + * Empty (the default) → same-origin `/api/delibera`, which is the production container path: + * `Delibera.Server` registers no CORS policy, so the browser must never call it directly. + * Set `NUXT_PUBLIC_DELIBERA_API_BASE` to an absolute origin ONLY for a static build, and only + * against a server that allows this origin via CORS. + * + * Note the tenant header in that case: EventSource cannot set headers, so the BFF no longer + * supplies X-Tenant-Id and the API falls back to its `default` tenant. + */ +function resolveApiBase(): string { + const configured = String(import.meta.env.NUXT_PUBLIC_DELIBERA_API_BASE ?? '').trim() + return `${configured.replace(/\/+$/, '')}/api/delibera` +} -async function request(path: string, init: RequestInit = {}): Promise { - const response = await fetch(`${BASE}${path}`, { +async function request(base: string, path: string, init: RequestInit = {}): Promise { + const response = await fetch(`${base}${path}`, { ...init, headers: { Accept: 'application/json', @@ -39,6 +62,8 @@ export interface DebateListFilters { } export function useDebates() { + const base = resolveApiBase() + /** `GET /debates` — returns DebateResponse[]. */ function list(filters: DebateListFilters = {}) { const query = new URLSearchParams() @@ -47,12 +72,12 @@ export function useDebates() { query.set('page', String(filters.page ?? 1)) query.set('pageSize', String(filters.pageSize ?? 20)) - return request(`/debates?${query.toString()}`) + return request(base, `/debates?${query.toString()}`) } /** `GET /debates/{id}` — 404 once the ~30 min record eviction has run. */ function get(id: string) { - return request(`/debates/${encodeURIComponent(id)}`) + return request(base, `/debates/${encodeURIComponent(id)}`) } /** @@ -62,7 +87,7 @@ export function useDebates() { * 150–200 s a debate takes; the async form is what makes the live view possible. */ function create(payload: CreateDebateRequest) { - return request('/debates/async', { + return request(base, '/debates/async', { method: 'POST', body: JSON.stringify(payload), }) @@ -75,7 +100,7 @@ export function useDebates() { */ async function cancel(id: string): Promise<'cancelled' | 'already-finished'> { try { - await request(`/debates/${encodeURIComponent(id)}`, { method: 'DELETE' }) + await request(base, `/debates/${encodeURIComponent(id)}`, { method: 'DELETE' }) return 'cancelled' } catch (error) { if (typeof error === 'object' && error !== null && (error as { status?: number }).status === 404) { @@ -88,27 +113,28 @@ export function useDebates() { /** `GET /debates/{id}/rounds` — paginated snapshot, for a completed debate. */ function rounds(id: string, page = 1, pageSize = 50) { return request( + base, `/debates/${encodeURIComponent(id)}/rounds?page=${page}&pageSize=${pageSize}`, ) } /** `GET /templates` — the source of truth for the template picker. Never hardcoded. */ function templates() { - return request('/templates') + return request(base, '/templates') } /** - * Markdown export URL. Built from the same-origin proxy rather than from + * Markdown export URL. Built from the resolved base rather than from * `DebateResponse.exportUrl`/`resultUrl`, which are absolute and computed from * Scheme://Host with no forwarded-header handling. */ function exportUrl(id: string): string { - return `${BASE}/debates/${encodeURIComponent(id)}/export/markdown` + return `${base}/debates/${encodeURIComponent(id)}/export/markdown` } /** SSE URL, same reasoning as exportUrl. */ function streamUrl(id: string): string { - return `${BASE}/debates/${encodeURIComponent(id)}/stream` + return `${base}/debates/${encodeURIComponent(id)}/stream` } return { list, get, create, cancel, rounds, templates, exportUrl, streamUrl } diff --git a/src/Delibera.WebUI/nuxt.config.ts b/src/Delibera.WebUI/nuxt.config.ts index ffe9070..277d82c 100644 --- a/src/Delibera.WebUI/nuxt.config.ts +++ b/src/Delibera.WebUI/nuxt.config.ts @@ -4,9 +4,15 @@ import { defineNuxtConfig } from 'nuxt/config' export default defineNuxtConfig({ compatibilityDate: '2026-01-01', - // SSR stays on: the BFF proxy runs server-side, which is the whole reason the browser - // never touches the API origin. Debates are a private, non-indexed tool surface, so the - // SEO argument for CSR does not apply here. + // SSR stays on for the container image: the BFF proxy runs server-side, which is the whole + // reason the browser never touches the API origin. Debates are a private, non-indexed tool + // surface, so the SEO argument for CSR does not apply here. + // + // The GITHUB PAGES build is the opposite case. `configure-pages` with + // `static_site_generator: nuxt` flips this to `target: 'static'` and injects `router.base` at + // build time — a static export has no Nitro server, so the BFF route does not exist there. + // That build therefore needs `NUXT_PUBLIC_DELIBERA_API_BASE` pointing at a reachable server + // origin that allows this one via CORS. See `resolveApiBase()` in app/composables/useDebates.ts. ssr: true, devtools: { enabled: false }, @@ -47,6 +53,16 @@ export default defineNuxtConfig({ nitro: { preset: 'node-server', + + // Needed ONLY by the static export (`nuxt generate` / GitHub Pages). + // + // Without an explicit route list, Nuxt 4 emits just the SPA fallback pair + // (200.html + 404.html) and no document for the real pages — a Pages deploy of that is a + // blank site. `/debates/[id]` is deliberately absent: its ids only exist at runtime, so it + // renders client-side from whatever the id in the URL is. + prerender: { + routes: ['/', '/debates/new'], + }, }, // Route rules are deliberately empty. Debate status is mutable: caching /debates/** with diff --git a/src/Delibera.WebUI/package.json b/src/Delibera.WebUI/package.json index e2187c5..f88beef 100644 --- a/src/Delibera.WebUI/package.json +++ b/src/Delibera.WebUI/package.json @@ -11,6 +11,7 @@ "scripts": { "dev": "nuxt dev", "build": "nuxt build", + "generate": "nuxt generate", "preview": "nuxt preview", "start": "node .output/server/index.mjs", "postinstall": "nuxt prepare", From c89df7e775f886a9e0863e3dd1ccddbd0db95fb4 Mon Sep 17 00:00:00 2001 From: Viktor Buzin Date: Fri, 9 Oct 2026 19:23:44 +0300 Subject: [PATCH 03/19] Workflows: fix an invalid gh JSON field, make category failures actionable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both found by validating the workflows rather than reading them. release.yml asked `gh release view --json url,tagName,isLatest`. `isLatest` is not one of the fields `gh release view` exposes — it belongs to `gh release list` (https://cli.github.com/manual/gh_release_view). The command fails with "Unknown JSON field", and it fails AFTER the release has been created, so a successful publish would have been reported as a red run. Now queries url,tagName,isDraft,isPrerelease. dockerhub-metadata.yml discarded the response from the categories PATCH into /dev/null, so a rejected slug produced a warning that said nothing about what to change. Docker Hub echoes the accepted values in the error body; it is now captured and printed. The description write is unaffected either way — that part was already a separate, fatal call. Categories are keyed by slug, not by the display name shown in the Docker Hub UI ("Machine learning & AI" and friends), so the slugs are the part most likely to need editing. They are inline and documented for that reason. Validation: all five workflow files parse under YamlDotNet with no tabs, and all 25 extracted `run:` bodies pass `bash -n` under Git Bash 5.2. --- .github/workflows/dockerhub-metadata.yml | 24 +++++++++++++++--------- .github/workflows/release.yml | 7 ++++++- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.github/workflows/dockerhub-metadata.yml b/.github/workflows/dockerhub-metadata.yml index c36ccde..3d3e0f5 100644 --- a/.github/workflows/dockerhub-metadata.yml +++ b/.github/workflows/dockerhub-metadata.yml @@ -71,17 +71,23 @@ jobs: "https://hub.docker.com/v2/repositories/techbuzzz/$repo/" > /dev/null echo " description + overview synced" - # Categories come from a fixed Docker Hub taxonomy and have changed between UI - # revisions, so a rejected value here must not cost us the description above. - if jq -nc --argjson c "$categories" '{categories:$c}' \ - | curl -fsS -X PATCH \ - -H "Authorization: JWT ${JWT}" \ - -H 'Content-Type: application/json' \ - --data-binary @- \ - "https://hub.docker.com/v2/repositories/techbuzzz/$repo/" > /dev/null 2>&1; then + # Categories come from a fixed Docker Hub taxonomy keyed by slug, not by the + # display name shown in the UI, so the slugs below are the part most likely to + # need fixing. A rejected value must not cost us the description above, but it + # must not be silent either — Docker Hub echoes the accepted values in the error + # body, so print it instead of discarding it. + cats_resp="$(jq -nc --argjson c "$categories" '{categories:$c}' \ + | curl -sS -w $'\n%{http_code}' -X PATCH \ + -H "Authorization: JWT ${JWT}" \ + -H 'Content-Type: application/json' \ + --data-binary @- \ + "https://hub.docker.com/v2/repositories/techbuzzz/$repo/")" + cats_code="${cats_resp##*$'\n'}" + if [ "$cats_code" = "200" ] || [ "$cats_code" = "201" ]; then echo " categories synced: $categories" else - echo " ::warning::categories rejected for $repo ($categories) — description still applied" + echo " ::warning::categories rejected for $repo (HTTP $cats_code): $categories" + echo " Docker Hub said: $(printf '%s' "${cats_resp%$'\n'*}" | head -c 600)" fi done <<'REPOS' delibera-server|ASP.NET Core host for multi-model AI council debates: REST, SSE streaming, MCP|["web","messaging","ai-machine-learning","network"] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 189512d..08f22de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,4 +115,9 @@ jobs: # cannot manufacture a release pointing at a tag that does not exist. gh release create "$TAG" --title "$title" --notes-file /tmp/release-body.md --verify-tag fi - gh release view "$TAG" --json url,tagName,isLatest --jq '" -> \(.tagName) latest=\(.isLatest) \(.url)"' \ No newline at end of file + # Only fields `gh release view --json` actually exposes. `isLatest` is NOT one of + # them (it belongs to `gh release list`), and asking for it fails with + # "Unknown JSON field" — AFTER the release was created, turning a successful + # publish into a red run. + gh release view "$TAG" --json url,tagName,isDraft,isPrerelease \ + --jq '" -> \(.tagName) draft=\(.isDraft) prerelease=\(.isPrerelease) \(.url)"' \ No newline at end of file From 7a7eeba9de79d2dbdcceffcb4808f51a2693934e Mon Sep 17 00:00:00 2001 From: Viktor Buzin Date: Fri, 9 Oct 2026 19:25:36 +0300 Subject: [PATCH 04/19] docs: add a release runbook Captures the order that actually matters, written after this cycle hit every silent failure mode it describes. The two worth stating plainly, because neither raises an error: - Tagging does not set the package version. publish-nuget packs from the csproj, so a tag ahead of the csproj packs the previous version and --skip-duplicate discards it without a word. The job is green and nothing ships. release.yml now fails loudly on that mismatch, but only for the release page, and only after the packages are gone. - Docker Hub cards stay empty unless the two repositories exist and DOCKERHUB_USERNAME/DOCKERHUB_TOKEN are set. Missing secrets fail in zero seconds, which is how you tell them apart from a wrong token. Also records the pre-tag check that actually catches the first one -- pack the package and confirm the filename -- and the Pages source toggle, which the API cannot create. --- README-RU.md | 4 ++ README.md | 4 ++ docs/ReleaseProcess.md | 144 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 docs/ReleaseProcess.md diff --git a/README-RU.md b/README-RU.md index 48bf223..ffce263 100644 --- a/README-RU.md +++ b/README-RU.md @@ -280,6 +280,10 @@ dotnet build Delibera.slnx -c Release Из клона `docker compose up -d` поднимает весь стек — сервер, веб-интерфейс, Ollama, Qdrant, pgvector и Redis. Пошаговое руководство: [docs/QuickStart-RU.md](docs/QuickStart-RU.md). +> 🧑‍🔧 **Публикуете релиз или обновляетесь со старой версии?** Смотрите +> [docs/ReleaseProcess.md](docs/ReleaseProcess.md) — там описан порядок, который важен, и два +> шага, которые падают молча, а не с ошибкой. + ### Минимальный пример ```csharp diff --git a/README.md b/README.md index 2e4971f..e7dda15 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,10 @@ dotnet build Delibera.slnx -c Release From a clone, `docker compose up -d` starts the whole stack — server, Web UI, Ollama, Qdrant, pgvector and Redis. See [docs/QuickStart.md](docs/QuickStart.md) for the walkthrough. +> 🧑‍🔧 **Publishing a release, or upgrading from an older one?** See +> [docs/ReleaseProcess.md](docs/ReleaseProcess.md) — it documents the order that matters, and the +> two steps that fail silently rather than loudly. + ### Minimal Example ```csharp diff --git a/docs/ReleaseProcess.md b/docs/ReleaseProcess.md new file mode 100644 index 0000000..cc0e2de --- /dev/null +++ b/docs/ReleaseProcess.md @@ -0,0 +1,144 @@ +# Release process + +The order below matters, and it is not the order it looks like it should be. Every step that +looks skippable has a way of failing *silently* rather than loudly, and two of them did. + +Throughout: `` is the value in `src/Delibera.Core/Delibera.Core.csproj`. That file is the +single source of truth — not the tag, not `CHANGELOG.md`. + +--- + +## Before you tag + +### 1. Bump the version + +```bash +# All five packaged projects, plus the Web UI manifest. +grep -rl '10\.5\.2' src --include=*.csproj +``` + +`Delibera.Core`, `Delibera.Server`, `Delibera.Redis`, `Delibera.Grpc`, `Delibera.Grpc.Client`, +and `src/Delibera.WebUI/package.json`. + +Then release notes: + +- `PackageReleaseNotes` in `Delibera.Core.csproj` and `Delibera.Server.csproj` +- `CHANGELOG.md` — convert `[Unreleased]` into `## [] - ` +- `docs/WhatsNew-v.md` — hand-written; this is what the release page is built from + +> ⚠️ **The tag does not set the package version.** `publish-nuget.yml` packs from the csproj. If +> you tag `v10.5.3` while the csproj still says `10.5.2`, the workflow packs `10.5.2.nupkg` and +> `dotnet nuget push --skip-duplicate` discards it without a word, because `10.5.2` already +> exists. The job goes green and nothing is published. +> +> `release.yml` now fails loudly on exactly this mismatch — but only for the release page, and +> only after the packages are already gone. + +### 2. Open a PR and merge it + +CI runs three gates on the PR: `build & test`, `build (verify only)`, `webui tests`. Wait for all +three. Squash-merge — the repo squash-merges, so a feature branch's SHAs never become ancestors +of `main`. + +### 3. Verify before tagging + +```bash +dotnet build Delibera.slnx -c Release -warnaserror +dotnet test Delibera.slnx -c Release --no-build + +# The check that actually catches the silent failure: +dotnet pack src/Delibera.Core/Delibera.Core.csproj -c Release --output ./artifacts +ls ./artifacts # must contain .nupkg — not the previous one +``` + +--- + +## Cut the tag + +```bash +git tag -a v -m "v" +git push origin v +``` + +Pushing `v*` starts four things in parallel: + +| Workflow | What it does | +|---|---| +| `publish-nuget` | packs Core/Server/Redis, pushes to nuget.org (OIDC, no stored API key) | +| `publish-docker` | builds `linux/amd64` + `linux/arm64` for both images, pushes, then runtime-smokes them | +| `release` | creates the GitHub Release page from the WhatsNew doc + auto PR notes | +| `dockerhub-metadata` | PATCHes the Docker Hub description, overview and categories | + +### Docker Hub requires two things that are easy to miss + +- **The repositories must already exist.** `techbuzzz/delibera-server` and + `techbuzzz/delibera-webui`. `docker push` into a repository that does not exist fails, and the + workflow cannot create it. +- **`DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` must be set** as repository secrets. Settings → + Secrets and variables → Actions. A *scoped access token* with Read & Write — Docker Hub has + retired classic PATs. + +Missing secrets fail in **zero seconds** with `Username and password required`. That timing is +the tell: a wrong token takes about a second and says `unauthorized`. + +--- + +## After tagging + +### Check what actually landed + +Green pipelines are not proof that something was published: + +```bash +curl -s https://api.nuget.org/v3-flatcontainer/delibera.core/index.json | grep +curl -s https://hub.docker.com/v2/repositories/techbuzzz/delibera-server/tags | grep +``` + +If the nupkg pushed fine but the csproj version was stale, NuGet looks untouched and the run is +green. That is the failure mode `--skip-duplicate` creates. + +### Backfilling a release page + +If a tag already exists and no release page does, run **Actions → release → Run workflow** and +set the `tag` input. It creates or updates; it never duplicates. + +### Backfilling Docker Hub cards + +Same for **dockerhub-metadata → Run workflow**. The cards are empty by default when a repository +is created through the API — `docker push` writes no metadata. The overviews live in +`dockerhub/*.md` and are synced from there. + +--- + +## GitHub Pages (one-time setup) + +**Settings → Pages → Source → GitHub Actions.** The API returns 404 until this is set, and +creating it needs an admin token — it is not automatable from CI. + +`pages.yml` deploys the Web UI as a **static** export on every merge to `main`. Two consequences: + +- A static export has **no Nitro server**, so the `/api/delibera/**` BFF route does not exist + there. The browser needs an absolute origin via `NUXT_PUBLIC_DELIBERA_API_BASE`, and that + origin must allow the Pages origin via CORS — which `Delibera.Server` does not do by default. + Unset, the site deploys and its requests 404: a UI preview, not an install. +- Nuxt 4 emits to `.output/public`, **not** `dist`. GitHub's own sample workflow still uploads + `./dist` because it predates Nuxt 4. + +--- + +## Changing a workflow + +Workflow files are only parsed by GitHub when they trigger — for `release` and +`dockerhub-metadata`, that is the tag push. A broken `run:` block therefore surfaces at the worst +possible moment. + +Before pushing a workflow change, both of these are cheap and have each caught a real bug here: + +```bash +# structure: tabs, block scalars, `on:` key +# bash syntax of every extracted run: body (Git Bash or any Linux) +bash -n