@shipfox/api-definitions 12.3.0 → 14.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 (223) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +45 -0
  3. package/dist/core/entities/index.d.ts +1 -1
  4. package/dist/core/entities/index.d.ts.map +1 -1
  5. package/dist/core/entities/index.js +1 -1
  6. package/dist/core/entities/index.js.map +1 -1
  7. package/dist/core/entities/integration-context.d.ts +4 -0
  8. package/dist/core/entities/integration-context.d.ts.map +1 -1
  9. package/dist/core/entities/integration-context.js.map +1 -1
  10. package/dist/core/entities/sync-state.d.ts +10 -4
  11. package/dist/core/entities/sync-state.d.ts.map +1 -1
  12. package/dist/core/entities/sync-state.js +26 -7
  13. package/dist/core/entities/sync-state.js.map +1 -1
  14. package/dist/core/entities/validation-diagnostic.d.ts +8 -0
  15. package/dist/core/entities/validation-diagnostic.d.ts.map +1 -0
  16. package/dist/core/entities/validation-diagnostic.js +3 -0
  17. package/dist/core/entities/validation-diagnostic.js.map +1 -0
  18. package/dist/core/entities/workflow-definition.d.ts +2 -0
  19. package/dist/core/entities/workflow-definition.d.ts.map +1 -1
  20. package/dist/core/entities/workflow-definition.js.map +1 -1
  21. package/dist/core/errors.d.ts +10 -0
  22. package/dist/core/errors.d.ts.map +1 -1
  23. package/dist/core/errors.js +9 -0
  24. package/dist/core/errors.js.map +1 -1
  25. package/dist/core/index.d.ts +2 -1
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/index.js +2 -1
  28. package/dist/core/index.js.map +1 -1
  29. package/dist/core/integrations.d.ts +3 -1
  30. package/dist/core/integrations.d.ts.map +1 -1
  31. package/dist/core/integrations.js +10 -2
  32. package/dist/core/integrations.js.map +1 -1
  33. package/dist/core/parse-definition.d.ts +4 -4
  34. package/dist/core/parse-definition.d.ts.map +1 -1
  35. package/dist/core/parse-definition.js +5 -5
  36. package/dist/core/parse-definition.js.map +1 -1
  37. package/dist/core/resolve-definition-at-ref.d.ts +66 -0
  38. package/dist/core/resolve-definition-at-ref.d.ts.map +1 -0
  39. package/dist/core/resolve-definition-at-ref.js +374 -0
  40. package/dist/core/resolve-definition-at-ref.js.map +1 -0
  41. package/dist/core/sync-definitions.d.ts +2 -2
  42. package/dist/core/sync-definitions.d.ts.map +1 -1
  43. package/dist/core/sync-definitions.js +5 -5
  44. package/dist/core/sync-definitions.js.map +1 -1
  45. package/dist/core/validate-definition.d.ts +3 -3
  46. package/dist/core/validate-definition.d.ts.map +1 -1
  47. package/dist/core/validate-definition.js +17 -12
  48. package/dist/core/validate-definition.js.map +1 -1
  49. package/dist/core/workflow-model/cron-trigger.d.ts +1 -1
  50. package/dist/core/workflow-model/cron-trigger.d.ts.map +1 -1
  51. package/dist/core/workflow-model/cron-trigger.js +9 -5
  52. package/dist/core/workflow-model/cron-trigger.js.map +1 -1
  53. package/dist/core/workflow-model/index.d.ts +1 -1
  54. package/dist/core/workflow-model/index.d.ts.map +1 -1
  55. package/dist/core/workflow-model/index.js.map +1 -1
  56. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +4 -2
  57. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
  58. package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
  59. package/dist/core/workflow-model/normalize-job-listening.d.ts.map +1 -1
  60. package/dist/core/workflow-model/normalize-job-listening.js +51 -30
  61. package/dist/core/workflow-model/normalize-job-listening.js.map +1 -1
  62. package/dist/core/workflow-model/normalize-triggers.d.ts +1 -1
  63. package/dist/core/workflow-model/normalize-triggers.d.ts.map +1 -1
  64. package/dist/core/workflow-model/normalize-triggers.js +48 -18
  65. package/dist/core/workflow-model/normalize-triggers.js.map +1 -1
  66. package/dist/core/workflow-model/normalize-workflow-document.d.ts +1 -1
  67. package/dist/core/workflow-model/normalize-workflow-document.d.ts.map +1 -1
  68. package/dist/core/workflow-model/normalize-workflow-document.js +3 -3
  69. package/dist/core/workflow-model/normalize-workflow-document.js.map +1 -1
  70. package/dist/core/workflow-model/parse-interpolation-field.d.ts.map +1 -1
  71. package/dist/core/workflow-model/parse-interpolation-field.js +1 -15
  72. package/dist/core/workflow-model/parse-interpolation-field.js.map +1 -1
  73. package/dist/core/workflow-model/validate-predicate-expression.d.ts +3 -1
  74. package/dist/core/workflow-model/validate-predicate-expression.d.ts.map +1 -1
  75. package/dist/core/workflow-model/validate-predicate-expression.js +3 -0
  76. package/dist/core/workflow-model/validate-predicate-expression.js.map +1 -1
  77. package/dist/core/workflow-model/validation-issue.d.ts +2 -1
  78. package/dist/core/workflow-model/validation-issue.d.ts.map +1 -1
  79. package/dist/core/workflow-model/validation-issue.js +12 -15
  80. package/dist/core/workflow-model/validation-issue.js.map +1 -1
  81. package/dist/db/db.d.ts +196 -4
  82. package/dist/db/db.d.ts.map +1 -1
  83. package/dist/db/db.js +3 -1
  84. package/dist/db/db.js.map +1 -1
  85. package/dist/db/definition-triggers.js +3 -1
  86. package/dist/db/definition-triggers.js.map +1 -1
  87. package/dist/db/definitions.d.ts +17 -0
  88. package/dist/db/definitions.d.ts.map +1 -1
  89. package/dist/db/definitions.js +190 -23
  90. package/dist/db/definitions.js.map +1 -1
  91. package/dist/db/index.d.ts +2 -1
  92. package/dist/db/index.d.ts.map +1 -1
  93. package/dist/db/index.js +2 -1
  94. package/dist/db/index.js.map +1 -1
  95. package/dist/db/schema/definitions.d.ts +17 -0
  96. package/dist/db/schema/definitions.d.ts.map +1 -1
  97. package/dist/db/schema/definitions.js +8 -0
  98. package/dist/db/schema/definitions.js.map +1 -1
  99. package/dist/db/schema/sync-states.d.ts +7 -3
  100. package/dist/db/schema/sync-states.d.ts.map +1 -1
  101. package/dist/db/schema/sync-states.js +5 -1
  102. package/dist/db/schema/sync-states.js.map +1 -1
  103. package/dist/db/schema/workflows.d.ts +78 -0
  104. package/dist/db/schema/workflows.d.ts.map +1 -0
  105. package/dist/db/schema/workflows.js +17 -0
  106. package/dist/db/schema/workflows.js.map +1 -0
  107. package/dist/db/sync-states.d.ts +2 -2
  108. package/dist/db/sync-states.d.ts.map +1 -1
  109. package/dist/db/sync-states.js +4 -4
  110. package/dist/db/sync-states.js.map +1 -1
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +5 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/metrics/index.d.ts +2 -0
  115. package/dist/metrics/index.d.ts.map +1 -0
  116. package/dist/metrics/index.js +3 -0
  117. package/dist/metrics/index.js.map +1 -0
  118. package/dist/metrics/instance.d.ts +4 -0
  119. package/dist/metrics/instance.d.ts.map +1 -0
  120. package/dist/metrics/instance.js +19 -0
  121. package/dist/metrics/instance.js.map +1 -0
  122. package/dist/presentation/dto/definition.d.ts.map +1 -1
  123. package/dist/presentation/dto/definition.js +11 -4
  124. package/dist/presentation/dto/definition.js.map +1 -1
  125. package/dist/presentation/inter-module.d.ts +9 -1
  126. package/dist/presentation/inter-module.d.ts.map +1 -1
  127. package/dist/presentation/inter-module.js +66 -2
  128. package/dist/presentation/inter-module.js.map +1 -1
  129. package/dist/presentation/routes/create-definition.d.ts.map +1 -1
  130. package/dist/presentation/routes/create-definition.js +16 -3
  131. package/dist/presentation/routes/create-definition.js.map +1 -1
  132. package/dist/presentation/routes/get-definition.d.ts.map +1 -1
  133. package/dist/presentation/routes/get-definition.js +18 -4
  134. package/dist/presentation/routes/get-definition.js.map +1 -1
  135. package/dist/presentation/routes/project-access.d.ts +1 -0
  136. package/dist/presentation/routes/project-access.d.ts.map +1 -1
  137. package/dist/presentation/routes/validate-definition.js +3 -3
  138. package/dist/presentation/routes/validate-definition.js.map +1 -1
  139. package/dist/temporal/activities/sync-activities.d.ts +3 -3
  140. package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
  141. package/dist/temporal/activities/sync-activities.js +7 -4
  142. package/dist/temporal/activities/sync-activities.js.map +1 -1
  143. package/dist/temporal/workflows/definition-sync.js +1 -1
  144. package/dist/temporal/workflows/definition-sync.js.map +1 -1
  145. package/dist/temporal/workflows/index.bundle.js +50 -22
  146. package/dist/tsconfig.test.tsbuildinfo +1 -1
  147. package/drizzle/0003_nebulous_nemesis.sql +11 -0
  148. package/drizzle/0004_workflow_ref_lookup.sql +1 -0
  149. package/drizzle/meta/0003_snapshot.json +627 -0
  150. package/drizzle/meta/0004_snapshot.json +649 -0
  151. package/drizzle/meta/_journal.json +14 -0
  152. package/drizzle.config.ts +1 -0
  153. package/package.json +7 -7
  154. package/src/core/entities/index.ts +1 -1
  155. package/src/core/entities/integration-context.ts +4 -0
  156. package/src/core/entities/sync-state.test.ts +49 -0
  157. package/src/core/entities/sync-state.ts +35 -12
  158. package/src/core/entities/validation-diagnostic.ts +8 -0
  159. package/src/core/entities/workflow-definition.ts +2 -0
  160. package/src/core/errors.ts +26 -0
  161. package/src/core/index.ts +12 -0
  162. package/src/core/integrations.test.ts +38 -0
  163. package/src/core/integrations.ts +10 -1
  164. package/src/core/parse-definition-diagnostics.test.ts +47 -0
  165. package/src/core/parse-definition.test.ts +29 -5
  166. package/src/core/parse-definition.ts +7 -7
  167. package/src/core/resolve-definition-at-ref.test.ts +792 -0
  168. package/src/core/resolve-definition-at-ref.ts +551 -0
  169. package/src/core/sync-definitions.test.ts +61 -2
  170. package/src/core/sync-definitions.ts +7 -7
  171. package/src/core/validate-definition.test.ts +31 -22
  172. package/src/core/validate-definition.ts +21 -13
  173. package/src/core/workflow-model/cron-trigger.ts +6 -2
  174. package/src/core/workflow-model/index.ts +1 -0
  175. package/src/core/workflow-model/invalid-workflow-model-error.ts +5 -1
  176. package/src/core/workflow-model/normalize-job-listening.ts +55 -23
  177. package/src/core/workflow-model/normalize-triggers.ts +44 -15
  178. package/src/core/workflow-model/normalize-workflow-document.test.ts +494 -36
  179. package/src/core/workflow-model/normalize-workflow-document.ts +8 -4
  180. package/src/core/workflow-model/parse-interpolation-field.ts +1 -23
  181. package/src/core/workflow-model/validate-predicate-expression.ts +6 -0
  182. package/src/core/workflow-model/validation-issue.ts +8 -13
  183. package/src/db/db.ts +7 -1
  184. package/src/db/definition-triggers.test.ts +44 -0
  185. package/src/db/definition-triggers.ts +1 -1
  186. package/src/db/definitions.test.ts +300 -1
  187. package/src/db/definitions.ts +282 -28
  188. package/src/db/index.ts +2 -0
  189. package/src/db/schema/definitions.test.ts +1 -0
  190. package/src/db/schema/definitions.ts +11 -0
  191. package/src/db/schema/sync-states.ts +12 -3
  192. package/src/db/schema/workflows.ts +23 -0
  193. package/src/db/sync-states.test.ts +87 -18
  194. package/src/db/sync-states.ts +6 -6
  195. package/src/db/workflow-lineage-migration.test.ts +158 -0
  196. package/src/index.ts +3 -1
  197. package/src/metrics/index.ts +1 -0
  198. package/src/metrics/instance.test.ts +50 -0
  199. package/src/metrics/instance.ts +23 -0
  200. package/src/presentation/dto/definition.test.ts +89 -1
  201. package/src/presentation/dto/definition.ts +8 -5
  202. package/src/presentation/inter-module.test.ts +177 -0
  203. package/src/presentation/inter-module.ts +77 -4
  204. package/src/presentation/routes/create-definition.test.ts +38 -0
  205. package/src/presentation/routes/create-definition.ts +20 -3
  206. package/src/presentation/routes/get-definition.test.ts +125 -11
  207. package/src/presentation/routes/get-definition.ts +21 -4
  208. package/src/presentation/routes/list-definitions.test.ts +1 -1
  209. package/src/presentation/routes/validate-definition.test.ts +1 -1
  210. package/src/presentation/routes/validate-definition.ts +3 -3
  211. package/src/presentation/subscribers/on-integration-connection-available.test.ts +3 -0
  212. package/src/temporal/activities/sync-activities.test.ts +46 -2
  213. package/src/temporal/activities/sync-activities.ts +9 -7
  214. package/src/temporal/workflows/definition-sync.ts +1 -1
  215. package/test/factories/definition.ts +1 -0
  216. package/test/globalSetup.ts +2 -0
  217. package/tsconfig.build.tsbuildinfo +1 -1
  218. package/dist/core/entities/validation-warning.d.ts +0 -6
  219. package/dist/core/entities/validation-warning.d.ts.map +0 -1
  220. package/dist/core/entities/validation-warning.js +0 -3
  221. package/dist/core/entities/validation-warning.js.map +0 -1
  222. package/src/core/entities/validation-warning.ts +0 -5
  223. package/src/core/parse-definition-warnings.test.ts +0 -33
