mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-08 18:05:10 +00:00
fix(blueprints): write compose.yaml so first-time apply is not shadowed (#1668)
createStack scaffolds compose.yaml; Blueprint was writing docker-compose.yml, so Compose discovery ran the nginx boilerplate. Align Blueprint writes with the canonical filename, clear alternate root Compose siblings on local/modern apply, and cover the regression paths.
This commit is contained in:
@@ -21,7 +21,8 @@ import { BlueprintAnalyzer } from './BlueprintAnalyzer';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
const MARKER_FILENAME = '.blueprint.json';
|
||||
const COMPOSE_FILENAME = 'docker-compose.yml';
|
||||
/** On-disk compose name for Blueprint applies. Must match createStack scaffold and Sencho discovery priority. */
|
||||
const COMPOSE_FILENAME = 'compose.yaml';
|
||||
const REMOTE_HTTP_TIMEOUT_MS = 30_000;
|
||||
|
||||
function isDeveloperModeEnabled(): boolean {
|
||||
@@ -447,6 +448,9 @@ export class BlueprintService {
|
||||
}
|
||||
await fs.writeStackFile(stackName, COMPOSE_FILENAME, composeContent);
|
||||
await fs.writeStackFile(stackName, MARKER_FILENAME, markerContent);
|
||||
// Clear lower-priority compose siblings so discovery cannot shadow compose.yaml.
|
||||
// Local + modern apply-local only; legacy remote has no sibling DELETE.
|
||||
await fs.removeAlternateRootComposeFiles(stackName);
|
||||
await assertPolicyGateAllows(
|
||||
stackName,
|
||||
nodeId,
|
||||
|
||||
@@ -578,6 +578,34 @@ export class FileSystemService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove non-canonical root Compose filenames so discovery cannot shadow
|
||||
* compose.yaml. Filename set is fixed inside this method. ENOENT is success;
|
||||
* other unlink failures are logged and skipped.
|
||||
*/
|
||||
async removeAlternateRootComposeFiles(stackName: string): Promise<void> {
|
||||
const stackDir = this.resolveStackDir(stackName);
|
||||
await this.assertRealWithinBase(stackDir);
|
||||
const baseResolved = path.resolve(this.baseDir);
|
||||
for (const file of IMPORT_COMPOSE_FILENAMES) {
|
||||
if (file === 'compose.yaml') continue;
|
||||
// Containment barrier at the unlink sink (same pattern as rollback orphan removal).
|
||||
const target = path.resolve(baseResolved, path.join(stackDir, file));
|
||||
if (!target.startsWith(baseResolved + path.sep)) {
|
||||
throw Object.assign(new Error('Path escapes compose directory'), { code: 'INVALID_PATH' });
|
||||
}
|
||||
try {
|
||||
await fsPromises.unlink(target);
|
||||
} catch (e: unknown) {
|
||||
if ((e as NodeJS.ErrnoException).code === 'ENOENT') continue;
|
||||
console.warn(
|
||||
`[FileSystemService] Could not remove alternate compose file ${sanitizeForLog(file)} in stack ${sanitizeForLog(stackName)}:`,
|
||||
sanitizeForLog((e as Error)?.message ?? String(e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteStack(stackName: string): Promise<void> {
|
||||
const stackDir = this.resolveStackDir(stackName);
|
||||
await this.assertRealWithinBase(stackDir);
|
||||
|
||||
Reference in New Issue
Block a user