Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions agent/internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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))
}
}

Expand Down
27 changes: 4 additions & 23 deletions web/actions/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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): {
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default function DeploymentsPage() {
(service.configuredReplicas || []).length > 0;

return (
<div className="space-y-4">
<div>
<DeploymentProgress
service={service}
changes={pendingChanges}
Expand All @@ -152,110 +152,112 @@ export default function DeploymentsPage() {

<DeploymentCanvas service={service} />

<RolloutHistory
serviceId={service.id}
projectSlug={projectSlug}
envName={envName}
actions={
service.deployments.length > 0 ? (
hasRunningDeployments ? (
<ButtonGroup>
<Button
variant="outline"
size="sm"
disabled={isLoading !== null}
onClick={() => setConfirmAction("redeploy")}
>
<RotateCcwIcon data-icon="inline-start" />
{isLoading === "redeploy" ? "Redeploying..." : "Redeploy"}
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
size="icon-sm"
disabled={isLoading !== null}
/>
}
<div className="mt-4">
<RolloutHistory
serviceId={service.id}
projectSlug={projectSlug}
envName={envName}
actions={
service.deployments.length > 0 ? (
hasRunningDeployments ? (
<ButtonGroup>
<Button
variant="outline"
size="sm"
disabled={isLoading !== null}
onClick={() => setConfirmAction("redeploy")}
>
<ChevronDownIcon />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem
disabled={isLoading !== null}
onClick={() =>
handleAction(
"restart",
() => restartService(service.id),
"Restart queued",
)
<RotateCcwIcon data-icon="inline-start" />
{isLoading === "redeploy" ? "Redeploying..." : "Redeploy"}
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
size="icon-sm"
disabled={isLoading !== null}
/>
}
>
<RefreshCwIcon />
Restart
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={isLoading !== null}
onClick={() => setConfirmAction("stop")}
className="text-orange-600 dark:text-orange-500"
>
<StopCircleIcon />
Stop All
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
disabled={isLoading !== null}
onClick={() => setConfirmAction("delete")}
>
<Trash2Icon />
Delete All
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
) : canStartAll ? (
<ButtonGroup>
<Button
variant="default"
size="sm"
disabled={isLoading !== null}
onClick={() =>
handleAction("start", () => deployService(service.id))
}
>
<PlayIcon data-icon="inline-start" />
{isLoading === "start" ? "Starting..." : "Start All"}
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="default"
size="icon-sm"
<ChevronDownIcon />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem
disabled={isLoading !== null}
/>
onClick={() =>
handleAction(
"restart",
() => restartService(service.id),
"Restart queued",
)
}
>
<RefreshCwIcon />
Restart
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={isLoading !== null}
onClick={() => setConfirmAction("stop")}
className="text-orange-600 dark:text-orange-500"
>
<StopCircleIcon />
Stop All
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
disabled={isLoading !== null}
onClick={() => setConfirmAction("delete")}
>
<Trash2Icon />
Delete All
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
) : canStartAll ? (
<ButtonGroup>
<Button
variant="default"
size="sm"
disabled={isLoading !== null}
onClick={() =>
handleAction("start", () => deployService(service.id))
}
>
<ChevronDownIcon />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem
variant="destructive"
disabled={isLoading !== null}
onClick={() => setConfirmAction("delete")}
<PlayIcon data-icon="inline-start" />
{isLoading === "start" ? "Starting..." : "Start All"}
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="default"
size="icon-sm"
disabled={isLoading !== null}
/>
}
>
<Trash2Icon />
Delete All
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
<ChevronDownIcon />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem
variant="destructive"
disabled={isLoading !== null}
onClick={() => setConfirmAction("delete")}
>
<Trash2Icon />
Delete All
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
) : undefined
) : undefined
) : undefined
}
/>
}
/>
</div>

<AlertDialog
open={confirmAction !== null}
Expand Down
2 changes: 2 additions & 0 deletions web/app/api/services/[id]/rollouts/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const dynamic = "force-dynamic";

import { NextRequest, NextResponse } from "next/server";
import { desc, eq } from "drizzle-orm";
import { db } from "@/db";
Expand Down
3 changes: 1 addition & 2 deletions web/app/api/v1/agent/builds/[id]/status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ export async function POST(
.then((r) => 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 (
Expand Down
6 changes: 5 additions & 1 deletion web/components/builds/build-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,11 @@ export function BuildDetails({
<Alert variant="destructive">
<XCircle className="size-4" />
<AlertTitle>Build Failed</AlertTitle>
<AlertDescription>{build.error}</AlertDescription>
<AlertDescription>
<pre className="whitespace-pre-wrap font-mono text-xs mt-1">
{build.error}
</pre>
</AlertDescription>
</Alert>
)}

Expand Down
4 changes: 2 additions & 2 deletions web/components/builds/builds-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,9 @@ export function BuildsViewer({
)}
</ItemDescription>
{build.error && (
<div className="mt-1 text-xs text-red-500 bg-red-500/10 rounded p-2">
<pre className="mt-1 text-xs text-red-500 bg-red-500/10 rounded p-2 whitespace-pre-wrap font-mono">
{build.error}
</div>
</pre>
)}
</ItemContent>
<ItemActions onClick={(e) => e.stopPropagation()}>
Expand Down
4 changes: 2 additions & 2 deletions web/components/service/create-service-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ export function CreateDockerServiceDialog({
/>
{error && <p className="text-sm text-red-500">{error}</p>}
<p className="text-xs text-muted-foreground">
Supported: Docker Hub, GitHub Container Registry (ghcr.io), or
any public registry
Supported: Docker Hub, GitHub Container Registry (ghcr.io), or any
public registry
</p>
</div>
<div className="flex justify-end gap-2">
Expand Down
14 changes: 4 additions & 10 deletions web/components/service/details/deployment-progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -252,9 +250,7 @@ export const DeploymentProgress = memo(function DeploymentProgress({
<Loader2 className="size-4 animate-spin" />
</div>
<div>
<p className="font-medium text-foreground">
Building
</p>
<p className="font-medium text-foreground">Building</p>
<p className="text-sm text-muted-foreground">
{BUILD_STATUS_LABELS[barState.buildStatus] || "Building"}
</p>
Expand Down Expand Up @@ -306,9 +302,7 @@ export const DeploymentProgress = memo(function DeploymentProgress({
<p className="font-medium text-foreground">
{isMigrating ? "Migrating" : "Deploying"}
</p>
<p className="text-sm text-muted-foreground">
{status}
</p>
<p className="text-sm text-muted-foreground">{status}</p>
</div>
</div>
{isMigrating ? (
Expand Down Expand Up @@ -343,7 +337,7 @@ export const DeploymentProgress = memo(function DeploymentProgress({
opacity: isVisible ? 1 : 0,
}}
>
<div className="overflow-hidden">{content}</div>
<div className="overflow-hidden pb-4">{content}</div>
</div>
);
});
Loading