@@ -23,7 +23,7 @@ export function normalizeWorkflowDocument(
23
23
  integrationValidationContext?: IntegrationValidationContext | undefined;
24
24
  stepSourceLocations?: WorkflowStepSourceLocationMap | undefined;
25
25
  /** Provide a fresh array for each call to collect non-fatal validation issues. */
26
- warnings?: WorkflowModelValidationIssue[] | undefined;
26
+ diagnostics?: WorkflowModelValidationIssue[] | undefined;
27
27
  },
28
28
  ): WorkflowModel {
29
29
  const issues: WorkflowModelValidationIssue[] = [];
@@ -65,10 +65,14 @@ export function normalizeWorkflowDocument(
65
65
 
66
66
  validateCycles(document.jobs, jobIdBySourceName, issues);
67
67
 
68
- const warnings = issues.filter((issue) => issue.severity === 'warning');
69
- options.warnings?.push(...warnings);
68
+ const diagnostics = issues.filter(
69
+ (issue) => !(issue.severity === 'error' && issue.scope === 'definition'),
70
+ );
71
+ options.diagnostics?.push(...diagnostics);
70
72
 
71
- const errors = issues.filter((issue) => issue.severity !== 'warning');
73
+ const errors = issues.filter(
74
+ (issue) => issue.severity === 'error' && issue.scope === 'definition',
75
+ );
72
76
  if (errors.length > 0) throw new InvalidWorkflowModelError(errors);
73
77
 
74
78
  return {
@@ -26,10 +26,10 @@ import {
26
26
  import type {WorkflowFieldTemplate} from '../entities/workflow-model.js';
27
27
  import type {
28
28
  WorkflowModelValidationIssue,
29
- WorkflowModelValidationIssueCode,
30
29
  WorkflowModelValidationIssuePathSegment,
31
30
  } from './invalid-workflow-model-error.js';
32
31
  import {validateDirectJobReferences} from './validate-job-references.js';
32
+ import {issue} from './validation-issue.js';
33
33
 
34
34
  export type StoredInterpolationField =
35
35
  | 'run'
@@ -513,25 +513,3 @@ function formatList(values: readonly string[]): string {
513
513
  function fieldLabel(field: WorkflowInterpolationField): string {
514
514
  return `Workflow ${field}`;
515
515
  }
516
-
517
- function issue(params: {
518
- code: WorkflowModelValidationIssueCode;
519
- message: string;
520
- path: readonly WorkflowModelValidationIssuePathSegment[];
521
- details?: Readonly<Record<string, unknown>>;
522
- }): WorkflowModelValidationIssue {
523
- if (params.details === undefined) {
524
- return {
525
- code: params.code,
526
- message: params.message,
527
- path: params.path,
528
- };
529
- }
530
-
531
- return {
532
- code: params.code,
533
- message: params.message,
534
- path: params.path,
535
- details: params.details,
536
- };
537
- }
@@ -21,6 +21,7 @@ import type {
21
21
  WorkflowModelValidationIssue,
22
22
  WorkflowModelValidationIssueCode,
23
23
  WorkflowModelValidationIssuePathSegment,
24
+ WorkflowModelValidationIssueScope,
24
25
  } from './invalid-workflow-model-error.js';
25
26
  import {validateDirectJobReferences} from './validate-job-references.js';
26
27
  import {issue} from './validation-issue.js';
@@ -35,6 +36,8 @@ export function validatePredicateExpression(params: {
35
36
  issues: WorkflowModelValidationIssue[];
36
37
  allowedJobReferences?: ReadonlySet<string>;
37
38
  typeOverlay?: ExpressionTypeEnvironment | undefined;
39
+ /** Scope for the invalidCode issue; other issues keep the definition scope. */
40
+ scope?: WorkflowModelValidationIssueScope | undefined;
38
41
  }): WorkflowExpression | undefined {
39
42
  const site = getWorkflowPredicateFieldMinimumFillTarget(params.field);
40
43
  const syntaxExpression = createSyntaxExpression(params);
@@ -150,6 +153,7 @@ function createSyntaxExpression(params: {
150
153
  invalidCode: WorkflowModelValidationIssueCode;
151
154
  invalidMessage: string;
152
155
  issues: WorkflowModelValidationIssue[];
156
+ scope?: WorkflowModelValidationIssueScope | undefined;
153
157
  }): WorkflowExpression | undefined {
154
158
  try {
155
159
  return createWorkflowExpression({
@@ -179,6 +183,7 @@ function invalidPredicateIssue(params: {
179
183
  invalidMessage: string;
180
184
  contextRoots: readonly string[];
181
185
  reason?: string;
186
+ scope?: WorkflowModelValidationIssueScope | undefined;
182
187
  }): WorkflowModelValidationIssue {
183
188
  return issue({
184
189
  code: params.invalidCode,
@@ -190,6 +195,7 @@ function invalidPredicateIssue(params: {
190
195
  contextRoots: params.contextRoots,
191
196
  reason: params.reason ?? 'Expression source did not parse or type-check.',
192
197
  },
198
+ ...(params.scope === undefined ? {} : {scope: params.scope}),
193
199
  });
194
200
  }
195
201
 
@@ -2,6 +2,7 @@ import type {
2
2
  WorkflowModelValidationIssue,
3
3
  WorkflowModelValidationIssueCode,
4
4
  WorkflowModelValidationIssuePathSegment,
5
+ WorkflowModelValidationIssueScope,
5
6
  WorkflowModelValidationIssueSeverity,
6
7
  } from './invalid-workflow-model-error.js';
7
8
 
@@ -11,21 +12,15 @@ export function issue(params: {
11
12
  path: readonly WorkflowModelValidationIssuePathSegment[];
12
13
  details?: Readonly<Record<string, unknown>>;
13
14
  severity?: WorkflowModelValidationIssueSeverity;
15
+ scope?: WorkflowModelValidationIssueScope;
14
16
  }): WorkflowModelValidationIssue {
17
+ const severity = params.severity ?? 'error';
18
+ const scope = params.scope ?? 'definition';
19
+ const base = {code: params.code, message: params.message, path: params.path, severity, scope};
20
+
15
21
  if (params.details === undefined) {
16
- return {
17
- code: params.code,
18
- message: params.message,
19
- path: params.path,
20
- ...(params.severity === undefined ? {} : {severity: params.severity}),
21
- };
22
+ return base;
22
23
  }
23
24
 
24
- return {
25
- code: params.code,
26
- message: params.message,
27
- path: params.path,
28
- details: params.details,
29
- ...(params.severity === undefined ? {} : {severity: params.severity}),
30
- };
25
+ return {...base, details: params.details};
31
26
  }
package/src/db/db.ts CHANGED
@@ -3,8 +3,14 @@ import {pgClient} from '@shipfox/node-postgres';
3
3
  import {workflowDefinitions} from './schema/definitions.js';
4
4
  import {definitionsOutbox} from './schema/outbox.js';
5
5
  import {definitionSyncStates} from './schema/sync-states.js';
6
+ import {workflowWorkflows} from './schema/workflows.js';
6
7
 
7
- export const schema = {workflowDefinitions, definitionsOutbox, definitionSyncStates};
8
+ export const schema = {
9
+ workflowDefinitions,
10
+ definitionsOutbox,
11
+ definitionSyncStates,
12
+ workflowWorkflows,
13
+ };
8
14
 
9
15
  let _db: NodePgDatabase<typeof schema> | undefined;
10
16
 
@@ -1,3 +1,4 @@
1
+ import {definitionResolvedEventSchema} from '@shipfox/api-definitions-dto';
1
2
  import type {WorkflowModel, WorkflowModelTrigger} from '#core/entities/workflow-model.js';
2
3
  import {definitionTriggersFor} from './definition-triggers.js';
3
4
 
@@ -85,6 +86,49 @@ describe('definitionTriggersFor', () => {
85
86
  });
86
87
  });
87
88
 
89
+ it('passes an omitted event through for integration triggers', () => {
90
+ const model = workflowModelWithTriggers([
91
+ {
92
+ id: 'on_any_github_event',
93
+ key: 'on_any_github_event',
94
+ source: 'github_acme',
95
+ },
96
+ ]);
97
+
98
+ const result = definitionTriggersFor(model);
99
+
100
+ expect(result).toEqual({
101
+ on_any_github_event: {
102
+ source: 'github_acme',
103
+ },
104
+ });
105
+ expect(result.on_any_github_event).not.toHaveProperty('event');
106
+ });
107
+
108
+ it('round trips a trigger without an event through the definition event schema', () => {
109
+ const dto = definitionTriggersFor(
110
+ workflowModelWithTriggers([
111
+ {
112
+ id: 'on_any_github_event',
113
+ key: 'on_any_github_event',
114
+ source: 'github_acme',
115
+ },
116
+ ]),
117
+ );
118
+
119
+ const result = definitionResolvedEventSchema.parse({
120
+ definitionId: '019e98ab-6656-7ca1-b9ad-1ca4442c479d',
121
+ projectId: '019e98ab-b90f-7265-b13c-8b441c991381',
122
+ workspaceId: '019e98ab-b90f-7265-b13c-8b441c991382',
123
+ configPath: '.shipfox/workflows/ci.yml',
124
+ triggers: dto,
125
+ });
126
+
127
+ expect(result.triggers).toEqual({
128
+ on_any_github_event: {source: 'github_acme'},
129
+ });
130
+ });
131
+
88
132
  it('returns an empty object when the model has no triggers', () => {
89
133
  const model = workflowModelWithTriggers([]);
90
134
 
@@ -6,7 +6,7 @@ export function definitionTriggersFor(model: WorkflowModel): Record<string, Trig
6
6
  model.triggers.map((trigger) => {
7
7
  const dto: TriggerDto = {
8
8
  source: trigger.source,
9
- event: trigger.event,
9
+ ...(trigger.event === undefined ? {} : {event: trigger.event}),
10
10
  };
11
11
  if (trigger.inputs !== undefined) dto.with = trigger.inputs;
12
12
  if (trigger.filter !== undefined) dto.filter = trigger.filter;
@@ -15,7 +15,7 @@ import {
15
15
  registerPublisher as registerInRegistry,
16
16
  renewDispatchClaim as renewFromRegistry,
17
17
  } from '@shipfox/node-module';
18
- import {sql} from 'drizzle-orm';
18
+ import {eq, sql} from 'drizzle-orm';
19
19
  import type {WorkflowDefinitionPayload} from '#core/entities/workflow-definition.js';
20
20
  import {normalizeWorkflowDocument} from '#core/workflow-model/index.js';
21
21
  import {agentValidationCatalog} from '#test/agent-validation-catalog.js';
@@ -30,6 +30,7 @@ import {
30
30
  } from './definitions.js';
31
31
  import {workflowDefinitions} from './schema/definitions.js';
32
32
  import {definitionsOutbox} from './schema/outbox.js';
33
+ import {workflowWorkflows} from './schema/workflows.js';
33
34
 
34
35
  const MS_PER_DAY = 24 * 60 * 60 * 1_000;
35
36
  let outboxRegistry = createOutboxRegistry();
@@ -390,6 +391,289 @@ describe('definition queries', () => {
390
391
  });
391
392
  });
392
393
 
394
+ describe('workflow lineage', () => {
395
+ test('upsertDefinition creates a lineage and returns its workflowId', async () => {
396
+ const definition = await upsertDefinition({
397
+ projectId,
398
+ workspaceId,
399
+ configPath: '.shipfox/workflows/lineage.yml',
400
+ name: 'Lineage',
401
+ ...definitionFields('Lineage'),
402
+ source: 'vcs',
403
+ ref: 'main',
404
+ });
405
+
406
+ expect(definition.workflowId).toBeDefined();
407
+ const lineages = await db()
408
+ .select()
409
+ .from(workflowWorkflows)
410
+ .where(eq(workflowWorkflows.projectId, projectId));
411
+ expect(lineages).toHaveLength(1);
412
+ expect(lineages[0]?.id).toBe(definition.workflowId);
413
+ expect(lineages[0]?.configPath).toBe('.shipfox/workflows/lineage.yml');
414
+ });
415
+
416
+ test('re-syncing the same config path reuses the lineage', async () => {
417
+ const first = await upsertDefinition({
418
+ projectId,
419
+ workspaceId,
420
+ configPath: 'a.yml',
421
+ name: 'A v1',
422
+ ...definitionFields('A v1'),
423
+ source: 'vcs',
424
+ ref: 'main',
425
+ });
426
+
427
+ const second = await upsertDefinition({
428
+ projectId,
429
+ workspaceId,
430
+ configPath: 'a.yml',
431
+ name: 'A v2',
432
+ ...definitionFields('A v2'),
433
+ source: 'vcs',
434
+ ref: 'main',
435
+ });
436
+
437
+ expect(second.workflowId).toBe(first.workflowId);
438
+ const lineages = await db()
439
+ .select()
440
+ .from(workflowWorkflows)
441
+ .where(eq(workflowWorkflows.projectId, projectId));
442
+ expect(lineages).toHaveLength(1);
443
+ });
444
+
445
+ test('manual and VCS rows at the same config path share one lineage', async () => {
446
+ const manual = await upsertDefinition({
447
+ projectId,
448
+ workspaceId,
449
+ configPath: 'shared.yml',
450
+ name: 'Shared manual',
451
+ ...definitionFields('Shared manual'),
452
+ });
453
+ const vcs = await upsertDefinition({
454
+ projectId,
455
+ workspaceId,
456
+ configPath: 'shared.yml',
457
+ name: 'Shared vcs',
458
+ ...definitionFields('Shared vcs'),
459
+ source: 'vcs',
460
+ ref: 'main',
461
+ });
462
+
463
+ expect(vcs.workflowId).toBe(manual.workflowId);
464
+ });
465
+
466
+ test('applyVcsDefinitionsBatch creates the lineage and reuses it across syncs', async () => {
467
+ await applyVcsDefinitionsBatch({
468
+ projectId,
469
+ workspaceId,
470
+ ref: 'main',
471
+ upserts: [
472
+ {
473
+ configPath: 'batch.yml',
474
+ name: 'Batch',
475
+ ...definitionFields('Batch'),
476
+ contentHash: 'h-1',
477
+ },
478
+ ],
479
+ });
480
+ const firstLineages = await db()
481
+ .select()
482
+ .from(workflowWorkflows)
483
+ .where(eq(workflowWorkflows.projectId, projectId));
484
+ expect(firstLineages).toHaveLength(1);
485
+
486
+ await applyVcsDefinitionsBatch({
487
+ projectId,
488
+ workspaceId,
489
+ ref: 'main',
490
+ upserts: [
491
+ {
492
+ configPath: 'batch.yml',
493
+ name: 'Batch v2',
494
+ ...definitionFields('Batch v2'),
495
+ contentHash: 'h-2',
496
+ },
497
+ ],
498
+ });
499
+ const secondLineages = await db()
500
+ .select()
501
+ .from(workflowWorkflows)
502
+ .where(eq(workflowWorkflows.projectId, projectId));
503
+ expect(secondLineages).toHaveLength(1);
504
+ expect(secondLineages[0]?.id).toBe(firstLineages[0]?.id);
505
+ });
506
+
507
+ test('soft-deleting a synced row keeps the lineage', async () => {
508
+ await applyVcsDefinitionsBatch({
509
+ projectId,
510
+ workspaceId,
511
+ ref: 'main',
512
+ upserts: [
513
+ {configPath: 'gone.yml', name: 'Gone', ...definitionFields('Gone'), contentHash: 'h-1'},
514
+ ],
515
+ });
516
+
517
+ const deletedCount = await softDeleteVcsDefinitionsNotIn({
518
+ projectId,
519
+ workspaceId,
520
+ ref: 'main',
521
+ keepConfigPaths: [],
522
+ });
523
+
524
+ expect(deletedCount).toBe(1);
525
+ const lineages = await db()
526
+ .select()
527
+ .from(workflowWorkflows)
528
+ .where(eq(workflowWorkflows.projectId, projectId));
529
+ expect(lineages).toHaveLength(1);
530
+ expect(lineages[0]?.configPath).toBe('gone.yml');
531
+ });
532
+
533
+ test('pathless manual definitions reuse the project lineage across saves', async () => {
534
+ const first = await upsertDefinition({
535
+ projectId,
536
+ workspaceId,
537
+ name: 'No path',
538
+ ...definitionFields('No path'),
539
+ });
540
+ const second = await upsertDefinition({
541
+ projectId,
542
+ workspaceId,
543
+ name: 'No path updated',
544
+ ...definitionFields('No path updated'),
545
+ });
546
+
547
+ const lineage = await db()
548
+ .select()
549
+ .from(workflowWorkflows)
550
+ .where(eq(workflowWorkflows.id, first.workflowId));
551
+ expect(lineage).toHaveLength(1);
552
+ expect(lineage[0]?.projectId).toBe(projectId);
553
+ expect(second.workflowId).toBe(first.workflowId);
554
+
555
+ const projectLineages = await db()
556
+ .select()
557
+ .from(workflowWorkflows)
558
+ .where(eq(workflowWorkflows.projectId, projectId));
559
+ expect(projectLineages).toHaveLength(1);
560
+ });
561
+
562
+ test('materializes a legacy row when it is read', async () => {
563
+ const legacyId = crypto.randomUUID();
564
+ await db()
565
+ .insert(workflowDefinitions)
566
+ .values({
567
+ id: legacyId,
568
+ projectId,
569
+ configPath: 'legacy.yml',
570
+ name: 'Legacy',
571
+ definition: definitionFields('Legacy'),
572
+ });
573
+
574
+ const definition = await getDefinitionById(legacyId);
575
+
576
+ expect(definition?.workflowId).toBeDefined();
577
+ const stored = await db()
578
+ .select({workflowId: workflowDefinitions.workflowId})
579
+ .from(workflowDefinitions)
580
+ .where(eq(workflowDefinitions.id, legacyId));
581
+ expect(stored[0]?.workflowId).toBe(definition?.workflowId);
582
+
583
+ const lineages = await db()
584
+ .select()
585
+ .from(workflowWorkflows)
586
+ .where(eq(workflowWorkflows.projectId, projectId));
587
+ expect(lineages).toHaveLength(1);
588
+ expect(lineages[0]?.configPath).toBe('legacy.yml');
589
+ });
590
+
591
+ test('materializes legacy pathless rows into one project lineage', async () => {
592
+ const firstId = crypto.randomUUID();
593
+ const secondId = crypto.randomUUID();
594
+ await db()
595
+ .insert(workflowDefinitions)
596
+ .values([
597
+ {
598
+ id: firstId,
599
+ projectId,
600
+ configPath: null,
601
+ name: 'Legacy one',
602
+ definition: definitionFields('Legacy one'),
603
+ },
604
+ {
605
+ id: secondId,
606
+ projectId,
607
+ configPath: null,
608
+ name: 'Legacy two',
609
+ definition: definitionFields('Legacy two'),
610
+ },
611
+ ]);
612
+
613
+ const definitions = await listDefinitionsByProject(projectId);
614
+
615
+ expect(definitions).toHaveLength(2);
616
+ expect(definitions[0]?.workflowId).toBe(definitions[1]?.workflowId);
617
+ const lineages = await db()
618
+ .select()
619
+ .from(workflowWorkflows)
620
+ .where(eq(workflowWorkflows.projectId, projectId));
621
+ expect(lineages).toHaveLength(1);
622
+ expect(lineages[0]?.configPath).toBeNull();
623
+ });
624
+
625
+ test('materializes an unchanged legacy VCS row during sync', async () => {
626
+ const legacyId = crypto.randomUUID();
627
+ await db()
628
+ .insert(workflowDefinitions)
629
+ .values({
630
+ id: legacyId,
631
+ projectId,
632
+ configPath: 'legacy-vcs.yml',
633
+ source: 'vcs',
634
+ ref: 'main',
635
+ name: 'Legacy VCS',
636
+ definition: definitionFields('Legacy VCS'),
637
+ contentHash: 'legacy-hash',
638
+ });
639
+
640
+ const result = await applyVcsDefinitionsBatch({
641
+ projectId,
642
+ workspaceId,
643
+ ref: 'main',
644
+ upserts: [
645
+ {
646
+ configPath: 'legacy-vcs.yml',
647
+ name: 'Legacy VCS',
648
+ ...definitionFields('Legacy VCS'),
649
+ contentHash: 'legacy-hash',
650
+ },
651
+ ],
652
+ });
653
+
654
+ expect(result.appliedCount).toBe(0);
655
+ const stored = await db()
656
+ .select({workflowId: workflowDefinitions.workflowId})
657
+ .from(workflowDefinitions)
658
+ .where(eq(workflowDefinitions.id, legacyId));
659
+ expect(stored[0]?.workflowId).toBeDefined();
660
+ });
661
+
662
+ test('getDefinitionById returns the workflowId', async () => {
663
+ const created = await upsertDefinition({
664
+ projectId,
665
+ workspaceId,
666
+ configPath: 'found.yml',
667
+ name: 'Found',
668
+ ...definitionFields('Found'),
669
+ });
670
+
671
+ const found = await getDefinitionById(created.id);
672
+
673
+ expect(found?.workflowId).toBe(created.workflowId);
674
+ });
675
+ });
676
+
393
677
  describe('getDefinitionById', () => {
394
678
  test('returns the definition when found', async () => {
395
679
  const created = await upsertDefinition({
@@ -519,6 +803,7 @@ describe('definition queries', () => {
519
803
  });
520
804
 
521
805
  expect(restored.id).toBe(original.id);
806
+ expect(restored.workflowId).toBe(original.workflowId);
522
807
  expect(restored.name).toBe('Recovered v2');
523
808
  expect(restored.deletedAt).toBeNull();
524
809
  const visible = await listDefinitionsByProject(projectId);
@@ -573,6 +858,11 @@ describe('definition queries', () => {
573
858
  expect(first.appliedCount).toBe(2);
574
859
  expect(await listOutboxRowsForProject(projectId)).toHaveLength(2);
575
860
 
861
+ await db()
862
+ .update(workflowDefinitions)
863
+ .set({fetchedAt: new Date(0), updatedAt: new Date(0)})
864
+ .where(eq(workflowDefinitions.projectId, projectId));
865
+
576
866
  const second = await applyVcsDefinitionsBatch({
577
867
  projectId,
578
868
  workspaceId,
@@ -585,6 +875,15 @@ describe('definition queries', () => {
585
875
 
586
876
  expect(second.appliedCount).toBe(1);
587
877
  expect(await listOutboxRowsForProject(projectId)).toHaveLength(3);
878
+
879
+ const refreshed = (
880
+ await db()
881
+ .select()
882
+ .from(workflowDefinitions)
883
+ .where(eq(workflowDefinitions.projectId, projectId))
884
+ ).find((row) => row.configPath === 'a.yml');
885
+ expect(refreshed?.fetchedAt.getTime()).toBeGreaterThan(0);
886
+ expect(refreshed?.updatedAt.getTime()).toBeGreaterThan(0);
588
887
  });
589
888
 
590
889
  test('softDeleteVcsDefinitionsNotIn writes a DEFINITION_DELETED event per pruned row', async () => {