mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-22 16:16:41 +00:00
fix(blueprints): fail closed on marker ownership for apply and withdraw (#1694)
* fix(blueprints): fail closed on marker ownership for apply and withdraw Require a matching .blueprint.json under the stack lock, persist required_blueprint_id on deletion intents, remove the legacy remote apply fallback, and protect the marker in the file explorer. * fix(blueprints): add CodeQL path barriers on ownership probes Use the canonical resolve-and-startsWith sanitizer inline at the marker and stack-directory fs sinks so js/path-injection clears. * fix(blueprints): block delete on failed withdraw and defer marker write Refuse Blueprint DELETE when pre-delete withdraw does not complete, and write .blueprint.json only after a successful deploy so failed applies cannot orphan stacks or claim an unapplied revision. * test(blueprints): align lock-order assert with deferred marker write Update the per-stack lock ordering expectations to compose, cleanup, deploy, then marker after the partial-apply fix. * fix(deps): bump postcss past GHSA-r28c-9q8g-f849 for npm audit Raise the Vitest/Vite transitive postcss to 8.5.23 so Backend CI audit --audit-level=high passes.
This commit is contained in:
@@ -89,6 +89,7 @@ describe('DeployedStackDeletionService ready transaction', () => {
|
||||
rollback_tags_json: '[]',
|
||||
override_paths_json: '[]',
|
||||
prune_volumes_requested: 0,
|
||||
required_blueprint_id: null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
@@ -111,6 +112,7 @@ describe('DeployedStackDeletionService ready transaction', () => {
|
||||
rollback_tags_json: '[]',
|
||||
override_paths_json: '[]',
|
||||
prune_volumes_requested: 0,
|
||||
required_blueprint_id: null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
@@ -129,6 +131,7 @@ describe('DeployedStackDeletionService ready transaction', () => {
|
||||
rollback_tags_json: '[]',
|
||||
override_paths_json: '[]',
|
||||
prune_volumes_requested: 0,
|
||||
required_blueprint_id: null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
@@ -154,3 +157,39 @@ describe('overrideDeletionContainmentBase', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeployedStackDeletionService blueprint ownership probe', () => {
|
||||
it('returns failed (not name_conflict) when marker read fails with non-ENOENT I/O', async () => {
|
||||
const { promises: fsPromises } = await import('fs');
|
||||
const { vi } = await import('vitest');
|
||||
const composeDir = process.env.COMPOSE_DIR!;
|
||||
const stackName = `del-probe-${Date.now()}`;
|
||||
const stackDir = path.join(composeDir, stackName);
|
||||
await fsPromises.mkdir(stackDir, { recursive: true });
|
||||
await fsPromises.writeFile(path.join(stackDir, 'compose.yaml'), 'services: {}\n');
|
||||
await fsPromises.writeFile(
|
||||
path.join(stackDir, '.blueprint.json'),
|
||||
JSON.stringify({ blueprintId: 7, revision: 1, lastApplied: 0 }),
|
||||
);
|
||||
|
||||
const accessErr = Object.assign(new Error('EACCES'), { code: 'EACCES' });
|
||||
const readSpy = vi.spyOn(fsPromises, 'readFile').mockRejectedValueOnce(accessErr);
|
||||
|
||||
const result = await DeployedStackDeletionService.getInstance().deleteDeployedStack({
|
||||
nodeId: NODE,
|
||||
stackName,
|
||||
pruneVolumes: false,
|
||||
actor: 'test',
|
||||
requireBlueprintId: 7,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe('failed');
|
||||
expect(result.error).toMatch(/EACCES|Failed to read|permission/i);
|
||||
}
|
||||
expect(readSpy).toHaveBeenCalled();
|
||||
readSpy.mockRestore();
|
||||
await fsPromises.rm(stackDir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user