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:
keyhan
2026-06-30 00:27:03 +03:30
parent 837f0fa63f
commit 8d1855b89c
21 changed files with 654 additions and 98 deletions
@@ -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);
+1
View File
@@ -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: {
+1
View File
@@ -36,6 +36,7 @@ const fa = {
validation: 'اطلاعات واردشده درست نیست. لطفاً ورودی‌ها را بررسی کنید.',
rateLimit: 'تعداد درخواست‌ها زیاد است. کمی صبر کنید و دوباره تلاش کنید.',
server: 'خطایی در سرور رخ داد. لطفاً کمی بعد دوباره تلاش کنید.',
runtimeMismatch: 'نوع پروژه انتخاب‌شده ({configured}) با محتوای فایل ({detected}) هم‌خوان نیست.',
},
language: {
+24
View File
@@ -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;