Compare commits

...
Author SHA1 Message Date
Douglas LowderandClaude Opus 5.5 0f6a755640 [eas-cli] Fix workflow:validate crash on job schemas without a type
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 14:51:48 -07:00
3 changed files with 83 additions and 4 deletions

No files matched your search

+1
View File
@@ -17,6 +17,7 @@ This is the log of notable changes to EAS CLI and related packages.
- [build-tools] Clear old Maestro CLI HTML reports before each test attempt so retries cannot upload an earlier report. ([#4409](https://github.com/expo/eas-cli/pull/4409) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] Keep the `eas simulator` session spinner on one line, so resizing the terminal no longer leaves copies of it behind. The `eas simulator:stop` command is now printed once before the spinner starts. ([#4488](https://github.com/expo/eas-cli/pull/4488) by [@szdziedzic](https://github.com/szdziedzic))
- [build-tools] Disable agent-device's internal idle timeouts in remote sessions, so only the session's own idle and duration limits end it. ([#4485](https://github.com/expo/eas-cli/pull/4485) by [@szdziedzic](https://github.com/szdziedzic))
- [eas-cli] Fix `eas workflow:validate` crash when the workflow schema contains a job entry without a `type`. ([#4505](https://github.com/expo/eas-cli/pull/4505) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
@@ -1,4 +1,11 @@
import { validateWorkflowStructure } from '../validation';
import { vol } from 'memfs';
import { WorkflowRevisionMutation } from '../../../graphql/mutations/WorkflowRevisionMutation';
import { ExpoGraphqlClient } from '../../context/contextUtils/createGraphqlClient';
import { validateWorkflowFileAsync, validateWorkflowStructure } from '../validation';
jest.mock('fs');
jest.mock('../../../graphql/mutations/WorkflowRevisionMutation');
const workflowSchema = {
type: 'object',
@@ -60,3 +67,72 @@ describe(validateWorkflowStructure, () => {
}).toThrow('must be a valid URI string');
});
});
const schemaWithUsesJob = {
type: 'object',
properties: {
jobs: {
type: 'object',
additionalProperties: {
anyOf: [
{
type: 'object',
properties: {
type: { type: 'string', const: 'slack' },
params: { type: 'object' },
},
required: ['type', 'params'],
additionalProperties: false,
},
{
type: 'object',
properties: {
uses: { type: 'string' },
with: { type: 'object' },
},
required: ['uses'],
additionalProperties: false,
},
],
},
},
},
required: ['jobs'],
additionalProperties: false,
};
describe(validateWorkflowFileAsync, () => {
const schemaPath = '/schema.json';
const graphqlClient = {} as ExpoGraphqlClient;
beforeEach(() => {
vol.reset();
vol.fromJSON({ [schemaPath]: JSON.stringify({ data: schemaWithUsesJob }) });
process.env.EXPO_TESTING_WORKFLOW_SCHEMA_PATH = schemaPath;
jest.mocked(WorkflowRevisionMutation.validateWorkflowYamlConfigAsync).mockResolvedValue();
});
afterEach(() => {
delete process.env.EXPO_TESTING_WORKFLOW_SCHEMA_PATH;
});
const validateAsync = (yamlConfig: string): Promise<void> =>
validateWorkflowFileAsync(
{ yamlConfig, filePath: 'workflow.yml' },
'/project',
graphqlClient,
'project-id'
);
it('accepts a valid workflow when the schema has a job entry without a type', async () => {
await expect(
validateAsync('jobs:\n notify:\n type: slack\n params:\n message: hi\n')
).resolves.toBeUndefined();
});
it('rejects unknown job types when the schema has a job entry without a type', async () => {
await expect(validateAsync('jobs:\n bad:\n type: not-a-job\n')).rejects.toThrow(
'The following jobs have invalid types: bad. Valid types are: slack'
);
});
});
@@ -206,7 +206,9 @@ function jobsFromWorkflow(parsedYaml: any): any[] {
}
function jobTypesFromWorkflowSchema(workflowJsonSchema: any): string[] {
return workflowJsonSchema?.properties?.jobs?.additionalProperties?.anyOf.map(
(props: any) => props.properties.type.const
);
const jobSchemas: any[] = workflowJsonSchema?.properties?.jobs?.additionalProperties?.anyOf ?? [];
// Some job schemas (for example, `uses:` jobs) do not declare a `type`.
return jobSchemas
.map(jobSchema => jobSchema?.properties?.type?.const)
.filter((type): type is string => typeof type === 'string');
}