diff --git a/agent/internal/build/build.go b/agent/internal/build/build.go index b8e31c4..ff64287 100644 --- a/agent/internal/build/build.go +++ b/agent/internal/build/build.go @@ -90,6 +90,14 @@ func truncateStr(s string, maxLen int) string { return s[:maxLen] } +func tailLines(output string, n int) string { + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) <= n { + return strings.TrimSpace(output) + } + return strings.Join(lines[len(lines)-n:], "\n") +} + func computeSecretsHash(secrets map[string]string) string { if len(secrets) == 0 { return "" @@ -220,7 +228,7 @@ func (b *Builder) buildAndPush(ctx context.Context, config *Config, buildDir str if err != nil { log.Printf("[build:%s] buildctl failed with output: %s", truncateStr(config.BuildID, 8), output) b.sendLog(config, fmt.Sprintf("Build error: %s", output)) - return fmt.Errorf("buildctl build failed: %w", err) + return fmt.Errorf("buildctl build failed:\n%s", tailLines(output, 20)) } } else { log.Printf("[build:%s] building with Railpack via buildctl for %s", truncateStr(config.BuildID, 8), platform) @@ -238,7 +246,7 @@ func (b *Builder) buildAndPush(ctx context.Context, config *Config, buildDir str if err != nil { log.Printf("[build:%s] railpack prepare failed with output: %s", truncateStr(config.BuildID, 8), output) b.sendLog(config, fmt.Sprintf("Railpack prepare error: %s", output)) - return fmt.Errorf("railpack prepare failed: %w", err) + return fmt.Errorf("railpack prepare failed:\n%s", tailLines(output, 20)) } b.sendLog(config, fmt.Sprintf("Building for %s...", platform)) @@ -268,7 +276,7 @@ func (b *Builder) buildAndPush(ctx context.Context, config *Config, buildDir str if err != nil { log.Printf("[build:%s] buildctl failed for %s: %s", truncateStr(config.BuildID, 8), platform, output) b.sendLog(config, fmt.Sprintf("Build error (%s): %s", platform, output)) - return fmt.Errorf("buildctl build failed for %s: %w", platform, err) + return fmt.Errorf("buildctl build failed for %s:\n%s", platform, tailLines(output, 20)) } } diff --git a/web/actions/projects.ts b/web/actions/projects.ts index 19f52a8..9d2aa1e 100644 --- a/web/actions/projects.ts +++ b/web/actions/projects.ts @@ -47,9 +47,11 @@ function isValidImageReferencePart(reference: string): boolean { const tagPattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/; const digestPattern = /^[A-Za-z0-9_+.-]+:[0-9a-fA-F]{32,256}$/; - return reference === "latest" || + return ( + reference === "latest" || tagPattern.test(reference) || - digestPattern.test(reference); + digestPattern.test(reference) + ); } function parseImageReference(image: string): { @@ -604,27 +606,6 @@ export async function deployService(serviceId: string) { } } - const existingDeployments = await db - .select() - .from(deployments) - .where(eq(deployments.serviceId, serviceId)); - - const inProgressStatuses = [ - "pending", - "pulling", - "starting", - "healthy", - "stopping", - ]; - - const hasInProgressDeployment = existingDeployments.some((d) => - inProgressStatuses.includes(d.status), - ); - - if (hasInProgressDeployment) { - throw new Error("A deployment is already in progress"); - } - const rolloutId = randomUUID(); await db.insert(rollouts).values({ diff --git a/web/app/(dashboard)/dashboard/projects/[slug]/[env]/services/[serviceId]/page.tsx b/web/app/(dashboard)/dashboard/projects/[slug]/[env]/services/[serviceId]/page.tsx index e9d674f..0314f07 100644 --- a/web/app/(dashboard)/dashboard/projects/[slug]/[env]/services/[serviceId]/page.tsx +++ b/web/app/(dashboard)/dashboard/projects/[slug]/[env]/services/[serviceId]/page.tsx @@ -132,7 +132,7 @@ export default function DeploymentsPage() { (service.configuredReplicas || []).length > 0; return ( -
+
- 0 ? ( - hasRunningDeployments ? ( - - - - - } +
+ 0 ? ( + hasRunningDeployments ? ( + + + + } > - - Restart - - - setConfirmAction("stop")} - className="text-orange-600 dark:text-orange-500" - > - - Stop All - - setConfirmAction("delete")} - > - - Delete All - - - - - ) : canStartAll ? ( - - - - + + + + onClick={() => + handleAction( + "restart", + () => restartService(service.id), + "Restart queued", + ) + } + > + + Restart + + + setConfirmAction("stop")} + className="text-orange-600 dark:text-orange-500" + > + + Stop All + + setConfirmAction("delete")} + > + + Delete All + + + + + ) : canStartAll ? ( + + + + + } > - - Delete All - - - - + + + + setConfirmAction("delete")} + > + + Delete All + + + + + ) : undefined ) : undefined - ) : undefined - } - /> + } + /> +
r[0]); if (githubRepo) { - const baseUrl = - process.env.APP_URL || "https://cloud.techulus.com"; + const baseUrl = process.env.APP_URL || "https://cloud.techulus.com"; const logUrl = `${baseUrl}/builds/${buildId}/logs`; if ( diff --git a/web/components/builds/build-details.tsx b/web/components/builds/build-details.tsx index e942a0a..0f0b27f 100644 --- a/web/components/builds/build-details.tsx +++ b/web/components/builds/build-details.tsx @@ -309,7 +309,11 @@ export function BuildDetails({ Build Failed - {build.error} + +
+							{build.error}
+						
+
)} diff --git a/web/components/builds/builds-viewer.tsx b/web/components/builds/builds-viewer.tsx index 9d724f3..8d53766 100644 --- a/web/components/builds/builds-viewer.tsx +++ b/web/components/builds/builds-viewer.tsx @@ -285,9 +285,9 @@ export function BuildsViewer({ )} {build.error && ( -
+
 										{build.error}
-									
+ )} e.stopPropagation()}> diff --git a/web/components/service/create-service-dialog.tsx b/web/components/service/create-service-dialog.tsx index 685a454..dc74668 100644 --- a/web/components/service/create-service-dialog.tsx +++ b/web/components/service/create-service-dialog.tsx @@ -188,8 +188,8 @@ export function CreateDockerServiceDialog({ /> {error &&

{error}

}

- Supported: Docker Hub, GitHub Container Registry (ghcr.io), or - any public registry + Supported: Docker Hub, GitHub Container Registry (ghcr.io), or any + public registry

diff --git a/web/components/service/details/deployment-progress.tsx b/web/components/service/details/deployment-progress.tsx index fe073c8..a561cbf 100644 --- a/web/components/service/details/deployment-progress.tsx +++ b/web/components/service/details/deployment-progress.tsx @@ -129,9 +129,7 @@ export function getBarState( const maxStageIndex = Math.max( ...service.deployments .filter((d) => inProgressStatuses.includes(d.status)) - .map((d) => - getStageIndex(mapDeploymentStatusToStage(d.status)), - ), + .map((d) => getStageIndex(mapDeploymentStatusToStage(d.status))), ); return { mode: "deploying", @@ -252,9 +250,7 @@ export const DeploymentProgress = memo(function DeploymentProgress({
-

- Building -

+

Building

{BUILD_STATUS_LABELS[barState.buildStatus] || "Building"}

@@ -306,9 +302,7 @@ export const DeploymentProgress = memo(function DeploymentProgress({

{isMigrating ? "Migrating" : "Deploying"}

-

- {status} -

+

{status}

{isMigrating ? ( @@ -343,7 +337,7 @@ export const DeploymentProgress = memo(function DeploymentProgress({ opacity: isVisible ? 1 : 0, }} > -
{content}
+
{content}
); }); diff --git a/web/components/service/details/pending-changes-banner.tsx b/web/components/service/details/pending-changes-banner.tsx index 6a3b010..6d2e09e 100644 --- a/web/components/service/details/pending-changes-banner.tsx +++ b/web/components/service/details/pending-changes-banner.tsx @@ -39,7 +39,8 @@ export const PendingChangesBanner = memo(function PendingChangesBanner({ const hasChanges = changes.length > 0; const showBanner = - barMode === "ready" && (hasChanges || (hasNoDeployments && totalReplicas > 0)); + barMode === "ready" && + (hasChanges || (hasNoDeployments && totalReplicas > 0)); const handleDeploy = async () => { setIsDeploying(true); @@ -66,7 +67,7 @@ export const PendingChangesBanner = memo(function PendingChangesBanner({ opacity: showBanner ? 1 : 0, }} > -
+
@@ -93,7 +94,9 @@ export const PendingChangesBanner = memo(function PendingChangesBanner({ {change.from} - {change.to} + + {change.to} +
))}
diff --git a/web/components/service/details/rollout-details.tsx b/web/components/service/details/rollout-details.tsx index 2c518f3..ef7eb90 100644 --- a/web/components/service/details/rollout-details.tsx +++ b/web/components/service/details/rollout-details.tsx @@ -123,9 +123,7 @@ export function RolloutDetails({ - + {config.label} {rollout.currentStage && isLive && ( diff --git a/web/components/service/details/rollout-history.tsx b/web/components/service/details/rollout-history.tsx index ab58a76..1c39224 100644 --- a/web/components/service/details/rollout-history.tsx +++ b/web/components/service/details/rollout-history.tsx @@ -1,12 +1,7 @@ "use client"; -import { - CheckCircle2, - Clock, - Loader2, - RotateCcw, - XCircle, -} from "lucide-react"; +import { useState } from "react"; +import { CheckCircle2, Clock, Loader2, RotateCcw, XCircle } from "lucide-react"; import Link from "next/link"; import useSWR from "swr"; import { @@ -118,11 +113,19 @@ export function RolloutHistory({ envName: string; actions?: React.ReactNode; }) { + const [hasInProgress, setHasInProgress] = useState(false); + const { data, isLoading } = useSWR<{ rollouts: RolloutListItem[] }>( `/api/services/${serviceId}/rollouts`, fetcher, { - refreshInterval: 10000, + refreshInterval: hasInProgress ? 3000 : 30000, + revalidateOnFocus: true, + onSuccess: (data) => { + setHasInProgress( + data?.rollouts?.some((r) => r.status === "in_progress") ?? false, + ); + }, }, ); @@ -174,10 +177,7 @@ export function RolloutHistory({ {formatRelativeTime(rollout.createdAt)} Duration:{" "} - {formatDuration( - rollout.createdAt, - rollout.completedAt, - )} + {formatDuration(rollout.createdAt, rollout.completedAt)} diff --git a/web/components/service/details/source-section.tsx b/web/components/service/details/source-section.tsx index 6c0ef04..cf0c2d6 100644 --- a/web/components/service/details/source-section.tsx +++ b/web/components/service/details/source-section.tsx @@ -144,11 +144,7 @@ export const SourceSection = memo(function SourceSection({ {!isEditing && ( - @@ -188,7 +184,11 @@ export const SourceSection = memo(function SourceSection({

- - @@ -299,9 +291,7 @@ export const SourceSection = memo(function SourceSection({ disabled={isSaving || !image.trim()} size="sm" > - {isSaving && ( - - )} + {isSaving && } Save