@shipfox/api-definitions 11.0.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 (148) hide show
  1. package/.turbo/turbo-build.log +13 -12
  2. package/CHANGELOG.md +81 -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 +732 -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/list-definitions.test.ts +1 -0
  141. package/src/presentation/routes/validate-definition.test.ts +1 -0
  142. package/src/presentation/routes/validate-definition.ts +7 -1
  143. package/src/temporal/activities/sync-activities.test.ts +16 -0
  144. package/src/temporal/activities/sync-activities.ts +18 -3
  145. package/src/temporal/workflows/definition-sync.ts +1 -1
  146. package/test/agent-validation-catalog.ts +17 -0
  147. package/test/fixtures/valid-listening-job.yml +2 -1
  148. package/tsconfig.build.tsbuildinfo +1 -1
@@ -65,7 +65,13 @@ describe('parseDefinition', () => {
65
65
  onResolve: 'cancel',
66
66
  },
67
67
  });
68
- expect(definition.model.jobs[0]?.name).toBeDefined();
68
+ expect(definition.model.jobs[0]).toMatchObject({
69
+ name: 'Review batch',
70
+ executionName: [
71
+ {kind: 'literal', value: 'Review batch '},
72
+ {kind: 'deferred', roots: ['execution'], fillTarget: 'execution-creation'},
73
+ ],
74
+ });
69
75
  });
70
76
 
