@shipfox/api-integration-core 10.2.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +78 -0
  3. package/dist/core/providers/source-control.d.ts +1 -1
  4. package/dist/core/providers/source-control.d.ts.map +1 -1
  5. package/dist/core/providers/source-control.js.map +1 -1
  6. package/dist/core/source-control-service.d.ts +7 -1
  7. package/dist/core/source-control-service.d.ts.map +1 -1
  8. package/dist/core/source-control-service.js +11 -0
  9. package/dist/core/source-control-service.js.map +1 -1
  10. package/dist/db/connections.d.ts +6 -0
  11. package/dist/db/connections.d.ts.map +1 -1
  12. package/dist/db/connections.js +6 -0
  13. package/dist/db/connections.js.map +1 -1
  14. package/dist/db/webhook-deliveries.d.ts +18 -1
  15. package/dist/db/webhook-deliveries.d.ts.map +1 -1
  16. package/dist/db/webhook-deliveries.js +65 -19
  17. package/dist/db/webhook-deliveries.js.map +1 -1
  18. package/dist/index.d.ts +5 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/presentation/inter-module.d.ts.map +1 -1
  23. package/dist/presentation/inter-module.js +10 -1
  24. package/dist/presentation/inter-module.js.map +1 -1
  25. package/dist/providers/github.d.ts.map +1 -1
  26. package/dist/providers/github.js +2 -1
  27. package/dist/providers/github.js.map +1 -1
  28. package/dist/providers/jira.d.ts.map +1 -1
  29. package/dist/providers/jira.js +35 -12
  30. package/dist/providers/jira.js.map +1 -1
  31. package/dist/providers/types.d.ts +1 -1
  32. package/dist/providers/types.js.map +1 -1
  33. package/dist/tsconfig.test.tsbuildinfo +1 -1
  34. package/package.json +20 -20
  35. package/src/core/agent-tool-selection.test.ts +1 -0
  36. package/src/core/providers/registry.test.ts +1 -0
  37. package/src/core/providers/source-control.ts +1 -0
  38. package/src/core/source-control-service.test.ts +25 -0
  39. package/src/core/source-control-service.ts +21 -0
  40. package/src/db/connections.test.ts +18 -0
  41. package/src/db/connections.ts +25 -0
  42. package/src/db/webhook-deliveries.test.ts +77 -0
  43. package/src/db/webhook-deliveries.ts +110 -21
  44. package/src/index.ts +13 -3
  45. package/src/presentation/inter-module.ts +11 -1
  46. package/src/presentation/routes/list-repositories.test.ts +1 -0
  47. package/src/providers/github.ts +2 -0
  48. package/src/providers/jira.test.ts +2 -0
  49. package/src/providers/jira.ts +38 -12
  50. package/src/providers/types.ts +1 -1
  51. package/test/env.ts +0 -1
  52. package/test/route-utils.ts +2 -1
  53. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-integration-core",
3
3
  "license": "MIT",
4
- "version": "10.2.0",
4
+ "version": "12.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -23,31 +23,31 @@
23
23
  "ajv": "^8.20.0",
24
24
  "drizzle-orm": "^0.45.2",
25
25
  "zod": "^4.4.3",
