@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.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +78 -0
- package/dist/core/providers/source-control.d.ts +1 -1
- package/dist/core/providers/source-control.d.ts.map +1 -1
- package/dist/core/providers/source-control.js.map +1 -1
- package/dist/core/source-control-service.d.ts +7 -1
- package/dist/core/source-control-service.d.ts.map +1 -1
- package/dist/core/source-control-service.js +11 -0
- package/dist/core/source-control-service.js.map +1 -1
- package/dist/db/connections.d.ts +6 -0
- package/dist/db/connections.d.ts.map +1 -1
- package/dist/db/connections.js +6 -0
- package/dist/db/connections.js.map +1 -1
- package/dist/db/webhook-deliveries.d.ts +18 -1
- package/dist/db/webhook-deliveries.d.ts.map +1 -1
- package/dist/db/webhook-deliveries.js +65 -19
- package/dist/db/webhook-deliveries.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +10 -1
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/providers/github.d.ts.map +1 -1
- package/dist/providers/github.js +2 -1
- package/dist/providers/github.js.map +1 -1
- package/dist/providers/jira.d.ts.map +1 -1
- package/dist/providers/jira.js +35 -12
- package/dist/providers/jira.js.map +1 -1
- package/dist/providers/types.d.ts +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +20 -20
- package/src/core/agent-tool-selection.test.ts +1 -0
- package/src/core/providers/registry.test.ts +1 -0
- package/src/core/providers/source-control.ts +1 -0
- package/src/core/source-control-service.test.ts +25 -0
- package/src/core/source-control-service.ts +21 -0
- package/src/db/connections.test.ts +18 -0
- package/src/db/connections.ts +25 -0
- package/src/db/webhook-deliveries.test.ts +77 -0
- package/src/db/webhook-deliveries.ts +110 -21
- package/src/index.ts +13 -3
- package/src/presentation/inter-module.ts +11 -1
- package/src/presentation/routes/list-repositories.test.ts +1 -0
- package/src/providers/github.ts +2 -0
- package/src/providers/jira.test.ts +2 -0
- package/src/providers/jira.ts +38 -12
- package/src/providers/types.ts +1 -1
- package/test/env.ts +0 -1
- package/test/route-utils.ts +2 -1
- 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": "
|
|
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": "
|
|
27
|
-
"@shipfox/api-auth-context": "
|
|
28
|
-
"@shipfox/api-workflows-dto": "
|
|
29
|
-
"@shipfox/api-integration-core-dto": "
|
|
30
|
-
"@shipfox/api-integration-spi": "0.
|
|
31
|
-
"@shipfox/api-workspaces-dto": "
|
|
32
|
-
"@shipfox/api-integration-gitea": "
|
|
33
|
-
"@shipfox/api-integration-github": "
|
|
34
|
-
"@shipfox/api-integration-jira": "
|
|
35
|
-
"@shipfox/api-integration-linear": "
|
|
36
|
-
"@shipfox/api-integration-sentry": "
|
|
37
|
-
"@shipfox/api-integration-slack": "
|
|
38
|
-
"@shipfox/api-integration-webhook": "
|
|
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.
|
|
41
|
-
"@shipfox/node-drizzle": "0.3.
|
|
42
|
-
"@shipfox/node-fastify": "0.4.
|
|
43
|
-
"@shipfox/node-module": "1.0.
|
|
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.
|
|
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.
|
|
50
|
+
"@shipfox/redact": "0.2.6"
|
|
51
51
|
},
|
|
52
52
|
"imports": {
|
|
53
53
|
"#*": "./dist/*"
|
|
@@ -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,
|
package/src/db/connections.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
87
|
+
event: string;
|
|
88
|
+
typedEvents: IntegrationOutboxEvent[];
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
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:
|
|
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 {
|
|
121
|
-
|
|
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 {
|
|
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,
|
package/src/providers/github.ts
CHANGED
|
@@ -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
|
|