@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.
Files changed (45) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +25 -0
  3. package/dist/core/entities/sync-state.d.ts +1 -1
  4. package/dist/core/entities/sync-state.d.ts.map +1 -1
  5. package/dist/core/errors.d.ts +2 -2
  6. package/dist/core/errors.d.ts.map +1 -1
  7. package/dist/core/integrations.d.ts +1 -1
  8. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +1 -1
  9. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
  10. package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
  11. package/dist/core/workflow-model/normalize-jobs.js +5 -29
  12. package/dist/core/workflow-model/normalize-jobs.js.map +1 -1
  13. package/dist/core/workflow-yaml/invalid-workflow-yaml-error.d.ts.map +1 -1
  14. package/dist/db/db.d.ts +10 -10
  15. package/dist/db/schema/definitions.d.ts +1 -1
  16. package/dist/db/schema/outbox.d.ts +1 -1
  17. package/dist/db/schema/sync-states.d.ts +3 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/presentation/routes/project-access.d.ts +1 -1
  22. package/dist/presentation/subscribers/index.d.ts +1 -0
  23. package/dist/presentation/subscribers/index.d.ts.map +1 -1
  24. package/dist/presentation/subscribers/index.js +1 -0
  25. package/dist/presentation/subscribers/index.js.map +1 -1
  26. package/dist/presentation/subscribers/on-integration-connection-available.d.ts +5 -0
  27. package/dist/presentation/subscribers/on-integration-connection-available.d.ts.map +1 -0
  28. package/dist/presentation/subscribers/on-integration-connection-available.js +32 -0
  29. package/dist/presentation/subscribers/on-integration-connection-available.js.map +1 -0
  30. package/dist/presentation/subscribers/start-definition-sync.d.ts +1 -0
  31. package/dist/presentation/subscribers/start-definition-sync.d.ts.map +1 -1
  32. package/dist/presentation/subscribers/start-definition-sync.js +30 -17
  33. package/dist/presentation/subscribers/start-definition-sync.js.map +1 -1
  34. package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
  35. package/dist/tsconfig.test.tsbuildinfo +1 -1
  36. package/package.json +13 -13
  37. package/src/core/workflow-model/invalid-workflow-model-error.ts +0 -1
  38. package/src/core/workflow-model/normalize-jobs.ts +5 -41
  39. package/src/core/workflow-model/normalize-workflow-document.test.ts +140 -13
  40. package/src/index.ts +12 -1
  41. package/src/presentation/subscribers/index.ts +1 -0
  42. package/src/presentation/subscribers/on-integration-connection-available.test.ts +132 -0
  43. package/src/presentation/subscribers/on-integration-connection-available.ts +44 -0
  44. package/src/presentation/subscribers/start-definition-sync.ts +35 -19
  45. 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.0.0",
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.0.0",
30
- "@shipfox/api-auth-context": "12.0.0",
31
- "@shipfox/api-definitions-dto": "12.0.0",
32
- "@shipfox/api-integration-core-dto": "12.0.0",
33
- "@shipfox/api-projects-dto": "12.0.0",
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.0.0",
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.1",
39
+ "@shipfox/node-fastify": "0.4.2",
40
40
  "@shipfox/node-error-monitoring": "0.3.0",
41
- "@shipfox/node-module": "1.0.5",
42
- "@shipfox/node-opentelemetry": "0.6.3",
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.4",
46
- "@shipfox/runner-labels": "0.1.3",
47
- "@shipfox/workflow-document": "3.0.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/*"
@@ -26,7 +26,6 @@ export type WorkflowModelValidationIssueCode =
26
26
  | 'invalid-listener-filter'
27
27
  | 'invalid-model'
28
28
  | 'invalid-job-if'
29
- | 'invalid-job-output'
30
29
  | 'invalid-job-success'
31
30
  | 'invalid-output-schema'
32
31
  | 'invalid-runner-label'
@@ -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(params: {
420
- sourceName: string;
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] = params.template;
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
- if (isScalarExpressionType(resultType)) return resultType;
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('reports non-scalar typed job output mappings', () => {
2621
+ it('infers structured typed job output mappings', () => {
2562
2622
  const document: WorkflowDocument = {
2563
- name: 'bad job output type',
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: 'object',
2575
- additionalProperties: false,
2576
- required: ['digest'],
2577
- properties: {digest: {type: 'string'}},
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 error = expectInvalid(document);
2653
+ const model = normalizeWorkflowDocument(document);
2591
2654
 
2592
- expect(error.issues).toEqual([
2593
- expect.objectContaining({
2594
- code: 'invalid-job-output',
2595
- path: ['jobs', 'build', 'outputs', 'metadata'],
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<DefinitionsEventMap & ProjectsEventMap>();
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
  ],
@@ -1,2 +1,3 @@
1
+ export {createOnIntegrationConnectionAvailable} from './on-integration-connection-available.js';
1
2
  export {onProjectSourceBound} from './on-project-source-bound.js';
2
3
  export {onProjectSourceCommitObserved} from './on-project-source-commit-observed.js';
@@ -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
- await temporalClient().workflow.start(DEFINITION_SYNC_WORKFLOW, {
17
- taskQueue: DEFINITIONS_TASK_QUEUE,
18
- workflowId,
19
- workflowIdConflictPolicy: 'USE_EXISTING',
20
- workflowIdReusePolicy: 'ALLOW_DUPLICATE',
21
- args: [
22
- {
23
- projectId: params.projectId,
24
- workspaceId: params.workspaceId,
25
- sourceConnectionId: params.sourceConnectionId,
26
- sourceExternalRepositoryId: params.externalRepositoryId,
27
- sourceRef: params.sourceRef,
28
- sourceCommitSha: params.sourceCommitSha,
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
- return params.sourceCommitSha
36
- ? `definition-sync:${params.projectId}:${params.sourceCommitSha}`
37
- : `definition-sync:${params.projectId}:bind`;
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
  }