This commit captures the current state before implementing the composable monorepo template system. Key changes included: Infrastructure: - Add CockroachDB provisioner adapter for database provisioning - Add Redis provisioner adapter for cache provisioning - Add build events system with PostgreSQL storage - Add WebSocket endpoint for real-time build progress Code agent improvements: - Fix Claude Code adapter to use default allowed tools instead of dangerously-skip-permissions - Add context-aware stream closing for cancellation support - Improve parser tests for edge cases Build system: - Add build event constants and metrics - Remove deprecated git_operations.go (replaced by pod_git_operations.go) - Add rollback logic for multi-step provisioning operations Documentation: - Add composable-monorepo feature documentation - Add DNS/Cloudflare service documentation - Update deployment and troubleshooting guides Cookbooks: - Add fullstack-app cookbook - Refactor landing-test with shared library Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
284 lines
8.6 KiB
Bash
Executable File
284 lines
8.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# SSE Stream Client Library for rdev API
|
|
# Provides functions for consuming Server-Sent Events from build streams
|
|
#
|
|
# Usage:
|
|
# source cookbooks/scripts/lib/stream-client.sh
|
|
# stream_build_with_progress "$API_URL" "$API_KEY" "$PROJECT_ID" "$TASK_ID"
|
|
|
|
# Colors for output
|
|
STREAM_RED='\033[0;31m'
|
|
STREAM_GREEN='\033[0;32m'
|
|
STREAM_YELLOW='\033[1;33m'
|
|
STREAM_BLUE='\033[0;34m'
|
|
STREAM_CYAN='\033[0;36m'
|
|
STREAM_NC='\033[0m'
|
|
|
|
# Progress bar width
|
|
PROGRESS_BAR_WIDTH=40
|
|
|
|
# Draw a progress bar
|
|
# Arguments: percentage (0-100)
|
|
draw_progress_bar() {
|
|
local percent="${1:-0}"
|
|
local filled=$((percent * PROGRESS_BAR_WIDTH / 100))
|
|
local empty=$((PROGRESS_BAR_WIDTH - filled))
|
|
|
|
printf "\r["
|
|
printf '%*s' "$filled" '' | tr ' ' '='
|
|
if [[ $filled -lt $PROGRESS_BAR_WIDTH ]]; then
|
|
printf ">"
|
|
printf '%*s' "$((empty - 1))" '' | tr ' ' ' '
|
|
fi
|
|
printf "] %3d%%" "$percent"
|
|
}
|
|
|
|
# Parse SSE data line and extract JSON
|
|
# Arguments: data line (after "data: " prefix)
|
|
parse_sse_data() {
|
|
local data="$1"
|
|
echo "$data"
|
|
}
|
|
|
|
# Stream build events with progress bar
|
|
# Arguments: api_url, api_key, project_id, task_id
|
|
# Options:
|
|
# --verbose Show all output (not just progress)
|
|
# --last-id Last-Event-ID for reconnection
|
|
stream_build_with_progress() {
|
|
local api_url="$1"
|
|
local api_key="$2"
|
|
local project_id="$3"
|
|
local task_id="$4"
|
|
shift 4
|
|
|
|
local verbose=false
|
|
local last_event_id=""
|
|
|
|
# Parse options
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--verbose)
|
|
verbose=true
|
|
shift
|
|
;;
|
|
--last-id)
|
|
last_event_id="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
local stream_url="${api_url}/projects/${project_id}/events?stream_id=${task_id}"
|
|
local curl_args=(
|
|
-s -N
|
|
-H "X-API-Key: ${api_key}"
|
|
-H "Accept: text/event-stream"
|
|
)
|
|
|
|
if [[ -n "$last_event_id" ]]; then
|
|
curl_args+=(-H "Last-Event-ID: ${last_event_id}")
|
|
fi
|
|
|
|
echo -e "${STREAM_CYAN}Streaming build events...${STREAM_NC}"
|
|
echo ""
|
|
|
|
# Track state
|
|
local current_phase="starting"
|
|
local current_percent=0
|
|
local last_event_id_received=""
|
|
|
|
# Stream events
|
|
curl "${curl_args[@]}" "$stream_url" 2>/dev/null | while IFS= read -r line; do
|
|
# Skip empty lines
|
|
[[ -z "$line" ]] && continue
|
|
|
|
# Parse event ID
|
|
if [[ "$line" == "id:"* ]]; then
|
|
last_event_id_received="${line#id: }"
|
|
continue
|
|
fi
|
|
|
|
# Skip event type lines (we parse data directly)
|
|
[[ "$line" == "event:"* ]] && continue
|
|
|
|
# Parse data lines
|
|
if [[ "$line" == "data:"* ]]; then
|
|
local data="${line#data: }"
|
|
local event_type
|
|
event_type=$(echo "$data" | jq -r '.type // ""' 2>/dev/null)
|
|
|
|
case "$event_type" in
|
|
build.started)
|
|
echo -e "${STREAM_GREEN}[BUILD STARTED]${STREAM_NC}"
|
|
current_phase="starting"
|
|
current_percent=0
|
|
draw_progress_bar 0
|
|
;;
|
|
|
|
build.progress)
|
|
current_phase=$(echo "$data" | jq -r '.phase // "unknown"' 2>/dev/null)
|
|
current_percent=$(echo "$data" | jq -r '.percentage // 0' 2>/dev/null | cut -d. -f1)
|
|
draw_progress_bar "$current_percent"
|
|
printf " [%s]" "$current_phase"
|
|
;;
|
|
|
|
build.output)
|
|
if [[ "$verbose" == "true" ]]; then
|
|
local content
|
|
content=$(echo "$data" | jq -r '.content // ""' 2>/dev/null)
|
|
[[ -n "$content" ]] && printf "\n%s" "$content"
|
|
fi
|
|
;;
|
|
|
|
build.tool_use)
|
|
local tool_name
|
|
tool_name=$(echo "$data" | jq -r '.tool_name // "unknown"' 2>/dev/null)
|
|
if [[ "$verbose" == "true" ]]; then
|
|
printf "\n${STREAM_YELLOW}[TOOL: %s]${STREAM_NC}" "$tool_name"
|
|
fi
|
|
;;
|
|
|
|
build.error)
|
|
local error_content
|
|
error_content=$(echo "$data" | jq -r '.content // ""' 2>/dev/null)
|
|
printf "\n${STREAM_RED}[ERROR] %s${STREAM_NC}" "$error_content"
|
|
;;
|
|
|
|
build.completed)
|
|
echo ""
|
|
draw_progress_bar 100
|
|
printf " [complete]"
|
|
echo ""
|
|
echo -e "${STREAM_GREEN}[BUILD COMPLETED]${STREAM_NC}"
|
|
local duration_ms
|
|
duration_ms=$(echo "$data" | jq -r '.duration_ms // 0' 2>/dev/null)
|
|
local duration_s=$((duration_ms / 1000))
|
|
echo "Duration: ${duration_s}s"
|
|
return 0
|
|
;;
|
|
|
|
build.failed)
|
|
echo ""
|
|
local error
|
|
error=$(echo "$data" | jq -r '.error // "unknown error"' 2>/dev/null)
|
|
echo -e "${STREAM_RED}[BUILD FAILED]${STREAM_NC}"
|
|
echo "Error: $error"
|
|
return 1
|
|
;;
|
|
|
|
connected)
|
|
local reconnecting
|
|
reconnecting=$(echo "$data" | jq -r '.reconnecting // false' 2>/dev/null)
|
|
if [[ "$reconnecting" == "true" ]]; then
|
|
echo -e "${STREAM_YELLOW}[RECONNECTED]${STREAM_NC}"
|
|
fi
|
|
;;
|
|
|
|
heartbeat)
|
|
# Silent heartbeat - just proves connection is alive
|
|
;;
|
|
esac
|
|
fi
|
|
done
|
|
|
|
# If we get here, the stream closed unexpectedly
|
|
echo ""
|
|
echo -e "${STREAM_YELLOW}[STREAM CLOSED]${STREAM_NC}"
|
|
echo "Last event ID: $last_event_id_received"
|
|
echo "To reconnect: stream_build_with_progress ... --last-id \"$last_event_id_received\""
|
|
return 2
|
|
}
|
|
|
|
# Simple stream consumer that just prints events
|
|
# Arguments: api_url, api_key, project_id, task_id
|
|
stream_build_simple() {
|
|
local api_url="$1"
|
|
local api_key="$2"
|
|
local project_id="$3"
|
|
local task_id="$4"
|
|
|
|
local stream_url="${api_url}/projects/${project_id}/events?stream_id=${task_id}"
|
|
|
|
curl -s -N \
|
|
-H "X-API-Key: ${api_key}" \
|
|
-H "Accept: text/event-stream" \
|
|
"$stream_url" 2>/dev/null | while IFS= read -r line; do
|
|
|
|
[[ -z "$line" ]] && continue
|
|
|
|
if [[ "$line" == "data:"* ]]; then
|
|
local data="${line#data: }"
|
|
local event_type content
|
|
event_type=$(echo "$data" | jq -r '.type // ""' 2>/dev/null)
|
|
|
|
case "$event_type" in
|
|
build.output|build.error)
|
|
content=$(echo "$data" | jq -r '.content // ""' 2>/dev/null)
|
|
[[ -n "$content" ]] && echo "$content"
|
|
;;
|
|
build.completed)
|
|
echo "[BUILD COMPLETED]"
|
|
return 0
|
|
;;
|
|
build.failed)
|
|
local error
|
|
error=$(echo "$data" | jq -r '.error // ""' 2>/dev/null)
|
|
echo "[BUILD FAILED] $error"
|
|
return 1
|
|
;;
|
|
esac
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Wait for build completion with polling fallback
|
|
# Arguments: api_url, api_key, task_id, timeout_seconds
|
|
# Returns: 0 on success, 1 on failure, 2 on timeout
|
|
wait_for_build_completion() {
|
|
local api_url="$1"
|
|
local api_key="$2"
|
|
local task_id="$3"
|
|
local timeout="${4:-600}"
|
|
|
|
local start_time=$(date +%s)
|
|
|
|
while true; do
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
if [[ $elapsed -ge $timeout ]]; then
|
|
return 2 # Timeout
|
|
fi
|
|
|
|
local response
|
|
response=$(curl -s -X GET "${api_url}/builds/${task_id}" \
|
|
-H "X-API-Key: ${api_key}" 2>/dev/null)
|
|
|
|
local status
|
|
status=$(echo "$response" | jq -r '.data.status // "unknown"' 2>/dev/null)
|
|
|
|
case "$status" in
|
|
completed)
|
|
local success
|
|
success=$(echo "$response" | jq -r '.data.result.success // false' 2>/dev/null)
|
|
if [[ "$success" == "true" ]]; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
;;
|
|
failed)
|
|
return 1
|
|
;;
|
|
running|pending)
|
|
sleep 5
|
|
;;
|
|
*)
|
|
sleep 5
|
|
;;
|
|
esac
|
|
done
|
|
}
|