fix netlify deploy output and uploads (#2107)
Co-authored-by: embire2 <ceo@openweb.co.za>
This commit is contained in:
@@ -7,6 +7,7 @@ import { useState } from 'react';
|
|||||||
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
||||||
import { chatId } from '~/lib/persistence/useChatHistory';
|
import { chatId } from '~/lib/persistence/useChatHistory';
|
||||||
import { getLocalStorage } from '~/lib/persistence/localStorage';
|
import { getLocalStorage } from '~/lib/persistence/localStorage';
|
||||||
|
import { formatBuildFailureOutput } from './deployUtils';
|
||||||
|
|
||||||
export function useGitHubDeploy() {
|
export function useGitHubDeploy() {
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
@@ -65,10 +66,12 @@ export function useGitHubDeploy() {
|
|||||||
// Then run it
|
// Then run it
|
||||||
await artifact.runner.runAction(actionData);
|
await artifact.runner.runAction(actionData);
|
||||||
|
|
||||||
if (!artifact.runner.buildOutput) {
|
const buildOutput = artifact.runner.buildOutput;
|
||||||
|
|
||||||
|
if (!buildOutput || buildOutput.exitCode !== 0) {
|
||||||
// Notify that build failed
|
// Notify that build failed
|
||||||
deployArtifact.runner.handleDeployAction('building', 'failed', {
|
deployArtifact.runner.handleDeployAction('building', 'failed', {
|
||||||
error: 'Build failed. Check the terminal for details.',
|
error: formatBuildFailureOutput(buildOutput?.output),
|
||||||
source: 'github',
|
source: 'github',
|
||||||
});
|
});
|
||||||
throw new Error('Build failed');
|
throw new Error('Build failed');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useState } from 'react';
|
|||||||
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
||||||
import { chatId } from '~/lib/persistence/useChatHistory';
|
import { chatId } from '~/lib/persistence/useChatHistory';
|
||||||
import { getLocalStorage } from '~/lib/persistence/localStorage';
|
import { getLocalStorage } from '~/lib/persistence/localStorage';
|
||||||
|
import { formatBuildFailureOutput } from './deployUtils';
|
||||||
|
|
||||||
export function useGitLabDeploy() {
|
export function useGitLabDeploy() {
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
@@ -65,10 +66,12 @@ export function useGitLabDeploy() {
|
|||||||
// Then run it
|
// Then run it
|
||||||
await artifact.runner.runAction(actionData);
|
await artifact.runner.runAction(actionData);
|
||||||
|
|
||||||
if (!artifact.runner.buildOutput) {
|
const buildOutput = artifact.runner.buildOutput;
|
||||||
|
|
||||||
|
if (!buildOutput || buildOutput.exitCode !== 0) {
|
||||||
// Notify that build failed
|
// Notify that build failed
|
||||||
deployArtifact.runner.handleDeployAction('building', 'failed', {
|
deployArtifact.runner.handleDeployAction('building', 'failed', {
|
||||||
error: 'Build failed. Check the terminal for details.',
|
error: formatBuildFailureOutput(buildOutput?.output),
|
||||||
source: 'gitlab',
|
source: 'gitlab',
|
||||||
});
|
});
|
||||||
throw new Error('Build failed');
|
throw new Error('Build failed');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { path } from '~/utils/path';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
||||||
import { chatId } from '~/lib/persistence/useChatHistory';
|
import { chatId } from '~/lib/persistence/useChatHistory';
|
||||||
|
import { formatBuildFailureOutput } from './deployUtils';
|
||||||
|
|
||||||
export function useNetlifyDeploy() {
|
export function useNetlifyDeploy() {
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
@@ -65,10 +66,12 @@ export function useNetlifyDeploy() {
|
|||||||
// Then run it
|
// Then run it
|
||||||
await artifact.runner.runAction(actionData);
|
await artifact.runner.runAction(actionData);
|
||||||
|
|
||||||
if (!artifact.runner.buildOutput) {
|
const buildOutput = artifact.runner.buildOutput;
|
||||||
|
|
||||||
|
if (!buildOutput || buildOutput.exitCode !== 0) {
|
||||||
// Notify that build failed
|
// Notify that build failed
|
||||||
deployArtifact.runner.handleDeployAction('building', 'failed', {
|
deployArtifact.runner.handleDeployAction('building', 'failed', {
|
||||||
error: 'Build failed. Check the terminal for details.',
|
error: formatBuildFailureOutput(buildOutput?.output),
|
||||||
source: 'netlify',
|
source: 'netlify',
|
||||||
});
|
});
|
||||||
throw new Error('Build failed');
|
throw new Error('Build failed');
|
||||||
@@ -81,7 +84,7 @@ export function useNetlifyDeploy() {
|
|||||||
const container = await webcontainer;
|
const container = await webcontainer;
|
||||||
|
|
||||||
// Remove /home/project from buildPath if it exists
|
// Remove /home/project from buildPath if it exists
|
||||||
const buildPath = artifact.runner.buildOutput.path.replace('/home/project', '');
|
const buildPath = buildOutput.path.replace('/home/project', '');
|
||||||
|
|
||||||
console.log('Original buildPath', buildPath);
|
console.log('Original buildPath', buildPath);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { path } from '~/utils/path';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
||||||
import { chatId } from '~/lib/persistence/useChatHistory';
|
import { chatId } from '~/lib/persistence/useChatHistory';
|
||||||
|
import { formatBuildFailureOutput } from './deployUtils';
|
||||||
|
|
||||||
export function useVercelDeploy() {
|
export function useVercelDeploy() {
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
@@ -64,10 +65,12 @@ export function useVercelDeploy() {
|
|||||||
// Then run it
|
// Then run it
|
||||||
await artifact.runner.runAction(actionData);
|
await artifact.runner.runAction(actionData);
|
||||||
|
|
||||||
if (!artifact.runner.buildOutput) {
|
const buildOutput = artifact.runner.buildOutput;
|
||||||
|
|
||||||
|
if (!buildOutput || buildOutput.exitCode !== 0) {
|
||||||
// Notify that build failed
|
// Notify that build failed
|
||||||
deployArtifact.runner.handleDeployAction('building', 'failed', {
|
deployArtifact.runner.handleDeployAction('building', 'failed', {
|
||||||
error: 'Build failed. Check the terminal for details.',
|
error: formatBuildFailureOutput(buildOutput?.output),
|
||||||
source: 'vercel',
|
source: 'vercel',
|
||||||
});
|
});
|
||||||
throw new Error('Build failed');
|
throw new Error('Build failed');
|
||||||
@@ -80,7 +83,7 @@ export function useVercelDeploy() {
|
|||||||
const container = await webcontainer;
|
const container = await webcontainer;
|
||||||
|
|
||||||
// Remove /home/project from buildPath if it exists
|
// Remove /home/project from buildPath if it exists
|
||||||
const buildPath = artifact.runner.buildOutput.path.replace('/home/project', '');
|
const buildPath = buildOutput.path.replace('/home/project', '');
|
||||||
|
|
||||||
// Check if the build path exists
|
// Check if the build path exists
|
||||||
let finalBuildPath = buildPath;
|
let finalBuildPath = buildPath;
|
||||||
|
|||||||
15
app/components/deploy/deployUtils.ts
Normal file
15
app/components/deploy/deployUtils.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const MAX_BUILD_OUTPUT_CHARS = 4000;
|
||||||
|
|
||||||
|
export function formatBuildFailureOutput(output?: string) {
|
||||||
|
const trimmed = output?.trim();
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
return 'Build failed with no output captured.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.length <= MAX_BUILD_OUTPUT_CHARS) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Build output (truncated):\n${trimmed.slice(-MAX_BUILD_OUTPUT_CHARS)}`;
|
||||||
|
}
|
||||||
@@ -395,7 +395,7 @@ export class ActionRunner {
|
|||||||
const buildProcess = await webcontainer.spawn('npm', ['run', 'build']);
|
const buildProcess = await webcontainer.spawn('npm', ['run', 'build']);
|
||||||
|
|
||||||
let output = '';
|
let output = '';
|
||||||
buildProcess.output.pipeTo(
|
const outputPromise = buildProcess.output.pipeTo(
|
||||||
new WritableStream({
|
new WritableStream({
|
||||||
write(data) {
|
write(data) {
|
||||||
output += data;
|
output += data;
|
||||||
@@ -404,8 +404,21 @@ export class ActionRunner {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const exitCode = await buildProcess.exit;
|
const exitCode = await buildProcess.exit;
|
||||||
|
await outputPromise.catch(() => {
|
||||||
|
// Ignore output piping errors; we still have whatever was captured
|
||||||
|
});
|
||||||
|
|
||||||
|
let buildDir = '';
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
|
const buildResult = {
|
||||||
|
path: buildDir,
|
||||||
|
exitCode,
|
||||||
|
output,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.buildOutput = buildResult;
|
||||||
|
|
||||||
// Trigger build failed alert
|
// Trigger build failed alert
|
||||||
this.onDeployAlert?.({
|
this.onDeployAlert?.({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -435,8 +448,6 @@ export class ActionRunner {
|
|||||||
// Check for common build directories
|
// Check for common build directories
|
||||||
const commonBuildDirs = ['dist', 'build', 'out', 'output', '.next', 'public'];
|
const commonBuildDirs = ['dist', 'build', 'out', 'output', '.next', 'public'];
|
||||||
|
|
||||||
let buildDir = '';
|
|
||||||
|
|
||||||
// Try to find the first existing build directory
|
// Try to find the first existing build directory
|
||||||
for (const dir of commonBuildDirs) {
|
for (const dir of commonBuildDirs) {
|
||||||
const dirPath = nodePath.join(webcontainer.workdir, dir);
|
const dirPath = nodePath.join(webcontainer.workdir, dir);
|
||||||
@@ -455,11 +466,15 @@ export class ActionRunner {
|
|||||||
buildDir = nodePath.join(webcontainer.workdir, 'dist');
|
buildDir = nodePath.join(webcontainer.workdir, 'dist');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const buildResult = {
|
||||||
path: buildDir,
|
path: buildDir,
|
||||||
exitCode,
|
exitCode,
|
||||||
output,
|
output,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.buildOutput = buildResult;
|
||||||
|
|
||||||
|
return buildResult;
|
||||||
}
|
}
|
||||||
async handleSupabaseAction(action: SupabaseAction) {
|
async handleSupabaseAction(action: SupabaseAction) {
|
||||||
const { operation, content, filePath } = action;
|
const { operation, content, filePath } = action;
|
||||||
|
|||||||
@@ -8,6 +8,23 @@ interface DeployRequestBody {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readNetlifyError(response: Response) {
|
||||||
|
try {
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
const data = (await response.json()) as { message?: string; error?: string } | undefined;
|
||||||
|
return data?.message || data?.error || JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
return text;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function action({ request }: ActionFunctionArgs) {
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
try {
|
try {
|
||||||
const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
|
const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
|
||||||
@@ -35,7 +52,11 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!createSiteResponse.ok) {
|
if (!createSiteResponse.ok) {
|
||||||
return json({ error: 'Failed to create site' }, { status: 400 });
|
const errorDetail = await readNetlifyError(createSiteResponse);
|
||||||
|
return json(
|
||||||
|
{ error: `Failed to create site${errorDetail ? `: ${errorDetail}` : ''}` },
|
||||||
|
{ status: createSiteResponse.status },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSite = (await createSiteResponse.json()) as any;
|
const newSite = (await createSiteResponse.json()) as any;
|
||||||
@@ -84,7 +105,11 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!createSiteResponse.ok) {
|
if (!createSiteResponse.ok) {
|
||||||
return json({ error: 'Failed to create site' }, { status: 400 });
|
const errorDetail = await readNetlifyError(createSiteResponse);
|
||||||
|
return json(
|
||||||
|
{ error: `Failed to create site${errorDetail ? `: ${errorDetail}` : ''}` },
|
||||||
|
{ status: createSiteResponse.status },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSite = (await createSiteResponse.json()) as any;
|
const newSite = (await createSiteResponse.json()) as any;
|
||||||
@@ -121,18 +146,22 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
skip_processing: false,
|
skip_processing: false,
|
||||||
draft: false, // Change this to false for production deployments
|
draft: false, // Change this to false for production deployments
|
||||||
function_schedules: [],
|
function_schedules: [],
|
||||||
required: Object.keys(fileDigests), // Add this line
|
|
||||||
framework: null,
|
framework: null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!deployResponse.ok) {
|
if (!deployResponse.ok) {
|
||||||
return json({ error: 'Failed to create deployment' }, { status: 400 });
|
const errorDetail = await readNetlifyError(deployResponse);
|
||||||
|
return json(
|
||||||
|
{ error: `Failed to create deployment${errorDetail ? `: ${errorDetail}` : ''}` },
|
||||||
|
{ status: deployResponse.status },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deploy = (await deployResponse.json()) as any;
|
const deploy = (await deployResponse.json()) as any;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 60;
|
const maxRetries = 60;
|
||||||
|
let filesUploaded = false;
|
||||||
|
|
||||||
// Poll until deploy is ready for file uploads
|
// Poll until deploy is ready for file uploads
|
||||||
while (retryCount < maxRetries) {
|
while (retryCount < maxRetries) {
|
||||||
@@ -142,12 +171,24 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!statusResponse.ok) {
|
||||||
|
const errorDetail = await readNetlifyError(statusResponse);
|
||||||
|
return json(
|
||||||
|
{ error: `Failed to check deployment status${errorDetail ? `: ${errorDetail}` : ''}` },
|
||||||
|
{ status: statusResponse.status },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const status = (await statusResponse.json()) as any;
|
const status = (await statusResponse.json()) as any;
|
||||||
|
|
||||||
if (status.state === 'prepared' || status.state === 'uploaded') {
|
if (!filesUploaded && (status.state === 'prepared' || status.state === 'uploaded')) {
|
||||||
// Upload all files regardless of required array
|
// Upload all files regardless of required array
|
||||||
for (const [filePath, content] of Object.entries(files)) {
|
for (const [filePath, content] of Object.entries(files)) {
|
||||||
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
|
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
|
||||||
|
const encodedPath = normalizedPath
|
||||||
|
.split('/')
|
||||||
|
.map((segment) => encodeURIComponent(segment))
|
||||||
|
.join('/');
|
||||||
|
|
||||||
let uploadSuccess = false;
|
let uploadSuccess = false;
|
||||||
let uploadRetries = 0;
|
let uploadRetries = 0;
|
||||||
@@ -155,7 +196,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
while (!uploadSuccess && uploadRetries < 3) {
|
while (!uploadSuccess && uploadRetries < 3) {
|
||||||
try {
|
try {
|
||||||
const uploadResponse = await fetch(
|
const uploadResponse = await fetch(
|
||||||
`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${normalizedPath}`,
|
`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${encodedPath}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -184,21 +225,21 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
return json({ error: `Failed to upload file ${filePath}` }, { status: 500 });
|
return json({ error: `Failed to upload file ${filePath}` }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filesUploaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.state === 'ready') {
|
if (status.state === 'ready') {
|
||||||
// Only return after files are uploaded
|
// Only return after files are uploaded
|
||||||
if (Object.keys(files).length === 0 || status.summary?.status === 'ready') {
|
return json({
|
||||||
return json({
|
success: true,
|
||||||
success: true,
|
deploy: {
|
||||||
deploy: {
|
id: status.id,
|
||||||
id: status.id,
|
state: status.state,
|
||||||
state: status.state,
|
url: status.ssl_url || status.url,
|
||||||
url: status.ssl_url || status.url,
|
},
|
||||||
},
|
site: siteInfo,
|
||||||
site: siteInfo,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.state === 'error') {
|
if (status.state === 'error') {
|
||||||
|
|||||||
Reference in New Issue
Block a user