Plan: SX CI Pipeline — build/test/deploy in s-expressions

Pipeline definitions as .sx files evaluated by a minimal Python runner.
CI primitives (shell-run, docker-build, git-diff-files) are boundary-declared
IO, only available to the runner. Steps are defcomp components composable
by nesting. Fixes pre-existing unclosed parens in isomorphic roadmap section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 13:36:50 +00:00
parent d618530f29
commit fab9bffc49
4 changed files with 413 additions and 2 deletions

View File

@@ -140,7 +140,9 @@
(dict :label "Glue Decoupling" :href "/plans/glue-decoupling"
:summary "Eliminate all cross-app model imports via glue service layer.")
(dict :label "Social Sharing" :href "/plans/social-sharing"
:summary "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")))
:summary "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")
(dict :label "SX CI Pipeline" :href "/plans/sx-ci"
:summary "Build, test, and deploy in s-expressions — CI pipelines as SX components.")))
(define bootstrappers-nav-items (list
(dict :label "Overview" :href "/bootstrappers/")

View File

@@ -2014,4 +2014,134 @@
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/suspense.sx")
(td :class "px-3 py-2 text-stone-700" "Streaming/suspension (new)")
(td :class "px-3 py-2 text-stone-600" "5"))))))))
(td :class "px-3 py-2 text-stone-600" "5")))))))))))))
;; ---------------------------------------------------------------------------
;; SX CI Pipeline
;; ---------------------------------------------------------------------------
(defcomp ~plan-sx-ci-content ()
(~doc-page :title "SX CI Pipeline"
(p :class "text-stone-500 text-sm italic mb-8"
"Build, test, and deploy Rose Ash using the same language the application is written in.")
(~doc-section :title "Context" :id "context"
(p :class "text-stone-600"
"Rose Ash currently uses shell scripts for CI: " (code "deploy.sh") " auto-detects changed services via git diff, builds Docker images, pushes to the registry, and restarts Swarm services. " (code "dev.sh") " starts the dev environment and runs tests. These work, but they are opaque imperative scripts with no reuse, no composition, and no relationship to SX.")
(p :class "text-stone-600"
"The CI pipeline is the last piece of infrastructure not expressed in s-expressions. Fixing that completes the \"one representation for everything\" claim — the same language that defines the spec, the components, the pages, the essays, and the deployment config also defines the build pipeline."))
(~doc-section :title "Design" :id "design"
(p :class "text-stone-600"
"Pipeline definitions are " (code ".sx") " files. A minimal Python CLI runner evaluates them using " (code "sx_ref.py") ". CI-specific IO primitives (shell execution, Docker, git) are boundary-declared and only available to the pipeline runner — never to web components.")
(~doc-code :code (highlight ";; pipeline/deploy.sx\n(let ((targets (if (= (length ARGS) 0)\n (~detect-changed :base \"HEAD~1\")\n (filter (fn (svc) (some (fn (a) (= a (get svc \"name\"))) ARGS))\n services))))\n (when (= (length targets) 0)\n (log-step \"No changes detected\")\n (exit 0))\n\n (log-step (str \"Deploying: \" (join \" \" (map (fn (s) (get s \"name\")) targets))))\n\n ;; Tests first\n (~unit-tests)\n (~sx-spec-tests)\n\n ;; Build, push, restart\n (for-each (fn (svc) (~build-service :service svc)) targets)\n (for-each (fn (svc) (~restart-service :service svc)) targets)\n\n (log-step \"Deploy complete\"))" "lisp"))
(p :class "text-stone-600"
"Pipeline steps are components. " (code "~unit-tests") ", " (code "~build-service") ", " (code "~detect-changed") " are " (code "defcomp") " definitions that compose by nesting — the same mechanism used for page layouts, navigation, and every other piece of the system."))
(~doc-section :title "CI Primitives" :id "primitives"
(p :class "text-stone-600"
"New IO primitives declared in " (code "boundary.sx") ", implemented only in the CI runner context:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Primitive")
(th :class "px-3 py-2 font-medium text-stone-600" "Signature")
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shell-run")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(command) -> dict")
(td :class "px-3 py-2 text-stone-700" "Execute shell command, return " (code "{:exit N :stdout \"...\" :stderr \"...\"}") ""))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shell-run!")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(command) -> dict")
(td :class "px-3 py-2 text-stone-700" "Execute shell command, throw on non-zero exit"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "docker-build")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(&key file tag context) -> nil")
(td :class "px-3 py-2 text-stone-700" "Build Docker image"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "docker-push")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(tag) -> nil")
(td :class "px-3 py-2 text-stone-700" "Push image to registry"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "docker-restart")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(service) -> nil")
(td :class "px-3 py-2 text-stone-700" "Restart Swarm service"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "git-diff-files")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(base head) -> list")
(td :class "px-3 py-2 text-stone-700" "List changed files between commits"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "git-branch")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "() -> string")
(td :class "px-3 py-2 text-stone-700" "Current branch name"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "log-step")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(message) -> nil")
(td :class "px-3 py-2 text-stone-700" "Formatted pipeline output"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "fail!")
(td :class "px-3 py-2 font-mono text-xs text-stone-600" "(message) -> nil")
(td :class "px-3 py-2 text-stone-700" "Abort pipeline with error")))))
(p :class "text-stone-600"
"The boundary system ensures these primitives are " (em "only") " available in the CI context. Web components cannot call " (code "shell-run!") " — the evaluator will refuse to resolve the symbol, just as it refuses to resolve any other unregistered IO primitive. The sandbox is structural, not a convention."))
(~doc-section :title "Reusable Steps" :id "steps"
(p :class "text-stone-600"
"Pipeline steps are components — same " (code "defcomp") " as UI components, same " (code "&key") " params, same composition by nesting:")
(~doc-code :code (highlight "(defcomp ~detect-changed (&key base)\n (let ((files (git-diff-files (or base \"HEAD~1\") \"HEAD\")))\n (if (some (fn (f) (starts-with? f \"shared/\")) files)\n services\n (filter (fn (svc)\n (some (fn (f) (starts-with? f (str (get svc \"dir\") \"/\"))) files))\n services))))\n\n(defcomp ~build-service (&key service)\n (let ((name (get service \"name\"))\n (tag (str registry \"/\" name \":latest\")))\n (log-step (str \"Building \" name))\n (docker-build :file (str (get service \"dir\") \"/Dockerfile\") :tag tag :context \".\")\n (docker-push tag)))\n\n(defcomp ~bootstrap-check ()\n (log-step \"Checking bootstrapped files are up to date\")\n (shell-run! \"python shared/sx/ref/bootstrap_js.py\")\n (shell-run! \"python shared/sx/ref/bootstrap_py.py\")\n (let ((diff (shell-run \"git diff --name-only shared/static/scripts/sx-ref.js shared/sx/ref/sx_ref.py\")))\n (when (not (= (get diff \"stdout\") \"\"))\n (fail! \"Bootstrapped files are stale — rebootstrap and commit\"))))" "lisp"))
(p :class "text-stone-600"
"Compare this to GitHub Actions YAML, where \"reuse\" means composite actions with " (code "uses:") " references, input/output mappings, shell script blocks inside YAML strings, and a " (a :href "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions" :class "text-violet-600 hover:underline" "100-page syntax reference") ". SX pipeline reuse is function composition. That is all it has ever been."))
(~doc-section :title "Pipelines" :id "pipelines"
(p :class "text-stone-600"
"Two primary pipelines, each a single " (code ".sx") " file:")
(div :class "space-y-4"
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "pipeline/test.sx")
(p :class "text-sm text-stone-600" "Unit tests, SX spec tests (Python + Node), bootstrap staleness check, Tailwind CSS check. Run locally or in CI.")
(p :class "text-sm font-mono text-violet-700 mt-1" "python -m shared.sx.ci pipeline/test.sx"))
(div :class "rounded border border-stone-200 p-4"
(h4 :class "font-semibold text-stone-700 mb-2" "pipeline/deploy.sx")
(p :class "text-sm text-stone-600" "Auto-detect changed services (or accept explicit args), run tests, build Docker images, push to registry, restart Swarm services.")
(p :class "text-sm font-mono text-violet-700 mt-1" "python -m shared.sx.ci pipeline/deploy.sx blog market"))))
(~doc-section :title "Why this matters" :id "why"
(p :class "text-stone-600"
"CI pipelines are the strongest test case for \"one representation for everything.\" GitHub Actions, GitLab CI, CircleCI — all use YAML. YAML is not a programming language. So every CI system reinvents conditionals (" (code "if:") " expressions evaluated as strings), iteration (" (code "matrix:") " strategies), composition (" (code "uses:") " references with input/output schemas), and error handling (" (code "continue-on-error:") " booleans) — all in a data format that was never designed for any of it.")
(p :class "text-stone-600"
"The result is a domain-specific language trapped inside YAML, with worse syntax than any language designed to be one. Every CI pipeline of sufficient complexity becomes a programming task performed in a notation that actively resists programming.")
(p :class "text-stone-600"
"SX pipelines use real conditionals, real functions, real composition, and real error handling — because SX is a real language. The pipeline definition and the application code are the same thing. An AI that can generate SX components can generate SX pipelines. A developer who reads SX pages can read SX deploys. The representation is universal."))
(~doc-section :title "Files" :id "files"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ci.py")
(td :class "px-3 py-2 text-stone-700" "Pipeline runner CLI (~150 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ci_primitives.py")
(td :class "px-3 py-2 text-stone-700" "CI IO primitive implementations"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/services.sx")
(td :class "px-3 py-2 text-stone-700" "Service registry (data)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/steps.sx")
(td :class "px-3 py-2 text-stone-700" "Reusable step components"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/test.sx")
(td :class "px-3 py-2 text-stone-700" "Test pipeline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pipeline/deploy.sx")
(td :class "px-3 py-2 text-stone-700" "Deploy pipeline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
(td :class "px-3 py-2 text-stone-700" "Add CI primitive declarations"))))))))

View File

@@ -512,6 +512,7 @@
"fragment-protocol" (~plan-fragment-protocol-content)
"glue-decoupling" (~plan-glue-decoupling-content)
"social-sharing" (~plan-social-sharing-content)
"sx-ci" (~plan-sx-ci-content)
:else (~plans-index-content)))
;; ---------------------------------------------------------------------------