mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
Merge pull request #12 from AnsoCode/feature/two-stage-teardown
fix: implement two-stage teardown for reliable atomic rollbacks
This commit is contained in:
@@ -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
@@ -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({
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user