mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(GitHub Node): Add timeout option to Dispatch and Wait for Completion (#35837)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4664e9029a
commit
a6f07dbba0
|
|
@ -10,12 +10,7 @@ import type {
|
|||
IWebhookResponseData,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
NodeApiError,
|
||||
NodeConnectionTypes,
|
||||
NodeOperationError,
|
||||
WAIT_INDEFINITELY,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError, NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
getFileSha,
|
||||
|
|
@ -25,6 +20,8 @@ import {
|
|||
validateJSON,
|
||||
} from './GenericFunctions';
|
||||
import { getRefs, getRepositories, getUsers, getWorkflows } from './SearchFunctions';
|
||||
import { configureWaitTillDate } from '../../utils/sendAndWait/configureWaitTillDate.util';
|
||||
import { limitWaitTimeOption } from '../../utils/sendAndWait/descriptions';
|
||||
import { removeTrailingSlash } from '../../utils/utilities';
|
||||
import { defaultWebhookDescription } from '../Webhook/description';
|
||||
import { pullRequestFields } from './descriptions/PullRequestDescription';
|
||||
|
|
@ -798,6 +795,26 @@ export class Github implements INodeType {
|
|||
},
|
||||
description: 'JSON object with input parameters for the workflow',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['dispatchAndWait'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
...limitWaitTimeOption,
|
||||
description:
|
||||
'Whether to limit how long to wait for the webhook to be called before the execution resumes',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// file
|
||||
|
|
@ -2422,6 +2439,10 @@ export class Github implements INodeType {
|
|||
throw new NodeOperationError(this.getNode(), 'Inputs: Invalid JSON');
|
||||
}
|
||||
|
||||
// Resolved before dispatching so a misconfigured wait limit doesn't leave an
|
||||
// already-triggered GitHub workflow with nothing waiting on it
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`;
|
||||
|
||||
body = {
|
||||
|
|
@ -2453,7 +2474,7 @@ export class Github implements INodeType {
|
|||
// Add signed resumeUrl to metadata for frontend to use in waiting tooltip
|
||||
this.setMetadata({ resumeUrl });
|
||||
|
||||
await this.putExecutionToWait(WAIT_INDEFINITELY);
|
||||
await this.putExecutionToWait(waitTill);
|
||||
return [this.getInputData()];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
|
||||
import { NodeApiError, NodeOperationError, WAIT_INDEFINITELY } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
|
||||
import * as utilities from '../../../../utils/utilities';
|
||||
|
|
@ -699,4 +699,103 @@ describe('Test Github Node', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dispatch and Wait - Limit Wait Time', () => {
|
||||
const now = 1683028800000;
|
||||
let githubNode: Github;
|
||||
let mockExecutionContext: any;
|
||||
|
||||
// Only the `limitWaitTime` values vary between these tests; everything else is a
|
||||
// valid dispatchAndWait configuration.
|
||||
const setLimitWaitTime = (values: object | undefined) => {
|
||||
mockExecutionContext.getNodeParameter.mockImplementation(
|
||||
(parameterName: string, _itemIndex: number, defaultValue: unknown) => {
|
||||
if (parameterName === 'options.limitWaitTime.values') return values ?? defaultValue;
|
||||
if (parameterName === 'resource') return 'workflow';
|
||||
if (parameterName === 'operation') return 'dispatchAndWait';
|
||||
if (parameterName === 'authentication') return 'accessToken';
|
||||
if (parameterName === 'owner') return 'testOwner';
|
||||
if (parameterName === 'repository') return 'testRepository';
|
||||
if (parameterName === 'workflowId') return 147025216;
|
||||
if (parameterName === 'ref') return 'main';
|
||||
if (parameterName === 'inputs') return '{}';
|
||||
return defaultValue;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ now });
|
||||
githubNode = new Github();
|
||||
mockExecutionContext = {
|
||||
getNode: vi.fn().mockReturnValue({ name: 'Github' }),
|
||||
getNodeParameter: vi.fn(),
|
||||
getInputData: vi.fn().mockReturnValue([{ json: {} }]),
|
||||
continueOnFail: vi.fn().mockReturnValue(false),
|
||||
putExecutionToWait: vi.fn(),
|
||||
setMetadata: vi.fn(),
|
||||
getCredentials: vi.fn().mockResolvedValue({
|
||||
server: 'https://api.github.com',
|
||||
user: 'test',
|
||||
accessToken: 'test',
|
||||
}),
|
||||
helpers: {
|
||||
returnJsonArray: vi.fn().mockReturnValue([{ json: {} }]),
|
||||
requestWithAuthentication: vi.fn().mockResolvedValue({}),
|
||||
constructExecutionMetaData: vi.fn().mockReturnValue([{ json: {} }]),
|
||||
},
|
||||
getWorkflowDataProxy: vi.fn().mockReturnValue({
|
||||
$execution: { resumeUrl: 'https://example.com/webhook' },
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should wait indefinitely when no limit is set', async () => {
|
||||
setLimitWaitTime(undefined);
|
||||
|
||||
await githubNode.execute.call(mockExecutionContext);
|
||||
|
||||
expect(mockExecutionContext.putExecutionToWait).toHaveBeenCalledWith(WAIT_INDEFINITELY);
|
||||
});
|
||||
|
||||
it('should wait for the configured time interval', async () => {
|
||||
setLimitWaitTime({
|
||||
limitType: 'afterTimeInterval',
|
||||
resumeAmount: 30,
|
||||
resumeUnit: 'minutes',
|
||||
});
|
||||
|
||||
await githubNode.execute.call(mockExecutionContext);
|
||||
|
||||
expect(mockExecutionContext.putExecutionToWait).toHaveBeenCalledWith(
|
||||
new Date(now + 30 * 60 * 1000),
|
||||
);
|
||||
});
|
||||
|
||||
it('should wait until the configured date and time', async () => {
|
||||
const maxDateAndTime = '2023-05-02T12:00:00.000Z';
|
||||
setLimitWaitTime({ limitType: 'atSpecifiedTime', maxDateAndTime });
|
||||
|
||||
await githubNode.execute.call(mockExecutionContext);
|
||||
|
||||
expect(mockExecutionContext.putExecutionToWait).toHaveBeenCalledWith(
|
||||
new Date(maxDateAndTime),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for an invalid date and time without dispatching the workflow', async () => {
|
||||
setLimitWaitTime({ limitType: 'atSpecifiedTime', maxDateAndTime: 'not-a-date' });
|
||||
|
||||
await expect(githubNode.execute.call(mockExecutionContext)).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
|
||||
expect(mockExecutionContext.helpers.requestWithAuthentication).not.toHaveBeenCalled();
|
||||
expect(mockExecutionContext.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user