Skip to content

Commit e7c11fa

Browse files
fix: stop deployment log polling after cancellation
deploymentStatus terminal-state list was missing CANCELLED, so csdx launch:logs kept polling the deployment-status and deployment-logs queries indefinitely after a deployment was cancelled. Co-Authored-By: Rohan Agrawal <rohan.agrawal@contentstack.com>
1 parent de0bdf5 commit e7c11fa

6 files changed

Lines changed: 137 additions & 3 deletions

File tree

src/adapters/file-upload.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,30 @@ describe('FileUpload Adapter', () => {
105105
expect(showSuggestionMock).not.toHaveBeenCalled();
106106
});
107107

108+
it('should exit with code 1 when deployment status is CANCELLED for new project', async () => {
109+
const fileUploadInstance = new FileUpload({
110+
config: {
111+
isExistingProject: false,
112+
currentDeploymentStatus: DeploymentStatus.CANCELLED,
113+
},
114+
log: logMock,
115+
exit: exitMock,
116+
} as any);
117+
118+
try {
119+
await fileUploadInstance.run();
120+
} catch (error: any) {
121+
expect(error.message).toBe('1');
122+
}
123+
124+
expect(handleNewProjectMock).toHaveBeenCalled();
125+
expect(prepareLaunchConfigMock).toHaveBeenCalled();
126+
expect(showLogsMock).toHaveBeenCalled();
127+
expect(exitMock).toHaveBeenCalledWith(1);
128+
expect(showDeploymentUrlMock).not.toHaveBeenCalled();
129+
expect(showSuggestionMock).not.toHaveBeenCalled();
130+
});
131+
108132
it('should continue normally when deployment status is not FAILED', async () => {
109133
const fileUploadInstance = new FileUpload({
110134
config: {

src/adapters/file-upload.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ export default class FileUpload extends BaseClass {
3434

3535
this.prepareLaunchConfig();
3636
await this.showLogs();
37-
if(this.config.currentDeploymentStatus === DeploymentStatus.FAILED) {
37+
if (
38+
this.config.currentDeploymentStatus === DeploymentStatus.FAILED ||
39+
this.config.currentDeploymentStatus === DeploymentStatus.CANCELLED
40+
) {
3841
this.exit(1);
3942
}
4043
this.showDeploymentUrl();

src/adapters/github.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,30 @@ describe('GitHub Adapter', () => {
723723
expect(showSuggestionMock).not.toHaveBeenCalled();
724724
});
725725

726+
it('should exit with code 1 when deployment status is CANCELLED for new project', async () => {
727+
const githubInstance = new GitHub({
728+
config: {
729+
isExistingProject: false,
730+
currentDeploymentStatus: DeploymentStatus.CANCELLED,
731+
},
732+
log: logMock,
733+
exit: exitMock,
734+
} as any);
735+
736+
try {
737+
await githubInstance.run();
738+
} catch (error: any) {
739+
expect(error.message).toBe('1');
740+
}
741+
742+
expect(handleNewProjectMock).toHaveBeenCalled();
743+
expect(prepareLaunchConfigMock).toHaveBeenCalled();
744+
expect(showLogsMock).toHaveBeenCalled();
745+
expect(exitMock).toHaveBeenCalledWith(1);
746+
expect(showDeploymentUrlMock).not.toHaveBeenCalled();
747+
expect(showSuggestionMock).not.toHaveBeenCalled();
748+
});
749+
726750
it('should continue normally when deployment status is not FAILED', async () => {
727751
const githubInstance = new GitHub({
728752
config: {

src/adapters/github.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ export default class GitHub extends BaseClass {
3535

3636
this.prepareLaunchConfig();
3737
await this.showLogs();
38-
if (this.config.currentDeploymentStatus === DeploymentStatus.FAILED) {
38+
if (
39+
this.config.currentDeploymentStatus === DeploymentStatus.FAILED ||
40+
this.config.currentDeploymentStatus === DeploymentStatus.CANCELLED
41+
) {
3942
this.exit(1);
4043
}
4144
this.showDeploymentUrl();

src/config/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const config = {
4242
launchHubUrls: '',
4343
launchBaseUrl: '',
4444
supportedAdapters: ['GitHub'],
45-
deploymentStatus: ['LIVE', 'FAILED', 'SKIPPED', 'DEPLOYED'],
45+
deploymentStatus: ['LIVE', 'FAILED', 'SKIPPED', 'DEPLOYED', 'CANCELLED'],
4646
pollingInterval: 1000,
4747
variablePreparationTypeOptions: [
4848
VariablePreparationTypeOptions.IMPORT_FROM_STACK,

src/util/logs-polling-utilities.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1+
import { EventEmitter } from 'events';
12
import { logPolling as cliUtilitiesJestMock } from '../test/mocks/cli-utilities';
3+
import LogPolling from './logs-polling-utilities';
4+
import defaultConfig from '../config';
25

36
type LogPollingCtor = typeof import('./logs-polling-utilities').default;
47

58
jest.mock('@contentstack/cli-utilities', () => cliUtilitiesJestMock);
9+
jest.mock('timers/promises', () => ({ setTimeout: jest.fn().mockResolvedValue(undefined) }));
10+
11+
function makeWatchQuery() {
12+
let subscriber: (result: any) => void = () => {};
13+
return {
14+
subscribe: jest.fn((cb: (result: any) => void) => {
15+
subscriber = cb;
16+
return { unsubscribe: jest.fn() };
17+
}),
18+
setVariables: jest.fn(),
19+
stopPolling: jest.fn(),
20+
emit: (result: any) => subscriber(result),
21+
};
22+
}
623

724
const CONFIG = {
825
deployment: 'd1',
@@ -115,3 +132,66 @@ describe('LogPolling Apollo deprecation regression', () => {
115132
expect(watchQuery).toHaveBeenCalledTimes(1);
116133
});
117134
});
135+
136+
describe('cancelled deployment stops log polling', () => {
137+
function buildInstance(deploymentStatus: string[]) {
138+
const statusWatchQuery = makeWatchQuery();
139+
const logsWatchQuery = makeWatchQuery();
140+
const config = {
141+
deployment: 'd1',
142+
environment: 'e1',
143+
pollingInterval: 1000,
144+
deploymentStatus,
145+
};
146+
const instance = new LogPolling({
147+
apolloManageClient: { watchQuery: jest.fn().mockReturnValue(statusWatchQuery) } as any,
148+
apolloLogsClient: { watchQuery: jest.fn().mockReturnValue(logsWatchQuery) } as any,
149+
config: config as any,
150+
$event: new EventEmitter(),
151+
});
152+
return { instance, statusWatchQuery, logsWatchQuery, config };
153+
}
154+
155+
it('stops status polling once the deployment status is CANCELLED', async () => {
156+
const { instance, statusWatchQuery } = buildInstance(['LIVE', 'FAILED', 'SKIPPED', 'DEPLOYED', 'CANCELLED']);
157+
158+
await instance.deploymentLogs();
159+
statusWatchQuery.emit({ data: { Deployment: { status: 'CANCELLED' } } });
160+
161+
expect(instance.deploymentStatus).toBe('CANCELLED');
162+
expect(statusWatchQuery.stopPolling).toHaveBeenCalledTimes(1);
163+
});
164+
165+
it('stops deployment-logs polling and emits DONE once status is CANCELLED', async () => {
166+
const { instance, statusWatchQuery, logsWatchQuery } = buildInstance([
167+
'LIVE',
168+
'FAILED',
169+
'SKIPPED',
170+
'DEPLOYED',
171+
'CANCELLED',
172+
]);
173+
const events: string[] = [];
174+
(instance as any).$event.on('deployment-logs', (e: any) => events.push(e.message));
175+
176+
await instance.deploymentLogs();
177+
statusWatchQuery.emit({ data: { Deployment: { status: 'CANCELLED' } } });
178+
await logsWatchQuery.emit({ data: { getLogs: [] } });
179+
180+
expect(logsWatchQuery.stopPolling).toHaveBeenCalledTimes(1);
181+
expect(events).toContain('DONE');
182+
});
183+
184+
it('regression guard: keeps polling forever if CANCELLED is missing from deploymentStatus', async () => {
185+
const { instance, statusWatchQuery } = buildInstance(['LIVE', 'FAILED', 'SKIPPED', 'DEPLOYED']);
186+
187+
await instance.deploymentLogs();
188+
statusWatchQuery.emit({ data: { Deployment: { status: 'CANCELLED' } } });
189+
190+
expect(instance.deploymentStatus).toBe('CANCELLED');
191+
expect(statusWatchQuery.stopPolling).not.toHaveBeenCalled();
192+
});
193+
194+
it('real app config (src/config) lists CANCELLED as a terminal deployment status', () => {
195+
expect(defaultConfig.deploymentStatus).toContain('CANCELLED');
196+
});
197+
});

0 commit comments

Comments
 (0)