Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9613e1e
Pull auth, linking into separate PR
savannahostrowski Jan 29, 2026
500ddcd
Simplify auth and make it work in vscode.dev
savannahostrowski Jan 29, 2026
c59546c
Fix signin for vscode.dev
savannahostrowski Jan 30, 2026
4bdbf6f
Fix account menu
savannahostrowski Jan 30, 2026
ca4b3c6
Clean up extension.ts comments
savannahostrowski Jan 30, 2026
d7a0f46
Clean up telemetry
savannahostrowski Jan 30, 2026
89ee8a9
Simplify testUtils for cloud
savannahostrowski Jan 30, 2026
dcfc94f
Update test to remove logs
savannahostrowski Jan 30, 2026
9acf670
Clean up constants
savannahostrowski Jan 30, 2026
3ea95f6
Move getExtensionVersion to extension.ts
savannahostrowski Jan 30, 2026
8506ac3
Move fetch user call to api.ts
savannahostrowski Jan 30, 2026
0bec24b
Rename to getUser and User
savannahostrowski Jan 30, 2026
767c6ee
Move upload
savannahostrowski Jan 30, 2026
3d6d55f
Clean up api.ts
savannahostrowski Jan 30, 2026
f2015bc
Clean up cloud controller
savannahostrowski Jan 30, 2026
7ffc3df
Clean up config.ts
savannahostrowski Jan 30, 2026
62e27b4
More cleanup for config logging
savannahostrowski Jan 30, 2026
74c41b7
More test cleanup
savannahostrowski Jan 30, 2026
ddcb14e
Fix polling
savannahostrowski Jan 30, 2026
e120bac
More test cleanup
savannahostrowski Jan 30, 2026
dbe831c
Remove noisy logging
savannahostrowski Jan 30, 2026
80fa2e1
Add deploy command
savannahostrowski Feb 2, 2026
6cdfd81
Merge branch 'main' into cloud-deploy
savannahostrowski Feb 2, 2026
e1834ef
Clean up
savannahostrowski Feb 4, 2026
1b166f8
Merge branch 'main' into cloud-deploy
savannahostrowski Feb 4, 2026
91aa29e
Undo constants extraction - too complicated
savannahostrowski Feb 4, 2026
58c8a81
Fix bad merge
savannahostrowski Feb 4, 2026
e82fba2
Simplify controller
savannahostrowski Feb 4, 2026
e285ad7
Clean up dialogs for easier testing
savannahostrowski Feb 4, 2026
e14b9eb
Fix comment
savannahostrowski Feb 4, 2026
e7c6fca
Add tinytar
savannahostrowski Feb 4, 2026
a548a45
Watch directory
savannahostrowski Feb 4, 2026
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
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "fastapi-vscode",
"displayName": "FastAPI Extension",
"description": "VS Code extension for FastAPI development",
"version": "0.0.4",
"version": "0.0.5",
"publisher": "FastAPILabs",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -91,6 +91,11 @@
"command": "fastapi-vscode.signOut",
"title": "Sign Out",
"category": "FastAPI Cloud"
},
{
"command": "fastapi-vscode.deploy",
"title": "Deploy Application",
"category": "FastAPI Cloud"
}
],
"keybindings": [
Expand Down Expand Up @@ -122,6 +127,10 @@
"command": "fastapi-vscode.signIn",
"when": "config.fastapi.cloud.enabled"
},
{
"command": "fastapi-vscode.deploy",
"when": "config.fastapi.cloud.enabled"
},
{
"command": "fastapi-vscode.signOut",
"when": "config.fastapi.cloud.enabled"
Expand Down Expand Up @@ -298,6 +307,7 @@
},
"dependencies": {
"posthog-node": "^5.24.1",
"tinytar": "^0.1.0",
"toml": "^3.0.0",
"web-tree-sitter": "^0.26.3"
},
Expand Down
40 changes: 12 additions & 28 deletions src/cloud/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,19 @@
import * as vscode from "vscode"
import { trackCloudSignOut } from "../../utils/telemetry"
import { AUTH_PROVIDER_ID } from "../auth"
import { Auth, Button } from "../constants"
import type { AuthProvider } from "../types"
import { ui } from "../ui/dialogs"

