feat: Add seed command support to preview deployments
Run project-specific initialization commands (DB migrations, fixture loading, etc.) automatically after containers are healthy, before the preview is marked ready. Configured via per-service `seed` arrays in .cw-preview.yml.
This commit is contained in:
@@ -10,6 +10,7 @@ vi.mock('./docker-client.js', () => ({
|
||||
isDockerAvailable: vi.fn(),
|
||||
composeUp: vi.fn(),
|
||||
composeDown: vi.fn(),
|
||||
execInContainer: vi.fn(),
|
||||
composePs: vi.fn(),
|
||||
listPreviewProjects: vi.fn(),
|
||||
getContainerLabels: vi.fn(),
|
||||
@@ -74,6 +75,7 @@ import {
|
||||
isDockerAvailable,
|
||||
composeUp,
|
||||
composeDown,
|
||||
execInContainer,
|
||||
composePs,
|
||||
listPreviewProjects,
|
||||
getContainerLabels,
|
||||
@@ -91,6 +93,7 @@ const mockComposeDown = vi.mocked(composeDown);
|
||||
const mockComposePs = vi.mocked(composePs);
|
||||
const mockListPreviewProjects = vi.mocked(listPreviewProjects);
|
||||
const mockGetContainerLabels = vi.mocked(getContainerLabels);
|
||||
const mockExecInContainer = vi.mocked(execInContainer);
|
||||
const mockDiscoverConfig = vi.mocked(discoverConfig);
|
||||
const mockAllocatePort = vi.mocked(allocatePort);
|
||||
const mockWaitForHealthy = vi.mocked(waitForHealthy);
|
||||
@@ -302,6 +305,76 @@ describe('PreviewManager', () => {
|
||||
expect(failedEvent).toBeDefined();
|
||||
});
|
||||
|
||||
it('runs seed commands after health check and before preview:ready', async () => {
|
||||
const seededConfig: PreviewConfig = {
|
||||
version: 1,
|
||||
services: {
|
||||
app: {
|
||||
name: 'app',
|
||||
build: '.',
|
||||
port: 3000,
|
||||
seed: ['npm run db:migrate', 'npm run db:seed'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockIsDockerAvailable.mockResolvedValue(true);
|
||||
mockComposeUp.mockResolvedValue(undefined);
|
||||
mockComposePs.mockResolvedValue([{ name: 'app', state: 'running', health: 'healthy' }]);
|
||||
mockDiscoverConfig.mockResolvedValue(seededConfig);
|
||||
mockCreatePreviewWorktree.mockResolvedValue(undefined);
|
||||
mockWaitForHealthy.mockResolvedValue([{ name: 'app', healthy: true }]);
|
||||
mockExecInContainer.mockResolvedValue({ stdout: '', stderr: '' });
|
||||
|
||||
const result = await manager.start({
|
||||
initiativeId: 'init-1',
|
||||
projectId: 'proj-1',
|
||||
branch: 'feature-x',
|
||||
});
|
||||
|
||||
expect(result.status).toBe('running');
|
||||
expect(mockExecInContainer).toHaveBeenCalledTimes(2);
|
||||
expect(mockExecInContainer).toHaveBeenCalledWith('cw-preview-abc123test', 'app', 'npm run db:migrate');
|
||||
expect(mockExecInContainer).toHaveBeenCalledWith('cw-preview-abc123test', 'app', 'npm run db:seed');
|
||||
|
||||
const readyEvent = eventBus.emitted.find((e) => e.type === 'preview:ready');
|
||||
expect(readyEvent).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits preview:failed and cleans up when seed command fails', async () => {
|
||||
const seededConfig: PreviewConfig = {
|
||||
version: 1,
|
||||
services: {
|
||||
app: {
|
||||
name: 'app',
|
||||
build: '.',
|
||||
port: 3000,
|
||||
seed: ['npm run db:migrate'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockIsDockerAvailable.mockResolvedValue(true);
|
||||
mockComposeUp.mockResolvedValue(undefined);
|
||||
mockDiscoverConfig.mockResolvedValue(seededConfig);
|
||||
mockCreatePreviewWorktree.mockResolvedValue(undefined);
|
||||
mockWaitForHealthy.mockResolvedValue([{ name: 'app', healthy: true }]);
|
||||
mockExecInContainer.mockRejectedValue(new Error('migration failed: relation already exists'));
|
||||
mockComposeDown.mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
manager.start({
|
||||
initiativeId: 'init-1',
|
||||
projectId: 'proj-1',
|
||||
branch: 'main',
|
||||
}),
|
||||
).rejects.toThrow('Preview build failed');
|
||||
|
||||
const failedEvent = eventBus.emitted.find((e) => e.type === 'preview:failed');
|
||||
expect(failedEvent).toBeDefined();
|
||||
expect(mockComposeDown).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('succeeds when no healthcheck endpoints are configured', async () => {
|
||||
const noHealthConfig: PreviewConfig = {
|
||||
version: 1,
|
||||
|
||||
Reference in New Issue
Block a user