bolt.diy/app/routes/api.deploy.ts

230 lines
7.0 KiB
TypeScript
Raw Normal View History

2025-02-24 17:24:00 +00:00
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 {
2025-02-24 17:24:32 +00:00
const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
2025-02-24 17:24:00 +00:00
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: {
2025-02-24 17:24:32 +00:00
Authorization: `Bearer ${token}`,
2025-02-24 17:24:00 +00:00
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: siteName,
custom_domain: null,
2025-02-24 17:24:32 +00:00
}),
2025-02-24 17:24:00 +00:00
});
if (!createSiteResponse.ok) {
return json({ error: 'Failed to create site' }, { status: 400 });
}
2025-02-24 17:24:32 +00:00
const newSite = (await createSiteResponse.json()) as any;
2025-02-24 17:24:00 +00:00
targetSiteId = newSite.id;
siteInfo = {
id: newSite.id,
name: newSite.name,
url: newSite.url,
2025-02-24 17:24:32 +00:00
chatId,
2025-02-24 17:24:00 +00:00
};
} else {
// Get existing site info
if (targetSiteId) {
const siteResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}`, {
headers: {
2025-02-24 17:24:32 +00:00
Authorization: `Bearer ${token}`,
2025-02-24 17:24:00 +00:00
},
});
2025-02-24 17:24:32 +00:00
2025-02-24 17:24:00 +00:00
if (siteResponse.ok) {
2025-02-24 17:24:32 +00:00
const existingSite = (await siteResponse.json()) as any;
2025-02-24 17:24:00 +00:00
siteInfo = {
id: existingSite.id,
name: existingSite.name,
url: existingSite.url,
2025-02-24 17:24:32 +00:00
chatId,
2025-02-24 17:24:00 +00:00
};
} else {
targetSiteId = undefined;
}
}
2025-02-24 17:24:32 +00:00
2025-02-24 17:24:00 +00:00
// 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: {
2025-02-24 17:24:32 +00:00
Authorization: `Bearer ${token}`,
2025-02-24 17:24:00 +00:00
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: siteName,
custom_domain: null,
2025-02-24 17:24:32 +00:00
}),
2025-02-24 17:24:00 +00:00
});
2025-02-24 17:24:32 +00:00
2025-02-24 17:24:00 +00:00
if (!createSiteResponse.ok) {
return json({ error: 'Failed to create site' }, { status: 400 });
}
2025-02-24 17:24:32 +00:00
const newSite = (await createSiteResponse.json()) as any;
2025-02-24 17:24:00 +00:00
targetSiteId = newSite.id;
siteInfo = {
id: newSite.id,
name: newSite.name,
url: newSite.url,
2025-02-24 17:24:32 +00:00
chatId,
2025-02-24 17:24:00 +00:00
};
}
}
// Create file digests
const fileDigests: Record<string, string> = {};
2025-02-24 17:24:32 +00:00
2025-02-24 17:24:00 +00:00
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: {
2025-02-24 17:24:32 +00:00
Authorization: `Bearer ${token}`,
2025-02-24 17:24:00 +00:00
'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
2025-02-24 17:24:32 +00:00
framework: null,
}),
2025-02-24 17:24:00 +00:00
});
if (!deployResponse.ok) {
return json({ error: 'Failed to create deployment' }, { status: 400 });
}
2025-02-24 17:24:32 +00:00
const deploy = (await deployResponse.json()) as any;
2025-02-24 17:24:00 +00:00
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: {
2025-02-24 17:24:32 +00:00
Authorization: `Bearer ${token}`,
2025-02-24 17:24:00 +00:00
},
});
2025-02-24 17:24:32 +00:00
const status = (await statusResponse.json()) as any;
2025-02-24 17:24:00 +00:00
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;
2025-02-24 17:24:32 +00:00
2025-02-24 17:24:00 +00:00
let uploadSuccess = false;
let uploadRetries = 0;
2025-02-24 17:24:32 +00:00
2025-02-24 17:24:00 +00:00
while (!uploadSuccess && uploadRetries < 3) {
try {
2025-02-24 17:24:32 +00:00
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,
2025-02-24 17:24:00 +00:00
},
2025-02-24 17:24:32 +00:00
);
2025-02-24 17:24:00 +00:00
uploadSuccess = uploadResponse.ok;
2025-02-24 17:24:32 +00:00
2025-02-24 17:24:00 +00:00
if (!uploadSuccess) {
console.error('Upload failed:', await uploadResponse.text());
uploadRetries++;
2025-02-24 17:24:32 +00:00
await new Promise((resolve) => setTimeout(resolve, 2000));
2025-02-24 17:24:00 +00:00
}
} catch (error) {
console.error('Upload error:', error);
uploadRetries++;
2025-02-24 17:24:32 +00:00
await new Promise((resolve) => setTimeout(resolve, 2000));
2025-02-24 17:24:00 +00:00
}
}
if (!uploadSuccess) {
return json({ error: `Failed to upload file ${filePath}` }, { status: 500 });
}
}
}
2025-02-24 17:24:32 +00:00
2025-02-24 17:24:00 +00:00
if (status.state === 'ready') {
// Only return after files are uploaded
if (Object.keys(files).length === 0 || status.summary?.status === 'ready') {
2025-02-24 17:24:32 +00:00
return json({
success: true,
2025-02-24 17:24:00 +00:00
deploy: {
id: status.id,
state: status.state,
2025-02-24 17:24:32 +00:00
url: status.ssl_url || status.url,
2025-02-24 17:24:00 +00:00
},
2025-02-24 17:24:32 +00:00
site: siteInfo,
2025-02-24 17:24:00 +00:00
});
}
}
if (status.state === 'error') {
return json({ error: status.error_message || 'Deploy preparation failed' }, { status: 500 });
}
retryCount++;
2025-02-24 17:24:32 +00:00
await new Promise((resolve) => setTimeout(resolve, 1000));
2025-02-24 17:24:00 +00:00
}
if (retryCount >= maxRetries) {
return json({ error: 'Deploy preparation timed out' }, { status: 500 });
}
// Make sure we're returning the deploy ID and site info
2025-02-24 17:24:32 +00:00
return json({
success: true,
2025-02-24 17:24:00 +00:00
deploy: {
id: deploy.id,
state: deploy.state,
},
2025-02-24 17:24:32 +00:00
site: siteInfo,
2025-02-24 17:24:00 +00:00
});
} catch (error) {
console.error('Deploy error:', error);
return json({ error: 'Deployment failed' }, { status: 500 });
}
2025-02-24 17:24:32 +00:00
}