Skip to content

Commit 25c93c3

Browse files
authored
Improve import status handling (#151)
1 parent 7746688 commit 25c93c3

4 files changed

Lines changed: 161 additions & 27 deletions

File tree

app/(dashboard)/projects/_components/import-github-dialog.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useCallback, useEffect, useState } from 'react'
44
import { FaGithub } from 'react-icons/fa'
55
import { MdRefresh } from 'react-icons/md'
6-
import type { ProjectTaskStatus, ProjectTaskType } from '@prisma/client'
6+
import type { ProjectStatus, ProjectTask, ResourceStatus } from '@prisma/client'
77
import { useRouter } from 'next/navigation'
88
import { toast } from 'sonner'
99

@@ -19,6 +19,7 @@ import {
1919
import { importProjectFromGitHub } from '@/lib/actions/project'
2020
import { env } from '@/lib/env'
2121
import { GET } from '@/lib/fetch-client'
22+
import { getProjectImportStatus } from '@/lib/util/project-import-status'
2223

2324
type Step = 'loading' | 'check-github-app' | 'select-repo'
2425

@@ -27,6 +28,12 @@ interface ImportGitHubDialogProps {
2728
onOpenChange: (open: boolean) => void
2829
}
2930

31+
type ImportStatusPollResponse = {
32+
status: ProjectStatus
33+
sandboxes: Array<{ status: ResourceStatus }>
34+
tasks: Array<Pick<ProjectTask, 'type' | 'status' | 'createdAt'>>
35+
}
36+
3037
export function ImportGitHubDialog({ open, onOpenChange }: ImportGitHubDialogProps) {
3138
const router = useRouter()
3239
const [step, setStep] = useState<Step>('loading')
@@ -97,20 +104,29 @@ export function ImportGitHubDialog({ open, onOpenChange }: ImportGitHubDialogPro
97104

98105
const pollImportStatus = async () => {
99106
try {
100-
const project = await GET<{ tasks: Array<{ type: ProjectTaskType; status: ProjectTaskStatus }> }>(
107+
const project = await GET<ImportStatusPollResponse>(
101108
`/api/projects/${importProjectId}`
102109
)
103110

104-
const cloneTask = project.tasks.find((task) => task.type === 'CLONE_REPOSITORY')
105-
if (!cloneTask || cloneTask.status === 'SUCCEEDED') {
111+
if (project.status === 'ERROR' || project.sandboxes.some((sandbox) => sandbox.status === 'ERROR')) {
112+
toast.error('Project creation failed because the sandbox could not start.')
113+
onOpenChange(false)
114+
setImportProjectId(null)
115+
router.refresh()
116+
return
117+
}
118+
119+
const importStatus = getProjectImportStatus({ tasks: project.tasks })
120+
121+
if (importStatus === 'IMPORTED') {
106122
toast.success('Repository imported successfully')
107123
onOpenChange(false)
108124
setImportProjectId(null)
109125
router.refresh()
110126
return
111127
}
112128

113-
if (cloneTask.status === 'FAILED' || cloneTask.status === 'CANCELLED') {
129+
if (importStatus === 'IMPORT_FAILED') {
114130
toast.error('Repository import failed. An empty project was created instead.')
115131
onOpenChange(false)
116132
setImportProjectId(null)

docs/prds/import-project-control-flow.md

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ repository as a Fulling project.
99

1010
This PRD exists to clarify what "success" means for:
1111

12+
- request acceptance
1213
- project creation
1314
- sandbox creation
1415
- repository cloning
@@ -22,6 +23,7 @@ This document covers the current import flow for:
2223
- creating the initial sandbox for that project
2324
- cloning the selected repository into the sandbox
2425
- representing clone failure without rolling back the project
26+
- deriving import status from the clone task lifecycle
2527

2628
This document does not define future repository analysis, skill installation, or
2729
deploy automation after import.
@@ -38,6 +40,18 @@ product success condition.
3840

3941
## Success Semantics
4042

43+
### Request acceptance success
44+
45+
The import request is considered accepted when the control plane successfully:
46+
47+
- verifies the selected GitHub installation and repository access
48+
- creates the project record
49+
- creates the initial sandbox record
50+
- creates the initial clone-repository task
51+
52+
At this point, the API may return success immediately even though the sandbox is
53+
not yet runnable and the repository has not yet been cloned.
54+
4155
### Project creation success
4256

4357
A project is considered successfully created when its sandbox is successfully created
@@ -53,11 +67,23 @@ This means:
5367
The import transaction is considered successful only when the repository is cloned
5468
successfully into the sandbox.
5569

70+
This is independent from request acceptance and independent from project creation
71+
success once the sandbox is already runnable.
72+
5673
## Failure Semantics
5774

5875
### Sandbox creation failure
5976

60-
If the sandbox fails to reach a runnable state, project creation is considered failed.
77+
If the sandbox enters `ERROR`, project creation is considered failed.
78+
79+
Current implementation note:
80+
81+
- explicit sandbox create/start failures transition the sandbox to `ERROR`
82+
- there is currently no startup timeout that converts a sandbox stuck in `STARTING`
83+
into `ERROR`
84+
85+
So today, "failed to reach runnable state" is fully represented only for explicit
86+
failures, not for indefinite startup stalls.
6187

6288
### Repository clone failure
6389

@@ -84,6 +110,7 @@ For the current stage of the product:
84110
- the user should still land in a usable project with an empty sandbox
85111
- no dedicated import-failure modal is required yet
86112
- the system should preserve existing code paths as much as possible
113+
- the UI should not wait forever on clone task state if the sandbox has already failed
87114

88115
## Status Requirements
89116

@@ -92,21 +119,49 @@ The system should represent two layers of status:
92119
1. Project resource status
93120
2. Import transaction status
94121

95-
For the current product behavior:
122+
### Project resource status
123+
124+
Project resource status is represented by `Project.status` and continues to mean
125+
resource lifecycle state only.
126+
127+
Examples:
128+
129+
- `CREATING`
130+
- `STARTING`
131+
- `RUNNING`
132+
- `ERROR`
133+
134+
### Import transaction status
135+
136+
Import transaction status is not a separate persisted `ProjectStatus` enum.
137+
It is derived from the latest `CLONE_REPOSITORY` task for the project.
138+
139+
Derived meaning:
140+
141+
- `WAITING_FOR_PREREQUISITES`, `PENDING`, or `RUNNING` => `IMPORTING`
142+
- `SUCCEEDED` => `IMPORTED`
143+
- `FAILED` or `CANCELLED` => `IMPORT_FAILED`
144+
145+
For the current product contract:
96146

97147
- project status may become `RUNNING`
98-
- import may independently become `IMPORT_FAILED`
148+
- import may independently derive to `IMPORT_FAILED`
99149

100150
The intended current UI meaning is:
101151

102-
- `RUNNING + IMPORT FAILED`
152+
- `RUNNING + IMPORT_FAILED`
103153

104154
This combination means:
105155

106156
- the sandbox is available
107157
- the project exists and is usable
108158
- the requested repository import did not complete successfully
109159

160+
Current implementation note:
161+
162+
- UI may choose to render this as `Needs Attention`
163+
- `Needs Attention` is a presentation label, not the underlying persisted import status
164+
110165
## Retry Behavior
111166

112167
Repository clone should automatically retry up to 3 times, matching the current system behavior.
@@ -132,6 +187,12 @@ If clone fails, the database must still clearly reflect:
132187
- project creation succeeded
133188
- import did not succeed
134189

190+
This currently means:
191+
192+
- `Project` persists the imported GitHub metadata
193+
- `ProjectTask` persists clone attempts, final success or failure, and error text
194+
- import status is inferred from task state rather than stored as a dedicated project column
195+
135196
## GitHub Metadata Requirements
136197

137198
If the repository later becomes unavailable or permissions change, the project should
@@ -150,15 +211,19 @@ This PRD does not define:
150211

151212
- a new import intent model
152213
- a dedicated import failure modal
214+
- a new `ProjectStatus` enum value for import outcomes
153215
- post-import repository analysis
154216
- skill installation after import
155217
- deployment after import
156218
- new manual retry workflows
219+
- sandbox startup timeout policy
157220

158221
## Implementation Notes
159222

160223
Current implementation should preserve this product contract:
161224

162-
- project creation success is anchored to sandbox success
225+
- request acceptance is synchronous and returns after control-plane state is created
226+
- project creation success is asynchronous and is anchored to sandbox success
163227
- clone failure is visible as an import failure, not as project creation failure
164228
- import logic may fail independently after the project already exists
229+
- import status should be derived from clone task state, not folded into `Project.status`

lib/util/project-display-status.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { ProjectStatus, ProjectTask, ProjectTaskStatus, ProjectTaskType } from '@prisma/client'
1+
import type { ProjectStatus, ProjectTask } from '@prisma/client'
2+
3+
import { getProjectImportStatus } from '@/lib/util/project-import-status'
24

35
export type ProjectDisplayStatus =
46
| 'CREATING'
@@ -30,9 +32,9 @@ export function getProjectDisplayStatus(
3032
project: ProjectDisplayStatusInput
3133
): ProjectDisplayStatus {
3234
const importProject = isImportProject(project)
33-
const cloneTask = getLatestTask(project.tasks, 'CLONE_REPOSITORY')
35+
const importStatus = getProjectImportStatus({ tasks: project.tasks })
3436

35-
if (cloneTask?.status === 'FAILED') {
37+
if (importStatus === 'IMPORT_FAILED') {
3638
return 'NEEDS_ATTENTION'
3739
}
3840

@@ -56,7 +58,7 @@ export function getProjectDisplayStatus(
5658
return 'UPDATING'
5759
}
5860

59-
if (importProject && cloneTask && isActiveTaskStatus(cloneTask.status)) {
61+
if (importProject && importStatus === 'IMPORTING') {
6062
return 'IMPORTING'
6163
}
6264

@@ -74,16 +76,3 @@ export function getProjectDisplayStatus(
7476

7577
return 'NEEDS_ATTENTION'
7678
}
77-
78-
function getLatestTask(
79-
tasks: ProjectDisplayStatusInput['tasks'],
80-
type: ProjectTaskType
81-
) {
82-
return tasks
83-
?.filter((task) => task.type === type)
84-
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]
85-
}
86-
87-
function isActiveTaskStatus(status: ProjectTaskStatus): boolean {
88-
return ['PENDING', 'WAITING_FOR_PREREQUISITES', 'RUNNING'].includes(status)
89-
}

lib/util/project-import-status.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { ProjectTask, ProjectTaskStatus, ProjectTaskType } from '@prisma/client'
2+
3+
export type ProjectImportStatus = 'IMPORTING' | 'IMPORTED' | 'IMPORT_FAILED' | 'NOT_STARTED'
4+
5+
type ProjectImportStatusInput = {
6+
tasks?: Pick<ProjectTask, 'type' | 'status' | 'createdAt'>[]
7+
}
8+
9+
/**
10+
* Derives the current import transaction state from the latest clone task.
11+
*
12+
* Expected inputs:
13+
* - Project tasks that may include one or more CLONE_REPOSITORY records.
14+
*
15+
* Expected outputs:
16+
* - Returns the current import state for the latest clone task.
17+
*
18+
* Out of scope:
19+
* - Does not inspect sandbox or project resource status.
20+
* - Does not infer whether a project originated from GitHub metadata.
21+
*/
22+
export function getProjectImportStatus(
23+
input: ProjectImportStatusInput
24+
): ProjectImportStatus {
25+
const cloneTask = getLatestTask(input.tasks, 'CLONE_REPOSITORY')
26+
27+
if (!cloneTask) {
28+
return 'NOT_STARTED'
29+
}
30+
31+
if (isActiveTaskStatus(cloneTask.status)) {
32+
return 'IMPORTING'
33+
}
34+
35+
if (cloneTask.status === 'SUCCEEDED') {
36+
return 'IMPORTED'
37+
}
38+
39+
if (cloneTask.status === 'FAILED' || cloneTask.status === 'CANCELLED') {
40+
return 'IMPORT_FAILED'
41+
}
42+
43+
return 'NOT_STARTED'
44+
}
45+
46+
export function getLatestProjectTask(
47+
tasks: ProjectImportStatusInput['tasks'],
48+
type: ProjectTaskType
49+
) {
50+
return getLatestTask(tasks, type)
51+
}
52+
53+
function getLatestTask(
54+
tasks: ProjectImportStatusInput['tasks'],
55+
type: ProjectTaskType
56+
) {
57+
return tasks
58+
?.filter((task) => task.type === type)
59+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]
60+
}
61+
62+
function isActiveTaskStatus(status: ProjectTaskStatus): boolean {
63+
return ['PENDING', 'WAITING_FOR_PREREQUISITES', 'RUNNING'].includes(status)
64+
}

0 commit comments

Comments
 (0)