From 5f91e16417f86f04bb8f5aad2fd49f008a24c057 Mon Sep 17 00:00:00 2001 From: Anso Date: Sun, 12 Apr 2026 19:26:29 -0400 Subject: [PATCH] fix(app-store): handle orphaned stack directories on template deploy (#530) * fix(app-store): handle orphaned stack directories on template deploy When a stack deployed via the App Store is later removed through Docker Desktop or the CLI (instead of through Sencho), its directory remains on disk without a compose file. The deploy endpoint previously rejected any re-deploy with a 409 if the directory existed, even if empty. Now the endpoint checks for a compose file before rejecting. If the directory exists but contains no compose file, it is treated as an orphaned remnant: cleaned up automatically and the deploy proceeds. Also makes FileSystemService.hasComposeFile public so the deploy endpoint can reuse it instead of duplicating the compose file check. * docs(app-store): document orphaned stack directory cleanup behavior --- backend/src/index.ts | 15 +++++++++++---- backend/src/services/FileSystemService.ts | 2 +- docs/features/app-store.mdx | 2 ++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 01cc4f44..afba83b7 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5687,10 +5687,17 @@ app.post('/api/templates/deploy', authMiddleware, async (req: Request, res: Resp try { await fsPromises.access(stackPath); - return res.status(409).json({ - error: `A stack directory named '${stackName}' already exists. Please choose a different Stack Name.`, - rolledBack: false - }); + + if (await fsService.hasComposeFile(stackPath)) { + return res.status(409).json({ + error: `A stack directory named '${stackName}' already exists. Please choose a different Stack Name.`, + rolledBack: false + }); + } + + // Orphaned directory left by external deletion (e.g. Docker Desktop). + console.log(`[Templates] Cleaned up orphaned stack directory: ${stackName}`); + await fsService.deleteStack(stackName); } catch { // Directory does not exist; proceed with deploy } diff --git a/backend/src/services/FileSystemService.ts b/backend/src/services/FileSystemService.ts index 25a5d243..f721dcc6 100644 --- a/backend/src/services/FileSystemService.ts +++ b/backend/src/services/FileSystemService.ts @@ -34,7 +34,7 @@ export class FileSystemService { return new FileSystemService(nodeId); } - private async hasComposeFile(dir: string): Promise { + async hasComposeFile(dir: string): Promise { const composeFiles = ['compose.yaml', 'compose.yml', 'docker-compose.yaml', 'docker-compose.yml']; for (const file of composeFiles) { try { diff --git a/docs/features/app-store.mdx b/docs/features/app-store.mdx index af2fcbd2..8b3ca619 100644 --- a/docs/features/app-store.mdx +++ b/docs/features/app-store.mdx @@ -74,6 +74,8 @@ Below the template variables, an **Add Custom Variable** section lets you define A toast notification confirms success or shows the error message. Large images may take a few minutes to pull. +If a previous stack with the same name was removed outside of Sencho (for example, through the Docker CLI or Docker Desktop), the leftover directory is cleaned up automatically and the deploy proceeds as normal. + Deploying a template requires the `stack:create` permission. Users without this permission will see the Deploy button disabled.