1
0
forked from wrenn/wrenn
Files
wrenn-releases/pkg/service/build_stream.go
Rafeed M. Bhuiyan 05ddf62399 v0.2.0 (#50)
Co-authored-by: Tasnim Kabir Sadik <tksadik@omukk.dev>

Reviewed-on: wrenn/wrenn#50
2026-05-24 21:10:37 +00:00

73 lines
2.5 KiB
Go

package service
import (
"context"
"encoding/json"
"log/slog"
"time"
"github.com/redis/go-redis/v9"
)
// buildStreamChannelPrefix is the Redis pub/sub channel prefix for live build
// events. One channel per build: wrenn:build:{buildID}.
const buildStreamChannelPrefix = "wrenn:build:"
func buildStreamChannel(buildID string) string {
return buildStreamChannelPrefix + buildID
}
// BuildStreamEvent is one event in a build's live stream. The same struct is
// published to Redis by the build worker and forwarded verbatim to admin
// WebSocket clients, so its JSON shape is the wire contract for both.
//
// Type discriminates the payload:
// - "step-start": Step, Phase, Cmd set.
// - "output": Step, Data (base64 PTY bytes) set.
// - "step-end": Step, Phase, Cmd, Exit, Ok, ElapsedMs set.
// - "build-status": Status, CurrentStep, TotalSteps, Error set.
type BuildStreamEvent struct {
Type string `json:"type"`
Step int `json:"step,omitempty"`
Phase string `json:"phase,omitempty"`
Cmd string `json:"cmd,omitempty"`
Data string `json:"data,omitempty"` // base64-encoded PTY output bytes
Exit int32 `json:"exit,omitempty"`
Ok bool `json:"ok,omitempty"`
ElapsedMs int64 `json:"elapsed_ms,omitempty"`
Status string `json:"status,omitempty"`
CurrentStep int32 `json:"current_step,omitempty"`
TotalSteps int32 `json:"total_steps,omitempty"`
Error string `json:"error,omitempty"`
T int64 `json:"t"` // unix milliseconds, set at publish time
}
// IsTerminalBuildStatus reports whether a build status is final (the worker
// will publish no further events for it).
func IsTerminalBuildStatus(status string) bool {
switch status {
case "success", "failed", "cancelled":
return true
default:
return false
}
}
// publishBuildEvent fire-and-forget publishes one event to a build's Redis
// channel. A missing/closed Redis connection only drops live events; the WS
// client always has the DB log history to fall back on.
func publishBuildEvent(ctx context.Context, rdb *redis.Client, buildID string, ev BuildStreamEvent) {
if rdb == nil {
return
}
ev.T = time.Now().UnixMilli()
payload, err := json.Marshal(ev)
if err != nil {
slog.Warn("build event marshal failed", "build_id", buildID, "error", err)
return
}
if err := rdb.Publish(ctx, buildStreamChannel(buildID), payload).Err(); err != nil {
slog.Debug("build event publish failed", "build_id", buildID, "error", err)
}
}