Merge pull request #12 from AnsoCode/feature/two-stage-teardown

fix: implement two-stage teardown for reliable atomic rollbacks
This commit is contained in:
Anso
2026-03-06 10:27:29 -05:00
committed by GitHub
4 changed files with 35 additions and 6 deletions
+2
View File
@@ -5,6 +5,8 @@ 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]
- **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.
- **Added:** Post-Deploy Health Probe to catch immediate container crashes that slip past Compose.
- **Changed:** Rollback engine respects a `canSilentlyRollback` flag to protect user-authored configurations.
+13 -1
View File
@@ -1095,7 +1095,19 @@ app.post('/api/templates/deploy', async (req: Request, res: Response) => {
const shouldRollback = parsed.rule ? parsed.rule.canSilentlyRollback : true;
if (shouldRollback) {
await fileSystemService.deleteStack(stackName);
try {
// Stage 1: Tell Docker to clean up ghost networks/containers
await composeService.downStack(stackName);
} catch (downErr) {
console.error("Rollback Stage 1 (Docker down) failed:", downErr);
}
try {
// Stage 2: Obliterate the files
await fileSystemService.deleteStack(stackName);
} catch (fsErr) {
console.error("Rollback Stage 2 (File deletion) failed:", fsErr);
}
}
res.status(500).json({
+14 -1
View File
@@ -1,9 +1,12 @@
import { spawn } from 'child_process';
import { spawn, exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import WebSocket from 'ws';
import DockerController from './DockerController';
import { LogFormatter } from './LogFormatter';
const execAsync = promisify(exec);
export class ComposeService {
private baseDir: string;
@@ -394,4 +397,14 @@ export class ComposeService {
});
});
}
public async downStack(stackName: string): Promise<void> {
const stackPath = path.join(this.baseDir, stackName);
try {
// Run down to clean up any partially created networks or containers
await execAsync(`docker compose -f compose.yaml down`, { cwd: stackPath });
} catch (error) {
console.warn(`[Teardown] Docker down failed or nothing to clean up for ${stackName}`);
}
}
}
+6 -4
View File
@@ -194,15 +194,17 @@ export class FileSystemService {
/**
* Delete a stack (entire directory and its contents)
*/
async deleteStack(stackName: string): Promise<void> {
public async deleteStack(stackName: string): Promise<void> {
const stackDir = path.join(this.baseDir, stackName);
try {
await fs.rm(stackDir, { recursive: true, force: true });
console.log('Stack deleted successfully:', stackName);
} catch (error) {
console.error('Error deleting stack:', error);
throw new Error(`Failed to delete stack: ${stackName}`);
} catch (error: any) {
if (error.code !== 'ENOENT') {
console.error('Error deleting stack directory:', error.message);
throw new Error(`Failed to delete stack directory: ${error.message}`);
}
}
}