feat: outgoing webhooks

Owners and admins can point a project at other systems: a signed JSON
POST (HMAC-SHA256 of the body) when tickets, comments, releases or sprints
change, filtered by event or family. Deliveries are retried a couple of
times, the last one is shown, and a ping can be sent from the settings.
Private addresses are refused unless WEBHOOKS_ALLOW_PRIVATE is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 15:36:19 +02:00
parent 046d77c813
commit d43fc620e9
37 changed files with 3660 additions and 28 deletions

View File

@@ -75,6 +75,13 @@ export type CreateSprintInput = {
startDate: string;
};
export type CreateWebhookInput = {
/** Event names, family.* or * */
events: Array<string>;
projectId: string;
url: string;
};
export type DevLinkKind = 'BRANCH' | 'COMMIT' | 'PULL_REQUEST';
export type DevLinkState = 'CLOSED' | 'MERGED' | 'OPEN';
@@ -240,6 +247,13 @@ export type UpdateSprintInput = {
startDate?: string | null | undefined;
};
export type UpdateWebhookInput = {
active?: boolean | null | undefined;
events?: Array<string> | null | undefined;
url?: string | null | undefined;
webhookId: string;
};
export type WorkflowStatusInput = {
archived?: boolean;
category: StatusCategory;
@@ -1656,6 +1670,106 @@ export type MyProjectsChangedSubscription = {
};
};
export type WebhookFieldsFragment = {
id: string;
url: string;
events: Array<string>;
active: boolean;
createdAt: string;
createdBy: { id: string; name: string };
lastDelivery: {
at: string;
event: string;
status: number | null;
ok: boolean;
error: string | null;
} | null;
};
export type WebhookDeliveryFieldsFragment = {
at: string;
event: string;
status: number | null;
ok: boolean;
error: string | null;
};
export type WebhooksQueryVariables = Exact<{
projectId: string;
}>;
export type WebhooksQuery = {
webhooks: Array<{
id: string;
url: string;
events: Array<string>;
active: boolean;
createdAt: string;
createdBy: { id: string; name: string };
lastDelivery: {
at: string;
event: string;
status: number | null;
ok: boolean;
error: string | null;
} | null;
}>;
};
export type WebhookEventsQueryVariables = Exact<{ [key: string]: never }>;
export type WebhookEventsQuery = { webhookEvents: Array<string> };
export type CreateWebhookMutationVariables = Exact<{
input: CreateWebhookInput;
}>;
export type CreateWebhookMutation = {
createWebhook: { id: string; secret: string };
};
export type UpdateWebhookMutationVariables = Exact<{
input: UpdateWebhookInput;
}>;
export type UpdateWebhookMutation = {
updateWebhook: {
id: string;
url: string;
events: Array<string>;
active: boolean;
createdAt: string;
createdBy: { id: string; name: string };
lastDelivery: {
at: string;
event: string;
status: number | null;
ok: boolean;
error: string | null;
} | null;
};
};
export type DeleteWebhookMutationVariables = Exact<{
webhookId: string;
}>;
export type DeleteWebhookMutation = { deleteWebhook: boolean };
export type TestWebhookMutationVariables = Exact<{
webhookId: string;
}>;
export type TestWebhookMutation = {
testWebhook: {
at: string;
event: string;
status: number | null;
ok: boolean;
error: string | null;
};
};
export const UserRefFieldsFragmentDoc = {
kind: 'Document',
definitions: [
@@ -2484,6 +2598,111 @@ export const SprintFieldsFragmentDoc = {
},
],
} as unknown as DocumentNode<SprintFieldsFragment, unknown>;
export const WebhookDeliveryFieldsFragmentDoc = {
kind: 'Document',
definitions: [
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'WebhookDeliveryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'WebhookDelivery' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
{ kind: 'Field', name: { kind: 'Name', value: 'event' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'ok' } },
{ kind: 'Field', name: { kind: 'Name', value: 'error' } },
],
},
},
],
} as unknown as DocumentNode<WebhookDeliveryFieldsFragment, unknown>;
export const WebhookFieldsFragmentDoc = {
kind: 'Document',
definitions: [
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'WebhookFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'Webhook' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'url' } },
{ kind: 'Field', name: { kind: 'Name', value: 'events' } },
{ kind: 'Field', name: { kind: 'Name', value: 'active' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'createdBy' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'UserRefFields' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'lastDelivery' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'WebhookDeliveryFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'UserRefFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'UserRef' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'WebhookDeliveryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'WebhookDelivery' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
{ kind: 'Field', name: { kind: 'Name', value: 'event' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'ok' } },
{ kind: 'Field', name: { kind: 'Name', value: 'error' } },
],
},
},
],
} as unknown as DocumentNode<WebhookFieldsFragment, unknown>;
export const GitIntegrationDocument = {
kind: 'Document',
definitions: [
@@ -9995,3 +10214,450 @@ export const MyProjectsChangedDocument = {
MyProjectsChangedSubscription,
MyProjectsChangedSubscriptionVariables
>;
export const WebhooksDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'Webhooks' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'projectId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'webhooks' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'projectId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'projectId' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'WebhookFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'UserRefFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'UserRef' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'WebhookDeliveryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'WebhookDelivery' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
{ kind: 'Field', name: { kind: 'Name', value: 'event' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'ok' } },
{ kind: 'Field', name: { kind: 'Name', value: 'error' } },
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'WebhookFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'Webhook' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'url' } },
{ kind: 'Field', name: { kind: 'Name', value: 'events' } },
{ kind: 'Field', name: { kind: 'Name', value: 'active' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'createdBy' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'UserRefFields' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'lastDelivery' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'WebhookDeliveryFields' },
},
],
},
},
],
},
},
],
} as unknown as DocumentNode<WebhooksQuery, WebhooksQueryVariables>;
export const WebhookEventsDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'WebhookEvents' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'webhookEvents' } },
],
},
},
],
} as unknown as DocumentNode<WebhookEventsQuery, WebhookEventsQueryVariables>;
export const CreateWebhookDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'CreateWebhook' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'input' },
},
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'CreateWebhookInput' },
},
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'createWebhook' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'input' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'input' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'secret' } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<
CreateWebhookMutation,
CreateWebhookMutationVariables
>;
export const UpdateWebhookDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'UpdateWebhook' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'input' },
},
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'UpdateWebhookInput' },
},
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'updateWebhook' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'input' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'input' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'WebhookFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'UserRefFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'UserRef' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'WebhookDeliveryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'WebhookDelivery' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
{ kind: 'Field', name: { kind: 'Name', value: 'event' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'ok' } },
{ kind: 'Field', name: { kind: 'Name', value: 'error' } },
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'WebhookFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'Webhook' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'url' } },
{ kind: 'Field', name: { kind: 'Name', value: 'events' } },
{ kind: 'Field', name: { kind: 'Name', value: 'active' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'createdBy' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'UserRefFields' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'lastDelivery' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'WebhookDeliveryFields' },
},
],
},
},
],
},
},
],
} as unknown as DocumentNode<
UpdateWebhookMutation,
UpdateWebhookMutationVariables
>;
export const DeleteWebhookDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'DeleteWebhook' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'webhookId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'deleteWebhook' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'webhookId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'webhookId' },
},
},
],
},
],
},
},
],
} as unknown as DocumentNode<
DeleteWebhookMutation,
DeleteWebhookMutationVariables
>;
export const TestWebhookDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'TestWebhook' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'webhookId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'testWebhook' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'webhookId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'webhookId' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'WebhookDeliveryFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'WebhookDeliveryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'WebhookDelivery' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
{ kind: 'Field', name: { kind: 'Name', value: 'event' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'ok' } },
{ kind: 'Field', name: { kind: 'Name', value: 'error' } },
],
},
},
],
} as unknown as DocumentNode<TestWebhookMutation, TestWebhookMutationVariables>;