Complete scratch iteration 004 setup and grouped route-action fixes
This commit is contained in:
@@ -5,7 +5,9 @@
|
||||
|
||||
.DESCRIPTION
|
||||
Discovers all *.sln files under src/ (excluding the root StellaOps.sln)
|
||||
and runs dotnet build on each. Pass -Test to also run dotnet test.
|
||||
and skips generated output trees such as node_modules, bin, obj, dist,
|
||||
coverage, and output before running dotnet build on each. Pass -Test to
|
||||
also run dotnet test.
|
||||
|
||||
.PARAMETER Test
|
||||
Also run dotnet test on each solution after building.
|
||||
@@ -147,10 +149,12 @@ if ($StopRepoHostProcesses) {
|
||||
Stop-RepoHostProcesses -Root $repoRoot
|
||||
}
|
||||
|
||||
$excludedSolutionRootPattern = '[\\/](node_modules|bin|obj|dist|coverage|output)[\\/]'
|
||||
|
||||
$solutions = Get-ChildItem -Path $srcDir -Filter '*.sln' -Recurse |
|
||||
Where-Object {
|
||||
$_.Name -ne 'StellaOps.sln' -and
|
||||
$_.FullName -notmatch '[\\/](node_modules|bin|obj)[\\/]'
|
||||
$_.FullName -notmatch $excludedSolutionRootPattern
|
||||
} |
|
||||
Sort-Object FullName
|
||||
|
||||
|
||||
@@ -62,10 +62,10 @@ if $STOP_REPO_HOST_PROCESSES; then
|
||||
stop_repo_host_processes
|
||||
fi
|
||||
|
||||
# Discover solutions (exclude root StellaOps.sln)
|
||||
# Discover repo-owned solutions only; skip generated output trees.
|
||||
mapfile -t SOLUTIONS < <(
|
||||
find "$SRC_DIR" \
|
||||
\( -path '*/node_modules/*' -o -path '*/bin/*' -o -path '*/obj/*' \) -prune -o \
|
||||
\( -path '*/node_modules/*' -o -path '*/bin/*' -o -path '*/obj/*' -o -path '*/dist/*' -o -path '*/coverage/*' -o -path '*/output/*' \) -prune -o \
|
||||
-name '*.sln' ! -name 'StellaOps.sln' -print | sort
|
||||
)
|
||||
|
||||
|
||||
@@ -142,6 +142,166 @@ function Get-ServiceHttpProbeUrl([string]$serviceName, [int]$containerPort, [str
|
||||
return "http://${probeHost}:$($Matches.port)$path"
|
||||
}
|
||||
|
||||
function Get-ComposeServiceRecords([string[]]$composeFiles) {
|
||||
$records = @()
|
||||
$seenContainers = @{}
|
||||
|
||||
foreach ($composeFile in $composeFiles) {
|
||||
$composePath = if ([System.IO.Path]::IsPathRooted($composeFile)) {
|
||||
$composeFile
|
||||
} else {
|
||||
Join-Path $ComposeDir $composeFile
|
||||
}
|
||||
|
||||
$expectedServices = Get-ComposeExpectedServices $composePath
|
||||
$services = Get-ComposeServices $composePath
|
||||
if ($expectedServices.Count -gt 0) {
|
||||
$allowed = @{}
|
||||
foreach ($name in $expectedServices) {
|
||||
$allowed[$name.ToLowerInvariant()] = $true
|
||||
}
|
||||
|
||||
$services = $services | Where-Object {
|
||||
$service = "$($_.Service)".ToLowerInvariant()
|
||||
$service -and $allowed.ContainsKey($service)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($svc in $services) {
|
||||
$name = "$($svc.Name)"
|
||||
if (-not $name -or $seenContainers.ContainsKey($name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$seenContainers[$name] = $true
|
||||
$records += [pscustomobject]@{
|
||||
ComposeFile = $composePath
|
||||
Service = "$($svc.Service)"
|
||||
Name = $name
|
||||
State = "$($svc.State)".ToLowerInvariant()
|
||||
Health = "$($svc.Health)".ToLowerInvariant()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
function Wait-ForComposeConvergence(
|
||||
[string[]]$composeFiles,
|
||||
[string]$successMessage,
|
||||
[int]$maxWaitSeconds = 180,
|
||||
[int]$restartAfterSeconds = 45,
|
||||
[int]$pollSeconds = 5,
|
||||
[switch]$RestartStalledServices
|
||||
) {
|
||||
$restartedServices = @{}
|
||||
$elapsed = 0
|
||||
|
||||
while ($elapsed -lt $maxWaitSeconds) {
|
||||
$records = Get-ComposeServiceRecords $composeFiles
|
||||
if ($records.Count -gt 0) {
|
||||
$pending = @()
|
||||
$blocking = @()
|
||||
|
||||
foreach ($record in $records) {
|
||||
if ($record.State -ne 'running') {
|
||||
$blocking += $record
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $record.Health -or $record.Health -eq 'healthy') {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($record.Health -eq 'starting') {
|
||||
$pending += $record
|
||||
continue
|
||||
}
|
||||
|
||||
$blocking += $record
|
||||
}
|
||||
|
||||
if ($blocking.Count -eq 0 -and $pending.Count -eq 0 -and $elapsed -gt $pollSeconds) {
|
||||
Write-Ok $successMessage
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($RestartStalledServices -and $elapsed -ge $restartAfterSeconds -and $blocking.Count -gt 0) {
|
||||
$restartGroups = @{}
|
||||
|
||||
foreach ($record in $blocking) {
|
||||
$restartKey = "$($record.ComposeFile)|$($record.Service)"
|
||||
if ($restartedServices.ContainsKey($restartKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $restartGroups.ContainsKey($record.ComposeFile)) {
|
||||
$restartGroups[$record.ComposeFile] = New-Object System.Collections.Generic.List[string]
|
||||
}
|
||||
|
||||
$restartGroups[$record.ComposeFile].Add($record.Service)
|
||||
$restartedServices[$restartKey] = $true
|
||||
}
|
||||
|
||||
foreach ($group in $restartGroups.GetEnumerator()) {
|
||||
$servicesToRestart = @($group.Value | Sort-Object -Unique)
|
||||
if ($servicesToRestart.Count -eq 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Warn "Restarting stalled services from $($group.Key): $($servicesToRestart -join ', ')"
|
||||
Push-Location $ComposeDir
|
||||
try {
|
||||
docker compose -f $group.Key restart @servicesToRestart | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Ok "Restarted stalled services: $($servicesToRestart -join ', ')"
|
||||
} else {
|
||||
Write-Warn "Failed to restart stalled services: $($servicesToRestart -join ', ')"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $pollSeconds
|
||||
$elapsed += $pollSeconds
|
||||
}
|
||||
|
||||
$finalRecords = Get-ComposeServiceRecords $composeFiles
|
||||
$blockingSummary = @(
|
||||
$finalRecords | ForEach-Object {
|
||||
if ($_.State -ne 'running') {
|
||||
"$($_.Name) (state=$($_.State))"
|
||||
}
|
||||
elseif ($_.Health -and $_.Health -ne 'healthy' -and $_.Health -ne 'starting') {
|
||||
"$($_.Name) (health=$($_.Health))"
|
||||
}
|
||||
}
|
||||
) | Where-Object { $_ }
|
||||
|
||||
$pendingSummary = @(
|
||||
$finalRecords | Where-Object {
|
||||
$_.State -eq 'running' -and $_.Health -eq 'starting'
|
||||
} | ForEach-Object {
|
||||
"$($_.Name) (health=starting)"
|
||||
}
|
||||
)
|
||||
|
||||
if ($blockingSummary.Count -gt 0) {
|
||||
Write-Warn "Timed out waiting for compose convergence after ${maxWaitSeconds}s. Blocking services: $($blockingSummary -join ', ')"
|
||||
} elseif ($pendingSummary.Count -gt 0) {
|
||||
Write-Warn "Timed out waiting for compose convergence after ${maxWaitSeconds}s. Still starting: $($pendingSummary -join ', ')"
|
||||
} else {
|
||||
Write-Warn "Timed out waiting for compose convergence after ${maxWaitSeconds}s."
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
# ─── 1. Check prerequisites ────────────────────────────────────────────────
|
||||
|
||||
function Test-Prerequisites {
|
||||
@@ -365,44 +525,7 @@ function Start-Infrastructure {
|
||||
}
|
||||
|
||||
Write-Host ' Waiting for containers to become healthy...' -ForegroundColor Gray
|
||||
$maxWait = 120
|
||||
$elapsed = 0
|
||||
while ($elapsed -lt $maxWait) {
|
||||
$expectedServices = Get-ComposeExpectedServices 'docker-compose.dev.yml'
|
||||
$services = Get-ComposeServices 'docker-compose.dev.yml'
|
||||
if ($expectedServices.Count -gt 0) {
|
||||
$allowed = @{}
|
||||
foreach ($name in $expectedServices) {
|
||||
$allowed[$name.ToLowerInvariant()] = $true
|
||||
}
|
||||
|
||||
$services = $services | Where-Object {
|
||||
$service = "$($_.Service)".ToLowerInvariant()
|
||||
$service -and $allowed.ContainsKey($service)
|
||||
}
|
||||
}
|
||||
if ($services.Count -gt 0) {
|
||||
$allHealthy = $true
|
||||
foreach ($svc in $services) {
|
||||
$state = "$($svc.State)".ToLowerInvariant()
|
||||
$health = "$($svc.Health)".ToLowerInvariant()
|
||||
if ($state -ne 'running') {
|
||||
$allHealthy = $false
|
||||
continue
|
||||
}
|
||||
if ($health -and $health -ne 'healthy') {
|
||||
$allHealthy = $false
|
||||
}
|
||||
}
|
||||
if ($allHealthy -and $elapsed -gt 5) {
|
||||
Write-Ok 'All infrastructure containers healthy'
|
||||
return
|
||||
}
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
$elapsed += 5
|
||||
}
|
||||
Write-Warn "Timed out waiting for healthy status after ${maxWait}s. Check with: docker compose -f docker-compose.dev.yml ps"
|
||||
[void](Wait-ForComposeConvergence -composeFiles @('docker-compose.dev.yml') -successMessage 'All infrastructure containers healthy' -maxWaitSeconds 120)
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
@@ -465,6 +588,13 @@ function Start-Platform {
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
[void](Wait-ForComposeConvergence `
|
||||
-composeFiles @('docker-compose.stella-ops.yml') `
|
||||
-successMessage 'Platform services converged from zero-state startup' `
|
||||
-RestartStalledServices `
|
||||
-maxWaitSeconds 180 `
|
||||
-restartAfterSeconds 45)
|
||||
}
|
||||
|
||||
function Test-ExpectedHttpStatus([string]$url, [int[]]$allowedStatusCodes, [int]$timeoutSeconds = 5, [int]$attempts = 6, [int]$retryDelaySeconds = 2) {
|
||||
|
||||
196
scripts/setup.sh
196
scripts/setup.sh
@@ -70,6 +70,179 @@ service_http_probe_url() {
|
||||
printf 'http://%s:%s%s' "$host" "$host_port" "$path"
|
||||
}
|
||||
|
||||
get_compose_service_records() {
|
||||
local seen_names=""
|
||||
local compose_file compose_path expected_services services_json line service name state health
|
||||
|
||||
for compose_file in "$@"; do
|
||||
if [[ "${compose_file}" = /* ]]; then
|
||||
compose_path="${compose_file}"
|
||||
else
|
||||
compose_path="${COMPOSE_DIR}/${compose_file}"
|
||||
fi
|
||||
|
||||
[[ -f "${compose_path}" ]] || continue
|
||||
|
||||
expected_services="$(docker compose -f "${compose_path}" config --services 2>/dev/null || true)"
|
||||
services_json="$(docker compose -f "${compose_path}" ps --format json 2>/dev/null || true)"
|
||||
[[ -n "${services_json}" ]] || continue
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ -z "${line}" ]] && continue
|
||||
service=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Service',''))" 2>/dev/null || true)
|
||||
name=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Name',''))" 2>/dev/null || true)
|
||||
state=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('State',''))" 2>/dev/null || true)
|
||||
health=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Health',''))" 2>/dev/null || true)
|
||||
|
||||
if [[ -n "${expected_services}" ]] && ! printf '%s\n' "${expected_services}" | grep -Fxq "${service}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
[[ -n "${name}" ]] || continue
|
||||
if printf '%s\n' "${seen_names}" | grep -Fxq "${name}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
seen_names="${seen_names}
|
||||
${name}"
|
||||
printf '%s|%s|%s|%s|%s\n' "${compose_path}" "${service}" "${name}" "${state,,}" "${health,,}"
|
||||
done <<< "${services_json}"
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_compose_convergence() {
|
||||
local success_message="$1"
|
||||
local restart_stalled="${2:-false}"
|
||||
local max_wait="${3:-180}"
|
||||
local restart_after="${4:-45}"
|
||||
shift 4
|
||||
local compose_files=("$@")
|
||||
|
||||
local elapsed=0
|
||||
local poll_seconds=5
|
||||
local restarted_services=""
|
||||
|
||||
while (( elapsed < max_wait )); do
|
||||
local records
|
||||
records="$(get_compose_service_records "${compose_files[@]}")"
|
||||
if [[ -n "${records}" ]]; then
|
||||
local pending=""
|
||||
local blocking=""
|
||||
local record compose_file service name state health
|
||||
|
||||
while IFS= read -r record; do
|
||||
[[ -z "${record}" ]] && continue
|
||||
IFS='|' read -r compose_file service name state health <<< "${record}"
|
||||
|
||||
if [[ "${state}" != "running" ]]; then
|
||||
blocking="${blocking}
|
||||
${compose_file}|${service}|${name}|state=${state}"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -z "${health}" || "${health}" == "healthy" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "${health}" == "starting" ]]; then
|
||||
pending="${pending}
|
||||
${compose_file}|${service}|${name}|health=starting"
|
||||
continue
|
||||
fi
|
||||
|
||||
blocking="${blocking}
|
||||
${compose_file}|${service}|${name}|health=${health}"
|
||||
done <<< "${records}"
|
||||
|
||||
if [[ -z "${blocking//$'\n'/}" && -z "${pending//$'\n'/}" && ${elapsed} -gt ${poll_seconds} ]]; then
|
||||
ok "${success_message}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "${restart_stalled}" == "true" && ${elapsed} -ge ${restart_after} && -n "${blocking//$'\n'/}" ]]; then
|
||||
local restart_targets=""
|
||||
while IFS= read -r record; do
|
||||
[[ -z "${record}" ]] && continue
|
||||
IFS='|' read -r compose_file service _ <<< "${record}"
|
||||
local restart_key="${compose_file}|${service}"
|
||||
if printf '%s\n' "${restarted_services}" | grep -Fxq "${restart_key}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
restarted_services="${restarted_services}
|
||||
${restart_key}"
|
||||
restart_targets="${restart_targets}
|
||||
${compose_file}|${service}"
|
||||
done <<< "${blocking}"
|
||||
|
||||
local compose_to_restart unique_compose_files service_to_restart
|
||||
local -a services_for_compose
|
||||
unique_compose_files=$(printf '%s\n' "${restart_targets}" | awk -F'|' 'NF { print $1 }' | sort -u)
|
||||
|
||||
while IFS= read -r compose_to_restart; do
|
||||
[[ -z "${compose_to_restart}" ]] && continue
|
||||
services_for_compose=()
|
||||
while IFS= read -r service_to_restart; do
|
||||
[[ -z "${service_to_restart}" ]] && continue
|
||||
services_for_compose+=("${service_to_restart}")
|
||||
done < <(printf '%s\n' "${restart_targets}" | awk -F'|' -v cf="${compose_to_restart}" 'NF && $1 == cf { print $2 }' | sort -u)
|
||||
|
||||
if (( ${#services_for_compose[@]} == 0 )); then
|
||||
continue
|
||||
fi
|
||||
|
||||
warn "Restarting stalled services from ${compose_to_restart}: ${services_for_compose[*]}"
|
||||
(
|
||||
cd "${COMPOSE_DIR}" &&
|
||||
docker compose -f "${compose_to_restart}" restart "${services_for_compose[@]}" >/dev/null
|
||||
) && ok "Restarted stalled services: ${services_for_compose[*]}" || \
|
||||
warn "Failed to restart stalled services: ${services_for_compose[*]}"
|
||||
done <<< "${unique_compose_files}"
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep "${poll_seconds}"
|
||||
elapsed=$((elapsed + poll_seconds))
|
||||
done
|
||||
|
||||
local final_records
|
||||
final_records="$(get_compose_service_records "${compose_files[@]}")"
|
||||
local final_blocking=""
|
||||
local final_pending=""
|
||||
local record compose_file service name state health
|
||||
|
||||
while IFS= read -r record; do
|
||||
[[ -z "${record}" ]] && continue
|
||||
IFS='|' read -r compose_file service name state health <<< "${record}"
|
||||
|
||||
if [[ "${state}" != "running" ]]; then
|
||||
final_blocking="${final_blocking}
|
||||
${name} (state=${state})"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -n "${health}" && "${health}" != "healthy" ]]; then
|
||||
if [[ "${health}" == "starting" ]]; then
|
||||
final_pending="${final_pending}
|
||||
${name} (health=starting)"
|
||||
else
|
||||
final_blocking="${final_blocking}
|
||||
${name} (health=${health})"
|
||||
fi
|
||||
fi
|
||||
done <<< "${final_records}"
|
||||
|
||||
if [[ -n "${final_blocking//$'\n'/}" ]]; then
|
||||
warn "Timed out waiting for compose convergence after ${max_wait}s. Blocking services: $(printf '%s\n' "${final_blocking}" | awk 'NF { print }' | paste -sd ', ' -)"
|
||||
elif [[ -n "${final_pending//$'\n'/}" ]]; then
|
||||
warn "Timed out waiting for compose convergence after ${max_wait}s. Still starting: $(printf '%s\n' "${final_pending}" | awk 'NF { print }' | paste -sd ', ' -)"
|
||||
else
|
||||
warn "Timed out waiting for compose convergence after ${max_wait}s."
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ─── 1. Check prerequisites ────────────────────────────────────────────────
|
||||
|
||||
check_prerequisites() {
|
||||
@@ -264,27 +437,7 @@ start_infra() {
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
|
||||
echo ' Waiting for containers to become healthy...'
|
||||
local max_wait=120
|
||||
local elapsed=0
|
||||
while (( elapsed < max_wait )); do
|
||||
local all_healthy=true
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
local health; health=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Health',''))" 2>/dev/null || true)
|
||||
if [[ -n "$health" && "$health" != "healthy" ]]; then
|
||||
all_healthy=false
|
||||
fi
|
||||
done < <(docker compose -f docker-compose.dev.yml ps --format json 2>/dev/null)
|
||||
|
||||
if [[ "$all_healthy" == "true" && $elapsed -gt 5 ]]; then
|
||||
ok 'All infrastructure containers healthy'
|
||||
cd "$ROOT"
|
||||
return
|
||||
fi
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
done
|
||||
warn "Timed out waiting for healthy status after ${max_wait}s."
|
||||
wait_for_compose_convergence 'All infrastructure containers healthy' false 120 45 docker-compose.dev.yml || true
|
||||
cd "$ROOT"
|
||||
}
|
||||
|
||||
@@ -330,6 +483,7 @@ start_platform() {
|
||||
docker compose -f docker-compose.stella-ops.yml up -d
|
||||
ok 'Platform services started'
|
||||
cd "$ROOT"
|
||||
wait_for_compose_convergence 'Platform services converged from zero-state startup' true 180 45 docker-compose.stella-ops.yml || true
|
||||
}
|
||||
|
||||
http_status() {
|
||||
|
||||
Reference in New Issue
Block a user