@shipfox/api-definitions 11.0.0 → 12.2.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 (174) hide show
  1. package/.turbo/turbo-build.log +13 -12
  2. package/CHANGELOG.md +106 -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 +5 -1
  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/errors.d.ts +2 -2
  16. package/dist/core/errors.d.ts.map +1 -1
  17. package/dist/core/integrations.d.ts +1 -1
  18. package/dist/core/parse-definition.d.ts +9 -7
  19. package/dist/core/parse-definition.d.ts.map +1 -1
  20. package/dist/core/parse-definition.js +14 -2
  21. package/dist/core/parse-definition.js.map +1 -1
  22. package/dist/core/sync-definitions.d.ts +2 -0
  23. package/dist/core/sync-definitions.d.ts.map +1 -1
  24. package/dist/core/sync-definitions.js +6 -5
  25. package/dist/core/sync-definitions.js.map +1 -1
  26. package/dist/core/validate-definition.d.ts +9 -5
  27. package/dist/core/validate-definition.d.ts.map +1 -1
  28. package/dist/core/validate-definition.js +23 -2
  29. package/dist/core/validate-definition.js.map +1 -1
  30. package/dist/core/workflow-model/index.d.ts +1 -1
  31. package/dist/core/workflow-model/index.d.ts.map +1 -1
  32. package/dist/core/workflow-model/index.js.map +1 -1
  33. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +3 -1
  34. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
  35. package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
  36. package/dist/core/workflow-model/normalize-if-condition.d.ts +0 -1
  37. package/dist/core/workflow-model/normalize-if-condition.d.ts.map +1 -1
  38. package/dist/core/workflow-model/normalize-if-condition.js +0 -1
  39. package/dist/core/workflow-model/normalize-if-condition.js.map +1 -1
  40. package/dist/core/workflow-model/normalize-job-checkout.d.ts +15 -3
  41. package/dist/core/workflow-model/normalize-job-checkout.d.ts.map +1 -1
  42. package/dist/core/workflow-model/normalize-job-checkout.js +83 -4
  43. package/dist/core/workflow-model/normalize-job-checkout.js.map +1 -1
  44. package/dist/core/workflow-model/normalize-job-listening.js +0 -1
  45. package/dist/core/workflow-model/normalize-job-listening.js.map +1 -1
  46. package/dist/core/workflow-model/normalize-job-success.d.ts.map +1 -1
  47. package/dist/core/workflow-model/normalize-job-success.js +0 -1
  48. package/dist/core/workflow-model/normalize-job-success.js.map +1 -1
  49. package/dist/core/workflow-model/normalize-jobs.d.ts.map +1 -1
  50. package/dist/core/workflow-model/normalize-jobs.js +315 -74
  51. package/dist/core/workflow-model/normalize-jobs.js.map +1 -1
  52. package/dist/core/workflow-model/normalize-step-gate.js +0 -1
  53. package/dist/core/workflow-model/normalize-step-gate.js.map +1 -1
  54. package/dist/core/workflow-model/normalize-triggers.d.ts.map +1 -1
  55. package/dist/core/workflow-model/normalize-triggers.js +0 -1
  56. package/dist/core/workflow-model/normalize-triggers.js.map +1 -1
  57. package/dist/core/workflow-model/normalize-workflow-document.d.ts +3 -0
  58. package/dist/core/workflow-model/normalize-workflow-document.d.ts.map +1 -1
  59. package/dist/core/workflow-model/normalize-workflow-document.js +34 -4
  60. package/dist/core/workflow-model/normalize-workflow-document.js.map +1 -1
  61. package/dist/core/workflow-model/parse-interpolation-field.d.ts +1 -1
  62. package/dist/core/workflow-model/parse-interpolation-field.d.ts.map +1 -1
  63. package/dist/core/workflow-model/parse-interpolation-field.js +27 -57
  64. package/dist/core/workflow-model/parse-interpolation-field.js.map +1 -1
  65. package/dist/core/workflow-model/validate-literal-name.d.ts +12 -0
  66. package/dist/core/workflow-model/validate-literal-name.d.ts.map +1 -0
  67. package/dist/core/workflow-model/validate-literal-name.js +36 -0
  68. package/dist/core/workflow-model/validate-literal-name.js.map +1 -0
  69. package/dist/core/workflow-model/validate-predicate-expression.d.ts +1 -2
  70. package/dist/core/workflow-model/validate-predicate-expression.d.ts.map +1 -1
  71. package/dist/core/workflow-model/validate-predicate-expression.js +31 -90
  72. package/dist/core/workflow-model/validate-predicate-expression.js.map +1 -1
  73. package/dist/core/workflow-model/validation-issue.d.ts +2 -1
  74. package/dist/core/workflow-model/validation-issue.d.ts.map +1 -1
  75. package/dist/core/workflow-model/validation-issue.js +8 -2
  76. package/dist/core/workflow-model/validation-issue.js.map +1 -1
  77. package/dist/core/workflow-model/workflow-field-label.d.ts.map +1 -1
  78. package/dist/core/workflow-model/workflow-field-label.js +16 -2
  79. package/dist/core/workflow-model/workflow-field-label.js.map +1 -1
  80. package/dist/core/workflow-yaml/invalid-workflow-yaml-error.d.ts.map +1 -1
  81. package/dist/db/db.d.ts +48 -10
  82. package/dist/db/db.d.ts.map +1 -1
  83. package/dist/db/schema/definitions.d.ts +1 -1
  84. package/dist/db/schema/outbox.d.ts +1 -1
  85. package/dist/db/schema/sync-states.d.ts +22 -3
  86. package/dist/db/schema/sync-states.d.ts.map +1 -1
  87. package/dist/db/schema/sync-states.js +3 -1
  88. package/dist/db/schema/sync-states.js.map +1 -1
  89. package/dist/db/sync-states.d.ts +2 -1
  90. package/dist/db/sync-states.d.ts.map +1 -1
  91. package/dist/db/sync-states.js +4 -0
  92. package/dist/db/sync-states.js.map +1 -1
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +3 -1
  95. package/dist/index.js.map +1 -1
  96. package/dist/presentation/dto/definition.d.ts.map +1 -1
  97. package/dist/presentation/dto/definition.js +2 -1
  98. package/dist/presentation/dto/definition.js.map +1 -1
  99. package/dist/presentation/routes/project-access.d.ts +1 -0
  100. package/dist/presentation/routes/project-access.d.ts.map +1 -1
  101. package/dist/presentation/routes/validate-definition.d.ts.map +1 -1
  102. package/dist/presentation/routes/validate-definition.js +5 -3
  103. package/dist/presentation/routes/validate-definition.js.map +1 -1
  104. package/dist/presentation/subscribers/index.d.ts +1 -0
  105. package/dist/presentation/subscribers/index.d.ts.map +1 -1
  106. package/dist/presentation/subscribers/index.js +1 -0
  107. package/dist/presentation/subscribers/index.js.map +1 -1
  108. package/dist/presentation/subscribers/on-integration-connection-available.d.ts +5 -0
  109. package/dist/presentation/subscribers/on-integration-connection-available.d.ts.map +1 -0
  110. package/dist/presentation/subscribers/on-integration-connection-available.js +32 -0
  111. package/dist/presentation/subscribers/on-integration-connection-available.js.map +1 -0
  112. package/dist/presentation/subscribers/start-definition-sync.d.ts +1 -0
  113. package/dist/presentation/subscribers/start-definition-sync.d.ts.map +1 -1
  114. package/dist/presentation/subscribers/start-definition-sync.js +30 -17
  115. package/dist/presentation/subscribers/start-definition-sync.js.map +1 -1
  116. package/dist/temporal/activities/sync-activities.d.ts +6 -2
  117. package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
  118. package/dist/temporal/activities/sync-activities.js +8 -1
  119. package/dist/temporal/activities/sync-activities.js.map +1 -1
  120. package/dist/temporal/workflows/definition-sync.js +4 -1
  121. package/dist/temporal/workflows/definition-sync.js.map +1 -1
  122. package/dist/temporal/workflows/index.bundle.js +9029 -3
  123. package/dist/tsconfig.test.tsbuildinfo +1 -1
  124. package/drizzle/0002_lumpy_ben_parker.sql +1 -0
  125. package/drizzle/meta/0002_snapshot.json +534 -0
  126. package/drizzle/meta/_journal.json +7 -0
  127. package/package.json +17 -17
  128. package/src/core/entities/index.ts +1 -0
  129. package/src/core/entities/sync-state.ts +23 -0
  130. package/src/core/entities/validation-warning.ts +5 -0
  131. package/src/core/parse-definition-warnings.test.ts +33 -0
  132. package/src/core/parse-definition.test.ts +7 -1
  133. package/src/core/parse-definition.ts +28 -10
  134. package/src/core/sync-definitions.test.ts +39 -0
  135. package/src/core/sync-definitions.ts +14 -4
  136. package/src/core/validate-definition.test.ts +128 -0
  137. package/src/core/validate-definition.ts +39 -8
  138. package/src/core/workflow-model/README.md +2 -2
  139. package/src/core/workflow-model/index.ts +1 -0
  140. package/src/core/workflow-model/invalid-workflow-model-error.ts +8 -3
  141. package/src/core/workflow-model/normalize-if-condition.ts +0 -2
  142. package/src/core/workflow-model/normalize-job-checkout.ts +105 -8
  143. package/src/core/workflow-model/normalize-job-listening.ts +0 -1
  144. package/src/core/workflow-model/normalize-job-success.ts +0 -1
  145. package/src/core/workflow-model/normalize-jobs.ts +335 -90
  146. package/src/core/workflow-model/normalize-step-gate.ts +0 -1
  147. package/src/core/workflow-model/normalize-triggers.ts +0 -1
  148. package/src/core/workflow-model/normalize-workflow-document.test.ts +876 -202
  149. package/src/core/workflow-model/normalize-workflow-document.ts +29 -4
  150. package/src/core/workflow-model/parse-interpolation-field.ts +50 -79
  151. package/src/core/workflow-model/validate-literal-name.ts +64 -0
  152. package/src/core/workflow-model/validate-predicate-expression.test.ts +153 -15
  153. package/src/core/workflow-model/validate-predicate-expression.ts +52 -120
  154. package/src/core/workflow-model/validation-issue.ts +4 -0
  155. package/src/core/workflow-model/workflow-field-label.ts +16 -2
  156. package/src/core/workflow-yaml/README.md +2 -2
  157. package/src/db/schema/sync-states.ts +4 -1
  158. package/src/db/sync-states.test.ts +57 -0
  159. package/src/db/sync-states.ts +6 -0
  160. package/src/index.ts +12 -1
  161. package/src/presentation/dto/definition.ts +1 -0
  162. package/src/presentation/routes/list-definitions.test.ts +1 -0
  163. package/src/presentation/routes/validate-definition.test.ts +1 -0
  164. package/src/presentation/routes/validate-definition.ts +7 -1
  165. package/src/presentation/subscribers/index.ts +1 -0
  166. package/src/presentation/subscribers/on-integration-connection-available.test.ts +132 -0
  167. package/src/presentation/subscribers/on-integration-connection-available.ts +44 -0
  168. package/src/presentation/subscribers/start-definition-sync.ts +35 -19
  169. package/src/temporal/activities/sync-activities.test.ts +16 -0
  170. package/src/temporal/activities/sync-activities.ts +18 -3
  171. package/src/temporal/workflows/definition-sync.ts +1 -1
  172. package/test/agent-validation-catalog.ts +17 -0
  173. package/test/fixtures/valid-listening-job.yml +2 -1
  174. package/tsconfig.build.tsbuildinfo +1 -1
