@shipfox/api-definitions 13.1.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 +35 -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 +444 -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
@@ -1,11 +1,26 @@
1
+ import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
1
2
  import {createWorkflowModelSnapshot} from '@shipfox/api-definitions-dto';
2
3
  import {definitionsInterModuleContract} from '@shipfox/api-definitions-dto/inter-module';
3
- import {defineInterModulePresentation, type InterModulePresentation} from '@shipfox/inter-module';
4
+ import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
5
+ import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
6
+ import {
7
+ createInterModuleKnownError,
8
+ defineInterModulePresentation,
9
+ type InterModuleMethodContract,
10
+ type InterModulePresentation,
11
+ } from '@shipfox/inter-module';
12
+ import {DefinitionAtRefError, listDefinitionsAtRef, resolveDefinitionAtRef} from '#core/index.js';
4
13
  import {getDefinitionById} from '#db/definitions.js';
5
14
 
6
- export function createDefinitionsInterModulePresentation(): InterModulePresentation<
7
- typeof definitionsInterModuleContract
8
- > {
15
+ export interface CreateDefinitionsInterModulePresentationParams {
16
+ projects: ProjectsModuleClient;
17
+ agent: AgentInterModuleClient;
18
+ integrations: IntegrationsModuleClient;
19
+ }
20
+
21
+ export function createDefinitionsInterModulePresentation(
22
+ params: CreateDefinitionsInterModulePresentationParams,
23
+ ): InterModulePresentation<typeof definitionsInterModuleContract> {
9
24
  return defineInterModulePresentation(definitionsInterModuleContract, {
10
25
  getDefinitionForWorkflowRun: async ({definitionId}) => {
11
26
  const definition = await getDefinitionById(definitionId);
@@ -14,6 +29,7 @@ export function createDefinitionsInterModulePresentation(): InterModulePresentat
14
29
  return {
15
30
  definition: {
16
31
  id: definition.id,
32
+ workflowId: definition.workflowId,
17
33
  projectId: definition.projectId,
18
34
  name: definition.name,
19
35
  model: createWorkflowModelSnapshot(definition.model),
@@ -21,5 +37,62 @@ export function createDefinitionsInterModulePresentation(): InterModulePresentat
21
37
  },
22
38
  };
23
39
  },
40
+ resolveDefinitionAtRef: async (input, context) => {
41
+ try {
42
+ return await resolveDefinitionAtRef({...input, ...params, signal: context.signal});
43
+ } catch (error) {
44
+ throw toDefinitionAtRefKnownError(
45
+ definitionsInterModuleContract.methods.resolveDefinitionAtRef,
46
+ error,
47
+ );
48
+ }
49
+ },
50
+ listDefinitionsAtRef: async (input, context) => {
51
+ try {
52
+ return await listDefinitionsAtRef({...input, ...params, signal: context.signal});
53
+ } catch (error) {
54
+ throw toDefinitionAtRefKnownError(
55
+ definitionsInterModuleContract.methods.listDefinitionsAtRef,
56
+ error,
57
+ );
58
+ }
59
+ },
24
60
  });
25
61
  }
62
+
63
+ function toDefinitionAtRefKnownError(method: InterModuleMethodContract, error: unknown): unknown {
64
+ if (!(error instanceof DefinitionAtRefError)) return error;
65
+ const {code, details} = error;
66
+ switch (code) {
67
+ case 'project-not-found':
68
+ return createInterModuleKnownError(method, 'project-not-found', {
69
+ projectId: String(details.projectId),
70
+ });
71
+ case 'ref-not-found':
72
+ return createInterModuleKnownError(method, 'ref-not-found', {ref: String(details.ref)});
73
+ case 'ref-invalid':
74
+ return createInterModuleKnownError(method, 'ref-invalid', {ref: String(details.ref)});
75
+ case 'ref-moved':
76
+ return createInterModuleKnownError(method, 'ref-moved', {
77
+ ref: String(details.ref),
78
+ expectedCommit: String(details.expectedCommit),
79
+ });
80
+ case 'file-not-found':
81
+ return createInterModuleKnownError(method, 'file-not-found', {
82
+ ref: String(details.ref),
83
+ configPath: String(details.configPath),
84
+ });
85
+ case 'content-too-large':
86
+ return createInterModuleKnownError(method, 'content-too-large', {
87
+ configPath: String(details.configPath),
88
+ });
89
+ case 'invalid-definition':
90
+ return createInterModuleKnownError(method, 'invalid-definition', {
91
+ errors: details.errors as Array<{message: string; path?: string}>,
92
+ });
93
+ case 'too-many-files':
94
+ return createInterModuleKnownError(method, 'too-many-files', {});
95
+ case 'source-unavailable':
96
+ return createInterModuleKnownError(method, 'source-unavailable', {});
97
+ }
98
+ }
@@ -149,6 +149,8 @@ jobs:
149
149
  capabilities: ['agent_tools' as const],
150
150
  },
