Detect and validate app runtime from uploaded archives.
Reject zip uploads when the selected runtime does not match archive contents, and re-validate before Kaniko builds to fail fast instead of producing the wrong Dockerfile. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,6 +13,8 @@ import { useAuthStore } from '@/lib/store';
|
||||
import { useDeployProgressStore } from '@/lib/deploy-progress-store';
|
||||
import { useDeployProgressActions } from '@/lib/use-deploy-progress-actions';
|
||||
import { notify } from '@/lib/notify';
|
||||
import { readRuntimeMismatch } from '@/lib/errors';
|
||||
import type { Dictionary } from '@/i18n/dictionaries/fa';
|
||||
import type {
|
||||
CreateApplicationDto,
|
||||
DeployCostPreview,
|
||||
@@ -23,6 +25,27 @@ import type {
|
||||
} from '@/types';
|
||||
import { Hexagon, FolderUp, Link as LinkIcon, CheckCircle, Package, Server, Scale, Home, Target, BarChart3, RotateCw, KeyRound, Rocket, Clock, Upload, XCircle, Database, Eye, EyeOff, RefreshCw, DollarSign, Wallet, CreditCard, Loader2, Globe, Copy, AlertCircle, ShieldCheck } from 'lucide-react';
|
||||
|
||||
const RUNTIME_LABELS: Record<string, string> = {
|
||||
nodejs: 'Node.js',
|
||||
laravel: 'Laravel',
|
||||
wordpress: 'WordPress',
|
||||
go: 'Go',
|
||||
php: 'PHP',
|
||||
python: 'Python',
|
||||
django: 'Django',
|
||||
dotnet: '.NET',
|
||||
};
|
||||
|
||||
function resolveDeployErrorFallback(err: unknown, fallback: string, dict: Dictionary): string {
|
||||
const mismatch = readRuntimeMismatch(err);
|
||||
if (!mismatch) return fallback;
|
||||
const configured = RUNTIME_LABELS[mismatch.configured] ?? mismatch.configured;
|
||||
const detected = RUNTIME_LABELS[mismatch.detected] ?? mismatch.detected;
|
||||
return dict.errors.runtimeMismatch
|
||||
.replace('{configured}', configured)
|
||||
.replace('{detected}', detected);
|
||||
}
|
||||
|
||||
/** WordPress uses the managed image stack — wizard hides env & optional services; strip if ever sent. */
|
||||
function sanitizePayloadForWordPressRuntime(payload: CreateApplicationDto): CreateApplicationDto {
|
||||
if (payload.runtime !== 'wordpress') return payload;
|
||||
@@ -479,7 +502,7 @@ export default function DeployPage() {
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setDeployStage('error');
|
||||
notify.error(err, 'Payment or deployment failed');
|
||||
notify.error(err, resolveDeployErrorFallback(err, 'Payment or deployment failed', t));
|
||||
setUploadProgress(0);
|
||||
setDbUploadProgress(0);
|
||||
setTimeout(() => setDeployStage('idle'), 2000);
|
||||
@@ -561,7 +584,7 @@ export default function DeployPage() {
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setDeployStage('error');
|
||||
notify.error(err, 'Payment failed');
|
||||
notify.error(err, resolveDeployErrorFallback(err, 'Payment failed', t));
|
||||
setUploadProgress(0);
|
||||
setDbUploadProgress(0);
|
||||
setTimeout(() => setDeployStage('idle'), 2000);
|
||||
@@ -616,7 +639,7 @@ export default function DeployPage() {
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setDeployStage('error');
|
||||
notify.error(err, 'Failed to create application');
|
||||
notify.error(err, resolveDeployErrorFallback(err, 'Failed to create application', t));
|
||||
setUploadProgress(0);
|
||||
setDbUploadProgress(0);
|
||||
setTimeout(() => setDeployStage('idle'), 2000);
|
||||
|
||||
@@ -37,6 +37,7 @@ const en: Dictionary = {
|
||||
validation: 'The information you entered is invalid. Please check your input.',
|
||||
rateLimit: 'Too many requests. Please wait a moment and try again.',
|
||||
server: 'A server error occurred. Please try again shortly.',
|
||||
runtimeMismatch: 'The selected project type ({configured}) does not match the uploaded archive ({detected}).',
|
||||
},
|
||||
|
||||
language: {
|
||||
|
||||
@@ -36,6 +36,7 @@ const fa = {
|
||||
validation: 'اطلاعات واردشده درست نیست. لطفاً ورودیها را بررسی کنید.',
|
||||
rateLimit: 'تعداد درخواستها زیاد است. کمی صبر کنید و دوباره تلاش کنید.',
|
||||
server: 'خطایی در سرور رخ داد. لطفاً کمی بعد دوباره تلاش کنید.',
|
||||
runtimeMismatch: 'نوع پروژه انتخابشده ({configured}) با محتوای فایل ({detected}) همخوان نیست.',
|
||||
},
|
||||
|
||||
language: {
|
||||
|
||||
@@ -22,6 +22,30 @@ export interface ClassifiedError {
|
||||
backendMessage?: string;
|
||||
}
|
||||
|
||||
export interface RuntimeMismatchPayload {
|
||||
configured: string;
|
||||
detected: string;
|
||||
signals?: string[];
|
||||
}
|
||||
|
||||
/** Reads structured runtime mismatch fields from a 400 upload response. */
|
||||
export function readRuntimeMismatch(err: unknown): RuntimeMismatchPayload | null {
|
||||
if (!axios.isAxiosError(err) || err.response?.status !== 400) return null;
|
||||
const data = err.response.data;
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
const body = data as Record<string, unknown>;
|
||||
if (typeof body.configured === 'string' && typeof body.detected === 'string') {
|
||||
return {
|
||||
configured: body.configured,
|
||||
detected: body.detected,
|
||||
signals: Array.isArray(body.signals)
|
||||
? body.signals.filter((s): s is string => typeof s === 'string')
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Flattens NestJS-style `message: string | string[]` into one string. */
|
||||
function readBackendMessage(data: unknown): string | undefined {
|
||||
if (!data || typeof data !== 'object') return undefined;
|
||||
|
||||
Reference in New Issue
Block a user