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 { chatId } from '~/lib/persistence/useChatHistory';
import { getLocalStorage } from '~/lib/persistence/localStorage';
import { formatBuildFailureOutput } from './deployUtils';
export function useGitHubDeploy() {
const [isDeploying, setIsDeploying] = useState(false);
@@ -65,10 +66,12 @@ export function useGitHubDeploy() {
// Then run it
await artifact.runner.runAction(actionData);
if (!artifact.runner.buildOutput) {
const buildOutput = artifact.runner.buildOutput;
if (!buildOutput || buildOutput.exitCode !== 0) {
// Notify that build failed
deployArtifact.runner.handleDeployAction('building', 'failed', {
error: 'Build failed. Check the terminal for details.',
error: formatBuildFailureOutput(buildOutput?.output),
source: 'github',
});
throw new Error('Build failed');

View File

@@ -7,6 +7,7 @@ import { useState } from 'react';
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
import { chatId } from '~/lib/persistence/useChatHistory';
import { getLocalStorage } from '~/lib/persistence/localStorage';
import { formatBuildFailureOutput } from './deployUtils';
export function useGitLabDeploy() {
const [isDeploying, setIsDeploying] = useState(false);
@@ -65,10 +66,12 @@ export function useGitLabDeploy() {
// Then run it
await artifact.runner.runAction(actionData);
if (!artifact.runner.buildOutput) {
const buildOutput = artifact.runner.buildOutput;
if (!buildOutput || buildOutput.exitCode !== 0) {
// Notify that build failed
deployArtifact.runner.handleDeployAction('building', 'failed', {
error: 'Build failed. Check the terminal for details.',
error: formatBuildFailureOutput(buildOutput?.output),
source: 'gitlab',
});
throw new Error('Build failed');

View File

@@ -7,6 +7,7 @@ import { path } from '~/utils/path';
import { useState } from 'react';
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
import { chatId } from '~/lib/persistence/useChatHistory';
import { formatBuildFailureOutput } from './deployUtils';
export function useNetlifyDeploy() {
const [isDeploying, setIsDeploying] = useState(false);
@@ -65,10 +66,12 @@ export function useNetlifyDeploy() {
// Then run it
await artifact.runner.runAction(actionData);
if (!artifact.runner.buildOutput) {
const buildOutput = artifact.runner.buildOutput;
if (!buildOutput || buildOutput.exitCode !== 0) {
// Notify that build failed
deployArtifact.runner.handleDeployAction('building', 'failed', {
error: 'Build failed. Check the terminal for details.',
error: formatBuildFailureOutput(buildOutput?.output),
source: 'netlify',
});
throw new Error('Build failed');
@@ -81,7 +84,7 @@ export function useNetlifyDeploy() {
const container = await webcontainer;
// 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);

View File

@@ -7,6 +7,7 @@ import { path } from '~/utils/path';
import { useState } from 'react';
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
import { chatId } from '~/lib/persistence/useChatHistory';
import { formatBuildFailureOutput } from './deployUtils';
export function useVercelDeploy() {
const [isDeploying, setIsDeploying] = useState(false);
@@ -64,10 +65,12 @@ export function useVercelDeploy() {
// Then run it
await artifact.runner.runAction(actionData);
if (!artifact.runner.buildOutput) {
const buildOutput = artifact.runner.buildOutput;
if (!buildOutput || buildOutput.exitCode !== 0) {
// Notify that build failed
deployArtifact.runner.handleDeployAction('building', 'failed', {
error: 'Build failed. Check the terminal for details.',
error: formatBuildFailureOutput(buildOutput?.output),
source: 'vercel',
});
throw new Error('Build failed');
@@ -80,7 +83,7 @@ export function useVercelDeploy() {
const container = await webcontainer;
// 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
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']);
let output = '';
buildProcess.output.pipeTo(
const outputPromise = buildProcess.output.pipeTo(
new WritableStream({
write(data) {
output += data;
@@ -404,8 +404,21 @@ export class ActionRunner {
);
const exitCode = await buildProcess.exit;
await outputPromise.catch(() => {
// Ignore output piping errors; we still have whatever was captured
});
let buildDir = '';
if (exitCode !== 0) {
const buildResult = {
path: buildDir,
exitCode,
output,
};
this.buildOutput = buildResult;
// Trigger build failed alert
this.onDeployAlert?.({
type: 'error',
@@ -435,8 +448,6 @@ export class ActionRunner {
// Check for common build directories
const commonBuildDirs = ['dist', 'build', 'out', 'output', '.next', 'public'];
let buildDir = '';
// Try to find the first existing build directory
for (const dir of commonBuildDirs) {
const dirPath = nodePath.join(webcontainer.workdir, dir);
@@ -455,11 +466,15 @@ export class ActionRunner {
buildDir = nodePath.join(webcontainer.workdir, 'dist');
}
return {
const buildResult = {
path: buildDir,
exitCode,
output,
};
this.buildOutput = buildResult;
return buildResult;
}
async handleSupabaseAction(action: SupabaseAction) {
const { operation, content, filePath } = action;

View File

@@ -8,6 +8,23 @@ interface DeployRequestBody {
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) {
try {
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) {
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;
@@ -84,7 +105,11 @@ export async function action({ request }: ActionFunctionArgs) {
});
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;
@@ -121,18 +146,22 @@ export async function action({ request }: ActionFunctionArgs) {
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 errorDetail = await readNetlifyError(deployResponse);
return json(
{ error: `Failed to create deployment${errorDetail ? `: ${errorDetail}` : ''}` },
{ status: deployResponse.status },
);
}
const deploy = (await deployResponse.json()) as any;
let retryCount = 0;
const maxRetries = 60;
let filesUploaded = false;
// Poll until deploy is ready for file uploads
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;
if (status.state === 'prepared' || status.state === 'uploaded') {
if (!filesUploaded && (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;
const encodedPath = normalizedPath
.split('/')
.map((segment) => encodeURIComponent(segment))
.join('/');
let uploadSuccess = false;
let uploadRetries = 0;
@@ -155,7 +196,7 @@ export async function action({ request }: ActionFunctionArgs) {
while (!uploadSuccess && uploadRetries < 3) {
try {
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',
headers: {
@@ -184,11 +225,12 @@ export async function action({ request }: ActionFunctionArgs) {
return json({ error: `Failed to upload file ${filePath}` }, { status: 500 });
}
}
filesUploaded = true;
}
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: {
@@ -199,7 +241,6 @@ export async function action({ request }: ActionFunctionArgs) {
site: siteInfo,
});
}
}
if (status.state === 'error') {
return json({ error: status.error_message || 'Deploy preparation failed' }, { status: 500 });