26
- "@shipfox/api-agent-dto": "10.0.0",
27
- "@shipfox/api-auth-context": "10.2.0",
28
- "@shipfox/api-workflows-dto": "10.0.0",
29
- "@shipfox/api-integration-core-dto": "9.0.2",
30
- "@shipfox/api-integration-spi": "0.2.2",
31
- "@shipfox/api-workspaces-dto": "10.2.0",
32
- "@shipfox/api-integration-gitea": "10.2.0",
33
- "@shipfox/api-integration-github": "10.2.0",
34
- "@shipfox/api-integration-jira": "10.2.0",
35
- "@shipfox/api-integration-linear": "10.2.0",
36
- "@shipfox/api-integration-sentry": "10.2.0",
37
- "@shipfox/api-integration-slack": "10.2.0",
38
- "@shipfox/api-integration-webhook": "10.2.0",
26
+ "@shipfox/api-agent-dto": "12.0.0",
27
+ "@shipfox/api-auth-context": "12.0.0",
28
+ "@shipfox/api-workflows-dto": "12.0.0",
29
+ "@shipfox/api-integration-core-dto": "12.0.0",
30
+ "@shipfox/api-integration-spi": "1.0.0",
31
+ "@shipfox/api-workspaces-dto": "12.0.0",
32
+ "@shipfox/api-integration-gitea": "12.0.0",
33
+ "@shipfox/api-integration-github": "12.0.0",
34
+ "@shipfox/api-integration-jira": "12.0.0",
35
+ "@shipfox/api-integration-linear": "12.0.0",
36
+ "@shipfox/api-integration-sentry": "12.0.0",
37
+ "@shipfox/api-integration-slack": "12.0.0",
38
+ "@shipfox/api-integration-webhook": "12.0.0",
39
39
  "@shipfox/config": "1.2.4",
40
- "@shipfox/inter-module": "0.2.2",
41
- "@shipfox/node-drizzle": "0.3.4",
42
- "@shipfox/node-fastify": "0.4.0",
43
- "@shipfox/node-module": "1.0.4",
40
+ "@shipfox/inter-module": "0.2.3",
41
+ "@shipfox/node-drizzle": "0.3.5",
42
+ "@shipfox/node-fastify": "0.4.1",
43
+ "@shipfox/node-module": "1.0.5",
44
44
  "@shipfox/node-opentelemetry": "0.6.3",
45
45
  "@shipfox/node-error-monitoring": "0.3.0",
46
46
  "@shipfox/node-outbox": "0.2.6",
47
- "@shipfox/node-postgres": "0.4.4",
47
+ "@shipfox/node-postgres": "0.5.0",
48
48
  "@shipfox/node-temporal": "0.4.4",
49
49
  "@shipfox/regex": "0.2.4",
50
- "@shipfox/redact": "0.2.5"
50
+ "@shipfox/redact": "0.2.6"
51
51
  },
52
52
  "imports": {
53
53
  "#*": "./dist/*"
@@ -109,6 +109,7 @@ function sourceProvider(provider: string) {
109
109
  await Promise.resolve();
110
110
  throw new Error('not used');
111
111
  },
112
+ resolveTriggerReference: () => null,
112
113
  },
113
114
  },
114
115
  };
@@ -22,6 +22,7 @@ function sourceControlAdapter() {
22
22
  await Promise.resolve();
23
23
  throw new Error('not used');
24
24
  },
25
+ resolveTriggerReference: () => null,
25
26
  };
26
27
  }
27
28
 
@@ -14,4 +14,5 @@ export type {
14
14
  RepositoryVisibility,
15
15
  ResolveRepositoryInput,
16
16
  SourceControlProvider,
17
+ TriggerReference,
17
18
  } from '@shipfox/api-integration-spi';
