Skip to content

Commit d161158

Browse files
🐛 Fix failed deployment UX (#41)
1 parent 9119b0b commit d161158

File tree

2 files changed

+206
-8
lines changed

2 files changed

+206
-8
lines changed

src/cloud/commands/deploy.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,12 @@ export async function deploy(context: DeployContext): Promise<boolean> {
147147
updateStatus,
148148
)
149149

150-
if (result) {
150+
const isSuccess =
151+
result !== null &&
152+
(result.status === DeploymentStatus.success ||
153+
result.status === DeploymentStatus.verifying_skipped)
154+
155+
if (isSuccess) {
151156
statusBarItem.text = `$(cloud) ${config.app_slug ?? "Deployed"}`
152157

153158
const action = await ui.showInformationMessage(
@@ -165,15 +170,15 @@ export async function deploy(context: DeployContext): Promise<boolean> {
165170
}
166171
return true
167172
}
168-
if (statusBarItem) {
169-
statusBarItem.text = "$(cloud) Deploy failed"
170-
}
173+
174+
statusBarItem.text = "$(cloud) Deploy failed"
171175
const action = await vscode.window.showErrorMessage(
172176
"Deployment failed.",
173-
"View Logs",
177+
"View Dashboard",
174178
)
175-
if (action === "View Logs") {
176-
vscode.commands.executeCommand("fastapi-vscode.viewLogs") //TODO: Wire this up
179+
if (action === "View Dashboard" && result?.dashboard_url) {
180+
vscode.env.openExternal(vscode.Uri.parse(result.dashboard_url))
181+
trackCloudDashboardOpened(config.app_id)
177182
}
178183
return false
179184
} catch (error) {
@@ -254,7 +259,7 @@ async function pollDeploymentStatus(
254259
}
255260

256261
if (failedStatuses.includes(deployment.status)) {
257-
return null
262+
return deployment
258263
}
259264

260265
const message =

src/test/cloud/commands/deploy.test.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,197 @@ suite("cloud/commands/deploy", () => {
213213
)
214214
assert.ok(fetchStub.calledOnce)
215215
})
216+
217+
test("shows dashboard link on deployment failure", async () => {
218+
sinon.stub(vscode.authentication, "getSession").resolves({
219+
accessToken: "test-token",
220+
account: { id: "test-id", label: "test-user" },
221+
id: "session-id",
222+
scopes: [],
223+
})
224+
const statusBarItem = { text: "" } as vscode.StatusBarItem
225+
const workspaceRoot = vscode.Uri.file("/test/workspace")
226+
227+
const configService = mockConfigService()
228+
configService.getConfig.resolves({ app_id: "app123", team_id: "team123" })
229+
230+
const mockDeployment: Deployment = {
231+
id: "deploy123",
232+
slug: "deploy-slug",
233+
status: DeploymentStatus.waiting_upload,
234+
url: "https://app.example.com",
235+
dashboard_url:
236+
"https://dashboard.fastapicloud.com/team-slug/apps/my-app/deployments",
237+
}
238+
const mockFailedDeployment: Deployment = {
239+
...mockDeployment,
240+
status: DeploymentStatus.building_image_failed,
241+
}
242+
243+
const apiService = mockApiService()
244+
apiService.createDeployment.resolves(mockDeployment)
245+
apiService.getUploadUrl.resolves({
246+
url: "https://s3.example.com",
247+
fields: {},
248+
})
249+
apiService.completeUpload.resolves()
250+
apiService.getDeployment.resolves(mockFailedDeployment)
251+
252+
sinon
253+
.stub(vscode.workspace, "findFiles")
254+
.resolves([vscode.Uri.file("/test/workspace/main.py")])
255+
const fs = stubFs()
256+
fs.fake.readFile.resolves(new Uint8Array([1, 2, 3]))
257+
sinon.stub(global, "fetch").resolves({ ok: true, status: 200 } as Response)
258+
259+
const openExternalStub = sinon
260+
.stub(vscode.env, "openExternal")
261+
.resolves(true)
262+
const errorMessageStub = sinon
263+
.stub(vscode.window, "showErrorMessage")
264+
.resolves("View Dashboard" as any)
265+
266+
const result = await deploy({
267+
workspaceRoot,
268+
configService,
269+
apiService,
270+
statusBarItem,
271+
})
272+
273+
assert.strictEqual(result, false)
274+
assert.strictEqual(statusBarItem.text, "$(cloud) Deploy failed")
275+
assert.ok(errorMessageStub.calledOnce)
276+
assert.strictEqual(errorMessageStub.firstCall.args[0], "Deployment failed.")
277+
assert.ok(openExternalStub.calledOnce)
278+
assert.strictEqual(
279+
openExternalStub.firstCall.args[0].toString(),
280+
"https://dashboard.fastapicloud.com/team-slug/apps/my-app/deployments",
281+
)
282+
})
283+
284+
test("does not open dashboard when user dismisses failure dialog", async () => {
285+
sinon.stub(vscode.authentication, "getSession").resolves({
286+
accessToken: "test-token",
287+
account: { id: "test-id", label: "test-user" },
288+
id: "session-id",
289+
scopes: [],
290+
})
291+
const statusBarItem = { text: "" } as vscode.StatusBarItem
292+
const workspaceRoot = vscode.Uri.file("/test/workspace")
293+
294+
const configService = mockConfigService()
295+
configService.getConfig.resolves({ app_id: "app123", team_id: "team123" })
296+
297+
const mockDeployment: Deployment = {
298+
id: "deploy123",
299+
slug: "deploy-slug",
300+
status: DeploymentStatus.waiting_upload,
301+
url: "https://app.example.com",
302+
dashboard_url:
303+
"https://dashboard.fastapicloud.com/team-slug/apps/my-app/deployments",
304+
}
305+
306+
const apiService = mockApiService()
307+
apiService.createDeployment.resolves(mockDeployment)
308+
apiService.getUploadUrl.resolves({
309+
url: "https://s3.example.com",
310+
fields: {},
311+
})
312+
apiService.completeUpload.resolves()
313+
apiService.getDeployment.resolves({
314+
...mockDeployment,
315+
status: DeploymentStatus.building_image_failed,
316+
})
317+
318+
sinon
319+
.stub(vscode.workspace, "findFiles")
320+
.resolves([vscode.Uri.file("/test/workspace/main.py")])
321+
const fs = stubFs()
322+
fs.fake.readFile.resolves(new Uint8Array([1, 2, 3]))
323+
sinon.stub(global, "fetch").resolves({ ok: true, status: 200 } as Response)
324+
325+
const openExternalStub = sinon
326+
.stub(vscode.env, "openExternal")
327+
.resolves(true)
328+
sinon.stub(vscode.window, "showErrorMessage").resolves(undefined as any)
329+
330+
const result = await deploy({
331+
workspaceRoot,
332+
configService,
333+
apiService,
334+
statusBarItem,
335+
})
336+
337+
assert.strictEqual(result, false)
338+
assert.ok(openExternalStub.notCalled)
339+
})
340+
341+
test("does not open dashboard on poll timeout", async () => {
342+
const clock = sinon.useFakeTimers({ shouldClearNativeTimers: true })
343+
344+
sinon.stub(vscode.authentication, "getSession").resolves({
345+
accessToken: "test-token",
346+
account: { id: "test-id", label: "test-user" },
347+
id: "session-id",
348+
scopes: [],
349+
})
350+
const statusBarItem = { text: "" } as vscode.StatusBarItem
351+
const workspaceRoot = vscode.Uri.file("/test/workspace")
352+
353+
const configService = mockConfigService()
354+
configService.getConfig.resolves({ app_id: "app123", team_id: "team123" })
355+
356+
const mockDeployment: Deployment = {
357+
id: "deploy123",
358+
slug: "deploy-slug",
359+
status: DeploymentStatus.waiting_upload,
360+
url: "https://app.example.com",
361+
dashboard_url:
362+
"https://dashboard.fastapicloud.com/team-slug/apps/my-app/deployments",
363+
}
364+
365+
const apiService = mockApiService()
366+
apiService.createDeployment.resolves(mockDeployment)
367+
apiService.getUploadUrl.resolves({
368+
url: "https://s3.example.com",
369+
fields: {},
370+
})
371+
apiService.completeUpload.resolves()
372+
apiService.getDeployment.resolves({
373+
...mockDeployment,
374+
status: DeploymentStatus.building,
375+
})
376+
377+
sinon
378+
.stub(vscode.workspace, "findFiles")
379+
.resolves([vscode.Uri.file("/test/workspace/main.py")])
380+
const fs = stubFs()
381+
fs.fake.readFile.resolves(new Uint8Array([1, 2, 3]))
382+
sinon.stub(global, "fetch").resolves({ ok: true, status: 200 } as Response)
383+
384+
const openExternalStub = sinon
385+
.stub(vscode.env, "openExternal")
386+
.resolves(true)
387+
sinon
388+
.stub(vscode.window, "showErrorMessage")
389+
.resolves("View Dashboard" as any)
390+
391+
const resultPromise = deploy({
392+
workspaceRoot,
393+
configService,
394+
apiService,
395+
statusBarItem,
396+
})
397+
398+
// 300 polls x 2000ms = 600000ms
399+
await clock.tickAsync(600_000)
400+
401+
const result = await resultPromise
402+
403+
assert.strictEqual(result, false)
404+
assert.strictEqual(statusBarItem.text, "$(cloud) Deploy failed")
405+
assert.ok(openExternalStub.notCalled)
406+
407+
clock.restore()
408+
})
216409
})

0 commit comments

Comments
 (0)