@shipfox/api-definitions 12.0.0 → 12.2.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 +25 -0
- package/dist/core/entities/sync-state.d.ts +1 -1
- package/dist/core/entities/sync-state.d.ts.map +1 -1
- package/dist/core/errors.d.ts +2 -2
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/integrations.d.ts +1 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +1 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
- package/dist/core/workflow-model/normalize-jobs.js +5 -29
- package/dist/core/workflow-model/normalize-jobs.js.map +1 -1
- package/dist/core/workflow-yaml/invalid-workflow-yaml-error.d.ts.map +1 -1
- package/dist/db/db.d.ts +10 -10
- package/dist/db/schema/definitions.d.ts +1 -1
- package/dist/db/schema/outbox.d.ts +1 -1
- package/dist/db/schema/sync-states.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/project-access.d.ts +1 -1
- package/dist/presentation/subscribers/index.d.ts +1 -0
- package/dist/presentation/subscribers/index.d.ts.map +1 -1
- package/dist/presentation/subscribers/index.js +1 -0
- package/dist/presentation/subscribers/index.js.map +1 -1
- package/dist/presentation/subscribers/on-integration-connection-available.d.ts +5 -0
- package/dist/presentation/subscribers/on-integration-connection-available.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-integration-connection-available.js +32 -0
- package/dist/presentation/subscribers/on-integration-connection-available.js.map +1 -0
- package/dist/presentation/subscribers/start-definition-sync.d.ts +1 -0
- package/dist/presentation/subscribers/start-definition-sync.d.ts.map +1 -1
- package/dist/presentation/subscribers/start-definition-sync.js +30 -17
- package/dist/presentation/subscribers/start-definition-sync.js.map +1 -1
- package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +13 -13
- package/src/core/workflow-model/invalid-workflow-model-error.ts +0 -1
- package/src/core/workflow-model/normalize-jobs.ts +5 -41
- package/src/core/workflow-model/normalize-workflow-document.test.ts +140 -13
- package/src/index.ts +12 -1
- package/src/presentation/subscribers/index.ts +1 -0
- package/src/presentation/subscribers/on-integration-connection-available.test.ts +132 -0
- package/src/presentation/subscribers/on-integration-connection-available.ts +44 -0
- package/src/presentation/subscribers/start-definition-sync.ts +35 -19
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-definitions",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -26,25 +26,25 @@
|
|
|
26
26
|
"js-yaml": "^4.1.0",
|
|
27
27
|
"yaml": "^2.8.3",
|
|
28
28
|
"zod": "^4.4.3",
|
|
29
|
-
"@shipfox/api-agent-dto": "12.
|
|
30
|
-
"@shipfox/api-auth-context": "12.
|
|
31
|
-
"@shipfox/api-definitions-dto": "12.
|
|
32
|
-
"@shipfox/api-integration-core-dto": "12.
|
|
33
|
-
"@shipfox/api-projects-dto": "12.
|
|
29
|
+
"@shipfox/api-agent-dto": "12.2.0",
|
|
30
|
+
"@shipfox/api-auth-context": "12.2.0",
|
|
31
|
+
"@shipfox/api-definitions-dto": "12.2.0",
|
|
32
|
+
"@shipfox/api-integration-core-dto": "12.2.0",
|
|
33
|
+
"@shipfox/api-projects-dto": "12.2.0",
|
|
34
34
|
"@shipfox/api-secrets-dto": "12.0.0",
|
|
35
35
|
"@shipfox/config": "1.2.4",
|
|
36
|
-
"@shipfox/expression": "2.
|
|
36
|
+
"@shipfox/expression": "2.1.0",
|
|
37
37
|
"@shipfox/inter-module": "0.2.3",
|
|
38
38
|
"@shipfox/node-drizzle": "0.3.5",
|
|
39
|
-
"@shipfox/node-fastify": "0.4.
|
|
39
|
+
"@shipfox/node-fastify": "0.4.2",
|
|
40
40
|
"@shipfox/node-error-monitoring": "0.3.0",
|
|
41
|
-
"@shipfox/node-module": "1.0.
|
|
42
|
-
"@shipfox/node-opentelemetry": "0.6.
|
|
41
|
+
"@shipfox/node-module": "1.0.6",
|
|
42
|
+
"@shipfox/node-opentelemetry": "0.6.4",
|
|
43
43
|
"@shipfox/node-outbox": "0.2.6",
|
|
44
44
|
"@shipfox/node-postgres": "0.5.0",
|
|
45
|
-
"@shipfox/node-temporal": "0.4.
|
|
46
|
-
"@shipfox/runner-labels": "0.
|
|
47
|
-
"@shipfox/workflow-document": "3.0.
|
|
45
|
+
"@shipfox/node-temporal": "0.4.5",
|
|
46
|
+
"@shipfox/runner-labels": "0.2.0",
|
|
47
|
+
"@shipfox/workflow-document": "3.0.1"
|
|
48
48
|
},
|
|
49
49
|
"imports": {
|
|
50
50
|
"#*": "./dist/*"
|
|
@@ -404,57 +404,21 @@ function normalizeJobOutputs(params: {
|
|
|
404
404
|
typeOverlay,
|
|
405
405
|
}) ?? [{kind: 'literal' as const, value: source}];
|
|
406
406
|
templates[key] = template;
|
|
407
|
-
types[key] = inferJobOutputType(
|
|
408
|
-
sourceName: params.sourceName,
|
|
409
|
-
key,
|
|
410
|
-
source,
|
|
411
|
-
template,
|
|
412
|
-
issues: params.issues,
|
|
413
|
-
});
|
|
407
|
+
types[key] = inferJobOutputType(template);
|
|
414
408
|
}
|
|
415
409
|
|
|
416
410
|
return {templates, types};
|
|
417
411
|
}
|
|
418
412
|
|
|
419
|
-
function inferJobOutputType(
|
|
420
|
-
|
|
421
|
-
key: string;
|
|
422
|
-
source: string;
|
|
423
|
-
template: WorkflowFieldTemplate;
|
|
424
|
-
issues: WorkflowModelValidationIssue[];
|
|
425
|
-
}): ExpressionType {
|
|
426
|
-
if (params.template.length !== 1) return 'string';
|
|
413
|
+
function inferJobOutputType(template: WorkflowFieldTemplate): ExpressionType {
|
|
414
|
+
if (template.length !== 1) return 'string';
|
|
427
415
|
|
|
428
|
-
const [segment] =
|
|
416
|
+
const [segment] = template;
|
|
429
417
|
if (segment?.kind !== 'deferred') return 'string';
|
|
430
418
|
|
|
431
419
|
const resultType = segment.expression.resultType;
|
|
432
420
|
if (resultType === undefined) return 'string';
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
params.issues.push(
|
|
436
|
-
issue({
|
|
437
|
-
code: 'invalid-job-output',
|
|
438
|
-
message: `Job output "${params.key}" must resolve to a scalar value.`,
|
|
439
|
-
path: ['jobs', params.sourceName, 'outputs', params.key],
|
|
440
|
-
details: {
|
|
441
|
-
output: params.key,
|
|
442
|
-
source: params.source,
|
|
443
|
-
},
|
|
444
|
-
}),
|
|
445
|
-
);
|
|
446
|
-
return 'string';
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function isScalarExpressionType(type: ExpressionType): boolean {
|
|
450
|
-
return (
|
|
451
|
-
type === 'string' ||
|
|
452
|
-
type === 'int' ||
|
|
453
|
-
type === 'double' ||
|
|
454
|
-
type === 'bool' ||
|
|
455
|
-
type === 'null' ||
|
|
456
|
-
type === 'timestamp'
|
|
457
|
-
);
|
|
421
|
+
return resultType;
|
|
458
422
|
}
|
|
459
423
|
|
|
460
424
|
function normalizeJobSteps(params: {
|
|
@@ -69,11 +69,21 @@ const integrationValidationContext = {
|
|
|
69
69
|
],
|
|
70
70
|
},
|
|
71
71
|
],
|
|
72
|
+
[
|
|
73
|
+
'jira',
|
|
74
|
+
{
|
|
75
|
+
selectors: [
|
|
76
|
+
{token: 'get_issue', kind: 'standalone', sensitivity: 'read', sensitive: false},
|
|
77
|
+
{token: 'add_comment', kind: 'standalone', sensitivity: 'write', sensitive: false},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
72
81
|
]),
|
|
73
82
|
workspaceConnectionSnapshot: new Map([
|
|
74
83
|
['github-main', {id: 'conn_1', provider: 'github', capabilities: ['agent_tools']}],
|
|
75
84
|
['sentry-main', {id: 'conn_2', provider: 'sentry', capabilities: []}],
|
|
76
85
|
['linear-main', {id: 'conn_3', provider: 'linear', capabilities: ['agent_tools']}],
|
|
86
|
+
['jira-main', {id: 'conn_4', provider: 'jira', capabilities: ['agent_tools']}],
|
|
77
87
|
]),
|
|
78
88
|
defaultConnectionSlug: 'github-main',
|
|
79
89
|
} satisfies IntegrationValidationContext;
|
|
@@ -647,6 +657,56 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
647
657
|
});
|
|
648
658
|
});
|
|
649
659
|
|
|
660
|
+
it('requires allow_write for Jira write tools', () => {
|
|
661
|
+
const document: WorkflowDocument = {
|
|
662
|
+
name: 'jira write integrations',
|
|
663
|
+
jobs: {
|
|
664
|
+
fix: {
|
|
665
|
+
steps: [
|
|
666
|
+
{
|
|
667
|
+
prompt: 'Comment on the issue.',
|
|
668
|
+
integrations: [{connection: 'jira-main', include: ['add_comment']}],
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const error = expectInvalid(document, {integrationValidationContext});
|
|
676
|
+
|
|
677
|
+
expect(error.issues).toMatchObject([
|
|
678
|
+
{
|
|
679
|
+
code: 'integration-write-not-allowed',
|
|
680
|
+
details: {tokens: ['add_comment']},
|
|
681
|
+
},
|
|
682
|
+
]);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('accepts Jira write tools when allow_write is true', () => {
|
|
686
|
+
const document: WorkflowDocument = {
|
|
687
|
+
name: 'jira write integrations',
|
|
688
|
+
jobs: {
|
|
689
|
+
fix: {
|
|
690
|
+
steps: [
|
|
691
|
+
{
|
|
692
|
+
prompt: 'Comment on the issue.',
|
|
693
|
+
integrations: [
|
|
694
|
+
{connection: 'jira-main', include: ['add_comment'], allow_write: true},
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const model = normalizeWorkflowDocument(document, {integrationValidationContext});
|
|
703
|
+
|
|
704
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
705
|
+
kind: 'agent',
|
|
706
|
+
integrations: [{connection: 'jira-main', include: ['add_comment'], allowWrite: true}],
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
650
710
|
it('reports connection failures before token checks', () => {
|
|
651
711
|
const document: WorkflowDocument = {
|
|
652
712
|
name: 'missing connection',
|
|
@@ -2558,9 +2618,9 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2558
2618
|
});
|
|
2559
2619
|
});
|
|
2560
2620
|
|
|
2561
|
-
it('
|
|
2621
|
+
it('infers structured typed job output mappings', () => {
|
|
2562
2622
|
const document: WorkflowDocument = {
|
|
2563
|
-
name: '
|
|
2623
|
+
name: 'structured job output type',
|
|
2564
2624
|
jobs: {
|
|
2565
2625
|
build: {
|
|
2566
2626
|
steps: [
|
|
@@ -2571,10 +2631,13 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2571
2631
|
metadata: {
|
|
2572
2632
|
type: 'json',
|
|
2573
2633
|
schema: {
|
|
2574
|
-
type: '
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2634
|
+
type: 'array',
|
|
2635
|
+
items: {
|
|
2636
|
+
type: 'object',
|
|
2637
|
+
additionalProperties: false,
|
|
2638
|
+
required: ['severity'],
|
|
2639
|
+
properties: {severity: {type: 'string'}},
|
|
2640
|
+
},
|
|
2578
2641
|
},
|
|
2579
2642
|
},
|
|
2580
2643
|
},
|
|
@@ -2587,14 +2650,78 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2587
2650
|
},
|
|
2588
2651
|
};
|
|
2589
2652
|
|
|
2590
|
-
const
|
|
2653
|
+
const model = normalizeWorkflowDocument(document);
|
|
2591
2654
|
|
|
2592
|
-
expect(
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2655
|
+
expect(model.jobs[0]?.outputTypes).toEqual({
|
|
2656
|
+
metadata: {
|
|
2657
|
+
kind: 'list',
|
|
2658
|
+
element: {
|
|
2659
|
+
kind: 'object',
|
|
2660
|
+
fields: {severity: 'string'},
|
|
2661
|
+
},
|
|
2662
|
+
},
|
|
2663
|
+
});
|
|
2664
|
+
});
|
|
2665
|
+
|
|
2666
|
+
it('authorizes indexed reads from structured job outputs', () => {
|
|
2667
|
+
const document: WorkflowDocument = {
|
|
2668
|
+
name: 'indexed structured job output',
|
|
2669
|
+
jobs: {
|
|
2670
|
+
review: {
|
|
2671
|
+
steps: [
|
|
2672
|
+
{
|
|
2673
|
+
key: 'inspect',
|
|
2674
|
+
run: 'npm run inspect',
|
|
2675
|
+
outputs: {
|
|
2676
|
+
findings: {
|
|
2677
|
+
type: 'json',
|
|
2678
|
+
schema: {
|
|
2679
|
+
type: 'array',
|
|
2680
|
+
items: {
|
|
2681
|
+
type: 'object',
|
|
2682
|
+
additionalProperties: false,
|
|
2683
|
+
required: ['severity'],
|
|
2684
|
+
properties: {severity: {type: 'string'}},
|
|
2685
|
+
},
|
|
2686
|
+
},
|
|
2687
|
+
},
|
|
2688
|
+
},
|
|
2689
|
+
},
|
|
2690
|
+
],
|
|
2691
|
+
outputs: {
|
|
2692
|
+
findings: interpolation('steps.inspect.outputs.findings'),
|
|
2693
|
+
},
|
|
2694
|
+
},
|
|
2695
|
+
summarize: {
|
|
2696
|
+
needs: ['review'],
|
|
2697
|
+
steps: [
|
|
2698
|
+
{
|
|
2699
|
+
run: `printf '%s\\n' ${interpolation('jobs.review.outputs.findings[0].severity')}`,
|
|
2700
|
+
},
|
|
2701
|
+
],
|
|
2702
|
+
},
|
|
2703
|
+
},
|
|
2704
|
+
};
|
|
2705
|
+
|
|
2706
|
+
const model = normalizeWorkflowDocument(document);
|
|
2707
|
+
const summarize = model.jobs.find((job) => job.key === 'summarize');
|
|
2708
|
+
|
|
2709
|
+
expect(summarize?.steps[0]).toMatchObject({
|
|
2710
|
+
kind: 'run',
|
|
2711
|
+
templates: {
|
|
2712
|
+
command: [
|
|
2713
|
+
{kind: 'literal', value: "printf '%s\\n' "},
|
|
2714
|
+
{
|
|
2715
|
+
kind: 'deferred',
|
|
2716
|
+
expression: {
|
|
2717
|
+
source: 'jobs.review.outputs.findings[0].severity',
|
|
2718
|
+
check: 'typed',
|
|
2719
|
+
},
|
|
2720
|
+
roots: ['jobs'],
|
|
2721
|
+
},
|
|
2722
|
+
],
|
|
2723
|
+
},
|
|
2724
|
+
});
|
|
2598
2725
|
});
|
|
2599
2726
|
|
|
2600
2727
|
it('rejects job output references without a direct needs edge', () => {
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
type DefinitionsEventMap,
|
|
7
7
|
definitionsEventSchemas,
|
|
8
8
|
} from '@shipfox/api-definitions-dto';
|
|
9
|
+
import {
|
|
10
|
+
INTEGRATION_CONNECTION_AVAILABLE,
|
|
11
|
+
type IntegrationsEventMap,
|
|
12
|
+
} from '@shipfox/api-integration-core-dto';
|
|
9
13
|
import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
|
|
10
14
|
import {
|
|
11
15
|
PROJECT_SOURCE_BOUND,
|
|
@@ -20,6 +24,7 @@ import {db, definitionsOutbox, migrationsPath} from '#db/index.js';
|
|
|
20
24
|
import {createDefinitionRoutes} from '#presentation/index.js';
|
|
21
25
|
import {createDefinitionsInterModulePresentation} from '#presentation/inter-module.js';
|
|
22
26
|
import {
|
|
27
|
+
createOnIntegrationConnectionAvailable,
|
|
23
28
|
onProjectSourceBound,
|
|
24
29
|
onProjectSourceCommitObserved,
|
|
25
30
|
} from '#presentation/subscribers/index.js';
|
|
@@ -46,7 +51,9 @@ export {db, definitionsOutbox, getDefinitionById, migrationsPath} from '#db/inde
|
|
|
46
51
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
47
52
|
const workflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');
|
|
48
53
|
|
|
49
|
-
const subscriber = subscriberFactory<
|
|
54
|
+
const subscriber = subscriberFactory<
|
|
55
|
+
DefinitionsEventMap & IntegrationsEventMap & ProjectsEventMap
|
|
56
|
+
>();
|
|
50
57
|
|
|
51
58
|
export interface CreateDefinitionsModuleOptions {
|
|
52
59
|
projects: ProjectsModuleClient;
|
|
@@ -73,6 +80,10 @@ export function createDefinitionsModule({
|
|
|
73
80
|
logger().info({event}, 'Definition resolved');
|
|
74
81
|
return Promise.resolve();
|
|
75
82
|
}),
|
|
83
|
+
subscriber(
|
|
84
|
+
INTEGRATION_CONNECTION_AVAILABLE,
|
|
85
|
+
createOnIntegrationConnectionAvailable(projects),
|
|
86
|
+
),
|
|
76
87
|
subscriber(PROJECT_SOURCE_BOUND, onProjectSourceBound),
|
|
77
88
|
subscriber(PROJECT_SOURCE_COMMIT_OBSERVED, onProjectSourceCommitObserved),
|
|
78
89
|
],
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type {IntegrationConnectionAvailableEvent} from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import type {DomainEvent} from '@shipfox/node-outbox';
|
|
3
|
+
import {createOnIntegrationConnectionAvailable} from './on-integration-connection-available.js';
|
|
4
|
+
|
|
5
|
+
const startMock = vi.fn();
|
|
6
|
+
|
|
7
|
+
vi.mock('@shipfox/node-temporal', () => ({
|
|
8
|
+
temporalClient: () => ({workflow: {start: startMock}}),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
function project(name: string, workspaceId: string) {
|
|
12
|
+
return {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
workspaceId,
|
|
15
|
+
sourceConnectionId: crypto.randomUUID(),
|
|
16
|
+
sourceExternalRepositoryId: `github:${name}`,
|
|
17
|
+
sourceRepositoryOwner: 'shipfox',
|
|
18
|
+
name,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('onIntegrationConnectionAvailable', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
startMock.mockReset();
|
|
25
|
+
startMock.mockResolvedValue({});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('paginates workspace projects and starts an event-keyed sync for each one', async () => {
|
|
29
|
+
const workspaceId = crypto.randomUUID();
|
|
30
|
+
const first = project('first', workspaceId);
|
|
31
|
+
const second = project('second', workspaceId);
|
|
32
|
+
const cursor = {createdAt: '2026-08-05T12:00:00.000Z', id: first.id};
|
|
33
|
+
const listProjectsByWorkspace = vi
|
|
34
|
+
.fn()
|
|
35
|
+
.mockResolvedValueOnce({projects: [first], nextCursor: cursor})
|
|
36
|
+
.mockResolvedValueOnce({projects: [second], nextCursor: null});
|
|
37
|
+
const handler = createOnIntegrationConnectionAvailable({listProjectsByWorkspace} as never);
|
|
38
|
+
const payload: IntegrationConnectionAvailableEvent = {
|
|
39
|
+
provider: 'linear',
|
|
40
|
+
workspaceId: first.workspaceId,
|
|
41
|
+
connectionId: crypto.randomUUID(),
|
|
42
|
+
slug: 'linear_shipfox',
|
|
43
|
+
};
|
|
44
|
+
const event: DomainEvent<IntegrationConnectionAvailableEvent> = {
|
|
45
|
+
id: crypto.randomUUID(),
|
|
46
|
+
type: 'integrations.connection.available',
|
|
47
|
+
payload,
|
|
48
|
+
createdAt: new Date(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
await handler(payload, event);
|
|
52
|
+
|
|
53
|
+
expect(listProjectsByWorkspace).toHaveBeenNthCalledWith(1, {
|
|
54
|
+
workspaceId: payload.workspaceId,
|
|
55
|
+
limit: 100,
|
|
56
|
+
});
|
|
57
|
+
expect(listProjectsByWorkspace).toHaveBeenNthCalledWith(2, {
|
|
58
|
+
workspaceId: payload.workspaceId,
|
|
59
|
+
limit: 100,
|
|
60
|
+
cursor,
|
|
61
|
+
});
|
|
62
|
+
expect(startMock).toHaveBeenCalledTimes(2);
|
|
63
|
+
for (const candidate of [first, second]) {
|
|
64
|
+
expect(startMock).toHaveBeenCalledWith('definitionSyncWorkflow', {
|
|
65
|
+
taskQueue: 'definitions-sync',
|
|
66
|
+
workflowId: `definition-sync:${candidate.id}:integration:${event.id}`,
|
|
67
|
+
workflowIdConflictPolicy: 'USE_EXISTING',
|
|
68
|
+
workflowIdReusePolicy: 'REJECT_DUPLICATE',
|
|
69
|
+
args: [
|
|
70
|
+
{
|
|
71
|
+
projectId: candidate.id,
|
|
72
|
+
workspaceId: candidate.workspaceId,
|
|
73
|
+
sourceConnectionId: candidate.sourceConnectionId,
|
|
74
|
+
sourceExternalRepositoryId: candidate.sourceExternalRepositoryId,
|
|
75
|
+
sourceRef: undefined,
|
|
76
|
+
sourceCommitSha: undefined,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('treats an already-started event-keyed sync as successfully delivered', async () => {
|
|
84
|
+
const workspaceId = crypto.randomUUID();
|
|
85
|
+
const candidate = project('first', workspaceId);
|
|
86
|
+
const alreadyStarted = new Error('Workflow execution already started');
|
|
87
|
+
alreadyStarted.name = 'WorkflowExecutionAlreadyStartedError';
|
|
88
|
+
startMock.mockRejectedValue(alreadyStarted);
|
|
89
|
+
const handler = createOnIntegrationConnectionAvailable({
|
|
90
|
+
listProjectsByWorkspace: vi.fn(() =>
|
|
91
|
+
Promise.resolve({projects: [candidate], nextCursor: null}),
|
|
92
|
+
),
|
|
93
|
+
} as never);
|
|
94
|
+
const payload: IntegrationConnectionAvailableEvent = {
|
|
95
|
+
provider: 'linear',
|
|
96
|
+
workspaceId,
|
|
97
|
+
connectionId: crypto.randomUUID(),
|
|
98
|
+
slug: 'linear_shipfox',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
await expect(
|
|
102
|
+
handler(payload, {
|
|
103
|
+
id: crypto.randomUUID(),
|
|
104
|
+
type: 'integrations.connection.available',
|
|
105
|
+
payload,
|
|
106
|
+
createdAt: new Date(),
|
|
107
|
+
}),
|
|
108
|
+
).resolves.toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('rethrows project lookup failures so the outbox event can retry', async () => {
|
|
112
|
+
const failure = new Error('projects unavailable');
|
|
113
|
+
const handler = createOnIntegrationConnectionAvailable({
|
|
114
|
+
listProjectsByWorkspace: vi.fn(() => Promise.reject(failure)),
|
|
115
|
+
} as never);
|
|
116
|
+
const payload: IntegrationConnectionAvailableEvent = {
|
|
117
|
+
provider: 'linear',
|
|
118
|
+
workspaceId: crypto.randomUUID(),
|
|
119
|
+
connectionId: crypto.randomUUID(),
|
|
120
|
+
slug: 'linear_shipfox',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await expect(
|
|
124
|
+
handler(payload, {
|
|
125
|
+
id: crypto.randomUUID(),
|
|
126
|
+
type: 'integrations.connection.available',
|
|
127
|
+
payload,
|
|
128
|
+
createdAt: new Date(),
|
|
129
|
+
}),
|
|
130
|
+
).rejects.toBe(failure);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {IntegrationConnectionAvailableEvent} from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
|
|
3
|
+
import {boundedMap} from '@shipfox/node-module';
|
|
4
|
+
import type {DomainEvent} from '@shipfox/node-outbox';
|
|
5
|
+
import {startDefinitionSync} from './start-definition-sync.js';
|
|
6
|
+
|
|
7
|
+
const PROJECT_PAGE_SIZE = 100;
|
|
8
|
+
const SYNC_START_CONCURRENCY = 10;
|
|
9
|
+
|
|
10
|
+
export function createOnIntegrationConnectionAvailable(
|
|
11
|
+
projects: Pick<ProjectsModuleClient, 'listProjectsByWorkspace'>,
|
|
12
|
+
) {
|
|
13
|
+
return async function onIntegrationConnectionAvailable(
|
|
14
|
+
payload: IntegrationConnectionAvailableEvent,
|
|
15
|
+
event: DomainEvent<IntegrationConnectionAvailableEvent>,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
let cursor: {createdAt: string; id: string} | undefined;
|
|
18
|
+
|
|
19
|
+
do {
|
|
20
|
+
const page = await projects.listProjectsByWorkspace({
|
|
21
|
+
workspaceId: payload.workspaceId,
|
|
22
|
+
limit: PROJECT_PAGE_SIZE,
|
|
23
|
+
...(cursor ? {cursor} : {}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await boundedMap(
|
|
27
|
+
page.projects,
|
|
28
|
+
SYNC_START_CONCURRENCY,
|
|
29
|
+
async (project) => {
|
|
30
|
+
await startDefinitionSync({
|
|
31
|
+
projectId: project.id,
|
|
32
|
+
workspaceId: project.workspaceId,
|
|
33
|
+
sourceConnectionId: project.sourceConnectionId,
|
|
34
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
35
|
+
requestId: `integration:${event.id}`,
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
{stopOnError: true},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
cursor = page.nextCursor ?? undefined;
|
|
42
|
+
} while (cursor);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -8,31 +8,47 @@ export interface StartDefinitionSyncParams {
|
|
|
8
8
|
externalRepositoryId: string;
|
|
9
9
|
sourceRef?: string | undefined;
|
|
10
10
|
sourceCommitSha?: string | undefined;
|
|
11
|
+
requestId?: string | undefined;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export async function startDefinitionSync(params: StartDefinitionSyncParams): Promise<void> {
|
|
14
15
|
const workflowId = buildWorkflowId(params);
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
try {
|
|
18
|
+
await temporalClient().workflow.start(DEFINITION_SYNC_WORKFLOW, {
|
|
19
|
+
taskQueue: DEFINITIONS_TASK_QUEUE,
|
|
20
|
+
workflowId,
|
|
21
|
+
workflowIdConflictPolicy: 'USE_EXISTING',
|
|
22
|
+
workflowIdReusePolicy: params.requestId ? 'REJECT_DUPLICATE' : 'ALLOW_DUPLICATE',
|
|
23
|
+
args: [
|
|
24
|
+
{
|
|
25
|
+
projectId: params.projectId,
|
|
26
|
+
workspaceId: params.workspaceId,
|
|
27
|
+
sourceConnectionId: params.sourceConnectionId,
|
|
28
|
+
sourceExternalRepositoryId: params.externalRepositoryId,
|
|
29
|
+
sourceRef: params.sourceRef,
|
|
30
|
+
sourceCommitSha: params.sourceCommitSha,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (
|
|
36
|
+
params.requestId &&
|
|
37
|
+
error instanceof Error &&
|
|
38
|
+
error.name === 'WorkflowExecutionAlreadyStartedError'
|
|
39
|
+
) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
function buildWorkflowId(params: StartDefinitionSyncParams): string {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
if (params.sourceCommitSha) {
|
|
48
|
+
return `definition-sync:${params.projectId}:${params.sourceCommitSha}`;
|
|
49
|
+
}
|
|
50
|
+
if (params.requestId) {
|
|
51
|
+
return `definition-sync:${params.projectId}:${params.requestId}`;
|
|
52
|
+
}
|
|
53
|
+
return `definition-sync:${params.projectId}:bind`;
|
|
38
54
|
}
|