71
77
  test('attaches source line locations to workflow model steps', () => {
@@ -1,17 +1,18 @@
1
- import type {AgentValidationCatalog} from '@shipfox/api-agent-dto/inter-module';
2
- import type {IntegrationValidationContext} from './entities/integration-context.js';
1
+ import type {ValidationWarning} from './entities/validation-warning.js';
3
2
  import type {WorkflowDefinitionPayload} from './entities/workflow-definition.js';
4
3
  import {DefinitionParseError} from './errors.js';
5
- import {validateDefinition} from './validate-definition.js';
4
+ import {type DefinitionValidationOptions, validateDefinition} from './validate-definition.js';
6
5
 
7
- export function parseDefinition(
6
+ export interface ParsedDefinition extends WorkflowDefinitionPayload {
7
+ warnings: ValidationWarning[];
8
+ }
9
+
10
+ export type ParseDefinitionOptions = DefinitionValidationOptions;
11
+
12
+ export function parseDefinitionWithWarnings(
8
13
  yamlString: string,
9
- options: {
10
- defaultRunnerLabels?: readonly string[];
11
- agentValidationCatalog: AgentValidationCatalog;
12
- integrationValidationContext?: IntegrationValidationContext;
13
- },
14
- ): WorkflowDefinitionPayload {
14
+ options: ParseDefinitionOptions,
15
+ ): ParsedDefinition {
15
16
  const result = validateDefinition(yamlString, options);
16
17
 
17
18
  if (!result.valid) {
@@ -24,5 +25,22 @@ export function parseDefinition(
24
25
  return {
25
26
  ...result.definition,
26
27
  sourceSnapshot: {content: yamlString, format: 'yaml'},
28
+ warnings: result.warnings,
29
+ };
30
+ }
31
+
32
+ export function parseDefinition(
33
+ yamlString: string,
34
+ options: ParseDefinitionOptions,
35
+ ): WorkflowDefinitionPayload {
36
+ const parsed = parseDefinitionWithWarnings(yamlString, options);
37
+ return stripDefinitionWarnings(parsed);
38
+ }
39
+
40
+ export function stripDefinitionWarnings(parsed: ParsedDefinition): WorkflowDefinitionPayload {
41
+ return {
42
+ document: parsed.document,
43
+ model: parsed.model,
44
+ sourceSnapshot: parsed.sourceSnapshot ?? null,
27
45
  };
28
46
  }
@@ -27,6 +27,19 @@ jobs:
27
27
  - run: pnpm test
28
28
  `;
29
29
 
30
+ const eventInterpolation = '$'.concat('{{ event.x }}');
31
+
32
+ const warningYaml = `
33
+ name: Warning only
34
+ runner: ubuntu-latest
35
+ jobs:
36
+ build:
37
+ steps:
38
+ - env:
39
+ MSG: '${eventInterpolation}'
40
+ run: eval "$MSG"
41
+ `;
42
+
30
43
  const validIntegrationYaml = `
31
44
  name: Agent CI
32
45
  runner: ubuntu-latest
@@ -230,6 +243,32 @@ describe('fetchAndParseWorkflows', () => {
230
243
  expect(result[0]?.name).toBe('CI');
231
244
  expect(result[0]?.path).toBe('.shipfox/workflows/ci.yml');
232
245
  expect(result[0]?.contentHash).toMatch(LOWERCASE_SHA256_HEX_RE);
246
+ expect(result[0]?.warnings).toEqual([]);
247
+ });
248
+
249
+ it('keeps warning-only definitions available to the sync path', async () => {
250
+ const result = await fetchAndParseWorkflows({
251
+ ...baseContext,
252
+ ref: 'main',
253
+ paths: ['.shipfox/workflows/warning.yml'],
254
+ sourceControl: sourceControl({
255
+ fetchFile: vi.fn(() =>
256
+ Promise.resolve({
257
+ path: '.shipfox/workflows/warning.yml',
258
+ ref: 'main',
259
+ content: warningYaml,
260
+ }),
261
+ ),
262
+ }),
263
+ });
264
+
265
+ expect(result[0]?.warnings).toMatchObject([
266
+ {
267
+ code: 're-evaluating-command',
268
+ path: 'jobs.build.steps.0.run',
269
+ },
270
+ ]);
271
+ expect(result[0]?.definition.model.jobs[0]?.steps).toHaveLength(1);
233
272
  });
234
273
 
235
274
  it('produces stable content hashes for identical content', async () => {
@@ -5,11 +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
9
  import type {WorkflowDefinitionPayload} from './entities/workflow-definition.js';
9
10
  import {DefinitionParseError, DefinitionSyncPermanentError} from './errors.js';
10
11
  import {hasAgentStepIntegrations} from './has-agent-step-integrations.js';
11
12
  import type {DefinitionsSourceControl} from './integrations.js';
12
- import {parseDefinition} from './parse-definition.js';
13
+ import {parseDefinitionWithWarnings, stripDefinitionWarnings} from './parse-definition.js';
13
14
 
14
15
  export const DEFAULT_WORKFLOW_PATH = '.shipfox/workflows/';
15
16
  export const MAX_WORKFLOW_FILES = 100;
@@ -79,6 +80,7 @@ export interface ParsedWorkflow {
79
80
  name: string;
80
81
  definition: WorkflowDefinitionPayload;
81
82
  contentHash: string;
83
+ warnings: ValidationWarning[];
82
84
  }
83
85
 
84
86
  export interface FetchAndParseWorkflowsParams extends SyncSourceContext {
@@ -152,13 +154,21 @@ function parseWorkflowSnapshot(params: {
152
154
  try {
153
155
  const definition =
154
156
  params.integrationValidationContext === undefined
155
- ? parseDefinition(params.content, {agentValidationCatalog: params.agentValidationCatalog})
156
- : parseDefinition(params.content, {
157
+ ? parseDefinitionWithWarnings(params.content, {
158
+ agentValidationCatalog: params.agentValidationCatalog,
159
+ })
160
+ : parseDefinitionWithWarnings(params.content, {
157
161
  agentValidationCatalog: params.agentValidationCatalog,
158
162
  integrationValidationContext: params.integrationValidationContext,
159
163
  });
160
164
  const contentHash = sha256Hex(params.content);
161
- return {path: params.path, name: definition.document.name, definition, contentHash};
165
+ return {
166
+ path: params.path,
167
+ name: definition.document.name,
168
+ definition: stripDefinitionWarnings(definition),
169
+ contentHash,
170
+ warnings: definition.warnings,
171
+ };
162
172
  } catch (error) {
163
173
  if (error instanceof DefinitionParseError) {
164
174
  throw new DefinitionSyncPermanentError(
@@ -5,6 +5,10 @@ function validateDefinition(yaml: string, options = {}) {
5
5
  return validateDefinitionBase(yaml, {agentValidationCatalog, ...options});
6
6
  }
7
7
 
8
+ function interpolation(source: string): string {
9
+ return '$'.concat('{{ ', source, ' }}');
10
+ }
11
+
8
12
  describe('validateDefinition', () => {
9
13
  test('valid YAML returns { valid: true, definition }', () => {
10
14
  const yaml = `
@@ -20,6 +24,7 @@ jobs:
20
24
 
21
25
  expect(result.valid).toBe(true);
22
26
  if (result.valid) {
27
+ expect(result.warnings).toEqual([]);
23
28
  expect(result.definition.document.name).toBe('Test');
24
29
  expect(result.definition.document.jobs.build?.steps).toHaveLength(1);
25
30
  expect(result.definition.model.jobs[0]?.id).toBe('build');
@@ -31,6 +36,7 @@ jobs:
31
36
 
32
37
  expect(result.valid).toBe(false);
33
38
  if (!result.valid) {
39
+ expect(result).not.toHaveProperty('warnings');
34
40
  expect(result.errors).toHaveLength(1);
35
41
  expect(result.errors[0]?.message).toContain('Invalid workflow YAML syntax');
36
42
  }
@@ -121,6 +127,128 @@ jobs:
121
127
  }
122
128
  });
123
129
 
130
+ test.each([
131
+ [
132
+ 'an env binding',
133
+ 'MSG',
134
+ `
135
+ name: Re-evaluating command
136
+ runner: ubuntu-latest
137
+ jobs:
138
+ build:
139
+ steps:
140
+ - env:
141
+ MSG: '${interpolation('event.x')}'
142
+ run: eval "$MSG"
143
+ `,
144
+ ],
145
+ [
146
+ 'a hoisted interpolation',
147
+ '__sf_0',
148
+ `
149
+ name: Re-evaluating command
150
+ runner: ubuntu-latest
151
+ jobs:
152
+ build:
153
+ steps:
154
+ - run: 'eval "${interpolation('event.x')}"'
155
+ `,
156
+ ],
157
+ ] as const)('returns one non-fatal warning for %s', (_description, valueName, yaml) => {
158
+ const result = validateDefinition(yaml);
159
+
160
+ expect(result.valid).toBe(true);
161
+ if (result.valid) {
162
+ expect(result.warnings).toHaveLength(1);
163
+ expect(result.warnings[0]).toMatchObject({
164
+ code: 're-evaluating-command',
165
+ path: 'jobs.build.steps.0.run',
166
+ });
167
+ expect(result.warnings[0]?.message).toContain(`$${valueName}`);
168
+ expect(result.warnings[0]?.message).toContain('eval');
169
+ expect(result.warnings[0]?.message).toContain('re-executed as code');
170
+ expect(result.definition.model.jobs[0]?.steps).toHaveLength(1);
171
+ }
172
+ });
173
+
174
+ test.each([
175
+ 'eval "$(cat script.sh)"',
176
+ "sh -c 'echo fixed'",
177
+ 'source ./scripts/setup.sh',
178
+ 'xargs cmd "$MSG"',
179
+ ])('does not warn for the common data-safe form %s', (run) => {
180
+ const result = validateDefinition(`
181
+ name: Safe command
182
+ runner: ubuntu-latest
183
+ jobs:
184
+ build:
185
+ steps:
186
+ - env:
187
+ MSG: fixed
188
+ run: |
189
+ ${run}
190
+ `);
191
+
192
+ expect(result).toMatchObject({valid: true, warnings: []});
193
+ });
194
+
195
+ test.each([
196
+ ['command substitution', `echo $(${interpolation('event.ref')})`, 'command substitution'],
197
+ ['backtick substitution', `echo \`${interpolation('event.ref')}\``, 'backtick substitution'],
198
+ ['arithmetic expansion', `echo $(( ${interpolation('event.ref')} + 1 ))`, 'shell arithmetic'],
199
+ ])('warns for interpolation inside %s', (_description, run, construct) => {
200
+ const result = validateDefinition(`
201
+ name: Unsafe interpolation
202
+ runner: ubuntu-latest
203
+ jobs:
204
+ build:
205
+ steps:
206
+ - run: |
207
+ ${run}
208
+ `);
209
+
210
+ expect(result.valid).toBe(true);
211
+ if (result.valid) {
212
+ expect(result.warnings).toHaveLength(1);
213
+ expect(result.warnings[0]).toMatchObject({
214
+ code: 're-evaluating-command',
215
+ path: 'jobs.build.steps.0.run',
216
+ });
217
+ expect(result.warnings[0]?.message).toContain('event.ref');
218
+ expect(result.warnings[0]?.message).toContain(construct);
219
+ }
220
+ });
221
+
222
+ test('warns for every repeated and unsafe flagged position', () => {
223
+ const result = validateDefinition(`
224
+ name: Multiple re-evaluating commands
225
+ runner: ubuntu-latest
226
+ jobs:
227
+ build:
228
+ steps:
229
+ - env:
230
+ MSG: fixed
231
+ run: |
232
+ eval "$MSG" "$MSG"; echo $(${interpolation('event.first')}); eval "$MSG"; echo $((1 + ${interpolation('event.second')}))
233
+ `);
234
+
235
+ expect(result.valid).toBe(true);
236
+ if (result.valid) {
237
+ expect(result.warnings).toHaveLength(5);
238
+ expect(result.warnings.every((warning) => warning.code === 're-evaluating-command')).toBe(
239
+ true,
240
+ );
241
+ expect(result.warnings.every((warning) => warning.path === 'jobs.build.steps.0.run')).toBe(
242
+ true,
243
+ );
244
+ expect(result.warnings.filter((warning) => warning.message.includes('eval'))).toHaveLength(3);
245
+ expect(result.warnings.some((warning) => warning.message.includes('event.first'))).toBe(true);
246
+ expect(result.warnings.some((warning) => warning.message.includes('event.second'))).toBe(
247
+ true,
248
+ );
249
+ }
250
+ });
251
+
124
252
  test('disabled Pi search tools return precise definition validation paths', () => {
125
253
  const yaml = `
126
254
  name: Pi tools
@@ -2,38 +2,60 @@ import type {AgentValidationCatalog} from '@shipfox/api-agent-dto/inter-module';
2
2
  import {InvalidWorkflowDocumentError} from '@shipfox/workflow-document';
3
3
  import {definitionDefaultRunnerLabels} from '../config.js';
4
4
  import type {IntegrationValidationContext} from './entities/integration-context.js';
5
+ import type {ValidationWarning} from './entities/validation-warning.js';
5
6
  import type {WorkflowDefinitionPayload} from './entities/workflow-definition.js';
6
- import {InvalidWorkflowModelError, normalizeWorkflowDocument} from './workflow-model/index.js';
7
+ import {
8
+ InvalidWorkflowModelError,
9
+ normalizeWorkflowDocument,
10
+ type WorkflowModelValidationIssue,
11
+ } from './workflow-model/index.js';
7
12
  import {InvalidWorkflowYamlError, parseWorkflowYamlWithLocations} from './workflow-yaml/index.js';
8
13
 
9
14
  export type ValidationError = {message: string; path?: string | undefined};
15
+ export type {ValidationWarning} from './entities/validation-warning.js';
16
+
17
+ export interface DefinitionValidationOptions {
18
+ defaultRunnerLabels?: readonly string[];
19
+ agentValidationCatalog: AgentValidationCatalog;
20
+ integrationValidationContext?: IntegrationValidationContext;
21
+ }
10
22
 
11
23
  export type ValidationResult =
12
- | {valid: true; definition: WorkflowDefinitionPayload}
24
+ | {valid: true; definition: WorkflowDefinitionPayload; warnings: ValidationWarning[]}
13
25
  | {valid: false; errors: ValidationError[]};
14
26
 
15
27
  export function validateDefinition(
16
28
  yamlContent: string,
17
- options: {
18
- defaultRunnerLabels?: readonly string[];
19
- agentValidationCatalog: AgentValidationCatalog;
20
- integrationValidationContext?: IntegrationValidationContext;
21
- },
29
+ options: DefinitionValidationOptions,
22
30
  ): ValidationResult {
23
31
  try {
24
32
  const {document, stepSourceLocations} = parseWorkflowYamlWithLocations(yamlContent);
33
+ const warnings: WorkflowModelValidationIssue[] = [];
25
34
  const model = normalizeWorkflowDocument(document, {
26
35
  defaultRunnerLabels: options.defaultRunnerLabels ?? definitionDefaultRunnerLabels,
27
36
  agentValidationCatalog: options.agentValidationCatalog,
28
37
  integrationValidationContext: options.integrationValidationContext,
29
38
  stepSourceLocations,
39
+ warnings,
30
40
  });
31
- return {valid: true, definition: {document, model}};
41
+ return {valid: true, definition: {document, model}, warnings: validationWarningsFor(warnings)};
32
42
  } catch (error) {
33
43
  return {valid: false, errors: validationErrorsFor(error)};
34
44
  }
35
45
  }
36
46
 
47
+ function validationWarningsFor(
48
+ issues: readonly WorkflowModelValidationIssue[],
49
+ ): ValidationWarning[] {
50
+ return issues.map((issue) =>
51
+ validationWarning({
52
+ code: issue.code,
53
+ message: issue.message,
54
+ path: issue.path.join('.') || undefined,
55
+ }),
56
+ );
57
+ }
58
+
37
59
  function validationErrorsFor(error: unknown): ValidationError[] {
38
60
  if (error instanceof InvalidWorkflowYamlError) {
39
61
  return [
@@ -69,3 +91,12 @@ function validationError(params: {message: string; path?: string | undefined}):
69
91
  if (params.path === undefined) return {message: params.message};
70
92
  return {message: params.message, path: params.path};
71
93
  }
94
+
95
+ function validationWarning(params: {
96
+ code: string;
97
+ message: string;
98
+ path?: string | undefined;
99
+ }): ValidationWarning {
100
+ if (params.path === undefined) return {code: params.code, message: params.message};
101
+ return {code: params.code, message: params.message, path: params.path};
102
+ }
@@ -33,7 +33,7 @@ its work. A test can set up one case, call one thing, and check what comes back.
33
33
  That keeps the next step easy to read.
34
34
  It is small.
35
35
 
36
- ## Installation / Setup
36
+ ## Installation and setup
37
37
 
38
38
  This code is part of `@shipfox/api-definitions`. It is not a standalone package.
39
39
 
@@ -64,7 +64,7 @@ const model = normalizeWorkflowDocument({
64
64
  console.log(model.jobs[0]?.steps[0]?.command.value);
65
65
  ```
66
66
 
67
- ## Behavior Notes
67
+ ## Behavior notes
68
68
 
69
69
  The model does not own project ids, commit shas, refs, dates, database rows, or
70
70
  file paths. Those fields belong to `WorkflowDefinition`.
@@ -5,5 +5,6 @@ export {
5
5
  type WorkflowModelValidationIssue,
6
6
  type WorkflowModelValidationIssueCode,
7
7
  type WorkflowModelValidationIssuePathSegment,
8
+ type WorkflowModelValidationIssueSeverity,
8
9
  } from './invalid-workflow-model-error.js';
9
10
  export {normalizeWorkflowDocument} from './normalize-workflow-document.js';
@@ -2,8 +2,10 @@ export const invalidWorkflowModelErrorCode = 'invalid-workflow-model';
2
2
 
3
3
  export type WorkflowModelValidationIssueCode =
4
4
  | 'context-unavailable-at-fill-site'
5
+ | 'dynamic-name-self-reference'
5
6
  | 'context-unavailable-at-predicate-site'
6
7
  | 'computed-context-key'
8
+ | 'checkout-target-invalid'
7
9
  | 'duplicate-job-id'
8
10
  | 'duplicate-step-id'
9
11
  | 'duplicate-trigger-id'
@@ -22,6 +24,7 @@ export type WorkflowModelValidationIssueCode =
22
24
  | 'invalid-interpolation-template'
23
25
  | 'invalid-duration'
24
26
  | 'invalid-listener-filter'
27
+ | 'invalid-model'
25
28
  | 'invalid-job-if'
26
29
  | 'invalid-job-output'
27
30
  | 'invalid-job-success'
@@ -42,6 +45,7 @@ export type WorkflowModelValidationIssueCode =
42
45
  | 'runner-context-not-bare'
43
46
  | 'runner-context-in-field'
44
47
  | 'runner-context-in-server-predicate'
48
+ | 're-evaluating-command'
45
49
  | 'self-job-dependency'
46
50
  | 'too-many-runner-labels'
47
51
  | 'unknown-secret-store'
@@ -49,16 +53,18 @@ export type WorkflowModelValidationIssueCode =
49
53
  | 'unknown-integration-method'
50
54
  | 'unknown-integration-tool'
51
55
  | 'unknown-job-dependency'
52
- | 'untrusted-context-in-field'
53
- | 'vars-context-in-server-predicate';
56
+ | 'untrusted-agent-selection-context';
54
57
 
55
58
  export type WorkflowModelValidationIssuePathSegment = string | number;
56
59
 
60
+ export type WorkflowModelValidationIssueSeverity = 'error' | 'warning';
61
+
57
62
  export interface WorkflowModelValidationIssue {
58
63
  readonly code: WorkflowModelValidationIssueCode;
59
64
  readonly message: string;
60
65
  readonly path: readonly WorkflowModelValidationIssuePathSegment[];
61
66
  readonly details?: Readonly<Record<string, unknown>>;
67
+ readonly severity?: WorkflowModelValidationIssueSeverity;
62
68
  }
63
69
 
64
70
  export class InvalidWorkflowModelError extends Error {
@@ -21,7 +21,6 @@ const ifExpressionSyntax = `\${{ }}`;
21
21
  export function normalizeIfCondition(params: {
22
22
  readonly field: Extract<WorkflowPredicateField, 'job.if' | 'step.if'>;
23
23
  readonly source: string | undefined;
24
- readonly site: 'job-activation' | 'step-dispatch';
25
24
  readonly path: readonly WorkflowModelValidationIssuePathSegment[];
26
25
  readonly invalidCode: Extract<
27
26
  WorkflowModelValidationIssueCode,
@@ -59,7 +58,6 @@ export function normalizeIfCondition(params: {
59
58
  return validatePredicateExpression({
60
59
  field: params.field,
61
60
  source: expressionSegment.expression.source,
62
- site: params.site,
63
61
  path: params.path,
64
62
  invalidCode: params.invalidCode,
65
63
  invalidMessage: params.invalidMessage,
@@ -1,16 +1,113 @@
1
- import {DEFAULT_JOB_CHECKOUT, type WorkflowModelJobCheckout} from '@shipfox/api-definitions-dto';
2
- import type {WorkflowDocumentJobCheckout} from '@shipfox/workflow-document';
1
+ import {
2
+ DEFAULT_JOB_CHECKOUT,
3
+ WORKFLOW_MODEL_CHECKOUT_TARGET_FIELDS,
4
+ type WorkflowFieldTemplate,
5
+ type WorkflowModelCheckout,
6
+ type WorkflowModelCheckoutTargetKey,
7
+ type WorkflowModelJobCheckout,
8
+ } from '@shipfox/api-definitions-dto';
9
+ import type {AvailabilitySite, ExpressionTypeEnvironment} from '@shipfox/expression';
10
+ import {
11
+ checkoutTargetValidationIssues,
12
+ type WorkflowDocumentCheckout,
13
+ type WorkflowDocumentJob,
14
+ } from '@shipfox/workflow-document';
15
+ import type {
16
+ WorkflowModelValidationIssue,
17
+ WorkflowModelValidationIssuePathSegment,
18
+ } from './invalid-workflow-model-error.js';
19
+ import {parseInterpolationField} from './parse-interpolation-field.js';
20
+ import {issue} from './validation-issue.js';
3
21
 
4
22
  export {DEFAULT_JOB_CHECKOUT};
5
23
 
6
- export function normalizeJobCheckout(
7
- checkout: WorkflowDocumentJobCheckout | undefined,
8
- ): WorkflowModelJobCheckout {
24
+ const DEFAULT_CHECKOUT_FETCH_DEPTH = 1;
25
+
26
+ export function normalizeJobCheckout(params: {
27
+ checkout: WorkflowDocumentJob['checkout'];
28
+ }): WorkflowModelJobCheckout | false {
29
+ if (params.checkout === false) {
30
+ return false;
31
+ }
32
+
33
+ const checkout = params.checkout ?? {};
9
34
  return {
10
35
  permissions: {
11
- contents: checkout?.permissions?.contents ?? DEFAULT_JOB_CHECKOUT.permissions.contents,
36
+ contents: checkout.permissions?.contents ?? DEFAULT_JOB_CHECKOUT.permissions.contents,
12
37
  },
13
- persistCredentials:
14
- checkout?.['persist-credentials'] ?? DEFAULT_JOB_CHECKOUT.persistCredentials,
38
+ persistCredentials: checkout['persist-credentials'] ?? DEFAULT_JOB_CHECKOUT.persistCredentials,
15
39
  };
16
40
  }
41
+
42
+ export function normalizeCheckout(params: {
43
+ checkout: WorkflowDocumentCheckout;
44
+ issues: WorkflowModelValidationIssue[];
45
+ path: readonly WorkflowModelValidationIssuePathSegment[];
46
+ fillSite: AvailabilitySite;
47
+ allowedJobReferences?: ReadonlySet<string>;
48
+ typeOverlay?: ExpressionTypeEnvironment | undefined;
49
+ }): WorkflowModelCheckout {
50
+ validateCheckoutTargetShape(params);
51
+
52
+ const templates: Partial<Record<WorkflowModelCheckoutTargetKey, WorkflowFieldTemplate>> = {};
53
+ for (const [key, field] of WORKFLOW_MODEL_CHECKOUT_TARGET_FIELDS) {
54
+ const source = params.checkout[key];
55
+ if (source === undefined) continue;
56
+
57
+ const template = parseInterpolationField({
58
+ field,
59
+ source,
60
+ path: [...params.path, key],
61
+ issues: params.issues,
62
+ fillSite: params.fillSite,
63
+ ...(params.allowedJobReferences === undefined
64
+ ? {}
65
+ : {allowedJobReferences: params.allowedJobReferences}),
66
+ ...(params.typeOverlay === undefined ? {} : {typeOverlay: params.typeOverlay}),
67
+ });
68
+ if (template === undefined) continue;
69
+
70
+ templates[key] = template;
71
+ }
72
+
73
+ const normalized = {
74
+ ...(params.checkout.project === undefined ? {} : {project: params.checkout.project}),
75
+ ...(params.checkout.connection === undefined ? {} : {connection: params.checkout.connection}),
76
+ ...(params.checkout.repository === undefined ? {} : {repository: params.checkout.repository}),
77
+ ...(params.checkout.ref === undefined ? {} : {ref: params.checkout.ref}),
78
+ fetchDepth: params.checkout['fetch-depth'] ?? DEFAULT_CHECKOUT_FETCH_DEPTH,
79
+ ...(params.checkout.path === undefined ? {} : {path: params.checkout.path}),
80
+ permissions: {
81
+ contents: params.checkout.permissions?.contents ?? DEFAULT_JOB_CHECKOUT.permissions.contents,
82
+ },
83
+ persistCredentials:
84
+ params.checkout['persist-credentials'] ?? DEFAULT_JOB_CHECKOUT.persistCredentials,
85
+ ...(params.checkout.force === undefined ? {} : {force: params.checkout.force}),
86
+ ...(Object.keys(templates).length === 0 ? {} : {templates}),
87
+ } satisfies WorkflowModelCheckout;
88
+
89
+ return normalized;
90
+ }
91
+
92
+ function validateCheckoutTargetShape(params: {
93
+ checkout: WorkflowDocumentCheckout;
94
+ issues: WorkflowModelValidationIssue[];
95
+ path: readonly WorkflowModelValidationIssuePathSegment[];
96
+ }): void {
97
+ for (const validationIssue of checkoutTargetValidationIssues(params.checkout)) {
98
+ const message =
99
+ validationIssue.kind === 'project-with-connection'
100
+ ? 'Checkout target "connection" cannot be combined with "project".'
101
+ : validationIssue.kind === 'project-with-repository'
102
+ ? 'Checkout target "repository" cannot be combined with "project".'
103
+ : 'Checkout target "connection" requires "repository".';
104
+ params.issues.push(
105
+ issue({
106
+ code: 'checkout-target-invalid',
107
+ message,
108
+ path: [...params.path, validationIssue.path],
109
+ details: {fields: validationIssue.fields},
110
+ }),
111
+ );
112
+ }
113
+ }
@@ -108,7 +108,6 @@ function normalizeListeningTrigger(params: {
108
108
  validatePredicateExpression({
109
109
  field: params.field,
110
110
  source: params.trigger.filter,
111
- site: 'job-activation',
112
111
  path: params.path,
113
112
  invalidCode: 'invalid-listener-filter',
114
113
  invalidMessage: `${params.field === 'listener.on' ? 'Listener on' : 'Listener until'} filter must be a valid CEL boolean expression.`,
@@ -17,7 +17,6 @@ export function normalizeJobSuccess(params: {
17
17
  const expression = validatePredicateExpression({
18
18
  field: 'job.success',
19
19
  source: params.source,
20
- site: 'job-resolution',
21
20
  path: ['jobs', params.sourceName, 'success'],
22
21
  invalidCode: 'invalid-job-success',
23
22
  invalidMessage: 'Job success must be a valid CEL boolean expression.',