feat: implement pre-deploy collision checks and universal two-stage teardown

This commit is contained in:
SaelixCode
2026-03-06 14:45:37 -05:00
parent c4805a17ac
commit b97952567d
3 changed files with 47 additions and 31 deletions
+3
View File
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- **Added:** Pre-deploy folder collision check to prevent silent configuration overwrites in the App Store.
- **Added:** UI subtitle during deployment to reassure users during long image downloads.
- **Changed:** Standardized manual stack deletion to use the Two-Stage Teardown (Compose Down -> File Wipe) to prevent ghost networks.
- **Fixed:** Atomic Rollback failure where non-empty directories caused silent file system errors.
- **Added:** Two-Stage Teardown mechanism to ensure `docker compose down` sweeps up ghost networks before deployment files are deleted.
- **Added:** Smart Error Parser with telemetry-ready rule IDs to translate cryptic Docker output.
+20 -14
View File
@@ -22,7 +22,7 @@ import { MonitorService } from './services/MonitorService';
import { templateService } from './services/TemplateService';
import { ErrorParser } from './utils/ErrorParser';
import YAML from 'yaml';
import { promises as fsPromises } from 'fs';
import fs, { promises as fsPromises } from 'fs';
const execAsync = promisify(exec);
@@ -568,24 +568,22 @@ app.post('/api/stacks', async (req: Request, res: Response) => {
}
});
app.delete('/api/stacks/:stackName', async (req: Request, res: Response) => {
app.delete('/api/stacks/:name', async (req: Request, res: Response) => {
const stackName = req.params.name as string;
try {
const stackName = req.params.stackName as string;
// Tear down the stack first to avoid ghost containers
// Stage 1: Tell Docker to clean up ghost networks/containers
try {
console.log(`Tearing down stack: ${stackName}`);
// Send the down command synchronously before deleting the files
await composeService.runCommand(stackName, 'down', terminalWs || undefined);
} catch (downError) {
console.warn(`Failed to tear down stack ${stackName}, proceeding with file deletion.`, downError);
await composeService.downStack(stackName);
} catch (downErr) {
console.warn(`[Teardown] Docker down failed or nothing to clean up for ${stackName}`);
}
// Stage 2: Obliterate the files
await fileSystemService.deleteStack(stackName);
res.json({ message: 'Stack deleted successfully' });
} catch (error) {
console.error('Failed to delete stack:', error);
res.status(500).json({ error: 'Failed to delete stack' });
res.json({ success: true });
} catch (error: any) {
res.status(500).json({ error: error.message || 'Failed to delete stack' });
}
});
@@ -1069,6 +1067,14 @@ app.post('/api/templates/deploy', async (req: Request, res: Response) => {
return res.status(400).json({ error: 'stackName and template are required' });
}
const stackPath = path.join(fileSystemService.getBaseDir(), stackName);
if (fs.existsSync(stackPath)) {
return res.status(409).json({
error: `A stack directory named '${stackName}' already exists. Please choose a different Stack Name.`,
rolledBack: false
});
}
// 1. Create stack directory
await fileSystemService.createStack(stackName);
+24 -17
View File
@@ -415,24 +415,31 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
</ScrollArea>
<SheetFooter className="pt-4 mt-auto border-t sm:justify-start">
<Button
onClick={handleDeploy}
disabled={isDeploying || !stackName.trim()}
className="w-full"
size="lg"
>
{isDeploying ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Deploying Stack...
</>
) : (
<>
<Rocket className="w-5 h-5 mr-2" />
Deploy {selectedTemplate.title}
</>
<div className="flex flex-col w-full gap-2">
<Button
onClick={handleDeploy}
disabled={isDeploying || !stackName.trim()}
className="w-full"
size="lg"
>
{isDeploying ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Deploying Stack...
</>
) : (
<>
<Rocket className="w-5 h-5 mr-2" />
Deploy {selectedTemplate.title}
</>
)}
</Button>
{isDeploying && (
<p className="text-center text-xs text-muted-foreground animate-pulse">
This may take a few minutes for large images.
</p>
)}
</Button>
</div>
</SheetFooter>
</div>
)}