@shipfox/api-definitions 10.2.0 → 12.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 (150) hide show
  1. package/.turbo/turbo-build.log +13 -12
  2. package/CHANGELOG.md +91 -0
  3. package/dist/core/entities/index.d.ts +1 -0
  4. package/dist/core/entities/index.d.ts.map +1 -1
  5. package/dist/core/entities/index.js +1 -0
  6. package/dist/core/entities/index.js.map +1 -1
  7. package/dist/core/entities/sync-state.d.ts +4 -0
  8. package/dist/core/entities/sync-state.d.ts.map +1 -1
  9. package/dist/core/entities/sync-state.js +10 -0
  10. package/dist/core/entities/sync-state.js.map +1 -1
  11. package/dist/core/entities/validation-warning.d.ts +6 -0
  12. package/dist/core/entities/validation-warning.d.ts.map +1 -0
  13. package/dist/core/entities/validation-warning.js +3 -0
  14. package/dist/core/entities/validation-warning.js.map +1 -0
  15. package/dist/core/parse-definition.d.ts +9 -7
  16. package/dist/core/parse-definition.d.ts.map +1 -1
  17. package/dist/core/parse-definition.js +14 -2
  18. package/dist/core/parse-definition.js.map +1 -1
  19. package/dist/core/sync-definitions.d.ts +2 -0
  20. package/dist/core/sync-definitions.d.ts.map +1 -1
  21. package/dist/core/sync-definitions.js +6 -5
  22. package/dist/core/sync-definitions.js.map +1 -1
  23. package/dist/core/validate-definition.d.ts +9 -5
  24. package/dist/core/validate-definition.d.ts.map +1 -1
  25. package/dist/core/validate-definition.js +23 -2
  26. package/dist/core/validate-definition.js.map +1 -1
  27. package/dist/core/workflow-model/index.d.ts +1 -1
  28. package/dist/core/workflow-model/index.d.ts.map +1 -1
  29. package/dist/core/workflow-model/index.js.map +1 -1
  30. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +3 -1
  31. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
  32. package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
  33. package/dist/core/workflow-model/normalize-if-condition.d.ts +0 -1
  34. package/dist/core/workflow-model/normalize-if-condition.d.ts.map +1 -1
  35. package/dist/core/workflow-model/normalize-if-condition.js +0 -1
  36. package/dist/core/workflow-model/normalize-if-condition.js.map +1 -1
  37. package/dist/core/workflow-model/normalize-job-checkout.d.ts +15 -3
  38. package/dist/core/workflow-model/normalize-job-checkout.d.ts.map +1 -1
  39. package/dist/core/workflow-model/normalize-job-checkout.js +83 -4
  40. package/dist/core/workflow-model/normalize-job-checkout.js.map +1 -1
  41. package/dist/core/workflow-model/normalize-job-listening.js +0 -1
  42. package/dist/core/workflow-model/normalize-job-listening.js.map +1 -1
  43. package/dist/core/workflow-model/normalize-job-success.d.ts.map +1 -1
  44. package/dist/core/workflow-model/normalize-job-success.js +0 -1
  45. package/dist/core/workflow-model/normalize-job-success.js.map +1 -1
  46. package/dist/core/workflow-model/normalize-jobs.d.ts.map +1 -1
  47. package/dist/core/workflow-model/normalize-jobs.js +310 -45
  48. package/dist/core/workflow-model/normalize-jobs.js.map +1 -1
  49. package/dist/core/workflow-model/normalize-step-gate.js +0 -1
  50. package/dist/core/workflow-model/normalize-step-gate.js.map +1 -1
  51. package/dist/core/workflow-model/normalize-triggers.d.ts.map +1 -1
  52. package/dist/core/workflow-model/normalize-triggers.js +0 -1
  53. package/dist/core/workflow-model/normalize-triggers.js.map +1 -1
  54. package/dist/core/workflow-model/normalize-workflow-document.d.ts +3 -0
  55. package/dist/core/workflow-model/normalize-workflow-document.d.ts.map +1 -1
  56. package/dist/core/workflow-model/normalize-workflow-document.js +34 -4
  57. package/dist/core/workflow-model/normalize-workflow-document.js.map +1 -1
  58. package/dist/core/workflow-model/parse-interpolation-field.d.ts +1 -1
  59. package/dist/core/workflow-model/parse-interpolation-field.d.ts.map +1 -1
  60. package/dist/core/workflow-model/parse-interpolation-field.js +27 -57
  61. package/dist/core/workflow-model/parse-interpolation-field.js.map +1 -1
  62. package/dist/core/workflow-model/validate-literal-name.d.ts +12 -0
  63. package/dist/core/workflow-model/validate-literal-name.d.ts.map +1 -0
  64. package/dist/core/workflow-model/validate-literal-name.js +36 -0
  65. package/dist/core/workflow-model/validate-literal-name.js.map +1 -0
  66. package/dist/core/workflow-model/validate-predicate-expression.d.ts +1 -2
  67. package/dist/core/workflow-model/validate-predicate-expression.d.ts.map +1 -1
  68. package/dist/core/workflow-model/validate-predicate-expression.js +31 -90
  69. package/dist/core/workflow-model/validate-predicate-expression.js.map +1 -1
  70. package/dist/core/workflow-model/validation-issue.d.ts +2 -1
  71. package/dist/core/workflow-model/validation-issue.d.ts.map +1 -1
  72. package/dist/core/workflow-model/validation-issue.js +8 -2
  73. package/dist/core/workflow-model/validation-issue.js.map +1 -1
  74. package/dist/core/workflow-model/workflow-field-label.d.ts.map +1 -1
  75. package/dist/core/workflow-model/workflow-field-label.js +16 -2
  76. package/dist/core/workflow-model/workflow-field-label.js.map +1 -1
  77. package/dist/db/db.d.ts +38 -0
  78. package/dist/db/db.d.ts.map +1 -1
  79. package/dist/db/schema/sync-states.d.ts +19 -0
  80. package/dist/db/schema/sync-states.d.ts.map +1 -1
  81. package/dist/db/schema/sync-states.js +3 -1
  82. package/dist/db/schema/sync-states.js.map +1 -1
  83. package/dist/db/sync-states.d.ts +2 -1
  84. package/dist/db/sync-states.d.ts.map +1 -1
  85. package/dist/db/sync-states.js +4 -0
  86. package/dist/db/sync-states.js.map +1 -1
  87. package/dist/presentation/dto/definition.d.ts.map +1 -1
  88. package/dist/presentation/dto/definition.js +2 -1
  89. package/dist/presentation/dto/definition.js.map +1 -1
  90. package/dist/presentation/routes/project-access.d.ts +1 -0
  91. package/dist/presentation/routes/project-access.d.ts.map +1 -1
  92. package/dist/presentation/routes/validate-definition.d.ts.map +1 -1
  93. package/dist/presentation/routes/validate-definition.js +5 -3
  94. package/dist/presentation/routes/validate-definition.js.map +1 -1
  95. package/dist/temporal/activities/sync-activities.d.ts +6 -2
  96. package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
  97. package/dist/temporal/activities/sync-activities.js +8 -1
  98. package/dist/temporal/activities/sync-activities.js.map +1 -1
  99. package/dist/temporal/workflows/definition-sync.js +4 -1
  100. package/dist/temporal/workflows/definition-sync.js.map +1 -1
  101. package/dist/temporal/workflows/index.bundle.js +9029 -3
  102. package/dist/tsconfig.test.tsbuildinfo +1 -1
  103. package/drizzle/0002_lumpy_ben_parker.sql +1 -0
  104. package/drizzle/meta/0002_snapshot.json +534 -0
  105. package/drizzle/meta/_journal.json +7 -0
  106. package/package.json +14 -14
  107. package/src/core/entities/index.ts +1 -0
  108. package/src/core/entities/sync-state.ts +23 -0
  109. package/src/core/entities/validation-warning.ts +5 -0
  110. package/src/core/parse-definition-warnings.test.ts +33 -0
  111. package/src/core/parse-definition.test.ts +7 -1
  112. package/src/core/parse-definition.ts +28 -10
  113. package/src/core/sync-definitions.test.ts +39 -0
  114. package/src/core/sync-definitions.ts +14 -4
  115. package/src/core/validate-definition.test.ts +128 -0
  116. package/src/core/validate-definition.ts +39 -8
  117. package/src/core/workflow-model/README.md +2 -2
  118. package/src/core/workflow-model/index.ts +1 -0
  119. package/src/core/workflow-model/invalid-workflow-model-error.ts +8 -2
  120. package/src/core/workflow-model/normalize-if-condition.ts +0 -2
  121. package/src/core/workflow-model/normalize-job-checkout.ts +105 -8
  122. package/src/core/workflow-model/normalize-job-listening.ts +0 -1
  123. package/src/core/workflow-model/normalize-job-success.ts +0 -1
  124. package/src/core/workflow-model/normalize-jobs.ts +330 -49
  125. package/src/core/workflow-model/normalize-step-gate.ts +0 -1
  126. package/src/core/workflow-model/normalize-triggers.ts +0 -1
  127. package/src/core/workflow-model/normalize-workflow-document.test.ts +759 -185
  128. package/src/core/workflow-model/normalize-workflow-document.ts +29 -4
  129. package/src/core/workflow-model/parse-interpolation-field.ts +50 -79
  130. package/src/core/workflow-model/validate-literal-name.ts +64 -0
  131. package/src/core/workflow-model/validate-predicate-expression.test.ts +153 -15
  132. package/src/core/workflow-model/validate-predicate-expression.ts +52 -120
  133. package/src/core/workflow-model/validation-issue.ts +4 -0
  134. package/src/core/workflow-model/workflow-field-label.ts +16 -2
  135. package/src/core/workflow-yaml/README.md +2 -2
  136. package/src/db/schema/sync-states.ts +4 -1
  137. package/src/db/sync-states.test.ts +57 -0
  138. package/src/db/sync-states.ts +6 -0
  139. package/src/presentation/dto/definition.ts +1 -0
  140. package/src/presentation/routes/create-definition.test.ts +3 -3
  141. package/src/presentation/routes/get-definition.test.ts +1 -1
  142. package/src/presentation/routes/list-definitions.test.ts +2 -1
  143. package/src/presentation/routes/validate-definition.test.ts +1 -0
  144. package/src/presentation/routes/validate-definition.ts +7 -1
  145. package/src/temporal/activities/sync-activities.test.ts +16 -0
  146. package/src/temporal/activities/sync-activities.ts +18 -3
  147. package/src/temporal/workflows/definition-sync.ts +1 -1
  148. package/test/agent-validation-catalog.ts +17 -0
  149. package/test/fixtures/valid-listening-job.yml +2 -1
  150. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,17 +1,16 @@