@@ -52,6 +52,7 @@ describe('integration source-control service', () => {
52
52
  await Promise.resolve();
53
53
  return {path: '.shipfox/workflows/ci.yml', ref: 'main', content: 'name: CI'};
54
54
  },
55
+ resolveTriggerReference: () => null,
55
56
  createCheckoutSpec: async (input) => {
56
57
  await Promise.resolve();
57
58
  return {repositoryUrl: repository.cloneUrl, ref: input.ref ?? repository.defaultBranch};
@@ -91,6 +92,30 @@ describe('integration source-control service', () => {
91
92
  expect(result.repository.externalRepositoryId).toBe('gitea:gitea-owner/platform');
92
93
  });
93
94
 
95
+ it('resolves a normalized trigger reference through an active source-control connection', async () => {
96
+ const resolveTriggerReference = vi.fn(() => ({
97
+ externalRepositoryId: repository.externalRepositoryId,
98
+ ref: 'refs/heads/main',
99
+ commit: 'a'.repeat(40),
100
+ actor: 'octocat',
101
+ }));
102
+ const service = createService({resolveTriggerReference});
103
+
104
+ await expect(
105
+ service.resolveTriggerReference({
106
+ workspaceId,
107
+ connectionId: connection.id,
108
+ payload: {ref: 'refs/heads/main'},
109
+ }),
110
+ ).resolves.toEqual({
111
+ externalRepositoryId: repository.externalRepositoryId,
112
+ ref: 'refs/heads/main',
113
+ commit: 'a'.repeat(40),
114
+ actor: 'octocat',
115
+ });
116
+ expect(resolveTriggerReference).toHaveBeenCalledWith({ref: 'refs/heads/main'});
117
+ });
118
+
94
119
  it('rejects a missing connection', async () => {
95
120
  const service = createService();
96
121
 
@@ -13,12 +13,14 @@ import type {
13
13
  FileSnapshot,
14
14
  RepositoryPage,
15
15
  RepositorySnapshot,
16
+ TriggerReference,
16
17
  } from './providers/source-control.js';
17
18
 
18
19
  export interface IntegrationSourceControlService {
19
20
  getConnection(connectionId: string): Promise<IntegrationConnection>;
20
21
  listRepositories(input: ListSourceRepositoriesInput): Promise<RepositoryPage>;
21
22
  resolveRepository(input: ResolveSourceRepositoryInput): Promise<ResolvedSourceRepository>;
23
+ resolveTriggerReference(input: ResolveTriggerReferenceInput): Promise<TriggerReference | null>;
22
24
  listFiles(input: ListSourceFilesInput): Promise<FilePage>;
23
25
  fetchFile(input: FetchSourceFileInput): Promise<FileSnapshot>;
24
26
  createCheckoutSpec(input: CreateSourceCheckoutSpecInput): Promise<CheckoutSpec>;
@@ -37,6 +39,12 @@ export interface ResolveSourceRepositoryInput {
37
39
  externalRepositoryId: string;
38
40
  }
39
41
 
42
+ export interface ResolveTriggerReferenceInput {
43
+ workspaceId: string;
44
+ connectionId: string;
45
+ payload: unknown;
46
+ }
47
+
40
48
  export interface ListSourceFilesInput extends ResolveSourceRepositoryInput {
41
49
  ref: string;
42
50
  prefix: string;
@@ -108,6 +116,19 @@ export function createSourceControlIntegrationService({
108
116
  return {connection, repository};
109
117
  },
110
118
 
119
+ async resolveTriggerReference({workspaceId, connectionId, payload}) {
120
+ const connection = await getConnection(connectionId);
121
+ if (connection.workspaceId !== workspaceId) {
122
+ throw new IntegrationConnectionWorkspaceMismatchError(connectionId);
123
+ }
124
+
125
+ // Not every integration source names a repository (for example Linear),
126
+ // so a missing source-control adapter is a valid null reference rather
127
+ // than a failed run creation.
128
+ const sourceControl = registry.get(connection.provider).adapters.source_control;
129
+ return sourceControl?.resolveTriggerReference(payload) ?? null;
130
+ },
131
+
111
132
  async listFiles({workspaceId, connectionId, externalRepositoryId, ref, prefix, limit, cursor}) {
112
133
  const connection = await getConnection(connectionId);
113
134
  if (connection.workspaceId !== workspaceId) {
@@ -5,6 +5,7 @@ import {
5
5
  createIntegrationConnection,
6
6
  deleteIntegrationConnection,
7
7
  getIntegrationConnectionById,
8
+ getIntegrationConnectionBySlug,
8
9
  listIntegrationConnections,
9
10
  listIntegrationConnectionsByProvider,
10
11
  resolveUniqueConnectionSlug,
@@ -63,6 +64,23 @@ describe('integration connection queries', () => {
63
64
  expect(result).toHaveLength(2);
64
65
  });
65
66
 
67
+ it('resolves a connection by slug within its workspace', async () => {
68
+ const connection = await upsertIntegrationConnection({
69
+ workspaceId,
70
+ provider: 'github',
71
+ externalAccountId: 'debug-1',
72
+ slug: 'github_main',
73
+ displayName: 'GitHub',
74
+ });
75
+
76
+ await expect(
77
+ getIntegrationConnectionBySlug({workspaceId, slug: 'github_main'}),
78
+ ).resolves.toMatchObject({id: connection.id, slug: 'github_main'});
79
+ await expect(
80
+ getIntegrationConnectionBySlug({workspaceId: crypto.randomUUID(), slug: 'github_main'}),
81
+ ).resolves.toBeUndefined();
82
+ });
83
+
66
84
  it('resolves a unique slug in workspace scope', async () => {
67
85
  await upsertIntegrationConnection({
68
86
  workspaceId,
@@ -191,6 +191,31 @@ export async function getIntegrationConnectionById(
191
191
 
192
192
  export type GetIntegrationConnectionByIdFn = typeof getIntegrationConnectionById;
193
193
 
194
+ export interface GetIntegrationConnectionBySlugParams {
195
+ workspaceId: string;
196
+ slug: string;
197
+ }
198
+
199
+ export async function getIntegrationConnectionBySlug(
200
+ params: GetIntegrationConnectionBySlugParams,
201
+ ): Promise<IntegrationConnection | undefined> {
202
+ const rows = await db()
203
+ .select()
204
+ .from(integrationConnections)
205
+ .where(
206
+ and(
207
+ eq(integrationConnections.workspaceId, params.workspaceId),
208
+ eq(integrationConnections.slug, params.slug),
209
+ ),
210
+ )
211
+ .limit(1);
212
+ const row = rows[0];
213
+ if (!row) return undefined;
214
+ return toIntegrationConnection(row);
215
+ }
216
+
217
+ export type GetIntegrationConnectionBySlugFn = typeof getIntegrationConnectionBySlug;
218
+
194
219
  export interface UpdateIntegrationConnectionLifecycleStatusParams {
195
220
  id: string;
196
221
  lifecycleStatus: IntegrationConnectionLifecycleStatus;
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  INTEGRATION_EVENT_RECEIVED,
3
3
  INTEGRATION_SOURCE_COMMIT_PUSHED,
4
+ INTEGRATION_SOURCE_REPOSITORY_UPDATED,
4
5
  type IntegrationEventReceivedEvent,
5
6
  type SourcePushPayload,
7
+ type SourceRepositoryIdentity,
6
8
  } from '@shipfox/api-integration-spi';
7
9
  import {and, eq, sql} from 'drizzle-orm';
8
10
  import {db} from './db.js';
@@ -13,6 +15,7 @@ import {
13
15
  publishIntegrationEventReceived,
14
16
  publishSourceCommitPushed,
15
17
  publishSourcePush,
18
+ publishSourceRepositoryUpdated,
16
19
  recordDeliveryOnly,
17
20
  } from './webhook-deliveries.js';
18
21
 
@@ -276,3 +279,77 @@ describe('publishSourceCommitPushed', () => {
276
279
  expect(await deliveriesFor(params.provider, params.deliveryId)).toHaveLength(0);
277
280
  });
278
281
  });
282
+
283
+ describe('publishSourceRepositoryUpdated', () => {
284
+ function buildParams() {
285
+ const deliveryId = crypto.randomUUID();
286
+ const repositories: SourceRepositoryIdentity[] = [
287
+ {
288
+ externalRepositoryId: 'github:42',
289
+ owner: 'acme',
290
+ name: 'platform-renamed',
291
+ defaultBranch: 'main',
292
+ },
293
+ {
294
+ externalRepositoryId: 'github:43',
295
+ owner: 'acme',
296
+ name: 'runner',
297
+ defaultBranch: 'trunk',
298
+ },
299
+ ];
300
+ return {
301
+ provider: 'github',
302
+ source: 'github_acme_production',
303
+ workspaceId: crypto.randomUUID(),
304
+ connectionId: crypto.randomUUID(),
305
+ connectionName: 'Acme Production',
306
+ deliveryId,
307
+ receivedAt: new Date().toISOString(),
308
+ rawPayload: {action: 'added', repositories_added: repositories},
309
+ event: 'installation_repositories.added',
310
+ repositories,
311
+ };
312
+ }
313
+
314
+ it('writes the generic envelope and one typed event per repository', async () => {
315
+ const params = buildParams();
316
+
317
+ const result = await db().transaction((tx) => publishSourceRepositoryUpdated({tx, ...params}));
318
+
319
+ expect(result.published).toBe(true);
320
+ const outbox = await outboxFor(params.deliveryId);
321
+ expect(outbox).toHaveLength(3);
322
+ expect(outbox.filter((row) => row.eventType === INTEGRATION_EVENT_RECEIVED)).toHaveLength(1);
323
+ expect(
324
+ outbox.filter((row) => row.eventType === INTEGRATION_SOURCE_REPOSITORY_UPDATED),
325
+ ).toHaveLength(2);
326
+ expect(
327
+ outbox.find((row) => row.eventType === INTEGRATION_EVENT_RECEIVED)?.payload,
328
+ ).toMatchObject({
329
+ event: 'installation_repositories.added',
330
+ payload: params.rawPayload,
331
+ });
332
+ expect(
333
+ outbox
334
+ .filter((row) => row.eventType === INTEGRATION_SOURCE_REPOSITORY_UPDATED)
335
+ .map((row) => row.payload),
336
+ ).toEqual(
337
+ expect.arrayContaining(
338
+ params.repositories.map((repository) =>
339
+ expect.objectContaining({repository, deliveryId: params.deliveryId}),
340
+ ),
341
+ ),
342
+ );
343
+ });
344
+
345
+ it('does not publish a duplicate delivery twice', async () => {
346
+ const params = buildParams();
347
+
348
+ const first = await db().transaction((tx) => publishSourceRepositoryUpdated({tx, ...params}));
349
+ const second = await db().transaction((tx) => publishSourceRepositoryUpdated({tx, ...params}));
350
+
351
+ expect(first.published).toBe(true);
352
+ expect(second.published).toBe(false);
353
+ expect(await outboxFor(params.deliveryId)).toHaveLength(3);
354
+ });
355
+ });
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  INTEGRATION_EVENT_RECEIVED,
3
3
  INTEGRATION_SOURCE_COMMIT_PUSHED,
4
+ INTEGRATION_SOURCE_REPOSITORY_UPDATED,
4
5
  type IntegrationEventReceivedEvent,
5
6
  type IntegrationsEventMap,
6
7
  type SourcePushPayload,
8
+ type SourceRepositoryIdentity,
7
9
  } from '@shipfox/api-integration-spi';
8
10
  import {writeOutboxEvent, writeOutboxEvents} from '@shipfox/node-outbox';
9
11
  import {lt} from 'drizzle-orm';
@@ -14,6 +16,12 @@ import {integrationsWebhookDeliveries} from './schema/webhook-deliveries.js';
14
16
  type IntegrationDb = ReturnType<typeof db>;
15
17
  type IntegrationTx = Parameters<Parameters<IntegrationDb['transaction']>[0]>[0];
16
18
  type Executor = IntegrationDb | IntegrationTx;
19
+ type IntegrationOutboxEvent = {
20
+ [K in keyof IntegrationsEventMap & string]: {
21
+ type: K;
22
+ payload: IntegrationsEventMap[K];
23
+ };
24
+ }[keyof IntegrationsEventMap & string];
17
25
 
18
26
  function connectionDedupScope(connectionId: string): string {
19
27
  return `connection:${connectionId}`;
@@ -66,7 +74,7 @@ export async function publishIntegrationEventReceived(
66
74
  return {published: true};
67
75
  }
68
76
 
69
- export interface PublishSourcePushParams {
77
+ interface PublishSourceEventsParams {
70
78
  tx: IntegrationTx;
71
79
  provider: string;
72
80
  source: string;
@@ -76,16 +84,12 @@ export interface PublishSourcePushParams {
76
84
  deliveryId: string;
77
85
  receivedAt: string;
78
86
  rawPayload: unknown;
79
- push: SourcePushPayload;
87
+ event: string;
88
+ typedEvents: IntegrationOutboxEvent[];
80
89
  }
81
90
 
82
- // Emits a single source-control push as two outbox rows: the generic
83
- // `INTEGRATION_EVENT_RECEIVED` envelope with the raw provider payload for triggers, and the
84
- // typed `INTEGRATION_SOURCE_COMMIT_PUSHED` event for domain modules. One delivery-dedup gates
85
- // both, so a redelivered webhook writes nothing. Requires a transaction: the dedup insert and
86
- // both outbox rows must commit or roll back together.
87
- export async function publishSourcePush(
88
- params: PublishSourcePushParams,
91
+ async function publishSourceEvents(
92
+ params: PublishSourceEventsParams,
89
93
  ): Promise<{published: boolean}> {
90
94
  const inserted = await params.tx
91
95
  .insert(integrationsWebhookDeliveries)
@@ -111,7 +115,7 @@ export async function publishSourcePush(
111
115
  payload: {
112
116
  provider: params.provider,
113
117
  source: params.source,
114
- event: 'push',
118
+ event: params.event,
115
119
  workspaceId: params.workspaceId,
116
120
  connectionId: params.connectionId,
117
121
  connectionName: params.connectionName,
@@ -120,22 +124,106 @@ export async function publishSourcePush(
120
124
  payload: params.rawPayload,
121
125
  },
122
126
  },
123
- {
124
- type: INTEGRATION_SOURCE_COMMIT_PUSHED,
125
- payload: {
126
- provider: params.provider,
127
- workspaceId: params.workspaceId,
128
- connectionId: params.connectionId,
129
- deliveryId: params.deliveryId,
130
- receivedAt: params.receivedAt,
131
- push: params.push,
132
- },
133
- },
127
+ ...params.typedEvents,
134
128
  ]);
135
129
 
136
130
  return {published: true};
137
131
  }
138
132
 
133
+ export interface PublishSourcePushParams {
134
+ tx: IntegrationTx;
135
+ provider: string;
136
+ source: string;
137
+ workspaceId: string;
138
+ connectionId: string;
139
+ connectionName: string;
140
+ deliveryId: string;
141
+ receivedAt: string;
142
+ rawPayload: unknown;
143
+ push: SourcePushPayload;
144
+ }
145
+
146
+ // Emits a single source-control push as two outbox rows: the generic
147
+ // `INTEGRATION_EVENT_RECEIVED` envelope with the raw provider payload for triggers, and the
148
+ // typed `INTEGRATION_SOURCE_COMMIT_PUSHED` event for domain modules. One delivery-dedup gates
149
+ // both, so a redelivered webhook writes nothing. Requires a transaction: the dedup insert and
150
+ // both outbox rows must commit or roll back together.
151
+ export function publishSourcePush(params: PublishSourcePushParams): Promise<{published: boolean}> {
152
+ return publishSourceEvents({
153
+ tx: params.tx,
154
+ provider: params.provider,
155
+ source: params.source,
156
+ workspaceId: params.workspaceId,
157
+ connectionId: params.connectionId,
158
+ connectionName: params.connectionName,
159
+ deliveryId: params.deliveryId,
160
+ receivedAt: params.receivedAt,
161
+ rawPayload: params.rawPayload,
162
+ event: 'push',
163
+ typedEvents: [
164
+ {
165
+ type: INTEGRATION_SOURCE_COMMIT_PUSHED,
166
+ payload: {
167
+ provider: params.provider,
168
+ workspaceId: params.workspaceId,
169
+ connectionId: params.connectionId,
170
+ deliveryId: params.deliveryId,
171
+ receivedAt: params.receivedAt,
172
+ push: params.push,
173
+ },
174
+ },
175
+ ],
176
+ });
177
+ }
178
+
179
+ export interface PublishSourceRepositoryUpdatedParams {
180
+ tx: IntegrationTx;
181
+ provider: string;
182
+ source: string;
183
+ workspaceId: string;
184
+ connectionId: string;
185
+ connectionName: string;
186
+ deliveryId: string;
187
+ receivedAt: string;
188
+ rawPayload: unknown;
189
+ event: string;
190
+ repositories: SourceRepositoryIdentity[];
191
+ }
192
+
193
+ // Emits the generic provider envelope for triggers and one typed repository event per
194
+ // normalized repository for domain modules. One delivery-dedup gates all rows, so a
195
+ // redelivered webhook writes nothing. Requires a transaction: the dedup insert and all
196
+ // outbox rows must commit or roll back together.
197
+ export function publishSourceRepositoryUpdated(
198
+ params: PublishSourceRepositoryUpdatedParams,
199
+ ): Promise<{published: boolean}> {
200
+ return publishSourceEvents({
201
+ tx: params.tx,
202
+ provider: params.provider,
203
+ source: params.source,
204
+ workspaceId: params.workspaceId,
205
+ connectionId: params.connectionId,
206
+ connectionName: params.connectionName,
207
+ deliveryId: params.deliveryId,
208
+ receivedAt: params.receivedAt,
209
+ rawPayload: params.rawPayload,
210
+ event: params.event,
211
+ typedEvents: params.repositories.map(
212
+ (repository): IntegrationOutboxEvent => ({
213
+ type: INTEGRATION_SOURCE_REPOSITORY_UPDATED,
214
+ payload: {
215
+ provider: params.provider,
216
+ workspaceId: params.workspaceId,
217
+ connectionId: params.connectionId,
218
+ deliveryId: params.deliveryId,
219
+ receivedAt: params.receivedAt,
220
+ repository,
221
+ },
222
+ }),
223
+ ),
224
+ });
225
+ }
226
+
139
227
  export interface PublishSourceCommitPushedParams {
140
228
  provider: string;
141
229
  workspaceId: string;
@@ -211,6 +299,7 @@ export async function pruneWebhookDeliveries(
211
299
 
212
300
  export type PublishIntegrationEventReceivedFn = typeof publishIntegrationEventReceived;
213
301
  export type PublishSourcePushFn = typeof publishSourcePush;
302
+ export type PublishSourceRepositoryUpdatedFn = typeof publishSourceRepositoryUpdated;
214
303
  export type PublishSourceCommitPushedFn = typeof publishSourceCommitPushed;
215
304
  export type ClaimWebhookDeliveryFn = typeof claimWebhookDelivery;
216
305
  export type RecordDeliveryOnlyFn = typeof recordDeliveryOnly;
package/src/index.ts CHANGED
@@ -114,11 +114,15 @@ export type {
114
114
  RepositoryVisibility,
115
115
  ResolveRepositoryInput,
116
116
  SourceControlProvider,
117
+ TriggerReference,
117
118
  } from '#core/providers/source-control.js';
118
119
  export type {IntegrationSourceControlService} from '#core/source-control-service.js';
119
120
  export {createSourceControlIntegrationService} from '#core/source-control-service.js';
120
- export type {GetIntegrationConnectionByIdFn} from '#db/connections.js';
121
- export {getIntegrationConnectionById} from '#db/connections.js';
121
+ export type {
122
+ GetIntegrationConnectionByIdFn,
123
+ GetIntegrationConnectionBySlugFn,
124
+ } from '#db/connections.js';
125
+ export {getIntegrationConnectionById, getIntegrationConnectionBySlug} from '#db/connections.js';
122
126
  export type {
123
127
  ClaimWebhookDeliveryFn,
124
128
  PublishIntegrationEventReceivedFn,
@@ -126,10 +130,16 @@ export type {
126
130
  PublishIntegrationEventReceivedResult,
127
131
  PublishSourcePushFn,
128
132
  PublishSourcePushParams,
133
+ PublishSourceRepositoryUpdatedFn,
134
+ PublishSourceRepositoryUpdatedParams,
129
135
  RecordDeliveryOnlyFn,
130
136
  RecordDeliveryOnlyParams,
131
137
  } from '#db/webhook-deliveries.js';
132
- export {claimWebhookDelivery, pruneWebhookDeliveries} from '#db/webhook-deliveries.js';
138
+ export {
139
+ claimWebhookDelivery,
140
+ pruneWebhookDeliveries,
141
+ publishSourceRepositoryUpdated,
142
+ } from '#db/webhook-deliveries.js';
133
143
  export {integrationRouteErrorHandler} from '#presentation/routes/errors.js';
134
144
 
135
145
  export interface CreateIntegrationsModuleOptions {
@@ -21,7 +21,7 @@ import {
21
21
  } from '#core/errors.js';
22
22
  import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
23
23
  import type {IntegrationSourceControlService} from '#core/source-control-service.js';
24
- import {getIntegrationConnectionById} from '#db/connections.js';
24
+ import {getIntegrationConnectionById, getIntegrationConnectionBySlug} from '#db/connections.js';
25
25
 
26
26
  export function createIntegrationsInterModulePresentation(params: {
27
27
  registry: IntegrationProviderRegistry;
@@ -41,6 +41,16 @@ export function createIntegrationsInterModulePresentation(params: {
41
41
  repository: resolved.repository,
42
42
  };
43
43
  }),
44
+ resolveConnection: async (input) => {
45
+ const resolved = await getIntegrationConnectionBySlug(input);
46
+ return resolved ? {id: resolved.id, provider: resolved.provider, slug: resolved.slug} : null;
47
+ },
48
+ resolveTriggerReference: async (input) =>
49
+ await known(
50
+ contract.methods.resolveTriggerReference,
51
+ input,
52
+ async () => await params.sourceControl.resolveTriggerReference(input),
53
+ ),
44
54
  listSourceFiles: async (input) =>
45
55
  await known(
46
56
  contract.methods.listSourceFiles,
@@ -126,6 +126,7 @@ describe('GET /integration-connections/:connectionId/repositories', () => {
126
126
  await Promise.resolve();
127
127
  throw new Error('not used');
128
128
  },
129
+ resolveTriggerReference: () => null,
129
130
  },
130
131
  },
131
132
  }),
@@ -10,6 +10,7 @@ import {db} from '#db/db.js';
10
10
  import {
11
11
  publishIntegrationEventReceived,
12
12
  publishSourcePush,
13
+ publishSourceRepositoryUpdated,
13
14
  recordDeliveryOnly,
14
15
  } from '#db/webhook-deliveries.js';
15
16
  import {retryConnectionSlugCollision, slugifyConnectionSlug} from '#providers/connection-slug.js';
@@ -111,6 +112,7 @@ async function loadGithubModuleParts(
111
112
  getExistingGithubConnection,
112
113
  connectGithubInstallation,
113
114
  publishIntegrationEventReceived,
115
+ publishSourceRepositoryUpdated,
114
116
  publishSourcePush,
115
117
  recordDeliveryOnly,
116
118
  getIntegrationConnectionById,
@@ -20,6 +20,8 @@ describe('jiraProviderModule', () => {
20
20
  const parts = await loadEnabledProviderModules();
21
21
  const jiraPart = parts.find((part) => part.provider.provider === 'jira');
22
22
  if (!jiraPart?.database) throw new Error('Jira provider database is not configured');
23
+ expect(jiraPart.provider.routes).toHaveLength(2);
24
+ expect(jiraPart.webhookProcessors).toEqual([expect.objectContaining({routeIds: ['jira']})]);
23
25
  const workspaceId = crypto.randomUUID();
24
26
  const cloudId = crypto.randomUUID();
25
27