export class AuthCommands {
constructor(
private authProvider: AuthProvider,
private onStateChanged: () => void,
) {}
export async function signOut(authProvider: AuthProvider): Promise<boolean> {
const confirm = await ui.showWarningMessage(
"Sign out of FastAPI Cloud?",
{ modal: true },
"Sign Out",
)

async signIn(): Promise<void> {
await vscode.authentication.getSession(AUTH_PROVIDER_ID, [], {
createIfNone: true,
})
if (confirm === "Sign Out") {
await authProvider.signOut()
trackCloudSignOut()
return true
}

async signOut(): Promise<boolean> {
const confirm = await vscode.window.showWarningMessage(
Auth.MSG_SIGN_OUT_CONFIRM,
{ modal: true },
Button.SIGN_OUT,
)

if (confirm === Button.SIGN_OUT) {
await this.authProvider.signOut()
trackCloudSignOut()
this.onStateChanged()
return true
}

return false
}
return false
}
268 changes: 268 additions & 0 deletions src/cloud/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
// @ts-expect-error - tinytar has no type definitions
import { tar } from "tinytar"
import * as vscode from "vscode"
import {
trackCloudAppOpened,
trackCloudDashboardOpened,
} from "../../utils/telemetry"
import type { ApiService } from "../api"
import { AUTH_PROVIDER_ID } from "../auth"
import type { ConfigService } from "../config"
import {
type App,
type Config,
type Deployment,
DeploymentStatus,
failedStatuses,
statusMessages,
} from "../types"
import { ui } from "../ui/dialogs"
import { createNewApp, pickExistingApp, pickTeam } from "../ui/pickers"

// Exclusion patterns - aligned with fastapi-cloud-cli
// See: https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/src/fastapi_cloud_cli/commands/deploy.py
const EXCLUDE_PARTS = [
".venv",
"__pycache__",
".mypy_cache",
".pytest_cache",
".git",
".gitignore",
".fastapicloudignore",
]

// 300 attempts x 2 seconds = 10 minutes maximum
const MAX_POLL_ATTEMPTS = 300
const DEPLOYMENT_POLL_INTERVAL_MS = 2000

export function shouldExclude(relativePath: string): boolean {
const parts = relativePath.split("/")

if (parts.some((part) => EXCLUDE_PARTS.includes(part))) {
return true
}

if (relativePath.endsWith(".pyc")) {
return true
}

const fileName = parts[parts.length - 1]
if (fileName === ".env" || fileName.startsWith(".env.")) {
return true
}

return false
}

export interface DeployContext {
workspaceRoot: vscode.Uri | null
configService: ConfigService
apiService: ApiService
statusBarItem: vscode.StatusBarItem
}

export async function deploy(context: DeployContext): Promise<boolean> {
const { workspaceRoot, configService, apiService, statusBarItem } = context

if (!workspaceRoot) {
ui.showErrorMessage("No workspace folder open")
return false
}

const updateStatus = (text: string) => {
statusBarItem.text = `$(sync~spin) ${text}`
}

const session = await vscode.authentication.getSession(AUTH_PROVIDER_ID, [], {
silent: true,
})
if (!session) {
const result = await ui.showErrorMessage(
"Please sign in to FastAPI Cloud first.",
"Sign In",
)
if (result === "Sign In") {
vscode.commands.executeCommand("fastapi-vscode.signIn")
}
return false
}

const existingConfig = await configService.getConfig(workspaceRoot)
const config: Config = existingConfig ?? { app_id: "", team_id: "" }
if (!config.app_id) {
const team = await pickTeam(apiService)
if (!team) return false

const choice = await ui.showQuickPick(
[
{
label: "$(link) Link Existing App",
description: "Connect to an app on FastAPI Cloud",
id: "link",
},
{
label: "$(add) Create New App",
description: "Create a new app and link it",
id: "create",
},
],
{ placeHolder: "Set up FastAPI Cloud" },
)
if (!choice) return false

let app: App | null
if (choice.id === "create") {
const folderName = workspaceRoot.path.split("/").pop() || "my-app"
app = await createNewApp(apiService, team, folderName)
} else {
app = await pickExistingApp(apiService, team)
}
if (!app) return false

config.app_id = app.id
config.team_id = team.id
config.app_slug = app.slug
await configService.writeConfig(workspaceRoot, config)
}

try {
updateStatus("Creating deployment...")
const deployment = await apiService.createDeployment(config.app_id)

updateStatus("Preparing files...")
const archive = await createArchive(workspaceRoot)

updateStatus("Uploading...")
const uploadInfo = await apiService.getUploadUrl(deployment.id)
await uploadToS3(uploadInfo.url, uploadInfo.fields, archive)

updateStatus("Starting build...")
await apiService.completeUpload(deployment.id)

// Poll for deployment status
const result = await pollDeploymentStatus(
apiService,
config.app_id,
deployment.id,
updateStatus,
)

if (result) {
const action = await ui.showInformationMessage(
"Deployed successfully!",
"Open App",
"View Dashboard",
)

if (action === "Open App" && result.url) {
vscode.env.openExternal(vscode.Uri.parse(result.url))
trackCloudAppOpened(config.app_id)
} else if (action === "View Dashboard" && result.dashboard_url) {
vscode.env.openExternal(vscode.Uri.parse(result.dashboard_url))
trackCloudDashboardOpened(config.app_id)
}
return true
}
if (statusBarItem) {
statusBarItem.text = "$(cloud) Deploy failed"
}
const action = await vscode.window.showErrorMessage(
"Deployment failed.",
"View Logs",
)
if (action === "View Logs") {
vscode.commands.executeCommand("fastapi-vscode.viewLogs")
}
return false
} catch (error) {
if (statusBarItem) {
statusBarItem.text = "$(cloud) Deploy failed"
}
vscode.window.showErrorMessage(
`Deploy failed: ${error instanceof Error ? error.message : "Unknown error"}`,
)
return false
}
}

async function createArchive(workspaceRoot: vscode.Uri): Promise<Uint8Array> {
const files = await vscode.workspace.findFiles(
new vscode.RelativePattern(workspaceRoot, "**/*"),
"{**/.venv/**,**/__pycache__/**,**/.git/**}",
)

const tarFiles: Array<{ name: string; data: Uint8Array }> = []

for (const file of files) {
const relativePath = file.path.replace(`${workspaceRoot.path}/`, "")

if (shouldExclude(relativePath)) continue

try {
const content = await vscode.workspace.fs.readFile(file)
tarFiles.push({
name: relativePath,
data: new Uint8Array(content),
})
} catch {
// Skip files we can't read
}
}

return tar(tarFiles) as Uint8Array
}

async function uploadToS3(
url: string,
fields: Record<string, string>,
archive: Uint8Array,
): Promise<void> {
const formData = new FormData()

for (const [key, value] of Object.entries(fields)) {
formData.append(key, value)
}

formData.append("file", new Blob([archive]))

const response = await fetch(url, {
method: "POST",
body: formData,
})

if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`)
}
}

async function pollDeploymentStatus(
apiService: ApiService,
appId: string,
deploymentId: string,
updateStatus: (text: string) => void,
): Promise<Deployment | null> {
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
const deployment = await apiService.getDeployment(appId, deploymentId)

if (
deployment.status === DeploymentStatus.success ||
deployment.status === DeploymentStatus.verifying_skipped
) {
return deployment
}

if (failedStatuses.includes(deployment.status)) {
return null
}

const message =
statusMessages[deployment.status] || `Status: ${deployment.status}`
updateStatus(message)

await new Promise((resolve) =>
setTimeout(resolve, DEPLOYMENT_POLL_INTERVAL_MS),
)
}

return null
}
Loading
Loading