@@ -2,8 +2,13 @@ import type {AgentValidationCatalog} from '@shipfox/api-agent-dto/inter-module';
2
2
  import {
3
3
  type AvailabilitySite,
4
4
  buildTypedRootsEnvironment,
5
+ classifyShellCodePosition,
5
6
  type ExpressionType,
6
7
  type ExpressionTypeEnvironment,
8
+ hoistPlannedRunCommand,
9
+ type ShellReevaluatingConstruct,
10
+ type UnsafeRunInterpolation,
11
+ UnsafeRunInterpolationError,
7
12
  type WorkflowJobTypeOverlay,
8
13
  type WorkflowStepTypeOverlay,
9
14
  } from '@shipfox/expression';
@@ -24,17 +29,22 @@ import type {
24
29
  WorkflowEnvTemplates,
25
30
  WorkflowFieldTemplate,
26
31
  WorkflowModelAgentStep,
32
+ WorkflowModelCheckoutStep,
27
33
  WorkflowModelJob,
28
34
  WorkflowModelRunStep,
29
35
  WorkflowModelStep,
36
+ WorkflowModelStepCheckout,
30
37
  WorkflowOutputTemplates,
31
38
  WorkflowStepSourceLocationMap,
32
39
  } from '../entities/workflow-model.js';