1
1
  import {
2
2
  type AvailabilitySite,
3
- analyzeContextRootKeyAccess,
4
- availabilitySites,
5
3
  createWorkflowExpression,
4
+ type ExpressionType,
6
5
  type ExpressionTypeEnvironment,
7
6
  extractExactContextRoots,
8
7
  getWorkflowContextDefinition,
9
- getWorkflowContextTypeEnvironment,
8
+ getWorkflowPredicateContextRoots,
9
+ getWorkflowPredicateFieldMinimumFillTarget,
10
+ getWorkflowPredicateFieldTypeEnvironment,
10
11
  InvalidWorkflowExpressionError,
11
12
  predicateSourceIsBooleanShaped,
12
- resolveContextRootAvailability,
13
13
  resolveContextRootHost,
14
- routeExpression,
15
14
  validateServerEvaluable,
16
15
  type WorkflowContextName,
17
16
  type WorkflowExpression,
@@ -30,7 +29,6 @@ import {workflowFieldLabel} from './workflow-field-label.js';
30
29
  export function validatePredicateExpression(params: {
31
30
  field: WorkflowPredicateField;
32
31
  source: string;
33
- site: AvailabilitySite;
34
32
  path: readonly WorkflowModelValidationIssuePathSegment[];
35
33
  invalidCode: WorkflowModelValidationIssueCode;
36
34
  invalidMessage: string;
@@ -38,6 +36,7 @@ export function validatePredicateExpression(params: {
38
36
  allowedJobReferences?: ReadonlySet<string>;
39
37
  typeOverlay?: ExpressionTypeEnvironment | undefined;
40
38
  }): WorkflowExpression | undefined {
39
+ const site = getWorkflowPredicateFieldMinimumFillTarget(params.field);
41
40
  const syntaxExpression = createSyntaxExpression(params);
42
41
  if (syntaxExpression === undefined) return undefined;
43
42
 
@@ -56,6 +55,7 @@ export function validatePredicateExpression(params: {
56
55
  ...serverEvaluability.violations.map((violation) =>
57
56
  runnerContextInServerPredicateIssue({
58
57
  ...params,
58
+ site,
59
59
  contextRoots,
60
60
  runnerRoots: violation.runnerRoots,
61
61
  }),
@@ -64,35 +64,18 @@ export function validatePredicateExpression(params: {
64
64
  return undefined;
65
65
  }
66
66
 
67
- const varsRoots = knownRoots.filter((root) => root === 'vars');
68
- if (varsRoots.length > 0) {
69
- params.issues.push(varsContextInServerPredicateIssue({...params, contextRoots}));
70
- return undefined;
71
- }
72
-
73
- const unavailableRoots = knownRoots.filter((root) => !isRootAvailableAt(root, params.site));
67
+ const predicateContextRoots = getWorkflowPredicateContextRoots(params.field);
68
+ const unavailableRoots = knownRoots.filter(
69
+ (root) => !predicateContextRoots.includes(root as (typeof predicateContextRoots)[number]),
70
+ );
74
71
  if (unavailableRoots.length > 0) {
75
72
  params.issues.push(
76
- unavailablePredicateContextIssue({...params, contextRoots, unavailableRoots}),
77
- );
78
- return undefined;
79
- }
80
-
81
- const route = routeExpression(syntaxExpression);
82
- if (route.fillTarget !== 'runner-fill' && !isSiteAtOrAfter(params.site, route.fillTarget)) {
83
- const unavailableRoots = unavailableRouteReferences(syntaxExpression);
84
- params.issues.push(
85
- unavailableRoots.length > 0
86
- ? unavailablePredicateContextIssue({
87
- ...params,
88
- contextRoots,
89
- unavailableRoots,
90
- })
91
- : routeUnavailablePredicateContextIssue({
92
- ...params,
93
- contextRoots,
94
- fillTarget: route.fillTarget,
95
- }),
73
+ unavailablePredicateContextIssue({
74
+ ...params,
75
+ site,
76
+ contextRoots,
77
+ unavailableRoots,
78
+ }),
96
79
  );
97
80
  return undefined;
98
81
  }
@@ -112,7 +95,11 @@ export function validatePredicateExpression(params: {
112
95
  return undefined;
113
96
  }
114
97
 
115
- if (params.typeOverlay === undefined && knownRoots.some((root) => hasSyntaxOnlyCheckMode(root))) {
98
+ if (
99
+ params.typeOverlay === undefined &&
100
+ knownRoots.length > 0 &&
101
+ knownRoots.every((root) => hasSyntaxOnlyCheckMode(root))
102
+ ) {
116
103
  if (
117
104
  isWorkflowFilterPredicateField(params.field) &&
118
105
  !predicateSourceIsBooleanShaped(syntaxExpression.source)
@@ -137,7 +124,7 @@ export function validatePredicateExpression(params: {
137
124
  mode: 'typed',
138
125
  // `undefined` preserves the legacy syntax-only path above. `{}` means
139
126
  // callers intentionally requested typed checking with the standard roots.
140
- typeEnvironment: mergeTypeEnvironments(knownRoots, params.typeOverlay),
127
+ typeEnvironment: mergeTypeEnvironments(params.field, knownRoots, params.typeOverlay),
141
128
  expectedResultType: 'bool',
142
129
  },
143
130
  });
@@ -241,27 +228,6 @@ function runnerContextInServerPredicateIssue(params: {
241
228
  });
242
229
  }
243
230
 
244
- function varsContextInServerPredicateIssue(params: {
245
- field: WorkflowPredicateField;
246
- source: string;
247
- site: AvailabilitySite;
248
- path: readonly WorkflowModelValidationIssuePathSegment[];
249
- contextRoots: readonly string[];
250
- }): WorkflowModelValidationIssue {
251
- return issue({
252
- code: 'vars-context-in-server-predicate',
253
- message: `${fieldLabel(params.field)} cannot reference vars because predicate evaluation does not include workflow variables.`,
254
- path: params.path,
255
- details: {
256
- field: params.field,
257
- source: params.source,
258
- contextRoots: params.contextRoots,
259
- rejectedRoots: ['vars'],
260
- site: params.site,
261
- },
262
- });
263
- }
264
-
265
231
  function unavailablePredicateContextIssue(params: {
266
232
  field: WorkflowPredicateField;
267
233
  source: string;
@@ -276,9 +242,9 @@ function unavailablePredicateContextIssue(params: {
276
242
  params.unavailableRoots,
277
243
  )} ${formatList(params.unavailableRoots)} that ${availabilityVerb(
278
244
  params.unavailableRoots,
279
- )} not available at ${describeAvailabilitySite(params.site)}. ${params.unavailableRoots
280
- .map(unavailableRootAvailabilityMessage)
281
- .join(' ')}`,
245
+ )} not supplied when ${fieldLabel(params.field)} is evaluated at ${describeAvailabilitySite(
246
+ params.site,
247
+ )}.`,
282
248
  path: params.path,
283
249
  details: {
284
250
  field: params.field,
@@ -290,60 +256,13 @@ function unavailablePredicateContextIssue(params: {
290
256
  });
291
257
  }
292
258
 
293
- function routeUnavailablePredicateContextIssue(params: {
294
- field: WorkflowPredicateField;
295
- source: string;
296
- site: AvailabilitySite;
297
- path: readonly WorkflowModelValidationIssuePathSegment[];
298
- contextRoots: readonly string[];
299
- fillTarget: AvailabilitySite;
300
- }): WorkflowModelValidationIssue {
301
- return issue({
302
- code: 'context-unavailable-at-predicate-site',
303
- message: `${fieldLabel(params.field)} requires context filled at ${describeAvailabilitySite(
304
- params.fillTarget,
305
- )}, but it is evaluated at ${describeAvailabilitySite(params.site)}.`,
306
- path: params.path,
307
- details: {
308
- field: params.field,
309
- source: params.source,
310
- contextRoots: params.contextRoots,
311
- fillTarget: params.fillTarget,
312
- site: params.site,
313
- },
314
- });
315
- }
316
-
317
- function isRootAvailableAt(root: string, site: AvailabilitySite): boolean {
318
- const availability = resolveContextRootAvailability(root);
319
- if (availability === undefined) return false;
320
-
321
- return availabilitySites.indexOf(availability) <= availabilitySites.indexOf(site);
322
- }
323
-
324
- function unavailableRootAvailabilityMessage(root: string): string {
325
- if (root === 'execution.failed') {
326
- return '"execution.failed" becomes available at step dispatch.';
327
- }
328
-
329
- const availability = resolveContextRootAvailability(root);
330
- if (availability === undefined) return `"${root}" is not available at any server site.`;
331
- return `"${root}" becomes available at ${describeAvailabilitySite(availability)}.`;
332
- }
333
-
334
- function unavailableRouteReferences(expression: WorkflowExpression): readonly string[] {
335
- const access = analyzeContextRootKeyAccess(expression, ['execution']);
336
- return access.references.some((reference) => reference.key === 'failed')
337
- ? ['execution.failed']
338
- : [];
339
- }
340
-
341
259
  function hasSyntaxOnlyCheckMode(root: string): boolean {
342
260
  if (!isWorkflowContextName(root)) return resolveContextRootHost(root) === 'server';
343
261
  return isWorkflowContextName(root) && getWorkflowContextDefinition(root).checkMode === 'syntax';
344
262
  }
345
263
 
346
264
  function mergeTypeEnvironments(
265
+ field: WorkflowPredicateField,
347
266
  roots: readonly string[],
348
267
  typeOverlay?: ExpressionTypeEnvironment,
349
268
  ): ExpressionTypeEnvironment {
@@ -351,28 +270,45 @@ function mergeTypeEnvironments(
351
270
 
352
271
  for (const root of roots) {
353
272
  const overlayType = typeOverlay?.[root];
354
- if (overlayType !== undefined) {
355
- typeEnvironment[root] = overlayType;
356
- continue;
357
- }
358
-
359
273
  if (!isWorkflowContextName(root)) {
360
- if (typeOverlay !== undefined) typeEnvironment[root] = {kind: 'map'};
274
+ if (overlayType !== undefined) typeEnvironment[root] = overlayType;
275
+ else typeEnvironment[root] = {kind: 'map'};
361
276
  continue;
362
277
  }
363
278
 
364
- const contextTypeEnvironment = getWorkflowContextTypeEnvironment(root);
279
+ const contextTypeEnvironment = getWorkflowPredicateFieldTypeEnvironment(field, root);
365
280
  if (contextTypeEnvironment === undefined) {
366
- if (typeOverlay !== undefined) typeEnvironment[root] = {kind: 'map'};
281
+ if (overlayType !== undefined) typeEnvironment[root] = overlayType;
282
+ else typeEnvironment[root] = {kind: 'map'};
367
283
  continue;
368
284
  }
369
285
 
370
- Object.assign(typeEnvironment, contextTypeEnvironment);
286
+ const contextType = contextTypeEnvironment[root];
287
+ if (contextType === undefined) continue;
288
+ typeEnvironment[root] =
289
+ overlayType === undefined ? contextType : overlayKnownFields(contextType, overlayType);
371
290
  }
372
291
 
373
292
  return typeEnvironment;
374
293
  }
375
294
 
295
+ function overlayKnownFields(base: ExpressionType, overlay: ExpressionType): ExpressionType {
296
+ if (typeof base === 'string' || typeof overlay === 'string') return overlay;
297
+ if (base.kind !== 'object' || overlay.kind !== 'object') return overlay;
298
+
299
+ // Preserve output specialization without reintroducing properties absent from this field's
300
+ // runtime shape.
301
+ return {
302
+ kind: 'object',
303
+ fields: Object.fromEntries(
304
+ Object.entries(base.fields).map(([key, fieldType]) => [
305
+ key,
306
+ overlay.fields[key] ?? fieldType,
307
+ ]),
308
+ ),
309
+ };
310
+ }
311
+
376
312
  function isWorkflowContextName(root: string): root is WorkflowContextName {
377
313
  return (workflowContextNames as readonly string[]).includes(root);
378
314
  }
@@ -397,10 +333,6 @@ function describeAvailabilitySite(site: AvailabilitySite): string {
397
333
  return availabilitySiteLabels[site];
398
334
  }
399
335
 
400
- function isSiteAtOrAfter(site: AvailabilitySite, fillTarget: AvailabilitySite): boolean {
401
- return availabilitySites.indexOf(site) >= availabilitySites.indexOf(fillTarget);
402
- }
403
-
404
336
  function formatList(values: readonly string[]): string {
405
337
  return values.map((value) => `"${value}"`).join(', ');
406
338
  }
@@ -2,6 +2,7 @@ import type {
2
2
  WorkflowModelValidationIssue,
3
3
  WorkflowModelValidationIssueCode,
4
4
  WorkflowModelValidationIssuePathSegment,
5
+ WorkflowModelValidationIssueSeverity,
5
6
  } from './invalid-workflow-model-error.js';
6
7
 
7
8
  export function issue(params: {
@@ -9,12 +10,14 @@ export function issue(params: {
9
10
  message: string;
10
11
  path: readonly WorkflowModelValidationIssuePathSegment[];
11
12
  details?: Readonly<Record<string, unknown>>;
13
+ severity?: WorkflowModelValidationIssueSeverity;
12
14
  }): WorkflowModelValidationIssue {
13
15
  if (params.details === undefined) {
14
16
  return {
15
17
  code: params.code,
16
18
  message: params.message,
17
19
  path: params.path,
20
+ ...(params.severity === undefined ? {} : {severity: params.severity}),
18
21
  };
19
22
  }
20
23
 
@@ -23,5 +26,6 @@ export function issue(params: {
23
26
  message: params.message,
24
27
  path: params.path,
25
28
  details: params.details,
29
+ ...(params.severity === undefined ? {} : {severity: params.severity}),
26
30
  };
27
31
  }
@@ -20,10 +20,24 @@ export function workflowFieldLabel(
20
20
  return 'Job runner interpolation';
21
21
  case 'job.outputs':
22
22
  return 'Job outputs mapping';
23
- case 'job.name':
24
- return 'Job name interpolation';
23
+ case 'workflow.run_name':
24
+ return 'Workflow run name interpolation';
25
+ case 'job.execution_name':
26
+ return 'Job execution name interpolation';
25
27
  case 'step.name':
26
28
  return 'Step name interpolation';
29
+ case 'step.working_directory':
30
+ return 'Step working directory interpolation';
31
+ case 'checkout.project':
32
+ return 'Checkout project interpolation';
33
+ case 'checkout.connection':
34
+ return 'Checkout connection interpolation';
35
+ case 'checkout.repository':
36
+ return 'Checkout repository interpolation';
37
+ case 'checkout.ref':
38
+ return 'Checkout ref interpolation';
39
+ case 'checkout.path':
40
+ return 'Checkout path interpolation';
27
41
  case 'step.success':
28
42
  return 'Step gate success';
29
43
  case 'step.feedback':
@@ -29,7 +29,7 @@ to read and easy to change.
29
29
 
30
30
  That keeps the next part clear too.
31
31
 
32
- ## Installation / Setup
32
+ ## Installation and setup
33
33
 
34
34
  This code is part of `@shipfox/api-definitions`. It is not a standalone package.
35
35
 
@@ -59,7 +59,7 @@ jobs:
59
59
  console.log(document.jobs.build.steps[0]?.run);
60
60
  ```
61
61
 
62
- ## Behavior Notes
62
+ ## Behavior notes
63
63
 
64
64
  The parser owns only YAML concerns. It does not set defaults. It does not change
65
65
  job ids. It does not build job graphs. It does not check expressions.
@@ -1,10 +1,11 @@
1
1
  import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
2
2
  import {sql} from 'drizzle-orm';
3
- import {index, pgEnum, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
3
+ import {index, jsonb, pgEnum, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
4
4
  import type {
5
5
  DefinitionSyncErrorCode,
6
6
  DefinitionSyncState,
7
7
  DefinitionSyncStatus,
8
+ DefinitionSyncWarning,
8
9
  } from '#core/entities/sync-state.js';
9
10
  import {pgTable} from './common.js';
10
11
 
@@ -42,6 +43,7 @@ export const definitionSyncStates = pgTable(
42
43
  status: definitionSyncStatusEnum('status').notNull().default('pending'),
43
44
  lastErrorCode: definitionSyncErrorCodeEnum('last_error_code'),
44
45
  lastErrorMessage: text('last_error_message'),
46
+ warnings: jsonb('warnings').notNull().default([]).$type<DefinitionSyncWarning[]>(),
45
47
  startedAt: timestamp('started_at', {withTimezone: true}),
46
48
  finishedAt: timestamp('finished_at', {withTimezone: true}),
47
49
  createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
@@ -71,6 +73,7 @@ export function toDefinitionSyncState(row: DefinitionSyncStateDb): DefinitionSyn
71
73
  status: row.status as DefinitionSyncStatus,
72
74
  lastErrorCode: row.lastErrorCode as DefinitionSyncErrorCode | null,
73
75
  lastErrorMessage: row.lastErrorMessage,
76
+ warnings: row.warnings ?? [],
74
77
  startedAt: row.startedAt,
75
78
  finishedAt: row.finishedAt,
76
79
  createdAt: row.createdAt,
@@ -1,3 +1,9 @@
1
+ import {
2
+ DEFINITION_SYNC_WARNING_CODE_MAX_LENGTH,
3
+ DEFINITION_SYNC_WARNING_MESSAGE_MAX_LENGTH,
4
+ DEFINITION_SYNC_WARNING_PATH_MAX_LENGTH,
5
+ DEFINITION_SYNC_WARNINGS_MAX_COUNT,
6
+ } from '@shipfox/api-definitions-dto';
1
7
  import {eq} from 'drizzle-orm';
2
8
  import {db} from './db.js';
3
9
  import {definitionSyncStates} from './schema/sync-states.js';
@@ -59,6 +65,57 @@ describe('definition sync state queries', () => {
59
65
  expect(second.startedAt?.getTime()).toBe(first.startedAt?.getTime());
60
66
  });
61
67
 
68
+ it('persists warnings and clears them on a subsequent sync', async () => {
69
+ const warnings = [
70
+ {
71
+ code: 're-evaluating-command',
72
+ message: 'Workflow data is re-executed as shell code.',
73
+ path: 'jobs.build.steps.0.run',
74
+ },
75
+ ];
76
+
77
+ const succeeded = await markDefinitionSyncState({
78
+ projectId,
79
+ sourceConnectionId,
80
+ sourceExternalRepositoryId: 'gitea-owner/platform',
81
+ ref: 'main',
82
+ status: 'succeeded',
83
+ warnings,
84
+ });
85
+
86
+ expect(succeeded.warnings).toEqual(warnings);
87
+
88
+ const syncing = await markDefinitionSyncState({
89
+ projectId,
90
+ sourceConnectionId,
91
+ sourceExternalRepositoryId: 'gitea-owner/platform',
92
+ ref: 'main',
93
+ status: 'syncing',
94
+ });
95
+
96
+ expect(syncing.warnings).toEqual([]);
97
+ });
98
+
99
+ it('bounds warning payloads before persisting them', async () => {
100
+ const state = await markDefinitionSyncState({
101
+ projectId,
102
+ sourceConnectionId,
103
+ sourceExternalRepositoryId: 'gitea-owner/platform',
104
+ ref: 'main',
105
+ status: 'succeeded',
106
+ warnings: Array.from({length: DEFINITION_SYNC_WARNINGS_MAX_COUNT + 1}, (_, index) => ({
107
+ code: `${index}-${'c'.repeat(DEFINITION_SYNC_WARNING_CODE_MAX_LENGTH)}`,
108
+ message: `${index}-${'m'.repeat(DEFINITION_SYNC_WARNING_MESSAGE_MAX_LENGTH)}`,
109
+ path: `${index}-${'p'.repeat(DEFINITION_SYNC_WARNING_PATH_MAX_LENGTH)}`,
110
+ })),
111
+ });
112
+
113
+ expect(state.warnings).toHaveLength(DEFINITION_SYNC_WARNINGS_MAX_COUNT);
114
+ expect(state.warnings[0]?.code).toHaveLength(DEFINITION_SYNC_WARNING_CODE_MAX_LENGTH);
115
+ expect(state.warnings[0]?.message).toHaveLength(DEFINITION_SYNC_WARNING_MESSAGE_MAX_LENGTH);
116
+ expect(state.warnings[0]?.path).toHaveLength(DEFINITION_SYNC_WARNING_PATH_MAX_LENGTH);
117
+ });
118
+
62
119
  it('clears stale finish data when a sync starts again', async () => {
63
120
  await markDefinitionSyncState({
64
121
  projectId,
@@ -3,7 +3,9 @@ import type {
3
3
  DefinitionSyncErrorCode,
4
4
  DefinitionSyncState,
5
5
  DefinitionSyncStatus,
6
+ DefinitionSyncWarning,
6
7
  } from '#core/entities/sync-state.js';
8
+ import {limitDefinitionSyncWarnings} from '#core/entities/sync-state.js';
7
9
  import {db} from './db.js';
8
10
  import {definitionSyncStates, toDefinitionSyncState} from './schema/sync-states.js';
9
11
 
@@ -18,6 +20,7 @@ export interface MarkDefinitionSyncParams extends DefinitionSyncStateKey {
18
20
  status: DefinitionSyncStatus;
19
21
  lastErrorCode?: DefinitionSyncErrorCode | null | undefined;
20
22
  lastErrorMessage?: string | null | undefined;
23
+ warnings?: readonly DefinitionSyncWarning[] | null | undefined;
21
24
  startedAt?: Date | null | undefined;
22
25
  finishedAt?: Date | null | undefined;
23
26
  }
@@ -26,6 +29,7 @@ export async function markDefinitionSyncState(
26
29
  params: MarkDefinitionSyncParams,
27
30
  ): Promise<DefinitionSyncState> {
28
31
  const now = new Date();
32
+ const warnings = limitDefinitionSyncWarnings(params.warnings ?? []);
29
33
  const [row] = await db()
30
34
  .insert(definitionSyncStates)
31
35
  .values({
@@ -36,6 +40,7 @@ export async function markDefinitionSyncState(
36
40
  status: params.status,
37
41
  lastErrorCode: params.lastErrorCode ?? null,
38
42
  lastErrorMessage: params.lastErrorMessage ?? null,
43
+ warnings,
39
44
  startedAt: params.startedAt ?? null,
40
45
  finishedAt: params.finishedAt ?? null,
41
46
  updatedAt: now,
@@ -51,6 +56,7 @@ export async function markDefinitionSyncState(
51
56
  status: params.status,
52
57
  lastErrorCode: params.lastErrorCode ?? null,
53
58
  lastErrorMessage: params.lastErrorMessage ?? null,
59
+ warnings,
54
60
  ...(params.startedAt !== undefined ? {startedAt: params.startedAt} : {}),
55
61
  ...(params.finishedAt !== undefined ? {finishedAt: params.finishedAt} : {}),
56
62
  updatedAt: now,
@@ -37,5 +37,6 @@ export function toDefinitionSyncSummaryDto(
37
37
  finished_at: syncState.finishedAt?.toISOString() ?? null,
38
38
  last_error_code: syncState.lastErrorCode,
39
39
  last_error_message: syncState.lastErrorMessage,
40
+ warnings: syncState.warnings,
40
41
  };
41
42
  }
@@ -27,7 +27,7 @@ describe('POST /api/definitions', () => {
27
27
  buildUserContext({
28
28
  userId: crypto.randomUUID(),
29
29
  email: 'user@example.com',
30
- memberships: [{workspaceId, role: 'admin'}],
30
+ memberships: [{workspaceId, role: 'admin', workspaceStatus: 'active'}],
31
31
  }),
32
32
  );
33
33
  done();
@@ -92,7 +92,7 @@ jobs:
92
92
  buildUserContext({
93
93
  userId: crypto.randomUUID(),
94
94
  email: 'user@example.com',
95
- memberships: [{workspaceId, role: 'admin'}],
95
+ memberships: [{workspaceId, role: 'admin', workspaceStatus: 'active'}],
96
96
  }),
97
97
  );
98
98
  done();
@@ -165,7 +165,7 @@ jobs:
165
165
  buildUserContext({
166
166
  userId: crypto.randomUUID(),
167
167
  email: 'user@example.com',
168
- memberships: [{workspaceId, role: 'admin'}],
168
+ memberships: [{workspaceId, role: 'admin', workspaceStatus: 'active'}],
169
169
  }),
170
170
  );
171
171
  done();
@@ -37,7 +37,7 @@ describe('GET /api/definitions/:id', () => {
37
37
  buildUserContext({
38
38
  userId: crypto.randomUUID(),
39
39
  email: 'user@example.com',
40
- memberships: [{workspaceId, role: 'admin'}],
40
+ memberships: [{workspaceId, role: 'admin', workspaceStatus: 'active'}],
41
41
  }),
42
42
  );
43
43
  done();
@@ -46,7 +46,7 @@ describe('GET /api/definitions', () => {
46
46
  buildUserContext({
47
47
  userId: crypto.randomUUID(),
48
48
  email: 'user@example.com',
49
- memberships: [{workspaceId, role: 'admin'}],
49
+ memberships: [{workspaceId, role: 'admin', workspaceStatus: 'active'}],
50
50
  }),
51
51
  );
52
52
  done();
@@ -99,6 +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
103
  });
103
104
  });
104
105
 
@@ -39,6 +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
43
  expect(body.workflow_document.name).toBe('Test');
43
44
  expect(body.workflow_model.kind).toBe('workflow');
44
45
  });
@@ -1,5 +1,9 @@
1
1
  import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
2
- import {definitionDtoSchema, definitionValidationErrorSchema} from '@shipfox/api-definitions-dto';
2
+ import {
3
+ definitionDtoSchema,
4
+ definitionValidationErrorSchema,
5
+ definitionValidationWarningSchema,
6
+ } from '@shipfox/api-definitions-dto';
3
7
  import {defineRoute} from '@shipfox/node-fastify';
4
8
  import {z} from 'zod';
5
9
  import {validateDefinition} from '#core/validate-definition.js';
@@ -13,6 +17,7 @@ const validationResultSchema = z.union([
13
17
  valid: z.literal(true),
14
18
  workflow_document: definitionDtoSchema.shape.workflow_document,
15
19
  workflow_model: definitionDtoSchema.shape.workflow_model,
20
+ warnings: z.array(definitionValidationWarningSchema),
16
21
  }),
17
22
  z.object({
18
23
  valid: z.literal(false),
@@ -42,6 +47,7 @@ export function buildValidateDefinitionRoute(agent: AgentInterModuleClient) {
42
47
  valid: true as const,
43
48
  workflow_document: result.definition.document,
44
49
  workflow_model: result.definition.model,
50
+ warnings: result.warnings,
45
51
  };
46
52
  }
47
53
 
@@ -224,6 +224,7 @@ describe('definition sync activities', () => {
224
224
 
225
225
  expect(result.appliedCount).toBe(1);
226
226
  expect(result.deletedCount).toBe(0);
227
+ expect(result.warnings).toEqual([]);
227
228
  });
228
229
 
229
230
  it('translates DefinitionSyncPermanentError into a non-retryable ApplicationFailure', async () => {
@@ -308,6 +309,7 @@ describe('definition sync activities', () => {
308
309
  expect(rows[0]?.status).toBe('failed');
309
310
  expect(rows[0]?.lastErrorCode).toBe('invalid-definition');
310
311
  expect(rows[0]?.lastErrorMessage).toBe('Invalid workflow at .shipfox/workflows/bad.yml');
312
+ expect(rows[0]?.warnings).toEqual([]);
311
313
  expect(rows[0]?.finishedAt).not.toBeNull();
312
314
  });
313
315
 
@@ -362,6 +364,13 @@ describe('definition sync activities', () => {
362
364
  sourceConnectionId,
363
365
  sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
364
366
  sourceRef: 'main',
367
+ warnings: [
368
+ {
369
+ code: 're-evaluating-command',
370
+ message: 'Workflow data is re-executed as shell code.',
371
+ path: 'jobs.build.steps.0.run',
372
+ },
373
+ ],
365
374
  });
366
375
  await result;
367
376
 
@@ -372,6 +381,13 @@ describe('definition sync activities', () => {
372
381
  expect(rows[0]?.status).toBe('succeeded');
373
382
  expect(rows[0]?.lastErrorCode).toBeNull();
374
383
  expect(rows[0]?.lastErrorMessage).toBeNull();
384
+ expect(rows[0]?.warnings).toEqual([
385
+ {
386
+ code: 're-evaluating-command',
387
+ message: 'Workflow data is re-executed as shell code.',
388
+ path: 'jobs.build.steps.0.run',
389
+ },
390
+ ]);
375
391
  });
376
392
  });
377
393
  });