@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.
- package/.turbo/turbo-build.log +13 -12
- package/CHANGELOG.md +81 -0
- package/dist/core/entities/index.d.ts +1 -0
- package/dist/core/entities/index.d.ts.map +1 -1
- package/dist/core/entities/index.js +1 -0
- package/dist/core/entities/index.js.map +1 -1
- package/dist/core/entities/sync-state.d.ts +4 -0
- package/dist/core/entities/sync-state.d.ts.map +1 -1
- package/dist/core/entities/sync-state.js +10 -0
- package/dist/core/entities/sync-state.js.map +1 -1
- package/dist/core/entities/validation-warning.d.ts +6 -0
- package/dist/core/entities/validation-warning.d.ts.map +1 -0
- package/dist/core/entities/validation-warning.js +3 -0
- package/dist/core/entities/validation-warning.js.map +1 -0
- package/dist/core/parse-definition.d.ts +9 -7
- package/dist/core/parse-definition.d.ts.map +1 -1
- package/dist/core/parse-definition.js +14 -2
- package/dist/core/parse-definition.js.map +1 -1
- package/dist/core/sync-definitions.d.ts +2 -0
- package/dist/core/sync-definitions.d.ts.map +1 -1
- package/dist/core/sync-definitions.js +6 -5
- package/dist/core/sync-definitions.js.map +1 -1
- package/dist/core/validate-definition.d.ts +9 -5
- package/dist/core/validate-definition.d.ts.map +1 -1
- package/dist/core/validate-definition.js +23 -2
- package/dist/core/validate-definition.js.map +1 -1
- package/dist/core/workflow-model/index.d.ts +1 -1
- package/dist/core/workflow-model/index.d.ts.map +1 -1
- package/dist/core/workflow-model/index.js.map +1 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +3 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
- package/dist/core/workflow-model/normalize-if-condition.d.ts +0 -1
- package/dist/core/workflow-model/normalize-if-condition.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-if-condition.js +0 -1
- package/dist/core/workflow-model/normalize-if-condition.js.map +1 -1
- package/dist/core/workflow-model/normalize-job-checkout.d.ts +15 -3
- package/dist/core/workflow-model/normalize-job-checkout.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-job-checkout.js +83 -4
- package/dist/core/workflow-model/normalize-job-checkout.js.map +1 -1
- package/dist/core/workflow-model/normalize-job-listening.js +0 -1
- package/dist/core/workflow-model/normalize-job-listening.js.map +1 -1
- package/dist/core/workflow-model/normalize-job-success.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-job-success.js +0 -1
- package/dist/core/workflow-model/normalize-job-success.js.map +1 -1
- package/dist/core/workflow-model/normalize-jobs.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-jobs.js +310 -45
- package/dist/core/workflow-model/normalize-jobs.js.map +1 -1
- package/dist/core/workflow-model/normalize-step-gate.js +0 -1
- package/dist/core/workflow-model/normalize-step-gate.js.map +1 -1
- package/dist/core/workflow-model/normalize-triggers.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-triggers.js +0 -1
- package/dist/core/workflow-model/normalize-triggers.js.map +1 -1
- package/dist/core/workflow-model/normalize-workflow-document.d.ts +3 -0
- package/dist/core/workflow-model/normalize-workflow-document.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-workflow-document.js +34 -4
- package/dist/core/workflow-model/normalize-workflow-document.js.map +1 -1
- package/dist/core/workflow-model/parse-interpolation-field.d.ts +1 -1
- package/dist/core/workflow-model/parse-interpolation-field.d.ts.map +1 -1
- package/dist/core/workflow-model/parse-interpolation-field.js +27 -57
- package/dist/core/workflow-model/parse-interpolation-field.js.map +1 -1
- package/dist/core/workflow-model/validate-literal-name.d.ts +12 -0
- package/dist/core/workflow-model/validate-literal-name.d.ts.map +1 -0
- package/dist/core/workflow-model/validate-literal-name.js +36 -0
- package/dist/core/workflow-model/validate-literal-name.js.map +1 -0
- package/dist/core/workflow-model/validate-predicate-expression.d.ts +1 -2
- package/dist/core/workflow-model/validate-predicate-expression.d.ts.map +1 -1
- package/dist/core/workflow-model/validate-predicate-expression.js +31 -90
- package/dist/core/workflow-model/validate-predicate-expression.js.map +1 -1
- package/dist/core/workflow-model/validation-issue.d.ts +2 -1
- package/dist/core/workflow-model/validation-issue.d.ts.map +1 -1
- package/dist/core/workflow-model/validation-issue.js +8 -2
- package/dist/core/workflow-model/validation-issue.js.map +1 -1
- package/dist/core/workflow-model/workflow-field-label.d.ts.map +1 -1
- package/dist/core/workflow-model/workflow-field-label.js +16 -2
- package/dist/core/workflow-model/workflow-field-label.js.map +1 -1
- package/dist/db/db.d.ts +38 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/schema/sync-states.d.ts +19 -0
- package/dist/db/schema/sync-states.d.ts.map +1 -1
- package/dist/db/schema/sync-states.js +3 -1
- package/dist/db/schema/sync-states.js.map +1 -1
- package/dist/db/sync-states.d.ts +2 -1
- package/dist/db/sync-states.d.ts.map +1 -1
- package/dist/db/sync-states.js +4 -0
- package/dist/db/sync-states.js.map +1 -1
- package/dist/presentation/dto/definition.d.ts.map +1 -1
- package/dist/presentation/dto/definition.js +2 -1
- package/dist/presentation/dto/definition.js.map +1 -1
- package/dist/presentation/routes/project-access.d.ts +1 -0
- package/dist/presentation/routes/project-access.d.ts.map +1 -1
- package/dist/presentation/routes/validate-definition.d.ts.map +1 -1
- package/dist/presentation/routes/validate-definition.js +5 -3
- package/dist/presentation/routes/validate-definition.js.map +1 -1
- package/dist/temporal/activities/sync-activities.d.ts +6 -2
- package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
- package/dist/temporal/activities/sync-activities.js +8 -1
- package/dist/temporal/activities/sync-activities.js.map +1 -1
- package/dist/temporal/workflows/definition-sync.js +4 -1
- package/dist/temporal/workflows/definition-sync.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +9029 -3
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0002_lumpy_ben_parker.sql +1 -0
- package/drizzle/meta/0002_snapshot.json +534 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +14 -14
- package/src/core/entities/index.ts +1 -0
- package/src/core/entities/sync-state.ts +23 -0
- package/src/core/entities/validation-warning.ts +5 -0
- package/src/core/parse-definition-warnings.test.ts +33 -0
- package/src/core/parse-definition.test.ts +7 -1
- package/src/core/parse-definition.ts +28 -10
- package/src/core/sync-definitions.test.ts +39 -0
- package/src/core/sync-definitions.ts +14 -4
- package/src/core/validate-definition.test.ts +128 -0
- package/src/core/validate-definition.ts +39 -8
- package/src/core/workflow-model/README.md +2 -2
- package/src/core/workflow-model/index.ts +1 -0
- package/src/core/workflow-model/invalid-workflow-model-error.ts +8 -2
- package/src/core/workflow-model/normalize-if-condition.ts +0 -2
- package/src/core/workflow-model/normalize-job-checkout.ts +105 -8
- package/src/core/workflow-model/normalize-job-listening.ts +0 -1
- package/src/core/workflow-model/normalize-job-success.ts +0 -1
- package/src/core/workflow-model/normalize-jobs.ts +330 -49
- package/src/core/workflow-model/normalize-step-gate.ts +0 -1
- package/src/core/workflow-model/normalize-triggers.ts +0 -1
- package/src/core/workflow-model/normalize-workflow-document.test.ts +732 -185
- package/src/core/workflow-model/normalize-workflow-document.ts +29 -4
- package/src/core/workflow-model/parse-interpolation-field.ts +50 -79
- package/src/core/workflow-model/validate-literal-name.ts +64 -0
- package/src/core/workflow-model/validate-predicate-expression.test.ts +153 -15
- package/src/core/workflow-model/validate-predicate-expression.ts +52 -120
- package/src/core/workflow-model/validation-issue.ts +4 -0
- package/src/core/workflow-model/workflow-field-label.ts +16 -2
- package/src/core/workflow-yaml/README.md +2 -2
- package/src/db/schema/sync-states.ts +4 -1
- package/src/db/sync-states.test.ts +57 -0
- package/src/db/sync-states.ts +6 -0
- package/src/presentation/dto/definition.ts +1 -0
- package/src/presentation/routes/list-definitions.test.ts +1 -0
- package/src/presentation/routes/validate-definition.test.ts +1 -0
- package/src/presentation/routes/validate-definition.ts +7 -1
- package/src/temporal/activities/sync-activities.test.ts +16 -0
- package/src/temporal/activities/sync-activities.ts +18 -3
- package/src/temporal/workflows/definition-sync.ts +1 -1
- package/test/agent-validation-catalog.ts +17 -0
- package/test/fixtures/valid-listening-job.yml +2 -1
- 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]
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
?
|
|
156
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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-
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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.',
|