33
- import type {WorkflowModelValidationIssue} from './invalid-workflow-model-error.js';
40
+ import type {
41
+ WorkflowModelValidationIssue,
42
+ WorkflowModelValidationIssuePathSegment,
43
+ } from './invalid-workflow-model-error.js';
34
44
  import {normalizeAgentIntegrations} from './normalize-agent-integrations.js';
35
45
  import {normalizeEnv} from './normalize-env.js';
36
46
  import {normalizeIfCondition} from './normalize-if-condition.js';
37
- import {normalizeJobCheckout} from './normalize-job-checkout.js';
47
+ import {normalizeCheckout, normalizeJobCheckout} from './normalize-job-checkout.js';
38
48
  import {normalizeJobListening} from './normalize-job-listening.js';
39
49
  import {normalizeJobSuccess} from './normalize-job-success.js';
40
50
  import {normalizeNeeds} from './normalize-needs.js';
@@ -43,6 +53,7 @@ import {normalizeStepOutputs} from './normalize-step-outputs.js';
43
53
  import {parseDurationMs} from './parse-duration-ms.js';
44
54
  import {parseInterpolationField} from './parse-interpolation-field.js';
45
55
  import {stableId} from './stable-id.js';
56
+ import {unescapeLiteralName, validateLiteralName} from './validate-literal-name.js';
46
57
  import {issue} from './validation-issue.js';
47
58
 
