Skip to content

Stream deployment script output to the task log live

Placeholder ppxd requested to merge feat/live-deployment-log-streaming into main

Summary

  • A long-running deployment action showed an empty Running node with no log lines until it completed: the agent's incremental output (received every Halibut GetStatus poll) went only to the server's Serilog, and the task log was populated once, in bulk, after completion. Operators couldn't see what an in-flight script was doing.
  • Thread an optional output sink from the executor → execution strategy → script observer. On the Halibut/Tentacle path the observer forwards each incremental batch as it arrives; the executor emits a new ScriptProgressReceivedEvent and DeploymentActivityLogger persists it to ServerTaskLog live, reusing the exact sequence / category / source / activity-node / masking logic of the bulk path.
  • The result is marked OutputStreamed so the post-completion bulk persist is skipped — lines are never duplicated.

Non-breaking

  • The sink is an optional param defaulting to null. Every other execution strategy (SSH, OpenClaw, local, K8s-API) and any caller that doesn't opt in behaves exactly as before: OutputStreamed stays false and the bulk persist runs unchanged.
  • Streaming failure falls back to the bulk persist (OutputStreamed=false) — output is never lost (at-least-once; may duplicate on the rare failure, preferred over loss).
  • Sequence allocation is already Interlocked, so concurrent streaming from parallel actions/targets is safe.
  • No schema change. (The optional param forced It.IsAny<ScriptOutputSink>() into existing observer mocks — test-only.)

Test plan

  • Unit — observer (HalibutScriptObserverStreamingTests): per-poll-batch delivery in order; stderr source tagging; empty-line skip; sink-throws → fallback (OutputStreamed=false, no loss); timeout streams its synthetic line + marks streamed; no-sink → unchanged.
  • Unit — activity logger (DeploymentActivityLoggerStreamingTests): live chunk persisted with category (stderr→Error), source, masking, monotonic sequence; empty chunk no-op; OutputStreamed=true skips bulk; false persists bulk (legacy).
  • Integration — real pipeline + DB (DeploymentLogWriterIntegrationTests): chunks persist incrementally and each line lands exactly once (final bulk skipped), stderr categorised, attributed to the action node; control path (OutputStreamed=false) persists at end.
  • Full unit suite 5766/5766; resume-reattach integration 4/4; full solution builds 0 errors.

Merge request reports

Loading