Skip to content
Closed
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
100 changes: 61 additions & 39 deletions app/api/tasks/[taskId]/deployment/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
}

// Look for Vercel check runs - try Preview Comments first as it's more likely to have the URL
// Note: We now fetch ALL deployment states, not just successful ones
const vercelPreviewCheck = checkRuns.check_runs.find(
(check) =>
check.app?.slug === 'vercel' && check.name === 'Vercel Preview Comments' && check.status === 'completed',
(check) => check.app?.slug === 'vercel' && check.name === 'Vercel Preview Comments',
)

const vercelDeploymentCheck = checkRuns.check_runs.find(
(check) =>
check.app?.slug === 'vercel' &&
check.name === 'Vercel' &&
check.conclusion === 'success' &&
check.status === 'completed',
(check) => check.app?.slug === 'vercel' && check.name === 'Vercel',
)

// Try to get preview URL from either check
Expand All @@ -183,17 +179,25 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
previewUrl = convertFeedbackUrlToDeploymentUrl(vercelDeploymentCheck.details_url)
}

if (previewUrl) {
// Store the preview URL in the database
await db.update(tasks).set({ previewUrl }).where(eq(tasks.id, taskId))
// If we found a Vercel check, return deployment info regardless of state
const vercelCheck = vercelDeploymentCheck || vercelPreviewCheck
if (vercelCheck) {
// Store the preview URL in the database if available
if (previewUrl) {
await db.update(tasks).set({ previewUrl }).where(eq(tasks.id, taskId))
}

return NextResponse.json({
success: true,
data: {
hasDeployment: true,
previewUrl,
checkId: vercelDeploymentCheck?.id || vercelPreviewCheck?.id,
createdAt: vercelDeploymentCheck?.completed_at || vercelPreviewCheck?.completed_at,
previewUrl: previewUrl || undefined,
checkId: vercelCheck.id,
status: vercelCheck.status,
conclusion: vercelCheck.conclusion,
createdAt: vercelCheck.started_at,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API returns started_at as createdAt for GitHub Checks, but the component uses this field to display "Deployed [time]". This shows the deployment START time instead of COMPLETION time, which is misleading.

View Details
📝 Patch Details
diff --git a/components/task-chat.tsx b/components/task-chat.tsx
index b803308..c6a8b7d 100644
--- a/components/task-chat.tsx
+++ b/components/task-chat.tsx
@@ -667,20 +667,28 @@ export function TaskChat({ taskId, task }: TaskChatProps) {
                     <div className="text-xs font-medium truncate">Vercel Preview</div>
                     <div className="text-xs text-muted-foreground">
                       {deployment.status === 'completed' && deployment.conclusion === 'success'
-                        ? deployment.createdAt
-                          ? `Deployed ${new Date(deployment.createdAt).toLocaleString()}`
+                        ? deployment.completedAt || deployment.updatedAt || deployment.createdAt
+                          ? `Deployed ${new Date(
+                              (deployment.completedAt || deployment.updatedAt || deployment.createdAt) as string,
+                            ).toLocaleString()}`
                           : 'Preview deployment'
-                        : deployment.status === 'in_progress' || deployment.status === 'queued'
-                          ? 'Deployment in progress...'
-                          : deployment.conclusion === 'failure' ||
-                              deployment.state === 'error' ||
-                              deployment.state === 'failure'
-                            ? 'Deployment failed'
-                            : deployment.conclusion === 'cancelled'
-                              ? 'Deployment cancelled'
-                              : deployment.state === 'pending'
-                                ? 'Deployment pending...'
-                                : 'Deployment status unknown'}
+                        : deployment.state === 'success'
+                          ? deployment.completedAt || deployment.updatedAt || deployment.createdAt
+                            ? `Deployed ${new Date(
+                                (deployment.completedAt || deployment.updatedAt || deployment.createdAt) as string,
+                              ).toLocaleString()}`
+                            : 'Preview deployment'
+                          : deployment.status === 'in_progress' || deployment.status === 'queued'
+                            ? 'Deployment in progress...'
+                            : deployment.conclusion === 'failure' ||
+                                deployment.state === 'error' ||
+                                deployment.state === 'failure'
+                              ? 'Deployment failed'
+                              : deployment.conclusion === 'cancelled'
+                                ? 'Deployment cancelled'
+                                : deployment.state === 'pending'
+                                  ? 'Deployment pending...'
+                                  : 'Deployment status unknown'}
                     </div>
                   </div>
                   {(deployment.status === 'in_progress' ||

Analysis

Deployment completion timestamp displayed incorrectly - shows start time instead of completion time

What fails: TaskChat component displays deployment start time instead of completion time for successful Vercel deployments.

How to reproduce:

  1. Create a pull request that triggers a Vercel Preview deployment
  2. Wait for the deployment to complete successfully
  3. View the task details and navigate to the Deployments tab
  4. Observe the "Deployed [time]" message

Result: Shows the timestamp when the deployment STARTED (from started_at), not when it COMPLETED (from completed_at)

Expected: Should show the timestamp when the deployment COMPLETED

Root cause: In commit f2e65cb, the API response was changed from createdAt: completed_at to createdAt: started_at, with a new field completedAt: completed_at added. However, the component still displayed using deployment.createdAt for all deployment sources.

Fix applied: Modified components/task-chat.tsx (line 670-671) to:

  • Use completedAt first (available for GitHub Checks API) - represents actual completion time
  • Fall back to updatedAt (available for GitHub Deployments and Commit Statuses APIs) - represents when status was updated to success
  • Fall back to createdAt as final fallback

This ensures accurate timestamps are displayed for successful deployments across all deployment sources.

completedAt: vercelCheck.completed_at,
detailsUrl: vercelCheck.details_url,
},
})
}
Expand Down Expand Up @@ -231,25 +235,39 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{

if (statuses && statuses.length > 0) {
const status = statuses[0]
if (status.state === 'success') {
let previewUrl = status.environment_url || status.target_url
if (previewUrl) {
// Convert feedback URL to actual deployment URL if needed
previewUrl = convertFeedbackUrlToDeploymentUrl(previewUrl)
// Store the preview URL in the database
// Return deployment info regardless of state
let previewUrl = status.environment_url || status.target_url
if (previewUrl) {
// Convert feedback URL to actual deployment URL if needed
previewUrl = convertFeedbackUrlToDeploymentUrl(previewUrl)
// Store the preview URL in the database only if successful
if (status.state === 'success') {
await db.update(tasks).set({ previewUrl }).where(eq(tasks.id, taskId))

return NextResponse.json({
success: true,
data: {
hasDeployment: true,
previewUrl,
deploymentId: deployment.id,
createdAt: deployment.created_at,
},
})
}

return NextResponse.json({
success: true,
data: {
hasDeployment: true,
previewUrl,
deploymentId: deployment.id,
state: status.state,
createdAt: deployment.created_at,
updatedAt: status.updated_at,
},
})
}
// Return deployment info even without URL if state is not success
return NextResponse.json({
success: true,
data: {
hasDeployment: true,
deploymentId: deployment.id,
state: status.state,
createdAt: deployment.created_at,
updatedAt: status.updated_at,
},
})
}
}
}
Expand All @@ -269,23 +287,27 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
per_page: 100,
})

const vercelStatus = statuses.find(
(status) =>
status.context?.toLowerCase().includes('vercel') && status.state === 'success' && status.target_url,
)
const vercelStatus = statuses.find((status) => status.context?.toLowerCase().includes('vercel'))

if (vercelStatus && vercelStatus.target_url) {
// Convert feedback URL to actual deployment URL if needed
const previewUrl = convertFeedbackUrlToDeploymentUrl(vercelStatus.target_url)
// Store the preview URL in the database
await db.update(tasks).set({ previewUrl }).where(eq(tasks.id, taskId))
if (vercelStatus) {
let previewUrl: string | null = null
if (vercelStatus.target_url) {
// Convert feedback URL to actual deployment URL if needed
previewUrl = convertFeedbackUrlToDeploymentUrl(vercelStatus.target_url)
// Store the preview URL in the database only if successful
if (vercelStatus.state === 'success') {
await db.update(tasks).set({ previewUrl }).where(eq(tasks.id, taskId))
}
}

return NextResponse.json({
success: true,
data: {
hasDeployment: true,
previewUrl,
previewUrl: previewUrl || undefined,
state: vercelStatus.state,
createdAt: vercelStatus.created_at,
updatedAt: vercelStatus.updated_at,
},
})
}
Expand Down
115 changes: 99 additions & 16 deletions components/task-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
RefreshCw,
MoreVertical,
MessageSquare,
ExternalLink,
} from 'lucide-react'
import { toast } from 'sonner'
import { Streamdown } from 'streamdown'
Expand Down Expand Up @@ -56,6 +57,12 @@ interface DeploymentInfo {
previewUrl?: string
message?: string
createdAt?: string
completedAt?: string
updatedAt?: string
status?: string // queued, in_progress, completed
conclusion?: string | null // success, failure, neutral, cancelled, skipped, timed_out, action_required
state?: string // pending, success, error, failure
detailsUrl?: string
}

export function TaskChat({ taskId, task }: TaskChatProps) {
Expand Down Expand Up @@ -646,24 +653,100 @@ export function TaskChat({ taskId, task }: TaskChatProps) {
</div>
) : (
<div className="space-y-2 px-2">
<a
href={deployment.previewUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-2 rounded-md hover:bg-muted/50 transition-colors"
>
<svg className="w-4 h-4 flex-shrink-0" viewBox="0 0 76 65" fill="currentColor">
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" />
</svg>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium truncate">Vercel Preview</div>
<div className="text-xs text-muted-foreground">
{deployment.createdAt
? `Deployed ${new Date(deployment.createdAt).toLocaleString()}`
: 'Preview deployment'}
{deployment.previewUrl ? (
<a
href={deployment.previewUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-2 rounded-md hover:bg-muted/50 transition-colors"
>
<svg className="w-4 h-4 flex-shrink-0" viewBox="0 0 76 65" fill="currentColor">
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" />
</svg>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium truncate">Vercel Preview</div>
<div className="text-xs text-muted-foreground">
{deployment.status === 'completed' && deployment.conclusion === 'success'
? deployment.createdAt
? `Deployed ${new Date(deployment.createdAt).toLocaleString()}`
: 'Preview deployment'
: deployment.status === 'in_progress' || deployment.status === 'queued'
? 'Deployment in progress...'
: deployment.conclusion === 'failure' ||
deployment.state === 'error' ||
deployment.state === 'failure'
? 'Deployment failed'
: deployment.conclusion === 'cancelled'
? 'Deployment cancelled'
: deployment.state === 'pending'
? 'Deployment pending...'
: 'Deployment status unknown'}
</div>
</div>
{(deployment.status === 'in_progress' ||
deployment.status === 'queued' ||
deployment.state === 'pending') && (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
)}
{(deployment.conclusion === 'failure' ||
deployment.state === 'error' ||
deployment.state === 'failure') && (
<svg className="h-4 w-4 text-destructive" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
{deployment.status === 'completed' && deployment.conclusion === 'success' && (
<svg className="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</a>
) : (
<div className="flex items-center gap-3 p-2 rounded-md bg-muted/30">
<svg className="w-4 h-4 flex-shrink-0" viewBox="0 0 76 65" fill="currentColor">
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" />
</svg>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium">Vercel Deployment</div>
<div className="text-xs text-muted-foreground">
{deployment.status === 'in_progress' || deployment.status === 'queued'
? 'Building...'
: deployment.state === 'pending'
? 'Pending...'
: deployment.conclusion === 'failure' ||
deployment.state === 'error' ||
deployment.state === 'failure'
? 'Build failed'
: deployment.conclusion === 'cancelled'
? 'Build cancelled'
: 'Building...'}
</div>
</div>
{(deployment.status === 'in_progress' ||
deployment.status === 'queued' ||
deployment.state === 'pending') && (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
)}
{(deployment.conclusion === 'failure' ||
deployment.state === 'error' ||
deployment.state === 'failure') && (
<svg className="h-4 w-4 text-destructive" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</div>
</a>
)}
{deployment.detailsUrl && (
<a
href={deployment.detailsUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<ExternalLink className="h-3 w-3" />
View deployment details
</a>
)}
</div>
)}
</div>
Expand Down
Loading