diff --git a/playbooks/deploy-vor.yaml b/playbooks/deploy-vor.yaml new file mode 100644 index 0000000..6890181 --- /dev/null +++ b/playbooks/deploy-vor.yaml @@ -0,0 +1,116 @@ +# Deploy vor (Inquisitor UI, https://gitea.phasefinal.com/vh/vor) to a +# Docker host following the PFI /opt/docker/ convention. Default +# target is ana-docker; the playbook is host-agnostic. +# +# Idempotent: rerunning is safe. Creates-gates + conditional when: +# checks skip work that's already done; `docker compose up -d` is +# itself idempotent (no restart unless compose content or env changed). +# +# Usage: +# scripts/elway ana-docker --playbook playbooks/deploy-vor.yaml +# scripts/elway ana-docker --playbook playbooks/deploy-vor.yaml --var ref=v0.2.0 +# +# Prereqs on the target host: +# - Docker + docker compose plugin +# - `traefik-net` docker network (external) +# - Target user (lkraven) has git SSH access to gitea.phasefinal.com +# - Target user is in the `docker` group. + +vars: + repo_url: git@gitea.phasefinal.com:vh/vor.git + ref: main + build_dir: /opt/docker/build/vor + image_tag: vor:local + compose_dir: /opt/docker/compose/vor + conf_dir: /opt/docker/conf/vor + host_port: "7879" + +steps: + # ── host-side directory prep ───────────────────────────────────────── + - name: Ensure /opt/docker/build parent exists + shell: mkdir -p /opt/docker/build + sudo: true + creates: /opt/docker/build + + - name: Chown /opt/docker/build to lkraven (only if mkdir'd by root above) + shell: chown lkraven:lkraven /opt/docker/build + sudo: true + when: '[ "$(stat -c %U /opt/docker/build)" != lkraven ]' + + # ── fetch / sync source ───────────────────────────────────────────── + - name: Clone vor repo if absent + shell: GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new" git clone {{ repo_url }} {{ build_dir }} + creates: "{{ build_dir }}/.git" + + - name: Fetch from origin + shell: cd {{ build_dir }} && git fetch --quiet origin + + - name: Reset working tree to {{ ref }} + # Accept either a branch name (resolves via origin/) or a + # full/short SHA (resolves directly). CI passes the triggering + # commit SHA via --var ref=${{ github.sha }}; manual runs pass + # branch names like main / v0.2.0. + shell: | + cd {{ build_dir }} + if sha=$(git rev-parse --verify --quiet "origin/{{ ref }}^{commit}"); then :; + elif sha=$(git rev-parse --verify --quiet "{{ ref }}^{commit}"); then :; + else echo "elway: ref not found: {{ ref }}" >&2; exit 1; fi + git reset --hard "$sha" + changed_when: '[ "$(cd {{ build_dir }} && git rev-parse HEAD)" != "$(cd {{ build_dir }} && (git rev-parse --verify --quiet "origin/{{ ref }}^{commit}" || git rev-parse --verify --quiet "{{ ref }}^{commit}"))" ]' + + # ── image build ───────────────────────────────────────────────────── + - name: Build image {{ image_tag }} + shell: cd {{ build_dir }} && docker build -t {{ image_tag }} . + + # ── compose + persistence dirs ────────────────────────────────────── + - name: Ensure compose dir exists + shell: mkdir -p {{ compose_dir }} + creates: "{{ compose_dir }}" + + - name: Ensure sessions dir exists + shell: mkdir -p {{ conf_dir }}/sessions + creates: "{{ conf_dir }}/sessions" + + - name: Ensure responses dir exists + shell: mkdir -p {{ conf_dir }}/responses + creates: "{{ conf_dir }}/responses" + + # ── deploy compose files ──────────────────────────────────────────── + - name: Upload compose.yaml + upload: + src: stacks/vor/compose.yaml + dest: "{{ compose_dir }}/compose.yaml" + mode: "0644" + + - name: Seed .env from template (only if absent) + upload: + src: stacks/vor/.env.example + dest: "{{ compose_dir }}/.env" + mode: "0644" + when: "[ ! -f {{ compose_dir }}/.env ]" + + # ── bring up + wait for ready ─────────────────────────────────────── + - name: docker compose up -d + shell: cd {{ compose_dir }} && docker compose up -d + + - name: Wait for /api/sessions to respond + shell: | + for i in $(seq 1 30); do + curl -sf -o /dev/null http://localhost:{{ host_port }}/api/sessions && exit 0 + sleep 1 + done + exit 1 + changed_when: "false" + +verify: + - name: /api/sessions returns 200 + shell: curl -sf -o /dev/null http://localhost:{{ host_port }}/api/sessions + changed_when: "false" + + - name: / returns the UI shell + shell: curl -sf http://localhost:{{ host_port }}/ | grep -q -i 'inquisitor\|vor' + changed_when: "false" + + - name: Container is in the traefik-net network + shell: docker inspect vor --format '{{json .NetworkSettings.Networks}}' | grep -q traefik-net + changed_when: "false" diff --git a/stacks/vor/.env.example b/stacks/vor/.env.example new file mode 100644 index 0000000..3c0a1e0 --- /dev/null +++ b/stacks/vor/.env.example @@ -0,0 +1,13 @@ +# Built locally from vh/vor by the deploy playbook; not pulled from a registry. +VOR_IMAGE=vor:local + +# Host port. Adjacent to task-board's 7878 since both are claude-tooling +# sidecars; keep them grouped for muscle memory. +VOR_PORT=7879 +VOR_BIND=0.0.0.0 + +# Persistence — sessions/ holds Claude's posted markdown, +# responses/ holds the user's published response markdown. Both are +# bind-mounted so they survive `docker compose up --force-recreate`. +VOR_SESSIONS_DIR=/opt/docker/conf/vor/sessions +VOR_RESPONSES_DIR=/opt/docker/conf/vor/responses diff --git a/stacks/vor/README.md b/stacks/vor/README.md new file mode 100644 index 0000000..d26e5dd --- /dev/null +++ b/stacks/vor/README.md @@ -0,0 +1,61 @@ +# vor + +[Inquisitor UI](https://gitea.phasefinal.com/vh/vor) deployed centrally. +A FastAPI sidecar that renders Claude's `/inquisitor` markdown as a +fillable form — Claude POSTs sessions, user fills + publishes via +browser, Claude reads the response back. + +**Server:** ana-docker +**Port:** 7879 (configurable via `.env`) +**Upstream repo:** [vh/vor](https://gitea.phasefinal.com/vh/vor) +**Image:** `vor:local` — built on the host from the git repo by the +deploy playbook. Not pulled from a registry. + +## Centralized vs local + +The upstream README describes a **local** deployment pattern (Claude +on the workstation posts to `127.0.0.1:8765`, response file lives on +the local filesystem). Running vor on ana-docker changes that: + +- Claude POSTs to `http://10.250.50.70:7879/api/sessions` +- The browser opens `http://10.250.50.70:7879/?session=` +- The response markdown is **not** written to a path Claude can read + off its own disk; Claude reads it back via + `GET /api/sessions/{id}` (`response_markdown` field) instead. + +Trade: central + always-on + multi-device (phone, tablet, second +workstation) at the cost of a network round-trip and an HTTP-fetch +step in Claude's flow instead of a file read. + +## Deploy + +Two paths — automated (preferred) and manual. + +### Automated (Gitea Actions, push-to-main) + +Every push to `main` on `vh/vor` triggers a deploy via the central +gitea-runner. Workflow template lives next to this README at +[`gitea-workflow-deploy.yaml.example`](gitea-workflow-deploy.yaml.example); +copy into the vor repo at `.gitea/workflows/deploy.yaml` (already +done on first wiring). Reuses the `DEPLOY_SSH_KEY` and +`MGMT_REPO_TOKEN` secrets at user scope; no per-repo setup needed +once those exist. + +### Manual (elway from a workstation) + +```bash +# First deploy / update to latest main +scripts/elway ana-docker --playbook playbooks/deploy-vor.yaml + +# Pin to a specific ref +scripts/elway ana-docker --playbook playbooks/deploy-vor.yaml --var ref=v0.2.0 +``` + +## Path layout (on ana-docker) + +| Host path | Container path | Purpose | Restic? | +|---|---|---|---| +| `/opt/docker/build/vor/` | — | git checkout used as docker build context | excluded | +| `/opt/docker/compose/vor/` | — | compose.yaml + .env | included (via `/opt/docker`) | +| `/opt/docker/conf/vor/sessions/` | `/app/sessions` | Claude-posted markdown | **included** | +| `/opt/docker/conf/vor/responses/` | `/app/responses` | user-published response markdown | **included** | diff --git a/stacks/vor/compose.yaml b/stacks/vor/compose.yaml new file mode 100644 index 0000000..126a8d6 --- /dev/null +++ b/stacks/vor/compose.yaml @@ -0,0 +1,45 @@ +# vor — Inquisitor UI sidecar. +# +# Image is built on the host from the vor git repo by the deploy +# playbook (`playbooks/deploy-vor.yaml`), which clones into +# /opt/docker/build/vor and runs `docker build -t vor:local .` before +# installing this compose and bringing it up. +# +# Sessions + responses persist under bind mounts so container rebuilds +# don't lose user-published response files (which Claude reads via the +# /api/sessions/{id} JSON path). +# +# All tunables live in .env — edit that, not this file. + +services: + vor: + image: ${VOR_IMAGE} + container_name: vor + restart: unless-stopped + ports: + - "${VOR_BIND:-0.0.0.0}:${VOR_PORT}:8765" + volumes: + - ${VOR_SESSIONS_DIR}:/app/sessions + - ${VOR_RESPONSES_DIR}:/app/responses + environment: + # Container always listens on 8765 internally; host port is the only knob. + - PYTHONUNBUFFERED=1 + healthcheck: + test: ["CMD-SHELL", "python -c 'import urllib.request,sys; r=urllib.request.urlopen(\"http://127.0.0.1:8765/api/sessions\",timeout=3); sys.exit(0 if r.status==200 else 1)' || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - tnet + labels: + - homepage.group=Toolchain + - homepage.name=vor + - homepage.icon=mdi-clipboard-text-search + - homepage.description=Inquisitor UI — Claude session ↔ user response sidecar + - homepage.href=http://10.250.50.70:${VOR_PORT} + +networks: + tnet: + name: traefik-net + external: true diff --git a/stacks/vor/gitea-workflow-deploy.yaml.example b/stacks/vor/gitea-workflow-deploy.yaml.example new file mode 100644 index 0000000..4ce2c1a --- /dev/null +++ b/stacks/vor/gitea-workflow-deploy.yaml.example @@ -0,0 +1,65 @@ +# Gitea Actions workflow for vor. +# +# THIS FILE LIVES IN THE VOR REPO, NOT HERE. +# Copy to vh/vor:.gitea/workflows/deploy.yaml and commit. +# +# Identical pattern to stacks/task-board/gitea-workflow-deploy.yaml.example +# — checks out vor + eshpfi-management, then runs the elway playbook +# pinned to the triggering commit SHA. The playbook is the single +# source of truth for "how vor is deployed", manual or automated. +# +# Required Actions secrets (already set at user scope from earlier +# task-board wiring; nothing new needed): +# +# DEPLOY_SSH_KEY Private SSH key authorized for lkraven@ana-docker. +# MGMT_REPO_TOKEN Gitea PAT (read:repository) on vh/esh-pfi-infrastructure. + +name: Deploy vor + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: pfi-fleet + + steps: + - name: Install playbook prerequisites + run: | + apt-get update -qq + apt-get install -y --no-install-recommends \ + python3 python3-yaml openssh-client + rm -rf /var/lib/apt/lists/* + + - name: Checkout vor (triggering repo) + uses: actions/checkout@v4 + + - name: Checkout management repo (eshpfi-management) + uses: actions/checkout@v4 + with: + repository: vh/esh-pfi-infrastructure + token: ${{ secrets.MGMT_REPO_TOKEN }} + path: _mgmt + + - name: Configure SSH to ana-docker + run: | + mkdir -p ~/.ssh + printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + cat > ~/.ssh/config <<'EOF' + Host ana-docker + HostName 10.250.50.70 + User lkraven + IdentityFile ~/.ssh/id_ed25519 + StrictHostKeyChecking accept-new + EOF + chmod 600 ~/.ssh/config + + - name: Deploy vor (elway playbook, pinned to this commit) + working-directory: _mgmt + run: | + scripts/elway ana-docker \ + --playbook playbooks/deploy-vor.yaml \ + --var ref=${{ github.sha }}