mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat: implement pre-deploy collision checks and universal two-stage teardown
This commit is contained in:
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user