From 9bde5960bfa8b5d58135c1a4015a11760383df29 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 8 Sep 2026 22:33:59 -0500 Subject: [PATCH] fix(deploy): collab URL, CT 116 prod compose, Forgejo migration - Read NEXT_PUBLIC_COLLAB_SERVER_URL in yjs provider; bake NEXT_PUBLIC_* at Docker build - Add prod compose, deploy scripts, CT 116 proxmox scaffolding - Forgejo Actions CI/deploy; migrate-origin and post-migration scripts --- .env.example | 55 +++- .forgejo/workflows/ci.yml | 41 +++ .forgejo/workflows/deploy-homelab.yml | 40 +++ .github/workflows/deploy-homelab.yml | 41 +++ .../[workspaceSlug]/settings/repos/page.tsx | 54 +++- apps/web/lib/yjs-provider.ts | 21 +- apps/worker/src/env.ts | 7 +- apps/worker/src/executor/remote-ag-ui.ts | 2 +- apps/worker/src/handle-job.test.ts | 4 + apps/worker/src/handle-job.ts | 2 +- docker/Dockerfile | 20 +- docker/docker-compose.agent-server.yml | 63 +---- docker/docker-compose.coolify.yml | 209 +-------------- docker/docker-compose.prod.standalone.yml | 14 + docker/docker-compose.prod.yml | 244 ++++++++++++++++++ docker/worker-entrypoint.sh | 17 ++ docs/deployment-production.md | 169 ++++++++++++ docs/forgejo-migration.md | 168 ++++++++++++ logs/app.log | 15 ++ package.json | 3 +- proxmox/ct116/README.md | 133 ++++++++++ proxmox/ct116/bootstrap.sh | 56 ++++ proxmox/ct116/deploy-webhook.sh | 43 +++ proxmox/ct116/traefik-ct100.example.yml | 35 +++ proxmox/ct116/webhook.hooks.json.example | 25 ++ scripts/build-coolify.sh | 27 +- scripts/build-prod.sh | 29 +++ scripts/ct116-post-migration.sh | 46 ++++ scripts/deploy-prod.sh | 66 +++++ scripts/git-sync-deploy.sh | 20 ++ scripts/migrate-origin-to-forgejo.sh | 52 ++++ 31 files changed, 1404 insertions(+), 317 deletions(-) create mode 100644 .forgejo/workflows/ci.yml create mode 100644 .forgejo/workflows/deploy-homelab.yml create mode 100644 .github/workflows/deploy-homelab.yml create mode 100644 docker/docker-compose.prod.standalone.yml create mode 100644 docker/docker-compose.prod.yml create mode 100755 docker/worker-entrypoint.sh create mode 100644 docs/deployment-production.md create mode 100644 docs/forgejo-migration.md create mode 100644 logs/app.log create mode 100644 proxmox/ct116/README.md create mode 100755 proxmox/ct116/bootstrap.sh create mode 100755 proxmox/ct116/deploy-webhook.sh create mode 100644 proxmox/ct116/traefik-ct100.example.yml create mode 100644 proxmox/ct116/webhook.hooks.json.example create mode 100755 scripts/build-prod.sh create mode 100755 scripts/ct116-post-migration.sh create mode 100755 scripts/deploy-prod.sh create mode 100755 scripts/git-sync-deploy.sh create mode 100755 scripts/migrate-origin-to-forgejo.sh diff --git a/.env.example b/.env.example index 1eccec9..6270c7f 100644 --- a/.env.example +++ b/.env.example @@ -2,11 +2,9 @@ # ECHODO — Environment Variables # ===================================================================== # Local dev: copy this file to .env and adjust values. -# Coolify (CT 107): paste these into the resource's "Environment Variables" -# tab. Coolify also accepts a .env file uploaded via the UI. -# -# See AGENT-DEPLOY.md for shared CT 102 service credentials and the -# CT 100 Traefik routing pattern. +# Production (CT 116 standalone LXC): paste into /opt/submatter/.env on the host. +# See docs/deployment-production.md and proxmox/ct116/README.md. +# See AGENT-DEPLOY.md for shared CT 102 service credentials and Traefik routing. # ===================================================================== # ----- Database (CT 102 shared Postgres) ----- @@ -44,6 +42,16 @@ REDIS_URL="redis://:CHANGE_ME@192.168.68.102:6379/11" # WORKER_CHECKPOINT_EVERY_N_STEPS=1 # WORKER_REPO_ROOT= # defaults to monorepo root (pnpm-workspace.yaml walk-up) # WORKER_WORKTREE_BASE=/tmp/echodo-worker-worktrees +# +# Phase B — OpenHands agent-server (optional; requires --profile agent-server) +# WORKER_SESSION_TYPE=minimal # set to `openhands` when agent-server is running +# AGENT_SERVER_URL=http://agent-server:8001 +# WORKER_OPENHANDS_WORKSPACE=/workspace/repo +# SESSION_API_KEY= # must match LOCAL_BACKEND_API_KEY on agent-server +# LOCAL_BACKEND_API_KEY= # generate: openssl rand -hex 32 +# OMNIROUTE_BASE_URL=https://omniroute.stillwell.cloud/v1 +# OMNIROUTE_API_KEY= +# OMNIROUTE_MODEL=auto/coding # ----- Auth (NextAuth v5) ----- # Local: http://localhost:3000 • Prod: https://echodo.stillwell.cloud @@ -72,10 +80,11 @@ AUTH_AUTHENTIK_ISSUER="https://auth.stillwell.cloud/application/o/echodo/" # ----- Realtime collab server (Hocuspocus / Yjs) ----- # Server-side reference (used by Next.js server actions, if any) COLLAB_SERVER_URL="ws://localhost:1234" -# Browser-facing — must be wss:// in production because the page is https +# Browser-facing — must be wss:// in production because the page is https. +# Baked into the web image at BUILD time (docker build args) — change requires rebuild. NEXT_PUBLIC_COLLAB_SERVER_URL="ws://localhost:1234" -# ----- Umami Analytics (CT 107 — analytics.stillwell.cloud) ----- +# ----- Umami Analytics (CT 112 — analytics.stillwell.cloud) ----- # In Umami: Settings → Websites → Add website. Copy the website UUID. NEXT_PUBLIC_UMAMI_SCRIPT="https://analytics.stillwell.cloud/script.js" NEXT_PUBLIC_UMAMI_WEBSITE_ID="" @@ -84,7 +93,7 @@ NEXT_PUBLIC_UMAMI_WEBSITE_ID="" NEXT_PUBLIC_LIBREDESK_URL="https://support.stillwell.cloud" NEXT_PUBLIC_LIBREDESK_WIDGET_ID="" -# ----- Directus Feedback (CT 107 — directus.stillwell.cloud) ----- +# ----- Directus Feedback (archived — was CT 107) ----- # Static token scoped write-only on the `feedback` collection NEXT_PUBLIC_DIRECTUS_URL="https://directus.stillwell.cloud" DIRECTUS_FEEDBACK_TOKEN="" @@ -110,20 +119,40 @@ ANTHROPIC_API_KEY="" GOOGLE_GENERATIVE_AI_API_KEY="" NVIDIA_API_KEY="" +# ----- OmniRoute (CT 108 — omniroute.stillwell.cloud) ----- +# Used by worker + web (Settings → AI provider tests) + Phase B agent-server. +OMNIROUTE_BASE_URL="https://omniroute.stillwell.cloud/v1" +OMNIROUTE_API_KEY="" +OMNIROUTE_MODEL=auto/coding + +# ----- Connected repos (Settings → Connected repos) ----- +# Token values live here; Settings stores only the env var NAME (e.g. FORGEJO_TOKEN). +# Forgejo self-hosted: +# Remote URL: https://forgejo.stillwell.cloud/rstillwell/Submatter.git +# Token env var name in UI: FORGEJO_TOKEN +FORGEJO_TOKEN="" +# GitHub.com: +# Remote URL: https://github.com/org/repo.git +# Token env var name in UI: GITHUB_TOKEN +GITHUB_TOKEN="" + +# ----- Forge webhooks (PR merged → convoy transition) ----- +FORGE_WEBHOOK_SECRET="" + # ----- MCP Server ----- MCP_SERVER_PORT=3001 # ===================================================================== -# Coolify Docker Compose build (8 GB hosts) +# Production Docker Compose build (8 GB hosts) # ===================================================================== # Serialize image builds to avoid kernel OOM (exit 255) during export. -# Coolify (8 GB host): set Custom Build Command to `bash scripts/build-coolify.sh` -# so compose builds collab → mcp → orchestrator → worker → web sequentially. -# COMPOSE_PARALLEL_LIMIT is documented for reference but the script is required. +# Dokploy / standalone: set Custom Build Command to `bash scripts/build-prod.sh` +# Local verify: pnpm docker:build:prod +# Legacy alias: pnpm docker:build:coolify # ===================================================================== # Local Docker Compose only (docker/docker-compose.yml) -# Not needed for Coolify (Coolify uses CT 102's shared Postgres/Redis). +# Not needed for production (uses CT 102 shared Postgres/Redis). # ===================================================================== POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..7cc9638 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + check: + name: Lint, type-check, test + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Type-check + run: pnpm type-check + + - name: Test + run: pnpm test diff --git a/.forgejo/workflows/deploy-homelab.yml b/.forgejo/workflows/deploy-homelab.yml new file mode 100644 index 0000000..7adb765 --- /dev/null +++ b/.forgejo/workflows/deploy-homelab.yml @@ -0,0 +1,40 @@ +name: Deploy homelab (CT 116) + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: deploy-homelab-main + cancel-in-progress: true + +jobs: + deploy: + name: Deploy to CT 116 + runs-on: [self-hosted, linux, axiom] + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Sync and deploy over SSH + env: + DEPLOY_HOST: ${{ secrets.HOMELAB_DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.HOMELAB_DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.HOMELAB_DEPLOY_PATH }} + DEPLOY_SSH_KEY: ${{ secrets.HOMELAB_DEPLOY_SSH_KEY }} + run: | + set -euo pipefail + if [[ -z "${DEPLOY_HOST:-}" || -z "${DEPLOY_USER:-}" || -z "${DEPLOY_SSH_KEY:-}" ]]; then + echo "Skipping deploy: set HOMELAB_DEPLOY_HOST, HOMELAB_DEPLOY_USER, HOMELAB_DEPLOY_SSH_KEY repo secrets." + exit 0 + fi + deploy_path="${DEPLOY_PATH:-/opt/submatter}" + mkdir -p ~/.ssh + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new \ + "${DEPLOY_USER}@${DEPLOY_HOST}" \ + "set -euo pipefail; cd '${deploy_path}' && bash scripts/git-sync-deploy.sh && bash scripts/deploy-prod.sh build" diff --git a/.github/workflows/deploy-homelab.yml b/.github/workflows/deploy-homelab.yml new file mode 100644 index 0000000..20e7a94 --- /dev/null +++ b/.github/workflows/deploy-homelab.yml @@ -0,0 +1,41 @@ +name: Deploy homelab (CT 116) + +on: + push: + branches: [main] + workflow_dispatch: + +# CI already ran on the PR; main deploy is serialized and can take 15–30 min on axiom. +concurrency: + group: deploy-homelab-main + cancel-in-progress: true + +jobs: + deploy: + name: Deploy to CT 116 + runs-on: [self-hosted, linux, axiom] + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Sync and deploy over SSH + env: + DEPLOY_HOST: ${{ secrets.HOMELAB_DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.HOMELAB_DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.HOMELAB_DEPLOY_PATH }} + DEPLOY_SSH_KEY: ${{ secrets.HOMELAB_DEPLOY_SSH_KEY }} + run: | + set -euo pipefail + if [[ -z "${DEPLOY_HOST:-}" || -z "${DEPLOY_USER:-}" || -z "${DEPLOY_SSH_KEY:-}" ]]; then + echo "Skipping deploy: set HOMELAB_DEPLOY_HOST, HOMELAB_DEPLOY_USER, HOMELAB_DEPLOY_SSH_KEY repo secrets." + exit 0 + fi + deploy_path="${DEPLOY_PATH:-/opt/submatter}" + mkdir -p ~/.ssh + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new \ + "${DEPLOY_USER}@${DEPLOY_HOST}" \ + "set -euo pipefail; cd '${deploy_path}' && bash scripts/git-sync-deploy.sh && bash scripts/deploy-prod.sh build" diff --git a/apps/web/app/(app)/[workspaceSlug]/settings/repos/page.tsx b/apps/web/app/(app)/[workspaceSlug]/settings/repos/page.tsx index 034398d..cdbf095 100644 --- a/apps/web/app/(app)/[workspaceSlug]/settings/repos/page.tsx +++ b/apps/web/app/(app)/[workspaceSlug]/settings/repos/page.tsx @@ -73,6 +73,7 @@ export default function ConnectedReposSettingsPage() { setParsedPreview(null); }, }); + const [connectError, setConnectError] = React.useState(null); const disconnectMut = api.connectedRepos.disconnect.useMutation({ onSuccess: () => void utils.connectedRepos.list.invalidate(), @@ -98,6 +99,7 @@ export default function ConnectedReposSettingsPage() { const openCreate = () => { setForm(EMPTY_FORM); setParsedPreview(null); + setConnectError(null); setDialogOpen(true); }; @@ -112,12 +114,18 @@ export default function ConnectedReposSettingsPage() { if (!workspaceSlug || !form.remoteUrl.trim()) { return; } - connectMut.mutate({ - workspace: workspaceSlug, - remoteUrl: form.remoteUrl.trim(), - displayName: form.displayName.trim() || undefined, - tokenEnvVar: form.tokenEnvVar.trim() || null, - }); + setConnectError(null); + connectMut.mutate( + { + workspace: workspaceSlug, + remoteUrl: form.remoteUrl.trim(), + displayName: form.displayName.trim() || undefined, + tokenEnvVar: form.tokenEnvVar.trim() || null, + }, + { + onError: (err) => setConnectError(err.message), + }, + ); }; return ( @@ -126,9 +134,27 @@ export default function ConnectedReposSettingsPage() {

Connected repos

- Paste a git remote URL to link Forgejo, GitHub, or other forges. API tokens are - referenced by env var name — never stored in the database. + Link a git remote for worker automation (draft PRs, forge webhooks). Paste the clone + URL — OAuth repo pickers are planned; today you set the API token on the server and + reference its env var name here.

+
+ Examples +
    +
  • + Forgejo (self-hosted):{" "} + https://forgejo.stillwell.cloud/rstillwell/Submatter.git + {" — "}token env var FORGEJO_TOKEN in{" "} + /opt/submatter/.env, then click Test connection. +
  • +
  • + GitHub.com:{" "} + https://github.com/org/repo.git + {" — "}token env var GITHUB_TOKEN (PAT with repo + scope). +
  • +
+
{canManage && (