diff --git a/backend/src/deployments/deployments.controller.ts b/backend/src/deployments/deployments.controller.ts index 31ba6cb..fb22846 100644 --- a/backend/src/deployments/deployments.controller.ts +++ b/backend/src/deployments/deployments.controller.ts @@ -62,4 +62,11 @@ export class DeploymentsController { await this.deploymentsService.restartDeployment(appId, req.user.id); return { message: 'Application restarted' }; } + + @Post('applications/:appId/redeploy') + @ApiOperation({ summary: 'Redeploy: rebuild from latest source (git pull / zip) and deploy new version' }) + async redeploy(@Param('appId') appId: string, @Request() req: any) { + const deployment = await this.deploymentsService.redeployApplication(appId, req.user.id); + return { message: 'Redeploy triggered', deployment }; + } } diff --git a/backend/src/deployments/deployments.service.ts b/backend/src/deployments/deployments.service.ts index 8d7d850..510cc7e 100644 --- a/backend/src/deployments/deployments.service.ts +++ b/backend/src/deployments/deployments.service.ts @@ -134,6 +134,36 @@ export class DeploymentsService { await this.kubernetesService.restartDeployment(app); } + /** + * Redeploy: re-build from latest source (git pull or existing zip) and deploy new version. + * For git-based apps this pulls the latest code, for zip-based it rebuilds from last uploaded zip. + */ + async redeployApplication(applicationId: string, userId: string): Promise { + const app = await this.applicationsService.findOne(applicationId, userId); + + if (!app.codePath && !app.gitUrl) { + throw new NotFoundException('No source code available. Upload code or set a git URL first.'); + } + + // Create new deployment record + const deployment = this.deploymentsRepository.create({ + applicationId: app.id, + triggeredBy: userId, + imageTag: `${app.name}:${Date.now()}`, + status: DeploymentStatus.PENDING, + version: `v${Date.now()}`, + }); + const saved = await this.deploymentsRepository.save(deployment); + + // Trigger async build & deploy pipeline (same as initial deploy) + this.executePipeline(saved.id, app).catch((error) => { + this.logger.error(`Redeploy pipeline failed for deployment ${saved.id}:`, error); + }); + + this.logger.log(`Redeploy triggered for ${app.name} (${app.gitUrl ? 'git: ' + app.gitUrl : 'zip'})`); + return saved; + } + async deleteAllForApplication(applicationId: string): Promise { await this.deploymentsRepository.delete({ applicationId }); } diff --git a/frontend/src/app/dashboard/apps/[id]/page.tsx b/frontend/src/app/dashboard/apps/[id]/page.tsx index 4eaeb65..7f255e7 100644 --- a/frontend/src/app/dashboard/apps/[id]/page.tsx +++ b/frontend/src/app/dashboard/apps/[id]/page.tsx @@ -94,6 +94,15 @@ export default function AppDetailPage() { onError: () => toast.error('Failed to restart application'), }); + const redeployMutation = useMutation({ + mutationFn: () => api.post(`/deployments/applications/${appId}/redeploy`), + onSuccess: () => { + invalidateAll(); + toast.success('Redeploy triggered — building new version from latest source'); + }, + onError: () => toast.error('Failed to trigger redeploy'), + }); + const deleteMutation = useMutation({ mutationFn: () => api.delete(`/applications/${appId}`), onSuccess: () => { @@ -230,6 +239,18 @@ export default function AppDetailPage() { {restartMutation.isPending ? '⏳...' : '🔄 Restart'} )} + + {/* Redeploy: rebuild from latest git/code */} + {!isInProgress && ( + + )} )}