151
151
  ],
152
+ eventCatalogs: [],
153
+ fixedEventProviders: [],
152
154
  defaultConnection: {
153
155
  id: sourceConnectionId,
154
156
  slug: 'github-main',
@@ -240,6 +242,42 @@ jobs:
240
242
  expect(res.json().code).toBe('invalid-workflow-definition');
241
243
  });
242
244
 
245
+ test('trigger-scoped validation errors remain client errors on create', async () => {
246
+ const res = await app.inject({
247
+ method: 'POST',
248
+ url: '/api/definitions',
249
+ payload: {
250
+ project_id: projectId,
251
+ config_path: 'test.yml',
252
+ yaml: `
253
+ name: Broken trigger
254
+ runner: ubuntu-latest
255
+ triggers:
256
+ nightly:
257
+ source: cron
258
+ event: tick
259
+ config:
260
+ schedule: "not a cron"
261
+ jobs:
262
+ build:
263
+ steps:
264
+ - run: echo hello
265
+ `,
266
+ },
267
+ });
268
+
269
+ expect(res.statusCode).toBe(400);
270
+ expect(res.json()).toMatchObject({
271
+ code: 'invalid-workflow-definition',
272
+ details: [
273
+ {
274
+ message: 'Cron trigger schedule must be a valid 5-field cron expression.',
275
+ path: 'triggers.nightly.config.schedule',
276
+ },
277
+ ],
278
+ });
279
+ });
280
+
243
281
  test('cyclic DAG returns 400 with dag error code', async () => {
244
282
  const cyclicYaml = `
245
283
  name: Cyclic
@@ -11,7 +11,11 @@ import {z} from 'zod';
11
11
  import {DefinitionParseError} from '#core/errors.js';
12
12
  import {hasAgentStepIntegrations} from '#core/has-agent-step-integrations.js';
13
13
  import {loadIntegrationValidationContext} from '#core/integrations.js';
14
- import {parseDefinition} from '#core/parse-definition.js';
14
+ import {
15
+ type ParseDefinitionOptions,
16
+ parseDefinitionWithDiagnostics,
17
+ stripDefinitionDiagnostics,
18
+ } from '#core/parse-definition.js';
15
19
  import {upsertDefinition} from '#db/definitions.js';
16
20
  import {toDefinitionDto} from '#presentation/dto/index.js';
17
21
  import {requireProjectAccess} from './project-access.js';
@@ -53,11 +57,11 @@ export function buildCreateDefinitionRoute(options: CreateDefinitionRouteOptions
53
57
  const project = await requireProjectAccess(request, projectId, options.projects);
54
58
 
55
59
  const agentValidationCatalog = await options.agent.getValidationCatalog({});
56
- const structurallyParsed = parseDefinition(yamlString, {agentValidationCatalog});
60
+ const structurallyParsed = parseDefinitionForCreate(yamlString, {agentValidationCatalog});
57
61
  const {integrations} = options;
58
62
  const parsed =
59
63
  integrations !== undefined && hasAgentStepIntegrations(structurallyParsed.document)
60
- ? parseDefinition(yamlString, {
64
+ ? parseDefinitionForCreate(yamlString, {
61
65
  agentValidationCatalog,
62
66
  integrationValidationContext: await loadIntegrationValidationContext(
63
67
  integrations,
@@ -84,3 +88,16 @@ export function buildCreateDefinitionRoute(options: CreateDefinitionRouteOptions
84
88
  },
85
89
  });
86
90
  }
91
+
92
+ function parseDefinitionForCreate(yamlString: string, options: ParseDefinitionOptions) {
93
+ const parsed = parseDefinitionWithDiagnostics(yamlString, options);
94
+ const errors = parsed.diagnostics
95
+ .filter((diagnostic) => diagnostic.severity === 'error')
96
+ .map(({message, path}) => ({message, ...(path === undefined ? {} : {path})}));
97
+
98
+ if (errors.length > 0) {
99
+ throw new DefinitionParseError(errors[0]?.message ?? 'Invalid definition', errors);
100
+ }
101
+
102
+ return stripDefinitionDiagnostics(parsed);
103
+ }
@@ -3,23 +3,27 @@ import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module'
3
3
  import type {FastifyInstance} from 'fastify';
4
4
  import Fastify from 'fastify';
5
5
  import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod';
6
+ import {softDeleteVcsDefinitionsNotIn} from '#db/definitions.js';
6
7
  import {definitionFactory} from '#test/index.js';
7
8
  import {buildGetDefinitionRoute} from './get-definition.js';
8
9
 
9
10
  const projectAccessState = vi.hoisted(() => ({workspaceId: ''}));
10
11
 
12
+ const getProjectById = vi.fn<ProjectsModuleClient['getProjectById']>(({projectId}) =>
13
+ Promise.resolve({
14
+ project: {
15
+ id: projectId,
16
+ workspaceId: projectAccessState.workspaceId,
17
+ sourceConnectionId: crypto.randomUUID(),
18
+ sourceExternalRepositoryId: 'repo',
19
+ sourceDefaultBranch: 'main',
20
+ name: 'Project',
21
+ },
22
+ }),
23
+ );
24
+
11
25
  const projects = {
12
- getProjectById: vi.fn(({projectId}) =>
13
- Promise.resolve({
14
- project: {
15
- id: projectId,
16
- workspaceId: projectAccessState.workspaceId,
17
- sourceConnectionId: crypto.randomUUID(),
18
- sourceExternalRepositoryId: 'repo',
19
- name: 'Project',
20
- },
21
- }),
22
- ),
26
+ getProjectById,
23
27
  requireProjectForWorkspace: vi.fn(),
24
28
  } as unknown as ProjectsModuleClient;
25
29
 
@@ -74,6 +78,116 @@ describe('GET /api/definitions/:id', () => {
74
78
  expect(res.json().code).toBe('not-found');
75
79
  });
76
80
 
81
+ test('returns 200 with the synced row when the id is a lineage id with a synced row', async () => {
82
+ const definition = await definitionFactory.create({
83
+ source: 'vcs',
84
+ ref: 'main',
85
+ configPath: '.shipfox/workflows/lineage.yml',
86
+ });
87
+ expect(definition.workflowId).not.toBe(definition.id);
88
+
89
+ const res = await app.inject({
90
+ method: 'GET',
91
+ url: `/api/definitions/${definition.workflowId}`,
92
+ });
93
+
94
+ expect(res.statusCode).toBe(200);
95
+ expect(res.json().id).toBe(definition.id);
96
+ expect(res.json().ref).toBe('main');
97
+ });
98
+
99
+ test('returns 404 when the id is a lineage id without a synced row', async () => {
100
+ const definition = await definitionFactory.create({
101
+ source: 'vcs',
102
+ ref: 'main',
103
+ configPath: '.shipfox/workflows/removed.yml',
104
+ });
105
+ await softDeleteVcsDefinitionsNotIn({
106
+ projectId: definition.projectId,
107
+ workspaceId,
108
+ ref: 'main',
109
+ keepConfigPaths: [],
110
+ });
111
+
112
+ const res = await app.inject({
113
+ method: 'GET',
114
+ url: `/api/definitions/${definition.workflowId}`,
115
+ });
116
+
117
+ expect(res.statusCode).toBe(404);
118
+ expect(res.json().code).toBe('not-found');
119
+ });
120
+
121
+ test('returns 404 when a lineage only has a non-default branch row', async () => {
122
+ const definition = await definitionFactory.create({
123
+ source: 'vcs',
124
+ ref: 'feature/lineage',
125
+ configPath: '.shipfox/workflows/non-default.yml',
126
+ });
127
+
128
+ const res = await app.inject({
129
+ method: 'GET',
130
+ url: `/api/definitions/${definition.workflowId}`,
131
+ });
132
+
133
+ expect(res.statusCode).toBe(404);
134
+ expect(res.json().code).toBe('not-found');
135
+ });
136
+
137
+ test('returns the row from the project default branch when a lineage has multiple refs', async () => {
138
+ const projectId = crypto.randomUUID();
139
+ const main = await definitionFactory.create({
140
+ projectId,
141
+ source: 'vcs',
142
+ ref: 'main',
143
+ configPath: '.shipfox/workflows/multi-ref.yml',
144
+ name: 'Main Workflow',
145
+ });
146
+ await definitionFactory.create({
147
+ projectId,
148
+ source: 'vcs',
149
+ ref: 'feature/lineage',
150
+ configPath: '.shipfox/workflows/multi-ref.yml',
151
+ name: 'Feature Workflow',
152
+ });
153
+
154
+ const res = await app.inject({
155
+ method: 'GET',
156
+ url: `/api/definitions/${main.workflowId}`,
157
+ });
158
+
159
+ expect(res.statusCode).toBe(200);
160
+ expect(res.json().id).toBe(main.id);
161
+ expect(res.json().name).toBe('Main Workflow');
162
+ expect(res.json().ref).toBe('main');
163
+ });
164
+
165
+ test('returns 403 through the project-access gate for a lineage in another workspace', async () => {
166
+ const definition = await definitionFactory.create({
167
+ source: 'vcs',
168
+ ref: 'main',
169
+ configPath: '.shipfox/workflows/elsewhere.yml',
170
+ });
171
+ getProjectById.mockResolvedValueOnce({
172
+ project: {
173
+ id: definition.projectId,
174
+ workspaceId: crypto.randomUUID(),
175
+ sourceConnectionId: crypto.randomUUID(),
176
+ sourceExternalRepositoryId: 'repo',
177
+ sourceDefaultBranch: 'main',
178
+ name: 'Project',
179
+ },
180
+ });
181
+
182
+ const res = await app.inject({
183
+ method: 'GET',
184
+ url: `/api/definitions/${definition.workflowId}`,
185
+ });
186
+
187
+ expect(res.statusCode).toBe(403);
188
+ expect(res.json().code).toBe('forbidden');
189
+ });
190
+
77
191
  test('returns 400 for invalid UUID', async () => {
78
192
  const res = await app.inject({
79
193
  method: 'GET',
@@ -2,7 +2,11 @@ import {definitionResponseSchema} from '@shipfox/api-definitions-dto';
2
2
  import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
3
3
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
4
  import {z} from 'zod';
5
- import {getDefinitionById} from '#db/definitions.js';
5
+ import {
6
+ getDefinitionById,
7
+ getDefinitionByWorkflowId,
8
+ getWorkflowLineageById,
9
+ } from '#db/definitions.js';
6
10
  import {toDefinitionDto} from '#presentation/dto/index.js';
7
11
  import {requireProjectAccess} from './project-access.js';
8
12
 
@@ -23,12 +27,25 @@ export function buildGetDefinitionRoute(projects: ProjectsModuleClient) {
23
27
  const {id} = request.params;
24
28
  const definition = await getDefinitionById(id);
25
29
 
26
- if (!definition) {
30
+ if (definition) {
31
+ await requireProjectAccess(request, definition.projectId, projects);
32
+ return toDefinitionDto(definition);
33
+ }
34
+
35
+ const lineage = await getWorkflowLineageById(id);
36
+ if (!lineage) {
37
+ throw new ClientError('Definition not found', 'not-found', {status: 404});
38
+ }
39
+
40
+ const project = await requireProjectAccess(request, lineage.projectId, projects);
41
+ const syncedDefinition = project.sourceDefaultBranch
42
+ ? await getDefinitionByWorkflowId({workflowId: id, ref: project.sourceDefaultBranch})
43
+ : undefined;
44
+ if (!syncedDefinition) {
27
45
  throw new ClientError('Definition not found', 'not-found', {status: 404});
28
46
  }
29
- await requireProjectAccess(request, definition.projectId, projects);
30
47
 
31
- return toDefinitionDto(definition);
48
+ return toDefinitionDto(syncedDefinition);
32
49
  },
33
50
  });
34
51
  }
@@ -99,7 +99,7 @@ describe('GET /api/definitions', () => {
99
99
  finished_at: finishedAt.toISOString(),
100
100
  last_error_code: null,
101
101
  last_error_message: null,
102
- warnings: [],
102
+ diagnostics: [],
103
103
  });
104
104
  });
105
105
 
@@ -39,7 +39,7 @@ jobs:
39
39
  expect(res.statusCode).toBe(200);
40
40
  const body = res.json();
41
41
  expect(body.valid).toBe(true);
42
- expect(body.warnings).toEqual([]);
42
+ expect(body.diagnostics).toEqual([]);
43
43
  expect(body.workflow_document.name).toBe('Test');
44
44
  expect(body.workflow_model.kind).toBe('workflow');
45
45
  });
@@ -1,8 +1,8 @@
1
1
  import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
2
2
  import {
3
3
  definitionDtoSchema,
4
+ definitionValidationDiagnosticSchema,
4
5
  definitionValidationErrorSchema,
5
- definitionValidationWarningSchema,
6
6
  } from '@shipfox/api-definitions-dto';
7
7
  import {defineRoute} from '@shipfox/node-fastify';
8
8
  import {z} from 'zod';
@@ -17,7 +17,7 @@ const validationResultSchema = z.union([
17
17
  valid: z.literal(true),
18
18
  workflow_document: definitionDtoSchema.shape.workflow_document,
19
19
  workflow_model: definitionDtoSchema.shape.workflow_model,
20
- warnings: z.array(definitionValidationWarningSchema),
20
+ diagnostics: z.array(definitionValidationDiagnosticSchema),
21
21
  }),
22
22
  z.object({
23
23
  valid: z.literal(false),
@@ -47,7 +47,7 @@ export function buildValidateDefinitionRoute(agent: AgentInterModuleClient) {
47
47
  valid: true as const,
48
48
  workflow_document: result.definition.document,
49
49
  workflow_model: result.definition.model,
50
- warnings: result.warnings,
50
+ diagnostics: result.diagnostics,
51
51
  };
52
52
  }
53
53
 
@@ -40,6 +40,7 @@ describe('onIntegrationConnectionAvailable', () => {
40
40
  workspaceId: first.workspaceId,
41
41
  connectionId: crypto.randomUUID(),
42
42
  slug: 'linear_shipfox',
43
+ capabilities: ['agent_tools'],
43
44
  };
44
45
  const event: DomainEvent<IntegrationConnectionAvailableEvent> = {
45
46
  id: crypto.randomUUID(),
@@ -96,6 +97,7 @@ describe('onIntegrationConnectionAvailable', () => {
96
97
  workspaceId,
97
98
  connectionId: crypto.randomUUID(),
98
99
  slug: 'linear_shipfox',
100
+ capabilities: ['agent_tools'],
99
101
  };
100
102
 
101
103
  await expect(
@@ -118,6 +120,7 @@ describe('onIntegrationConnectionAvailable', () => {
118
120
  workspaceId: crypto.randomUUID(),
119
121
  connectionId: crypto.randomUUID(),
120
122
  slug: 'linear_shipfox',
123
+ capabilities: ['agent_tools'],
121
124
  };
122
125
 
123
126
  await expect(
@@ -26,6 +26,18 @@ jobs:
26
26
  - run: pnpm test
27
27
  `;
28
28
 
29
+ const warningEventInterpolation = '$'.concat('{{ event.x }}');
30
+ const warningYaml = [
31
+ 'name: Warning only',
32
+ 'runner: ubuntu-latest',
33
+ 'jobs:',
34
+ ' build:',
35
+ ' steps:',
36
+ ' - env:',
37
+ ` MSG: '${warningEventInterpolation}'`,
38
+ ' run: eval "$MSG"',
39
+ ].join('\n');
40
+
29
41
  function sourceControl(
30
42
  overrides: Partial<DefinitionsSourceControl> = {},
31
43
  ): DefinitionsSourceControl {
@@ -224,7 +236,37 @@ describe('definition sync activities', () => {
224
236
 
225
237
  expect(result.appliedCount).toBe(1);
226
238
  expect(result.deletedCount).toBe(0);
227
- expect(result.warnings).toEqual([]);
239
+ expect(result.diagnostics).toEqual([]);
240
+ });
241
+
242
+ it('adds the workflow file path to persisted diagnostics', async () => {
243
+ const source = sourceControl({
244
+ fetchFile: vi.fn(() =>
245
+ Promise.resolve({
246
+ path: '.shipfox/workflows/warning.yml',
247
+ ref: 'main',
248
+ content: warningYaml,
249
+ }),
250
+ ),
251
+ });
252
+ const activities = createDefinitionSyncActivities(source, agent);
253
+
254
+ const result = await activities.fetchAndApplyDefinitionWorkflows({
255
+ projectId,
256
+ workspaceId: crypto.randomUUID(),
257
+ sourceConnectionId,
258
+ sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
259
+ sourceRef: 'main',
260
+ paths: ['.shipfox/workflows/warning.yml'],
261
+ });
262
+
263
+ expect(result.diagnostics).toHaveLength(1);
264
+ expect(result.diagnostics[0]).toMatchObject({
265
+ code: 're-evaluating-command',
266
+ path: 'jobs.build.steps.0.run',
267
+ severity: 'warning',
268
+ filePath: '.shipfox/workflows/warning.yml',
269
+ });
228
270
  });
229
271
 
230
272
  it('translates DefinitionSyncPermanentError into a non-retryable ApplicationFailure', async () => {
@@ -364,11 +406,12 @@ describe('definition sync activities', () => {
364
406
  sourceConnectionId,
365
407
  sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
366
408
  sourceRef: 'main',
367
- warnings: [
409
+ diagnostics: [
368
410
  {
369
411
  code: 're-evaluating-command',
370
412
  message: 'Workflow data is re-executed as shell code.',
371
413
  path: 'jobs.build.steps.0.run',
414
+ severity: 'warning',
372
415
  },
373
416
  ],
374
417
  });
@@ -386,6 +429,7 @@ describe('definition sync activities', () => {
386
429
  code: 're-evaluating-command',
387
430
  message: 'Workflow data is re-executed as shell code.',
388
431
  path: 'jobs.build.steps.0.run',
432
+ severity: 'warning',
389
433
  },
390
434
  ]);
391
435
  });
@@ -3,7 +3,7 @@ import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/i
3
3
  import {markErrorReported} from '@shipfox/node-error-monitoring';
4
4
  import {Context} from '@temporalio/activity';
5
5
  import {ApplicationFailure} from '@temporalio/common';
6
- import type {DefinitionSyncErrorCode, DefinitionSyncWarning} from '#core/entities/sync-state.js';
6
+ import type {DefinitionSyncDiagnostic, DefinitionSyncErrorCode} from '#core/entities/sync-state.js';
7
7
  import {
8
8
  classifySyncFailure,
9
9
  discoverWorkflowFiles,
@@ -50,11 +50,11 @@ export interface DiscoverWorkflowsActivityResult {
50
50
  export interface FetchAndApplyActivityResult {
51
51
  appliedCount: number;
52
52
  deletedCount: number;
53
- warnings: DefinitionSyncWarning[];
53
+ diagnostics: DefinitionSyncDiagnostic[];
54
54
  }
55
55
 
56
56
  export interface MarkSyncSucceededActivityInput extends SyncRefScopedInput {
57
- warnings?: readonly DefinitionSyncWarning[] | undefined;
57
+ diagnostics?: readonly DefinitionSyncDiagnostic[] | undefined;
58
58
  }
59
59
 
60
60
  export interface DefinitionSyncActivityOptions {
@@ -98,7 +98,7 @@ function createPrepareDefinitionSyncActivity(sourceControl: DefinitionsSourceCon
98
98
  status: 'syncing',
99
99
  lastErrorCode: null,
100
100
  lastErrorMessage: null,
101
- warnings: [],
101
+ diagnostics: [],
102
102
  startedAt: new Date(),
103
103
  finishedAt: null,
104
104
  });
@@ -169,7 +169,9 @@ function createFetchAndApplyActivity(
169
169
 
170
170
  return {
171
171
  ...result,
172
- warnings: definitions.flatMap((entry) => entry.warnings),
172
+ diagnostics: definitions.flatMap((entry) =>
173
+ entry.diagnostics.map((diagnostic) => ({...diagnostic, filePath: entry.path})),
174
+ ),
173
175
  };
174
176
  });
175
177
  };
@@ -187,7 +189,7 @@ function createMarkSyncSucceededActivity() {
187
189
  status: 'succeeded',
188
190
  lastErrorCode: null,
189
191
  lastErrorMessage: null,
190
- warnings: input.warnings ?? [],
192
+ diagnostics: input.diagnostics ?? [],
191
193
  finishedAt: new Date(),
192
194
  });
193
195
  };
@@ -205,7 +207,7 @@ function createMarkSyncFailedActivity() {
205
207
  status: 'failed',
206
208
  lastErrorCode: input.code,
207
209
  lastErrorMessage: input.message,
208
- warnings: [],
210
+ diagnostics: [],
209
211
  finishedAt: new Date(),
210
212
  });
