fix netlify deploy output and uploads (#2107)

Co-authored-by: embire2 <ceo@openweb.co.za>
This commit is contained in:
Keoma Wright
2026-02-05 22:45:56 +02:00
committed by GitHub
parent 3f6050b227
commit 409696fa3c
7 changed files with 114 additions and 31 deletions

View File

@@ -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');

View File

@@ -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');

View File

@@ -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);

View File

@@ -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;

View 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)}`;
}

View File

@@ -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;

View File

@@ -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') {