48
59
  export interface NormalizeContext {
@@ -178,6 +189,8 @@ function normalizeJob(params: {
178
189
  sourceName: params.sourceName,
179
190
  jobId: id,
180
191
  job: params.job,
192
+ workflowEnvKeys: Object.keys(params.document.env ?? {}),
193
+ jobEnvKeys: Object.keys(params.job.env ?? {}),
181
194
  issues: params.issues,
182
195
  stepSourceLocations: params.stepSourceLocations,
183
196
  fillSite: stepFillSite,
@@ -194,7 +207,9 @@ function normalizeJob(params: {
194
207
  issues: params.issues,
195
208
  defaultRunnerLabels: params.context.defaultRunnerLabels,
196
209
  });
197
- const checkout = normalizeJobCheckout(params.job.checkout);
210
+ const checkout = normalizeJobCheckout({
211
+ checkout: params.job.checkout,
212
+ });
198
213
  const jobEnv = normalizeEnv({
199
214
  env: params.job.env,
200
215
  path: ['jobs', params.sourceName, 'env'],
@@ -212,7 +227,6 @@ function normalizeJob(params: {
212
227
  const condition = normalizeIfCondition({
213
228
  field: 'job.if',
214
229
  source: params.job.if,
215
- site: 'job-activation',
216
230
  path: ['jobs', params.sourceName, 'if'],
217
231
  invalidCode: 'invalid-job-if',
218
232
  invalidMessage: 'Job if must be a valid wrapped CEL boolean expression.',
@@ -242,17 +256,29 @@ function normalizeJob(params: {
242
256
  issues: params.issues,
243
257
  allowedJobReferences,
244
258
  });
245
- const name =
246
- params.job.name === undefined
259
+ if (params.job.name !== undefined) {
260
+ validateLiteralName({
261
+ field: 'job.name',
262
+ dynamicField: 'execution_name',
263
+ source: params.job.name,
264
+ path: ['jobs', params.sourceName, 'name'],
265
+ message: 'Job name must be literal. Move runtime interpolation to execution_name.',
266
+ issues: params.issues,
267
+ });
268
+ }
269
+ const name = params.job.name === undefined ? undefined : unescapeLiteralName(params.job.name);
270
+ const executionName =
271
+ params.job.execution_name === undefined
247
272
  ? undefined
248
273
  : (parseInterpolationField({
249
- field: 'job.name',
250
- source: params.job.name,
251
- path: ['jobs', params.sourceName, 'name'],
274
+ field: 'job.execution_name',
275
+ source: params.job.execution_name,
276
+ path: ['jobs', params.sourceName, 'execution_name'],
252
277
  issues: params.issues,
278
+ fillSite: 'execution-creation',
253
279
  allowedJobReferences,
254
280
  typeOverlay: upstreamJobsTypeOverlay,
255
- }) ?? [{kind: 'literal' as const, value: params.job.name}]);
281
+ }) ?? [{kind: 'literal' as const, value: params.job.execution_name}]);
256
282
 
257
283
  return {
258
284
  id,
@@ -268,6 +294,7 @@ function normalizeJob(params: {
268
294
  ...(executionTimeoutMs === undefined ? {} : {executionTimeoutMs}),
269
295
  ...(listening === undefined ? {} : {listening}),
270
296
  ...(name === undefined ? {} : {name}),
297
+ ...(executionName === undefined ? {} : {executionName}),
271
298
  ...jobEnv,
272
299
  dependencies,
273
300
  steps,
@@ -377,63 +404,29 @@ function normalizeJobOutputs(params: {
377
404
  typeOverlay,
378
405
  }) ?? [{kind: 'literal' as const, value: source}];
379
406
  templates[key] = template;
380
- types[key] = inferJobOutputType({
381
- sourceName: params.sourceName,
382
- key,
383
- source,
384
- template,
385
- issues: params.issues,
386
- });
407
+ types[key] = inferJobOutputType(template);
387
408
  }
388
409
 
389
410
  return {templates, types};
390
411
  }
391
412
 
392
- function inferJobOutputType(params: {
393
- sourceName: string;
394
- key: string;
395
- source: string;
396
- template: WorkflowFieldTemplate;
397
- issues: WorkflowModelValidationIssue[];
398
- }): ExpressionType {
399
- if (params.template.length !== 1) return 'string';
413
+ function inferJobOutputType(template: WorkflowFieldTemplate): ExpressionType {
414
+ if (template.length !== 1) return 'string';
400
415
 
401
- const [segment] = params.template;
416
+ const [segment] = template;
402
417
  if (segment?.kind !== 'deferred') return 'string';
403
418
 
404
419
  const resultType = segment.expression.resultType;
405
420
  if (resultType === undefined) return 'string';
406
- if (isScalarExpressionType(resultType)) return resultType;
407
-
408
- params.issues.push(
409
- issue({
410
- code: 'invalid-job-output',
411
- message: `Job output "${params.key}" must resolve to a scalar value.`,
412
- path: ['jobs', params.sourceName, 'outputs', params.key],
413
- details: {
414
- output: params.key,
415
- source: params.source,
416
- },
417
- }),
418
- );
419
- return 'string';
420
- }
421
-
422
- function isScalarExpressionType(type: ExpressionType): boolean {
423
- return (
424
- type === 'string' ||
425
- type === 'int' ||
426
- type === 'double' ||
427
- type === 'bool' ||
428
- type === 'null' ||
429
- type === 'timestamp'
430
- );
421
+ return resultType;
431
422
  }
432
423
 
433
424
  function normalizeJobSteps(params: {
434
425
  sourceName: string;
435
426
  jobId: string;
436
427
  job: WorkflowDocumentJob;
428
+ workflowEnvKeys: readonly string[];
429
+ jobEnvKeys: readonly string[];
437
430
  issues: WorkflowModelValidationIssue[];
438
431
  stepSourceLocations: WorkflowStepSourceLocationMap | undefined;
439
432
  fillSite: AvailabilitySite;
@@ -445,13 +438,15 @@ function normalizeJobSteps(params: {
445
438
  }): readonly WorkflowModelStep[] {
446
439
  const usedStepIds = new Map<string, number>();
447
440
 
448
- return params.job.steps.map((step, index) =>
449
- normalizeStep({
441
+ return params.job.steps.flatMap((step, index) => {
442
+ const normalized = normalizeStep({
450
443
  step,
451
444
  index,
452
445
  sourceName: params.sourceName,
453
446
  jobId: params.jobId,
454
447
  allSteps: params.job.steps,
448
+ workflowEnvKeys: params.workflowEnvKeys,
449
+ jobEnvKeys: params.jobEnvKeys,
455
450
  usedStepIds,
456
451
  issues: params.issues,
457
452
  stepSourceLocations: params.stepSourceLocations,
@@ -461,8 +456,9 @@ function normalizeJobSteps(params: {
461
456
  upstreamJobs: params.upstreamJobs,
462
457
  directNeedJobs: params.directNeedJobs,
463
458
  context: params.context,
464
- }),
465
- );
459
+ });
460
+ return normalized === undefined ? [] : [normalized];
461
+ });
466
462
  }
467
463
 
468
464
  function normalizeStep(params: {
@@ -471,6 +467,8 @@ function normalizeStep(params: {
471
467
  sourceName: string;
472
468
  jobId: string;
473
469
  allSteps: readonly WorkflowDocumentStep[];
470
+ workflowEnvKeys: readonly string[];
471
+ jobEnvKeys: readonly string[];
474
472
  usedStepIds: Map<string, number>;
475
473
  issues: WorkflowModelValidationIssue[];
476
474
  stepSourceLocations: WorkflowStepSourceLocationMap | undefined;
@@ -480,7 +478,7 @@ function normalizeStep(params: {
480
478
  upstreamJobs: readonly WorkflowJobTypeOverlay[];
481
479
  directNeedJobs: readonly WorkflowJobTypeOverlay[];
482
480
  context: NormalizeContext;
483
- }): WorkflowModelStep {
481
+ }): WorkflowModelStep | undefined {
484
482
  const stepKey = params.step.key;
485
483
  const stepId =
486
484
  stepKey === undefined
@@ -532,7 +530,6 @@ function normalizeStep(params: {
532
530
  const condition = normalizeIfCondition({
533
531
  field: 'step.if',
534
532
  source: params.step.if,
535
- site: 'step-dispatch',
536
533
  path: ['jobs', params.sourceName, 'steps', params.index, 'if'],
537
534
  invalidCode: 'invalid-step-if',
538
535
  invalidMessage: 'Step if must be a valid wrapped CEL boolean expression.',
@@ -567,10 +564,25 @@ function normalizeStep(params: {
567
564
  allowedJobReferences: params.allowedJobReferences,
568
565
  typeOverlay,
569
566
  });
567
+ const workingDirectory =
568
+ params.step.working_directory === undefined
569
+ ? undefined
570
+ : parseInterpolationField({
571
+ field: 'step.working_directory',
572
+ source: params.step.working_directory,
573
+ path: ['jobs', params.sourceName, 'steps', params.index, 'working_directory'],
574
+ issues: params.issues,
575
+ fillSite: params.fillSite,
576
+ allowedJobReferences: params.allowedJobReferences,
577
+ typeOverlay,
578
+ });
570
579
  const stepBase = {
571
580
  id: stepId,
572
581
  ...(stepKey === undefined ? {} : {key: stepKey}),
573
582
  ...(params.step.name === undefined ? {} : {name: params.step.name}),
583
+ ...(params.step.working_directory === undefined
584
+ ? {}
585
+ : {workingDirectory: params.step.working_directory}),
574
586
  ...(outputs === undefined ? {} : {outputs}),
575
587
  ...(sourceLocation === undefined ? {} : {sourceLocation}),
576
588
  ...(condition === undefined ? {} : {if: condition}),
@@ -583,7 +595,10 @@ function normalizeStep(params: {
583
595
  stepBase,
584
596
  sourceName: params.sourceName,
585
597
  stepIndex: params.index,
598
+ workflowEnvKeys: params.workflowEnvKeys,
599
+ jobEnvKeys: params.jobEnvKeys,
586
600
  name,
601
+ workingDirectory,
587
602
  issues: params.issues,
588
603
  fillSite: params.fillSite,
589
604
  allowedJobReferences: params.allowedJobReferences,
@@ -598,6 +613,7 @@ function normalizeStep(params: {
598
613
  sourceName: params.sourceName,
599
614
  stepIndex: params.index,
600
615
  name,
616
+ workingDirectory,
601
617
  issues: params.issues,
602
618
  fillSite: params.fillSite,
603
619
  allowedJobReferences: params.allowedJobReferences,
@@ -606,9 +622,55 @@ function normalizeStep(params: {
606
622
  });
607
623
  }
608
624
 
609
- // workflowDocumentStepSchema requires either `run` or an agent `prompt`; this
610
- // keeps the model-step union honest if callers bypass the document parser.
611
- throw new Error(`Workflow step "${stepId}" is neither a run nor an agent step`);
625
+ if (params.step.checkout !== undefined) {
626
+ return normalizeCheckoutStep({
627
+ step: params.step,
628
+ stepBase,
629
+ name,
630
+ sourceName: params.sourceName,
631
+ stepIndex: params.index,
632
+ issues: params.issues,
633
+ fillSite: params.fillSite,
634
+ allowedJobReferences: params.allowedJobReferences,
635
+ typeOverlay,
636
+ });
637
+ }
638
+
639
+ // Keep the model-step union honest if callers bypass the document parser.
640
+ throw new Error(`Workflow step "${stepId}" is neither a run, agent, nor checkout step`);
641
+ }
642
+
643
+ function normalizeCheckoutStep(params: {
644
+ step: WorkflowDocumentStep;
645
+ stepBase: WorkflowModelStepBaseFields;
646
+ name: WorkflowFieldTemplate | undefined;
647
+ sourceName: string;
648
+ stepIndex: number;
649
+ issues: WorkflowModelValidationIssue[];
650
+ fillSite: AvailabilitySite;
651
+ allowedJobReferences: ReadonlySet<string>;
652
+ typeOverlay?: ExpressionTypeEnvironment | undefined;
653
+ }): WorkflowModelCheckoutStep {
654
+ const checkout = params.step.checkout;
655
+ if (checkout === undefined) {
656
+ throw new Error('Checkout step normalization requires checkout settings');
657
+ }
658
+
659
+ const normalizedCheckout: WorkflowModelStepCheckout = normalizeCheckout({
660
+ checkout,
661
+ issues: params.issues,
662
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'checkout'],
663
+ fillSite: params.fillSite,
664
+ allowedJobReferences: params.allowedJobReferences,
665
+ typeOverlay: params.typeOverlay,
666
+ });
667
+
668
+ return {
669
+ ...params.stepBase,
670
+ kind: 'checkout',
671
+ checkout: normalizedCheckout,
672
+ ...(params.name === undefined ? {} : {templates: {name: params.name}}),
673
+ };
612
674
  }
613
675
 
614
676
  function normalizeRunStep(params: {
@@ -616,7 +678,10 @@ function normalizeRunStep(params: {
616
678
  stepBase: WorkflowModelStepBaseFields;
617
679
  sourceName: string;
618
680
  stepIndex: number;
681
+ workflowEnvKeys: readonly string[];
682
+ jobEnvKeys: readonly string[];
619
683
  name: WorkflowFieldTemplate | undefined;
684
+ workingDirectory: WorkflowFieldTemplate | undefined;
620
685
  issues: WorkflowModelValidationIssue[];
621
686
  fillSite: AvailabilitySite;
622
687
  allowedJobReferences: ReadonlySet<string>;
@@ -643,9 +708,19 @@ function normalizeRunStep(params: {
643
708
  allowedJobReferences: params.allowedJobReferences,
644
709
  typeOverlay: params.typeOverlay,
645
710
  });
711
+ addReevaluatingCommandWarnings({
712
+ command: commandTemplate,
713
+ source: params.step.run,
714
+ workflowEnvKeys: params.workflowEnvKeys,
715
+ jobEnvKeys: params.jobEnvKeys,
716
+ stepEnvKeys: Object.keys(params.step.env ?? {}),
717
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'run'],
718
+ issues: params.issues,
719
+ });
646
720
  const templates = optionalRunStepTemplates({
647
721
  command: commandTemplate,
648
722
  name: params.name,
723
+ workingDirectory: params.workingDirectory,
649
724
  env: stepEnv.templates?.env,
650
725
  });
651
726
 
@@ -658,12 +733,110 @@ function normalizeRunStep(params: {
658
733
  };
659
734
  }
660
735
 
736
+ function addReevaluatingCommandWarnings(params: {
737
+ command: WorkflowFieldTemplate | undefined;
738
+ source: string;
739
+ workflowEnvKeys: readonly string[];
740
+ jobEnvKeys: readonly string[];
741
+ stepEnvKeys: readonly string[];
742
+ path: readonly WorkflowModelValidationIssuePathSegment[];
743
+ issues: WorkflowModelValidationIssue[];
744
+ }): void {
745
+ const envKeys = new Set([...params.workflowEnvKeys, ...params.jobEnvKeys, ...params.stepEnvKeys]);
746
+ let hoisted: ReturnType<typeof hoistPlannedRunCommand>;
747
+
748
+ try {
749
+ hoisted = hoistPlannedRunCommand({
750
+ field: {
751
+ segments: params.command ?? [{kind: 'literal', value: params.source}],
752
+ },
753
+ reservedNames: envKeys,
754
+ });
755
+ } catch (error) {
756
+ if (!(error instanceof UnsafeRunInterpolationError)) return;
757
+
758
+ for (const occurrence of error.occurrences) {
759
+ params.issues.push(
760
+ issue({
761
+ code: 're-evaluating-command',
762
+ message: unsafeInterpolationWarningMessage(occurrence),
763
+ path: params.path,
764
+ severity: 'warning',
765
+ }),
766
+ );
767
+ }
768
+
769
+ if (error.partial === undefined) return;
770
+ hoisted = error.partial;
771
+ }
772
+
773
+ try {
774
+ const matches = classifyShellCodePosition({
775
+ command: hoisted.command,
776
+ workflowDataNames: [...envKeys, ...hoisted.bindings.map((binding) => binding.name)],
777
+ }).matches;
778
+
779
+ for (const match of matches) {
780
+ params.issues.push(
781
+ issue({
782
+ code: 're-evaluating-command',
783
+ message: reevaluatingCommandWarningMessage(match.construct, match.name),
784
+ path: params.path,
785
+ severity: 'warning',
786
+ }),
787
+ );
788
+ }
789
+ } catch {
790
+ return;
791
+ }
792
+ }
793
+
794
+ function unsafeInterpolationWarningMessage(error: UnsafeRunInterpolation): string {
795
+ const regionLabel = {
796
+ single: 'single-quoted shell text',
797
+ double: 'double-quoted shell text',
798
+ 'dollar-single': 'ANSI-C shell text',
799
+ 'dollar-double': 'dollar-double-quoted shell text',
800
+ 'paren-sub': 'command substitution',
801
+ arith: 'shell arithmetic',
802
+ backtick: 'backtick substitution',
803
+ 'param-brace': 'shell parameter expansion',
804
+ heredoc: 'a heredoc',
805
+ 'line-comment': 'a shell comment',
806
+ escape: 'a shell escape',
807
+ }[error.region];
808
+
809
+ return `Value "${error.source}" is inside ${regionLabel} and will be re-executed as code. Hoisting cannot protect it; move the interpolation to a normal quoted argument instead.`;
810
+ }
811
+
812
+ function reevaluatingCommandWarningMessage(
813
+ construct: ShellReevaluatingConstruct,
814
+ name: string,
815
+ ): string {
816
+ const constructLabel = {
817
+ eval: 'eval',
818
+ 'sh-c': 'sh -c',
819
+ 'bash-c': 'bash -c',
820
+ source: 'source',
821
+ let: 'let',
822
+ 'declare-i': 'declare -i',
823
+ arithmetic: 'shell arithmetic',
824
+ awk: 'awk',
825
+ jq: 'jq',
826
+ sed: 'sed',
827
+ 'xargs-sh-c': 'xargs sh -c',
828
+ }[construct];
829
+
830
+ return `Value "$${name}" is passed to ${constructLabel} and re-executed as code. Hoisting cannot protect it; pass the value to a fixed program instead.`;
831
+ }
832
+
661
833
  function normalizeAgentStep(params: {
662
834
  step: WorkflowDocumentStep;
663
835
  stepBase: WorkflowModelStepBaseFields;
664
836
  sourceName: string;
665
837
  stepIndex: number;
666
838
  name: WorkflowFieldTemplate | undefined;
839
+ workingDirectory: WorkflowFieldTemplate | undefined;
667
840
  issues: WorkflowModelValidationIssue[];
668
841
  fillSite: AvailabilitySite;
669
842
  allowedJobReferences: ReadonlySet<string>;
@@ -707,12 +880,27 @@ function normalizeAgentStep(params: {
707
880
  allowedJobReferences: params.allowedJobReferences,
708
881
  typeOverlay: params.typeOverlay,
709
882
  });
883
+ const thinkingTemplate =
884
+ params.step.thinking === undefined
885
+ ? undefined
886
+ : parseInterpolationField({
887
+ field: 'agent.thinking',
888
+ source: params.step.thinking,
889
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'thinking'],
890
+ issues: params.issues,
891
+ fillSite: params.fillSite,
892
+ allowedJobReferences: params.allowedJobReferences,
893
+ typeOverlay: params.typeOverlay,
894
+ });
710
895
  validateAgentStep({
711
896
  step: params.step,
712
897
  sourceName: params.sourceName,
713
898
  stepIndex: params.stepIndex,
714
899
  issues: params.issues,
715
- validateLiteralProvider: providerTemplate === undefined,
900
+ validateLiteralModel:
901
+ params.step.model !== undefined && !hasInterpolationSyntax(params.step.model),
902
+ validateLiteralProvider:
903
+ params.step.provider !== undefined && !hasInterpolationSyntax(params.step.provider),
716
904
  agentValidationCatalog: params.context.agentValidationCatalog,
717
905
  });
718
906
  const integrations = normalizeAgentIntegrations({
@@ -726,7 +914,9 @@ function normalizeAgentStep(params: {
726
914
  prompt: promptTemplate,
727
915
  model: modelTemplate,
728
916
  provider: providerTemplate,
917
+ thinking: thinkingTemplate,
729
918
  name: params.name,
919
+ workingDirectory: params.workingDirectory,
730
920
  });
731
921
 
732
922
  return {
@@ -748,47 +938,83 @@ function validateAgentStep(params: {
748
938
  sourceName: string;
749
939
  stepIndex: number;
750
940
  issues: WorkflowModelValidationIssue[];
941
+ validateLiteralModel: boolean;
751
942
  validateLiteralProvider: boolean;
752
943
  agentValidationCatalog: AgentValidationCatalog;
753
944
  }): void {
754
945
  validateHarnessThinking(params);
755
946
  validateHarnessTools(params);
756
- if (!params.validateLiteralProvider) return;
757
-
758
947
  const providerId = params.step.provider;
759
- if (providerId === undefined) return;
760
-
761
- const provider = params.agentValidationCatalog.providers.find((entry) => entry.id === providerId);
762
948
  const harness = params.step.harness;
763
- if (provider === undefined && harness === 'pi') return;
949
+ const provider =
950
+ providerId === undefined
951
+ ? undefined
952
+ : params.agentValidationCatalog.providers.find((entry) => entry.id === providerId);
953
+
954
+ if (params.validateLiteralProvider && providerId !== undefined) {
955
+ if (provider === undefined) {
956
+ if (harness === undefined || harness === 'pi') return;
957
+ params.issues.push(
958
+ issue({
959
+ code: 'invalid-provider',
960
+ message: `Provider "${providerId}" is not supported.`,
961
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
962
+ details: {provider: providerId},
963
+ }),
964
+ );
965
+ return;
966
+ }
764
967
 
765
- if (provider === undefined || provider.support_status !== 'supported') {
766
- params.issues.push(
767
- issue({
768
- code: 'invalid-provider',
769
- message: `Provider "${providerId}" is not supported.`,
770
- path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
771
- details: {provider: providerId},
772
- }),
968
+ if (provider.support_status !== 'supported') {
969
+ params.issues.push(
970
+ issue({
971
+ code: 'invalid-provider',
972
+ message: `Provider "${providerId}" is not supported.`,
973
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
974
+ details: {provider: providerId},
975
+ }),
976
+ );
977
+ return;
978
+ }
979
+
980
+ if (harness === undefined) return;
981
+
982
+ const descriptor = params.agentValidationCatalog.harnesses.find(
983
+ (entry) => entry.id === harness,
773
984
  );
774
- return;
985
+ if (!descriptor?.supported_provider_ids.includes(providerId)) {
986
+ params.issues.push(
987
+ issue({
988
+ code: 'harness-provider-incompatible',
989
+ message: `Harness "${harness}" does not support provider: ${providerId}. Supported providers: ${descriptor?.supported_provider_ids.join(', ') ?? ''}.`,
990
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
991
+ details: {
992
+ harness,
993
+ provider: providerId,
994
+ supportedProviders: descriptor?.supported_provider_ids ?? [],
995
+ },
996
+ }),
997
+ );
998
+ return;
999
+ }
775
1000
  }
776
1001
 
777
- if (harness === undefined) return;
1002
+ if (!params.validateLiteralModel || providerId === undefined || harness === undefined) return;
1003
+ if (provider === undefined || provider.support_status !== 'supported') return;
1004
+
1005
+ const model = params.step.model;
1006
+ if (model === undefined) return;
778
1007
 
779
1008
  const descriptor = params.agentValidationCatalog.harnesses.find((entry) => entry.id === harness);
780
- if (descriptor?.supported_provider_ids.includes(providerId)) return;
1009
+ const modelIds = descriptor?.model_ids_by_provider?.[providerId];
1010
+ if (modelIds === undefined || modelIds.includes(model)) return;
781
1011
 
782
1012
  params.issues.push(
783
1013
  issue({
784
- code: 'harness-provider-incompatible',
785
- message: `Harness "${harness}" does not support provider: ${providerId}. Supported providers: ${descriptor?.supported_provider_ids.join(', ') ?? ''}.`,
786
- path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
787
- details: {
788
- harness,
789
- provider: providerId,
790
- supportedProviders: descriptor?.supported_provider_ids ?? [],
791
- },
1014
+ code: 'invalid-model',
1015
+ message: `Agent model "${model}" is not available for harness "${harness}" and provider "${providerId}".`,
1016
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'model'],
1017
+ details: {harness, provider: providerId, model},
792
1018
  }),
793
1019
  );
794
1020
  }
@@ -844,11 +1070,14 @@ function validateHarnessThinking(params: {
844
1070
  }): void {
845
1071
  const {harness, thinking} = params.step;
846
1072
  if (harness === undefined || thinking === undefined) return;
1073
+ // An interpolated level is only known when the step dispatches, so the agent
1074
+ // module checks it against the resolved harness there.
1075
+ if (hasInterpolationSyntax(thinking)) return;
847
1076
 
848
1077
  const supportedLevels =
849
1078
  params.agentValidationCatalog.harnesses.find((entry) => entry.id === harness)
850
1079
  ?.thinking_levels ?? [];
851
- if (supportedLevels.includes(thinking)) return;
1080
+ if ((supportedLevels as readonly string[]).includes(thinking)) return;
852
1081
  params.issues.push(
853
1082
  issue({
854
1083
  code: 'harness-thinking-incompatible',
@@ -953,27 +1182,35 @@ function validateRunnerLabels(params: {
953
1182
 
954
1183
  type WorkflowModelStepBaseFields = Pick<
955
1184
  WorkflowModelStep,
956
- 'id' | 'key' | 'name' | 'outputs' | 'sourceLocation' | 'gate'
1185
+ 'id' | 'key' | 'name' | 'workingDirectory' | 'outputs' | 'sourceLocation' | 'gate'
957
1186
  >;
958
1187
 
959
1188
  function optionalRunStepTemplates(params: {
960
1189
  command: WorkflowFieldTemplate | undefined;
961
1190
  name: WorkflowFieldTemplate | undefined;
1191
+ workingDirectory: WorkflowFieldTemplate | undefined;
962
1192
  env: WorkflowEnvTemplates | undefined;
963
1193
  }):
964
1194
  | {
965
1195
  command?: WorkflowFieldTemplate;
966
1196
  name?: WorkflowFieldTemplate;
1197
+ workingDirectory?: WorkflowFieldTemplate;
967
1198
  env?: WorkflowEnvTemplates;
968
1199
  }
969
1200
  | undefined {
970
- if (params.command === undefined && params.name === undefined && params.env === undefined) {
1201
+ if (
1202
+ params.command === undefined &&
1203
+ params.name === undefined &&
1204
+ params.workingDirectory === undefined &&
1205
+ params.env === undefined
1206
+ ) {
971
1207
  return undefined;
972
1208
  }
973
1209
 
974
1210
  return {
975
1211
  ...(params.command === undefined ? {} : {command: params.command}),
976
1212
  ...(params.name === undefined ? {} : {name: params.name}),
1213
+ ...(params.workingDirectory === undefined ? {} : {workingDirectory: params.workingDirectory}),
977
1214
  ...(params.env === undefined ? {} : {env: params.env}),
978
1215
  };
979
1216
  }
@@ -982,20 +1219,26 @@ function optionalAgentStepTemplates(params: {
982
1219
  prompt: WorkflowFieldTemplate | undefined;
983
1220
  model: WorkflowFieldTemplate | undefined;
984
1221
  provider: WorkflowFieldTemplate | undefined;
1222
+ thinking: WorkflowFieldTemplate | undefined;
985
1223
  name: WorkflowFieldTemplate | undefined;
1224
+ workingDirectory: WorkflowFieldTemplate | undefined;
986
1225
  }):
987
1226
  | {
988
1227
  prompt?: WorkflowFieldTemplate;
989
1228
  model?: WorkflowFieldTemplate;
990
1229
  provider?: WorkflowFieldTemplate;
1230
+ thinking?: WorkflowFieldTemplate;
991
1231
  name?: WorkflowFieldTemplate;
1232
+ workingDirectory?: WorkflowFieldTemplate;
992
1233
  }
993
1234
  | undefined {
994
1235
  if (
995
1236
  params.prompt === undefined &&
996
1237
  params.model === undefined &&
997
1238
  params.provider === undefined &&
998
- params.name === undefined
1239
+ params.thinking === undefined &&
1240
+ params.name === undefined &&
1241
+ params.workingDirectory === undefined
999
1242
  ) {
1000
1243
  return undefined;
1001
1244
  }
@@ -1004,6 +1247,8 @@ function optionalAgentStepTemplates(params: {
1004
1247
  ...(params.prompt === undefined ? {} : {prompt: params.prompt}),
1005
1248
  ...(params.model === undefined ? {} : {model: params.model}),
1006
1249
  ...(params.provider === undefined ? {} : {provider: params.provider}),
1250
+ ...(params.thinking === undefined ? {} : {thinking: params.thinking}),
1007
1251
  ...(params.name === undefined ? {} : {name: params.name}),
1252
+ ...(params.workingDirectory === undefined ? {} : {workingDirectory: params.workingDirectory}),
1008
1253
  };
1009
1254
  }