211
213
  };
@@ -65,7 +65,7 @@ export async function definitionSyncWorkflow(
65
65
  const {paths} = await discoverDefinitionWorkflows(source);
66
66
  const applied = await fetchAndApplyDefinitionWorkflows({...source, paths});
67
67
 
68
- await markDefinitionSyncSucceeded({...source, warnings: applied.warnings});
68
+ await markDefinitionSyncSucceeded({...source, diagnostics: applied.diagnostics});
69
69
 
70
70
  return {
71
71
  sourceRef,
@@ -48,6 +48,7 @@ export const definitionFactory = Factory.define<WorkflowDefinition, DefinitionTr
48
48
 
49
49
  return {
50
50
  id: crypto.randomUUID(),
51
+ workflowId: crypto.randomUUID(),
51
52
  projectId,
52
53
  configPath: `.shipfox/workflows/test-${sequence}.yml`,
53
54
  source: 'manual',
@@ -8,6 +8,7 @@ export async function setup() {
8
8
  createPostgresClient();
9
9
 
10
10
  await db().execute(sql`DROP TABLE IF EXISTS definitions_workflow_definitions CASCADE`);
11
+ await db().execute(sql`DROP TABLE IF EXISTS definitions_workflows CASCADE`);
11
12
  await db().execute(sql`DROP TABLE IF EXISTS definitions_sync_states CASCADE`);
12
13
  await db().execute(sql`DROP TABLE IF EXISTS definitions_outbox CASCADE`);
13
14
  await db().execute(sql`DROP TYPE IF EXISTS definitions_sync_status CASCADE`);
@@ -17,6 +18,7 @@ export async function setup() {
17
18
  await db().execute(sql`DROP TABLE IF EXISTS drizzle.__drizzle_migrations_definitions CASCADE`);
18
19
  await runMigrations(db(), migrationsPath, '__drizzle_migrations_definitions');
19
20
  await db().execute(sql`TRUNCATE TABLE definitions_workflow_definitions CASCADE`);
21
+ await db().execute(sql`TRUNCATE TABLE definitions_workflows CASCADE`);
20
22
  await db().execute(sql`TRUNCATE TABLE definitions_sync_states CASCADE`);
21
23
  await db().execute(sql`TRUNCATE TABLE definitions_outbox CASCADE`);
22
24