Spaces:
Running
Running
import { type ActionFunctionArgs, json } from '@remix-run/cloudflare'; | |
import crypto from 'crypto'; | |
import type { NetlifySiteInfo } from '~/types/netlify'; | |
interface DeployRequestBody { | |
siteId?: string; | |
files: Record<string, string>; | |
chatId: string; | |
} | |
export async function action({ request }: ActionFunctionArgs) { | |
try { | |
const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string }; | |
if (!token) { | |
return json({ error: 'Not connected to Netlify' }, { status: 401 }); | |
} | |
let targetSiteId = siteId; | |
let siteInfo: NetlifySiteInfo | undefined; | |
// If no siteId provided, create a new site | |
if (!targetSiteId) { | |
const siteName = `bolt-diy-${chatId}-${Date.now()}`; | |
const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', { | |
method: 'POST', | |
headers: { | |
Authorization: `Bearer ${token}`, | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
name: siteName, | |
custom_domain: null, | |
}), | |
}); | |
if (!createSiteResponse.ok) { | |
return json({ error: 'Failed to create site' }, { status: 400 }); | |
} | |
const newSite = (await createSiteResponse.json()) as any; | |
targetSiteId = newSite.id; | |
siteInfo = { | |
id: newSite.id, | |
name: newSite.name, | |
url: newSite.url, | |
chatId, | |
}; | |
} else { | |
// Get existing site info | |
if (targetSiteId) { | |
const siteResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}`, { | |
headers: { | |
Authorization: `Bearer ${token}`, | |
}, | |
}); | |
if (siteResponse.ok) { | |
const existingSite = (await siteResponse.json()) as any; | |
siteInfo = { | |
id: existingSite.id, | |
name: existingSite.name, | |
url: existingSite.url, | |
chatId, | |
}; | |
} else { | |
targetSiteId = undefined; | |
} | |
} | |
// If no siteId provided or site doesn't exist, create a new site | |
if (!targetSiteId) { | |
const siteName = `bolt-diy-${chatId}-${Date.now()}`; | |
const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', { | |
method: 'POST', | |
headers: { | |
Authorization: `Bearer ${token}`, | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
name: siteName, | |
custom_domain: null, | |
}), | |
}); | |
if (!createSiteResponse.ok) { | |
return json({ error: 'Failed to create site' }, { status: 400 }); | |
} | |
const newSite = (await createSiteResponse.json()) as any; | |
targetSiteId = newSite.id; | |
siteInfo = { | |
id: newSite.id, | |
name: newSite.name, | |
url: newSite.url, | |
chatId, | |
}; | |
} | |
} | |
// Create file digests | |
const fileDigests: Record<string, string> = {}; | |
for (const [filePath, content] of Object.entries(files)) { | |
// Ensure file path starts with a forward slash | |
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath; | |
const hash = crypto.createHash('sha1').update(content).digest('hex'); | |
fileDigests[normalizedPath] = hash; | |
} | |
// Create a new deploy with digests | |
const deployResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys`, { | |
method: 'POST', | |
headers: { | |
Authorization: `Bearer ${token}`, | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
files: fileDigests, | |
async: true, | |
skip_processing: false, | |
draft: false, // Change this to false for production deployments | |
function_schedules: [], | |
required: Object.keys(fileDigests), // Add this line | |
framework: null, | |
}), | |
}); | |
if (!deployResponse.ok) { | |
return json({ error: 'Failed to create deployment' }, { status: 400 }); | |
} | |
const deploy = (await deployResponse.json()) as any; | |
let retryCount = 0; | |
const maxRetries = 60; | |
// Poll until deploy is ready for file uploads | |
while (retryCount < maxRetries) { | |
const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys/${deploy.id}`, { | |
headers: { | |
Authorization: `Bearer ${token}`, | |
}, | |
}); | |
const status = (await statusResponse.json()) as any; | |
if (status.state === 'prepared' || status.state === 'uploaded') { | |
// Upload all files regardless of required array | |
for (const [filePath, content] of Object.entries(files)) { | |
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath; | |
let uploadSuccess = false; | |
let uploadRetries = 0; | |
while (!uploadSuccess && uploadRetries < 3) { | |
try { | |
const uploadResponse = await fetch( | |
`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${normalizedPath}`, | |
{ | |
method: 'PUT', | |
headers: { | |
Authorization: `Bearer ${token}`, | |
'Content-Type': 'application/octet-stream', | |
}, | |
body: content, | |
}, | |
); | |
uploadSuccess = uploadResponse.ok; | |
if (!uploadSuccess) { | |
console.error('Upload failed:', await uploadResponse.text()); | |
uploadRetries++; | |
await new Promise((resolve) => setTimeout(resolve, 2000)); | |
} | |
} catch (error) { | |
console.error('Upload error:', error); | |
uploadRetries++; | |
await new Promise((resolve) => setTimeout(resolve, 2000)); | |
} | |
} | |
if (!uploadSuccess) { | |
return json({ error: `Failed to upload file ${filePath}` }, { status: 500 }); | |
} | |
} | |
} | |
if (status.state === 'ready') { | |
// Only return after files are uploaded | |
if (Object.keys(files).length === 0 || status.summary?.status === 'ready') { | |
return json({ | |
success: true, | |
deploy: { | |
id: status.id, | |
state: status.state, | |
url: status.ssl_url || status.url, | |
}, | |
site: siteInfo, | |
}); | |
} | |
} | |
if (status.state === 'error') { | |
return json({ error: status.error_message || 'Deploy preparation failed' }, { status: 500 }); | |
} | |
retryCount++; | |
await new Promise((resolve) => setTimeout(resolve, 1000)); | |
} | |
if (retryCount >= maxRetries) { | |
return json({ error: 'Deploy preparation timed out' }, { status: 500 }); | |
} | |
// Make sure we're returning the deploy ID and site info | |
return json({ | |
success: true, | |
deploy: { | |
id: deploy.id, | |
state: deploy.state, | |
}, | |
site: siteInfo, | |
}); | |
} catch (error) { | |
console.error('Deploy error:', error); | |
return json({ error: 'Deployment failed' }, { status: 500 }); | |
} | |
} | |