cial/docs/architecture/deploy-pipeline.md
Eliot M e223ba45ec docs(self-edit): document synchronous /api/v1/self/deploy
Update API docs, recipes, design doc, deploy-pipeline architecture,
and deploy-logs ops doc to match the new synchronous behaviour
(commit 8505981). The endpoint now returns 200/500 with status,
durationMs, exitCode, errorSummary, and an inline logTail (last
~8KB) — no polling, no companion GET endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 14:56:48 +00:00

4.4 KiB

Deploy pipeline

End-to-end flow when something hits POST /api/v1/self/deploy.

Components

┌────────────────────────┐    JSONL    ┌────────────────────────┐
│   core-back            │   over UDS  │   edge supervisor      │
│ ─────────────          │ ──────────► │ ─────────────          │
│  /api/v1/self/deploy   │             │  /run/cial-supervisor  │
│      │                 │             │      .sock             │
│      ▼                 │             │      │                 │
│  DeployService         │             │      ▼                 │
│      │                 │             │  spawn / SIGTERM       │
│      ▼                 │             │  child processes       │
│  BuildRunner           │             └────────────────────────┘
│  (single-flight)       │
│      │                 │
│      ▼                 │
│  spawn pnpm build      │
└────────────────────────┘

Lifecycle

  1. EnqueueDeployService.start({ scope })BuildRunner.enqueue().
    • One build at a time. New requests either coalesce (same mode + scope) or replace the queued slot.
  2. Buildpnpm <filters> build runs in monorepoRoot.
    • Filters depend on scope (platform vs all) — see runner.ts.
    • Stdout/stderr streamed to the WS bridge as deploy.log events.
  3. Restart — on build success, SupervisorClient.restart('platform' | 'all') over the Unix socket.
    • Supervisor SIGTERMs each named child, respawns it, sends restart.ack + restart.done.
    • For edge (only in all + unrestricted): supervisor exits, Docker restart-policy recreates the container.

Files

File Role
core/back/src/modules/deploy/runner.ts BuildRunner — pnpm spawn + log streaming
core/back/src/modules/deploy/service.ts Glue — runner ↔ supervisor ↔ WS broadcast
core/back/src/modules/deploy/supervisor-client.ts JSONL-over-UDS client
core/edge/src/supervisor-ipc.ts Wire protocol + scope sets
core/edge/src/supervisor.ts (prod) IPC server + child management
core/edge/src/supervisor.dev.ts (dev) Same, but for pnpm dev:tenant
core/back/src/modules/self/router.ts The agent-facing endpoints

Build filters

// runner.ts
const PLATFORM_FILTERS = [
  '--filter', '@cial/platform-front',
  '--filter', '@cial/platform-back',
];
const ALL_FILTERS = [
  '--filter', '@cial/protocol',
  '--filter', '@cial/sdk',
  '--filter', '@cial/core-ui',
  '--filter', '@cial/back',
  '--filter', '@cial/front',
  '--filter', '@cial/edge',
  '--filter', '@cial/platform-back',
  '--filter', '@cial/platform-front',
];

Restart sets

// supervisor-ipc.ts
const PLATFORM_RESTARTABLES = new Set(['platform-front', 'platform-back']);
const ALL_RESTARTABLES = new Set([
  'platform-front', 'platform-back',
  'core-front', 'core-back',
  'edge',  // exits supervisor → docker restart-policy bounces container
]);

WS events

The deploy service broadcasts these to the requesting user's connected tabs:

  • deploy.start — { deployId, mode, targets[], sessionId }
  • deploy.log — { deployId, stream, line }
  • deploy.restart.start — { service }
  • deploy.restart.done — { service, pid, durationMs }
  • deploy.done — { deployId, ok, exitCode, durationMs, errorSummary }
  • deploy.cancelled — { deployId }

Self-edit calls use requestedByUserId = '__self__', so WS broadcasts go nowhere. Instead, POST /api/v1/self/deploy is synchronous: the route handler subscribes to DeployService events (via service.waitForDone(deployId)), awaits the terminal done/cancelled event, then responds once with the final row + a tail of the log file. The agent gets a single blocking answer — no polling, no WS subscription needed.