@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
@@ -0,0 +1,551 @@
1
+ import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
2
+ import type {
3
+ TriggerDto,
4
+ WorkflowModelSnapshot,
5
+ WorkflowSourceSnapshot,
6
+ } from '@shipfox/api-definitions-dto';
7
+ import {createWorkflowModelSnapshot} from '@shipfox/api-definitions-dto';
8
+ import {
9
+ type IntegrationsModuleClient,
10
+ integrationsInterModuleContract,
11
+ } from '@shipfox/api-integration-core-dto/inter-module';
12
+ import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
13
+ import {isInterModuleKnownError} from '@shipfox/inter-module';
14
+ import {boundedMap} from '@shipfox/node-module';
15
+ import {definitionTriggersFor} from '#db/definition-triggers.js';
16
+ import {findOrCreateWorkflowLineage} from '#db/definitions.js';
17
+ import {recordDefinitionRefResolution} from '#metrics/index.js';
18
+ import type {ValidationDiagnostic} from './entities/validation-diagnostic.js';
19
+ import {
20
+ DefinitionAtRefError,
21
+ type DefinitionAtRefErrorCode,
22
+ DefinitionParseError,
23
+ } from './errors.js';
24
+ import {hasAgentStepIntegrations} from './has-agent-step-integrations.js';
25
+ import {loadIntegrationValidationContext} from './integrations.js';
26
+ import type {ParsedDefinition} from './parse-definition.js';
27
+ import {parseDefinitionWithDiagnostics} from './parse-definition.js';
28
+ import {
29
+ DEFAULT_WORKFLOW_PATH,
30
+ FILE_FETCH_CONCURRENCY,
31
+ MAX_WORKFLOW_FILE_BYTES,
32
+ MAX_WORKFLOW_FILES,
33
+ } from './sync-definitions.js';
34
+ import type {ValidationError} from './validate-definition.js';
35
+
36
+ export interface ResolveDefinitionAtRefParams {
37
+ projectId: string;
38
+ ref: string;
39
+ configPath: string;
40
+ expectedCommit?: string | undefined;
41
+ projects: ProjectsModuleClient;
42
+ agent: AgentInterModuleClient;
43
+ integrations: IntegrationsModuleClient;
44
+ signal?: AbortSignal;
45
+ }
46
+
47
+ export interface ValidationWarning {
48
+ code: string;
49
+ message: string;
50
+ path?: string | undefined;
51
+ }
52
+
53
+ export interface ResolvedDefinitionAtRef {
54
+ workflow: {id: string; configPath: string};
55
+ commit: string;
56
+ model: WorkflowModelSnapshot;
57
+ sourceSnapshot: WorkflowSourceSnapshot;
58
+ triggers: Record<string, TriggerDto>;
59
+ warnings: ValidationWarning[];
60
+ }
61
+
62
+ export interface ListDefinitionsAtRefParams {
63
+ projectId: string;
64
+ ref: string;
65
+ projects: ProjectsModuleClient;
66
+ agent: AgentInterModuleClient;
67
+ integrations: IntegrationsModuleClient;
68
+ signal?: AbortSignal;
69
+ }
70
+
71
+ export interface DefinitionAtRefFile {
72
+ configPath: string;
73
+ name: string | null;
74
+ valid: boolean;
75
+ errors: ValidationError[];
76
+ warnings: ValidationWarning[];
77
+ triggers: Record<string, TriggerDto>;
78
+ }
79
+
80
+ export interface DefinitionsAtRefListing {
81
+ commit: string;
82
+ files: DefinitionAtRefFile[];
83
+ }
84
+
85
+ interface ResolvedProjectSource {
86
+ workspaceId: string;
87
+ connectionId: string;
88
+ externalRepositoryId: string;
89
+ }
90
+
91
+ /**
92
+ * Resolves a workflow definition file at a git ref without persisting it.
93
+ * The ref is pinned to a commit, the file is fetched at that commit, and the
94
+ * result is validated with the sync pipeline. Only the workflow lineage row is
95
+ * created so the dev run can be numbered; no definition row and no outbox
96
+ * event are written.
97
+ */
98
+ export async function resolveDefinitionAtRef(
99
+ params: ResolveDefinitionAtRefParams,
100
+ ): Promise<ResolvedDefinitionAtRef> {
101
+ try {
102
+ return await resolveDefinitionAtRefUnsafe(params);
103
+ } catch (error) {
104
+ if (error instanceof DefinitionAtRefError) {
105
+ recordDefinitionRefResolution(error.code);
106
+ }
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ async function resolveDefinitionAtRefUnsafe(
112
+ params: ResolveDefinitionAtRefParams,
113
+ ): Promise<ResolvedDefinitionAtRef> {
114
+ throwIfAborted(params.signal);
115
+ const source = await requireProjectSource(params.projects, params.projectId, params.signal);
116
+ const commit = await resolveRefToCommit({
117
+ integrations: params.integrations,
118
+ source,
119
+ ref: params.ref,
120
+ signal: params.signal,
121
+ });
122
+ throwIfAborted(params.signal);
123
+ if (params.expectedCommit !== undefined && commit !== params.expectedCommit) {
124
+ throw new DefinitionAtRefError(
125
+ 'ref-moved',
126
+ `Git ref ${params.ref} no longer resolves to the expected commit`,
127
+ {ref: params.ref, expectedCommit: params.expectedCommit},
128
+ );
129
+ }
130
+
131
+ const snapshot = await fetchFileAtCommit({
132
+ integrations: params.integrations,
133
+ source,
134
+ commit,
135
+ ref: params.ref,
136
+ configPath: params.configPath,
137
+ signal: params.signal,
138
+ });
139
+ assertFileSize(snapshot.content, snapshot.path);
140
+
141
+ const parsed = await parseDefinitionAtRef({
142
+ content: snapshot.content,
143
+ agent: params.agent,
144
+ integrations: params.integrations,
145
+ source,
146
+ signal: params.signal,
147
+ });
148
+ throwIfAborted(params.signal);
149
+ const workflowId = await findOrCreateWorkflowLineage({
150
+ projectId: params.projectId,
151
+ configPath: params.configPath,
152
+ });
153
+ recordDefinitionRefResolution('resolved');
154
+
155
+ return {
156
+ workflow: {id: workflowId, configPath: params.configPath},
157
+ commit,
158
+ model: createWorkflowModelSnapshot(parsed.model),
159
+ sourceSnapshot: {content: snapshot.content, format: 'yaml'},
160
+ triggers: definitionTriggersFor(parsed.model),
161
+ warnings: warningsFor(parsed.diagnostics),
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Lists the workflow files at a git ref with their validation state. Applies
167
+ * the sync limits (100 files, 1 MB per file). One invalid file does not fail
168
+ * the listing; it is reported as invalid.
169
+ */
170
+ export async function listDefinitionsAtRef(
171
+ params: ListDefinitionsAtRefParams,
172
+ ): Promise<DefinitionsAtRefListing> {
173
+ try {
174
+ return await listDefinitionsAtRefUnsafe(params);
175
+ } catch (error) {
176
+ if (error instanceof DefinitionAtRefError) {
177
+ recordDefinitionRefResolution(error.code);
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ type ListingEntry =
184
+ | {path: string; content: string; definition: ParsedDefinition}
185
+ | {path: string; errors: ValidationError[]};
186
+
187
+ async function listDefinitionsAtRefUnsafe(
188
+ params: ListDefinitionsAtRefParams,
189
+ ): Promise<DefinitionsAtRefListing> {
190
+ throwIfAborted(params.signal);
191
+ const source = await requireProjectSource(params.projects, params.projectId, params.signal);
192
+ const commit = await resolveRefToCommit({
193
+ integrations: params.integrations,
194
+ source,
195
+ ref: params.ref,
196
+ signal: params.signal,
197
+ });
198
+ throwIfAborted(params.signal);
199
+ const paths = await listWorkflowFilesAtCommit({
200
+ integrations: params.integrations,
201
+ source,
202
+ commit,
203
+ signal: params.signal,
204
+ });
205
+
206
+ const fetched = await boundedMap(
207
+ paths,
208
+ FILE_FETCH_CONCURRENCY,
209
+ (path) =>
210
+ fetchListingFile({
211
+ integrations: params.integrations,
212
+ source,
213
+ commit,
214
+ ref: params.ref,
215
+ path,
216
+ signal: params.signal,
217
+ }),
218
+ {stopOnError: true, signal: params.signal},
219
+ );
220
+ throwIfAborted(params.signal);
221
+ const agentValidationCatalog = await callWithSignal(
222
+ params.agent.getValidationCatalog,
223
+ {},
224
+ params.signal,
225
+ );
226
+ throwIfAborted(params.signal);
227
+ let entries = fetched.map((entry) => {
228
+ throwIfAborted(params.signal);
229
+ return parseListingEntry(entry, {agentValidationCatalog});
230
+ });
231
+
232
+ const needsIntegrationContext = entries.some(
233
+ (entry) => 'definition' in entry && hasAgentStepIntegrations(entry.definition.document),
234
+ );
235
+ if (needsIntegrationContext) {
236
+ const integrationValidationContext = await loadAtRefIntegrationValidationContext({
237
+ integrations: params.integrations,
238
+ source,
239
+ signal: params.signal,
240
+ });
241
+ entries = entries.map((entry) =>
242
+ 'definition' in entry && hasAgentStepIntegrations(entry.definition.document)
243
+ ? parseListingEntry(
244
+ {path: entry.path, content: entry.content},
245
+ {agentValidationCatalog, integrationValidationContext},
246
+ )
247
+ : entry,
248
+ );
249
+ }
250
+
251
+ throwIfAborted(params.signal);
252
+ recordDefinitionRefResolution('resolved');
253
+ return {commit, files: entries.map((entry) => listingFileFor(entry))};
254
+ }
255
+
256
+ async function requireProjectSource(
257
+ projects: ProjectsModuleClient,
258
+ projectId: string,
259
+ signal: AbortSignal | undefined,
260
+ ): Promise<ResolvedProjectSource> {
261
+ const {project} = await callWithSignal(projects.getProjectById, {projectId}, signal);
262
+ if (project === null) {
263
+ throw new DefinitionAtRefError('project-not-found', `Project not found: ${projectId}`, {
264
+ projectId,
265
+ });
266
+ }
267
+ return {
268
+ workspaceId: project.workspaceId,
269
+ connectionId: project.sourceConnectionId,
270
+ externalRepositoryId: project.sourceExternalRepositoryId,
271
+ };
272
+ }
273
+
274
+ async function resolveRefToCommit(params: {
275
+ integrations: IntegrationsModuleClient;
276
+ source: ResolvedProjectSource;
277
+ ref: string;
278
+ signal: AbortSignal | undefined;
279
+ }): Promise<string> {
280
+ try {
281
+ const resolved = await callWithSignal(
282
+ params.integrations.resolveSourceRef,
283
+ {
284
+ workspaceId: params.source.workspaceId,
285
+ connectionId: params.source.connectionId,
286
+ externalRepositoryId: params.source.externalRepositoryId,
287
+ ref: params.ref,
288
+ },
289
+ params.signal,
290
+ );
291
+ return resolved.commit;
292
+ } catch (error) {
293
+ if (isInterModuleKnownError(integrationsInterModuleContract.methods.resolveSourceRef, error)) {
294
+ if (error.code === 'ref-not-found') {
295
+ throw new DefinitionAtRefError('ref-not-found', `Git ref not found: ${params.ref}`, {
296
+ ref: params.ref,
297
+ });
298
+ }
299
+ if (error.code === 'ref-invalid') {
300
+ throw new DefinitionAtRefError(
301
+ 'ref-invalid',
302
+ `Git ref is not a resolvable branch or tag name: ${params.ref}`,
303
+ {ref: params.ref},
304
+ );
305
+ }
306
+ throw sourceUnavailable(error, 'The source repository is unavailable');
307
+ }
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ async function listWorkflowFilesAtCommit(params: {
313
+ integrations: IntegrationsModuleClient;
314
+ source: ResolvedProjectSource;
315
+ commit: string;
316
+ signal: AbortSignal | undefined;
317
+ }): Promise<string[]> {
318
+ let page: Awaited<ReturnType<IntegrationsModuleClient['listSourceFiles']>>;
319
+ try {
320
+ page = await callWithSignal(
321
+ params.integrations.listSourceFiles,
322
+ {
323
+ workspaceId: params.source.workspaceId,
324
+ connectionId: params.source.connectionId,
325
+ externalRepositoryId: params.source.externalRepositoryId,
326
+ ref: params.commit,
327
+ prefix: DEFAULT_WORKFLOW_PATH,
328
+ limit: MAX_WORKFLOW_FILES,
329
+ },
330
+ params.signal,
331
+ );
332
+ } catch (error) {
333
+ throwIfAborted(params.signal);
334
+ throw sourceUnavailable(error, 'The workflow files at the ref could not be listed');
335
+ }
336
+ if (page.nextCursor) {
337
+ throw new DefinitionAtRefError(
338
+ 'too-many-files',
339
+ `More than ${MAX_WORKFLOW_FILES} workflow files were found`,
340
+ );
341
+ }
342
+ return page.files
343
+ .filter((file) => file.path.endsWith('.yml') || file.path.endsWith('.yaml'))
344
+ .map((file) => file.path);
345
+ }
346
+
347
+ async function fetchFileAtCommit(params: {
348
+ integrations: IntegrationsModuleClient;
349
+ source: ResolvedProjectSource;
350
+ commit: string;
351
+ ref: string;
352
+ configPath: string;
353
+ signal: AbortSignal | undefined;
354
+ }): Promise<{path: string; content: string}> {
355
+ try {
356
+ return await callWithSignal(
357
+ params.integrations.fetchSourceFile,
358
+ {
359
+ workspaceId: params.source.workspaceId,
360
+ connectionId: params.source.connectionId,
361
+ externalRepositoryId: params.source.externalRepositoryId,
362
+ ref: params.commit,
363
+ path: params.configPath,
364
+ },
365
+ params.signal,
366
+ );
367
+ } catch (error) {
368
+ throwIfAborted(params.signal);
369
+ if (isInterModuleKnownError(integrationsInterModuleContract.methods.fetchSourceFile, error)) {
370
+ if (error.code === 'provider-failure' && error.details.reason === 'file-not-found') {
371
+ throw new DefinitionAtRefError(
372
+ 'file-not-found',
373
+ `Workflow file not found at ${params.ref}: ${params.configPath}`,
374
+ {ref: params.ref, configPath: params.configPath},
375
+ );
376
+ }
377
+ throw sourceUnavailable(error, 'The workflow file at the ref could not be fetched');
378
+ }
379
+ throw error;
380
+ }
381
+ }
382
+
383
+ function assertFileSize(content: string, path: string): void {
384
+ if (Buffer.byteLength(content, 'utf8') > MAX_WORKFLOW_FILE_BYTES) {
385
+ throw new DefinitionAtRefError(
386
+ 'content-too-large',
387
+ `Workflow file is larger than ${MAX_WORKFLOW_FILE_BYTES} bytes: ${path}`,
388
+ {configPath: path},
389
+ );
390
+ }
391
+ }
392
+
393
+ async function parseDefinitionAtRef(params: {
394
+ content: string;
395
+ agent: AgentInterModuleClient;
396
+ integrations: IntegrationsModuleClient;
397
+ source: ResolvedProjectSource;
398
+ signal: AbortSignal | undefined;
399
+ }): Promise<ParsedDefinition> {
400
+ const agentValidationCatalog = await callWithSignal(
401
+ params.agent.getValidationCatalog,
402
+ {},
403
+ params.signal,
404
+ );
405
+ throwIfAborted(params.signal);
406
+ const firstPass = parseWorkflowDefinition(params.content, {agentValidationCatalog});
407
+ if (!hasAgentStepIntegrations(firstPass.document)) return firstPass;
408
+
409
+ const integrationValidationContext = await loadAtRefIntegrationValidationContext({
410
+ integrations: params.integrations,
411
+ source: params.source,
412
+ signal: params.signal,
413
+ });
414
+ return parseWorkflowDefinition(params.content, {
415
+ agentValidationCatalog,
416
+ integrationValidationContext,
417
+ });
418
+ }
419
+
420
+ function parseWorkflowDefinition(
421
+ content: string,
422
+ options: Parameters<typeof parseDefinitionWithDiagnostics>[1],
423
+ ): ParsedDefinition {
424
+ try {
425
+ return parseDefinitionWithDiagnostics(content, options);
426
+ } catch (error) {
427
+ if (error instanceof DefinitionParseError) {
428
+ throw new DefinitionAtRefError(
429
+ 'invalid-definition',
430
+ `Invalid workflow definition: ${error.message}`,
431
+ {errors: (error.details ?? []) as ValidationError[]},
432
+ );
433
+ }
434
+ throw error;
435
+ }
436
+ }
437
+
438
+ async function fetchListingFile(params: {
439
+ integrations: IntegrationsModuleClient;
440
+ source: ResolvedProjectSource;
441
+ commit: string;
442
+ ref: string;
443
+ path: string;
444
+ signal: AbortSignal | undefined;
445
+ }): Promise<{path: string; content: string} | {path: string; errors: ValidationError[]}> {
446
+ try {
447
+ const snapshot = await fetchFileAtCommit({
448
+ integrations: params.integrations,
449
+ source: params.source,
450
+ commit: params.commit,
451
+ ref: params.ref,
452
+ configPath: params.path,
453
+ signal: params.signal,
454
+ });
455
+ assertFileSize(snapshot.content, snapshot.path);
456
+ return {path: snapshot.path, content: snapshot.content};
457
+ } catch (error) {
458
+ if (error instanceof DefinitionAtRefError && isPerFileListingError(error.code)) {
459
+ return {path: params.path, errors: [{message: error.message}]};
460
+ }
461
+ throw error;
462
+ }
463
+ }
464
+
465
+ function parseListingEntry(
466
+ entry: {path: string; content: string} | {path: string; errors: ValidationError[]},
467
+ options: Parameters<typeof parseDefinitionWithDiagnostics>[1],
468
+ ): ListingEntry {
469
+ if ('errors' in entry) return entry;
470
+ try {
471
+ const definition = parseDefinitionWithDiagnostics(entry.content, options);
472
+ return {path: entry.path, content: entry.content, definition};
473
+ } catch (error) {
474
+ if (error instanceof DefinitionParseError) {
475
+ return {path: entry.path, errors: (error.details ?? []) as ValidationError[]};
476
+ }
477
+ throw error;
478
+ }
479
+ }
480
+
481
+ function listingFileFor(entry: ListingEntry): DefinitionAtRefFile {
482
+ if ('definition' in entry) {
483
+ return {
484
+ configPath: entry.path,
485
+ name: entry.definition.document.name,
486
+ valid: true,
487
+ errors: [],
488
+ warnings: warningsFor(entry.definition.diagnostics),
489
+ triggers: definitionTriggersFor(entry.definition.model),
490
+ };
491
+ }
492
+ return {
493
+ configPath: entry.path,
494
+ name: null,
495
+ valid: false,
496
+ errors: entry.errors,
497
+ warnings: [],
498
+ triggers: {},
499
+ };
500
+ }
501
+
502
+ function warningsFor(diagnostics: readonly ValidationDiagnostic[]): ValidationWarning[] {
503
+ return diagnostics
504
+ .filter((diagnostic) => diagnostic.severity === 'warning')
505
+ .map((diagnostic) => ({
506
+ code: diagnostic.code,
507
+ message: diagnostic.message,
508
+ ...(diagnostic.path === undefined ? {} : {path: diagnostic.path}),
509
+ }));
510
+ }
511
+
512
+ async function loadAtRefIntegrationValidationContext(params: {
513
+ integrations: IntegrationsModuleClient;
514
+ source: ResolvedProjectSource;
515
+ signal: AbortSignal | undefined;
516
+ }) {
517
+ try {
518
+ return await loadIntegrationValidationContext(
519
+ params.integrations,
520
+ params.source.workspaceId,
521
+ params.source.connectionId,
522
+ params.signal,
523
+ );
524
+ } catch (error) {
525
+ throwIfAborted(params.signal);
526
+ throw sourceUnavailable(error, 'Integration validation context is unavailable');
527
+ }
528
+ }
529
+
530
+ function isPerFileListingError(code: DefinitionAtRefErrorCode): boolean {
531
+ return code === 'file-not-found' || code === 'content-too-large' || code === 'invalid-definition';
532
+ }
533
+
534
+ function callWithSignal<Input, Output>(
535
+ method: (input: Input, options?: {signal?: AbortSignal}) => Promise<Output>,
536
+ input: Input,
537
+ signal: AbortSignal | undefined,
538
+ ): Promise<Output> {
539
+ return signal === undefined ? method(input) : method(input, {signal});
540
+ }
541
+
542
+ function throwIfAborted(signal: AbortSignal | undefined): void {
543
+ if (signal?.aborted) throw signal.reason ?? new Error('Operation aborted');
544
+ }
545
+
546
+ function sourceUnavailable(error: unknown, message: string): DefinitionAtRefError {
547
+ return new DefinitionAtRefError(
548
+ 'source-unavailable',
549
+ `${message}: ${error instanceof Error ? error.message : String(error)}`,
550
+ );
551
+ }
@@ -79,6 +79,11 @@ const integrationValidationContext = {
79
79
  workspaceConnectionSnapshot: new Map([
80
80
  ['github-main', {id: 'connection-1', provider: 'github', capabilities: ['agent_tools']}],
81
81
  ]),
82
+ eventCatalogs: new Map([
83
+ ['github', new Set(['push', 'pull_request.opened'])],
84
+ ['webhook', new Set(['received'])],
85
+ ]),
86
+ fixedEventProviders: new Set(['webhook']),
82
87
  } satisfies IntegrationValidationContext;
83
88
 
84
89
  function sourceControl(
@@ -243,7 +248,7 @@ describe('fetchAndParseWorkflows', () => {
243
248
  expect(result[0]?.name).toBe('CI');
244
249
  expect(result[0]?.path).toBe('.shipfox/workflows/ci.yml');
245
250
  expect(result[0]?.contentHash).toMatch(LOWERCASE_SHA256_HEX_RE);
246
- expect(result[0]?.warnings).toEqual([]);
251
+ expect(result[0]?.diagnostics).toEqual([]);
247
252
  });
248
253
 
249
254
  it('keeps warning-only definitions available to the sync path', async () => {
@@ -262,15 +267,69 @@ describe('fetchAndParseWorkflows', () => {
262
267
  }),
263
268
  });
264
269
 
265
- expect(result[0]?.warnings).toMatchObject([
270
+ expect(result[0]?.diagnostics).toMatchObject([
266
271
  {
267
272
  code: 're-evaluating-command',
268
273
  path: 'jobs.build.steps.0.run',
274
+ severity: 'warning',
269
275
  },
270
276
  ]);
271
277
  expect(result[0]?.definition.model.jobs[0]?.steps).toHaveLength(1);
272
278
  });
273
279
 
280
+ it('keeps definitions with inert trigger-scoped errors available to the sync path', async () => {
281
+ const brokenTriggerPath = '.shipfox/workflows/broken-trigger.yml';
282
+ const validTriggerPath = '.shipfox/workflows/valid-trigger.yml';
283
+ const brokenTriggerYaml = `
284
+ name: Broken trigger
285
+ runner: ubuntu-latest
286
+ triggers:
287
+ nightly:
288
+ source: cron
289
+ event: tick
290
+ config:
291
+ schedule: "not a cron"
292
+ on_demand:
293
+ source: manual
294
+ event: fire
295
+ jobs:
296
+ build:
297
+ steps:
298
+ - run: pnpm test
299
+ `;
300
+
301
+ const fetchFile = vi.fn(({path}: {path: string}) =>
302
+ Promise.resolve({
303
+ path,
304
+ ref: 'main',
305
+ content: path === brokenTriggerPath ? brokenTriggerYaml : validYaml,
306
+ }),
307
+ );
308
+ const result = await fetchAndParseWorkflows({
309
+ ...baseContext,
310
+ ref: 'main',
311
+ paths: [brokenTriggerPath, validTriggerPath],
312
+ sourceControl: sourceControl({fetchFile}),
313
+ });
314
+
315
+ expect(result).toHaveLength(2);
316
+ expect(result[0]?.diagnostics).toEqual([
317
+ {
318
+ code: 'invalid-cron-schedule',
319
+ message: 'Cron trigger schedule must be a valid 5-field cron expression.',
320
+ path: 'triggers.nightly.config.schedule',
321
+ severity: 'error',
322
+ },
323
+ ]);
324
+ // The broken cron trigger is inert; the manual trigger stays active.
325
+ expect(result[0]?.definition.model.triggers.map((trigger) => trigger.key)).toEqual([
326
+ 'on_demand',
327
+ ]);
328
+ expect(result[1]?.path).toBe(validTriggerPath);
329
+ expect(result[1]?.diagnostics).toEqual([]);
330
+ expect(result[1]?.definition.model.jobs[0]?.steps).toHaveLength(1);
331
+ });
332
+
274
333
  it('produces stable content hashes for identical content', async () => {
275
334
  const sourceControlA = sourceControl();
276
335
  const sourceControlB = sourceControl();
@@ -5,12 +5,12 @@ import {isInterModuleKnownError} from '@shipfox/inter-module';
5
5
  import {boundedMap} from '@shipfox/node-module';
6
6
  import type {IntegrationValidationContext} from './entities/integration-context.js';
7
7
  import type {DefinitionSyncErrorCode} from './entities/sync-state.js';
8
- import type {ValidationWarning} from './entities/validation-warning.js';
8
+ import type {ValidationDiagnostic} from './entities/validation-diagnostic.js';
9
9
  import type {WorkflowDefinitionPayload} from './entities/workflow-definition.js';
10
10
  import {DefinitionParseError, DefinitionSyncPermanentError} from './errors.js';
11
11
  import {hasAgentStepIntegrations} from './has-agent-step-integrations.js';
12
12
  import type {DefinitionsSourceControl} from './integrations.js';
13
- import {parseDefinitionWithWarnings, stripDefinitionWarnings} from './parse-definition.js';
13
+ import {parseDefinitionWithDiagnostics, stripDefinitionDiagnostics} from './parse-definition.js';
14
14
 
15
15
  export const DEFAULT_WORKFLOW_PATH = '.shipfox/workflows/';
16
16
  export const MAX_WORKFLOW_FILES = 100;
@@ -80,7 +80,7 @@ export interface ParsedWorkflow {
80
80
  name: string;
81
81
  definition: WorkflowDefinitionPayload;
82
82
  contentHash: string;
83
- warnings: ValidationWarning[];
83
+ diagnostics: ValidationDiagnostic[];
84
84
  }
85
85
 
86
86
  export interface FetchAndParseWorkflowsParams extends SyncSourceContext {
@@ -154,10 +154,10 @@ function parseWorkflowSnapshot(params: {
154
154
  try {
155
155
  const definition =
156
156
  params.integrationValidationContext === undefined
157
- ? parseDefinitionWithWarnings(params.content, {
157
+ ? parseDefinitionWithDiagnostics(params.content, {
158
158
  agentValidationCatalog: params.agentValidationCatalog,
159
159
  })
160
- : parseDefinitionWithWarnings(params.content, {
160
+ : parseDefinitionWithDiagnostics(params.content, {
161
161
  agentValidationCatalog: params.agentValidationCatalog,
162
162
  integrationValidationContext: params.integrationValidationContext,
163
163
  });
@@ -165,9 +165,9 @@ function parseWorkflowSnapshot(params: {
165
165
  return {
166
166
  path: params.path,
167
167
  name: definition.document.name,
168
- definition: stripDefinitionWarnings(definition),
168
+ definition: stripDefinitionDiagnostics(definition),
169
169
  contentHash,
170
- warnings: definition.warnings,
170
+ diagnostics: definition.diagnostics,
171
171
  };
172
172
  } catch (error) {
173
173
  if (error instanceof DefinitionParseError) {