From 9c009b07dbd59d963bc6f6654b98c894c8b3f451 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 3 Jul 2026 15:20:14 +0000 Subject: [PATCH] sx-gitea deploy: live serving for sx.sx-web.org MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/gitea/serve.sx: durable live forge on the kernel persist store (SX_PERSIST_DIR) with idempotent seeding (instance id, admin user + rotating token, welcome repo), blocking in the native http-listen loop via host/native-handler — the same wiring that serves blog.rose-ash.com. lib/gitea/serve.sh: full-stack launcher (every substrate the eight phases compose, in dependency order, + dream/session for the cookie bridge) — container entrypoint and local launcher in one. docker-compose.dev-sx-gitea.yml: sx_docs image, bind-mounted worktree + binary, /root/sx-gitea-persist for durable state, externalnet so Caddy can proxy sx.sx-web.org. Serving JIT off until validated for this path. Smoke-tested locally: pages, authed API, markdown-rendered issues, pkt-line ref advertisement, 401 gating, and full state survival across a restart against the same persist dir. Co-Authored-By: Claude Fable 5 --- docker-compose.dev-sx-gitea.yml | 44 ++++++++ lib/gitea/serve.sh | 177 ++++++++++++++++++++++++++++++++ lib/gitea/serve.sx | 61 +++++++++++ 3 files changed, 282 insertions(+) create mode 100644 docker-compose.dev-sx-gitea.yml create mode 100644 lib/gitea/serve.sh create mode 100644 lib/gitea/serve.sx diff --git a/docker-compose.dev-sx-gitea.yml b/docker-compose.dev-sx-gitea.yml new file mode 100644 index 00000000..bb88eb9c --- /dev/null +++ b/docker-compose.dev-sx-gitea.yml @@ -0,0 +1,44 @@ +# sx-gitea live service — the federated git forge (lib/gitea) served by the +# native http-listen server via lib/gitea/serve.sh. Joins externalnet so Caddy +# can reverse_proxy sx.sx-web.org to it. Durable state on a host dir. +# +# Usage: +# sudo mkdir -p /root/sx-gitea-persist && sudo chown 10001:10001 /root/sx-gitea-persist +# docker compose -p sx-gitea -f docker-compose.dev-sx-gitea.yml up -d +# docker compose -p sx-gitea -f docker-compose.dev-sx-gitea.yml logs -f +# docker compose -p sx-gitea -f docker-compose.dev-sx-gitea.yml down + +services: + sx_gitea: + image: registry.rose-ash.com:5000/sx_docs:latest + container_name: sx-gitea-1 + entrypoint: ["bash", "/app/lib/gitea/serve.sh"] + working_dir: /app + environment: + SX_PROJECT_DIR: /app + SX_SERVER: /app/bin/sx_server + HOST_PORT: "8000" + # Bind all interfaces so Caddy (on externalnet) can reach it. + SX_HTTP_HOST: "0.0.0.0" + # Durable persist store root — repos/issues/PRs/tokens survive restarts. + SX_PERSIST_DIR: /data/persist + # Forge identity + admin. The token gates every mutation (repo create, + # push, issues, PRs) — rotate by editing here and recreating. + SX_INSTANCE: "sx.sx-web.org" + SX_GITEA_ADMIN: "giles" + SX_GITEA_TOKEN: "sxg-9f2e6c81a4d35b07" + OCAMLRUNPARAM: "b" + # Serving JIT stays OFF for the forge until validated under it. + volumes: + - ./spec:/app/spec:ro + - ./lib:/app/lib:ro + - ./hosts/ocaml/_build/default/bin/sx_server.exe:/app/bin/sx_server:ro + - /root/sx-gitea-persist:/data/persist + networks: + - externalnet + - default + restart: unless-stopped + +networks: + externalnet: + external: true diff --git a/lib/gitea/serve.sh b/lib/gitea/serve.sh new file mode 100644 index 00000000..80a87d91 --- /dev/null +++ b/lib/gitea/serve.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# sx-gitea live server launcher. Loads the FULL forge stack (every substrate +# the eight phases compose: git, datalog/acl, dream, smalltalk/content, +# relations, scheme/flow, apl/feed, events, haskell/search, fed) into one +# sx_server process and calls (gitea/serve! PORT ...) — the native +# http-listen loop serving the dream app. Runs in the FOREGROUND, so this +# doubles as a container entrypoint and a local launcher. +# +# Usage: +# bash lib/gitea/serve.sh # serve on $HOST_PORT (default 8940) +# HOST_PORT=8941 bash lib/gitea/serve.sh +# +# Env: +# SX_INSTANCE instance id baked into AP actor ids (default sx.sx-web.org) +# SX_GITEA_ADMIN admin username (default giles) +# SX_GITEA_TOKEN admin bearer token (REQUIRED in production) +# SX_PERSIST_DIR durable store root (kernel persist ops) +# SX_HTTP_HOST bind address (0.0.0.0 in containers) + +set -uo pipefail +cd "${SX_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo .)}" + +SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" +if [ ! -x "$SX_SERVER" ]; then + SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe" +fi +if [ ! -x "$SX_SERVER" ]; then + echo "ERROR: sx_server.exe not found." >&2 + exit 1 +fi + +PORT="${HOST_PORT:-8940}" +INSTANCE="${SX_INSTANCE:-sx.sx-web.org}" +ADMIN="${SX_GITEA_ADMIN:-giles}" +TOKEN="${SX_GITEA_TOKEN:-dev-token-change-me}" + +# The full stack, in dependency order — the same lists the conformance +# runner uses (base MODULES + every suite's extras), plus the host +# native-handler bridge and the serve boot module. +MODULES=( + # kernel + parser (sx-parse for the wire protocol) + "spec/stdlib.sx" + "spec/parser.sx" + "lib/r7rs.sx" + # persist (durable backend rides kernel persist/* ops) + "lib/persist/event.sx" + "lib/persist/backend.sx" + "lib/persist/log.sx" + "lib/persist/kv.sx" + "lib/persist/durable.sx" + # canonical form + sx-git + "lib/artdag/dag.sx" + "lib/git/object.sx" + "lib/git/ref.sx" + "lib/git/dag.sx" + "lib/git/worktree.sx" + "lib/git/diff.sx" + "lib/git/merge.sx" + "lib/git/porcelain.sx" + # datalog + acl (access control) + "lib/datalog/tokenizer.sx" + "lib/datalog/parser.sx" + "lib/datalog/unify.sx" + "lib/datalog/db.sx" + "lib/datalog/builtins.sx" + "lib/datalog/aggregates.sx" + "lib/datalog/strata.sx" + "lib/datalog/eval.sx" + "lib/datalog/api.sx" + "lib/datalog/magic.sx" + "lib/acl/schema.sx" + "lib/acl/facts.sx" + "lib/acl/engine.sx" + # dream (web) + "lib/dream/types.sx" + "lib/dream/router.sx" + "lib/dream/middleware.sx" + "lib/dream/error.sx" + "lib/dream/html.sx" + "lib/dream/json.sx" + "lib/dream/auth.sx" + "lib/dream/session.sx" + "lib/dream/api.sx" + # relations (issue graph) + "lib/relations/schema.sx" + "lib/relations/engine.sx" + "lib/relations/api.sx" + # smalltalk + content (markdown issue/PR bodies) + "lib/smalltalk/tokenizer.sx" + "lib/smalltalk/parser.sx" + "lib/guest/reflective/class-chain.sx" + "lib/smalltalk/runtime.sx" + "lib/guest/reflective/env.sx" + "lib/smalltalk/eval.sx" + "lib/content/block.sx" + "lib/content/doc.sx" + "lib/content/render.sx" + "lib/content/api.sx" + "lib/content/meta.sx" + "lib/content/text.sx" + "lib/content/section.sx" + "lib/content/table.sx" + "lib/content/markdown.sx" + "lib/content/md-import.sx" + # scheme + flow (PR lifecycle, notification delivery) + "lib/guest/lex.sx" + "lib/guest/reflective/quoting.sx" + "lib/scheme/parser.sx" + "lib/scheme/eval.sx" + "lib/scheme/runtime.sx" + "lib/flow/spec.sx" + "lib/flow/store.sx" + "lib/flow/remote.sx" + "lib/flow/host.sx" + "lib/flow/api.sx" + # apl + feed (timelines) + events notify + "lib/apl/runtime.sx" + "lib/feed/normalize.sx" + "lib/feed/stream.sx" + "lib/feed/api.sx" + "lib/feed/fanout.sx" + "lib/feed/dedupe.sx" + "lib/feed/aggregate.sx" + "lib/feed/rank.sx" + "lib/feed/acl.sx" + "lib/feed/mute.sx" + "lib/feed/page.sx" + "lib/feed/notify.sx" + "lib/feed/home.sx" + "lib/feed/fed.sx" + "lib/events/notify.sx" + # haskell + search + "lib/haskell/tokenizer.sx" + "lib/haskell/layout.sx" + "lib/haskell/parser.sx" + "lib/haskell/desugar.sx" + "lib/haskell/runtime.sx" + "lib/haskell/match.sx" + "lib/haskell/eval.sx" + "lib/haskell/map.sx" + "lib/haskell/set.sx" + "lib/haskell/testlib.sx" + "lib/search/tokenize.sx" + "lib/search/index.sx" + "lib/search/query.sx" + "lib/search/parse.sx" + "lib/search/rank.sx" + "lib/search/rankq.sx" + "lib/search/testlib.sx" + # the forge + "lib/gitea/repo.sx" + "lib/gitea/access.sx" + "lib/gitea/web.sx" + "lib/gitea/wire.sx" + "lib/gitea/issues.sx" + "lib/gitea/pr.sx" + "lib/gitea/activity.sx" + "lib/gitea/search.sx" + "lib/gitea/fed.sx" + # native http bridge + serve boot + "lib/host/server.sx" + "lib/gitea/serve.sx" +) + +EPOCH=1 +{ + for M in "${MODULES[@]}"; do + echo "(epoch $EPOCH)"; echo "(load \"$M\")"; EPOCH=$((EPOCH+1)) + done + # content classes must exist before the first issue/PR page renders + echo "(epoch $EPOCH)" + echo "(eval \"(begin (st-bootstrap-classes!) (content/bootstrap!) (content-bootstrap-markdown!) (content-bootstrap-table!))\")" + EPOCH=$((EPOCH+1)) + # boot the durable forge + block in the native server loop + echo "(epoch $EPOCH)" + echo "(eval \"(gitea/serve! $PORT \\\"$INSTANCE\\\" \\\"$ADMIN\\\" \\\"$TOKEN\\\")\")" +} | exec "$SX_SERVER" diff --git a/lib/gitea/serve.sx b/lib/gitea/serve.sx new file mode 100644 index 00000000..2030f8bd --- /dev/null +++ b/lib/gitea/serve.sx @@ -0,0 +1,61 @@ +; lib/gitea/serve.sx — live-serving boot for sx-gitea. +; +; The live forge runs on the kernel's DURABLE persist backend (perform -> +; sx_persist_store under $SX_PERSIST_DIR), so repos, issues, PRs, tokens +; and activity all survive restarts. Seeding is idempotent: the instance +; id and admin token are (re)written each boot (rotating the env rotates +; the token), the admin user and welcome repo create-only. +; +; gitea/serve! blocks inside the kernel http-listen loop, bridging the +; dream app through host/native-handler — the exact wiring that serves +; blog.rose-ash.com. +; +; Requires: the full gitea stack (see lib/gitea/serve.sh) plus +; lib/host/server.sx (host/native-handler). + +(define gitea/live-forge-cache false) + +(define + gitea/live-forge + (fn + () + (begin + (if + gitea/live-forge-cache + nil + (set! gitea/live-forge-cache (gitea/forge (persist/durable-backend)))) + gitea/live-forge-cache))) + +(define + gitea/welcome-md + "# sx-gitea\n\nA federated git forge written entirely in SX — repos, issues, pull\nrequests, activity, search and ForgeFed federation composed from the\nx-on-sx subsystems (sx-git, acl, content, relations, flow, feed,\nevents, search) on the OCaml kernel. Zero third-party dependencies.\n\nBrowse: `//`, `/issues`, `/pulls`, `/activity`,\n`///search?q=...`\n\nClone over the native smart-HTTP wire: `GET ///info/refs`.\n") + +(define + gitea/seed! + (fn + (forge instance admin token) + (begin + (gitea/instance! forge instance) + (gitea/user-create! forge admin) + (gitea/token-create! forge admin token) + (let + ((res (gitea/repo-create! forge admin "welcome" {:description "Welcome to sx-gitea — a federated git forge in plain SX"}))) + (if + (get res :conflict) + nil + (let + ((grepo (gitea/repo-git forge admin "welcome"))) + (begin + (git/add! grepo "README.md" gitea/welcome-md) + (git/commit! grepo {:message "welcome" :time 0 :author admin}) + nil)))) + forge))) + +; blocks: the kernel http-listen loop serves the forge +(define + gitea/serve! + (fn + (port instance admin token) + (let + ((forge (gitea/seed! (gitea/live-forge) instance admin token))) + (http-listen port (host/native-handler (gitea/app forge))))))