diff --git a/CHANGELOG.md b/CHANGELOG.md index 8116a497f3..c70fc8a8fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,11 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐ŸŽ‰ New features - Show fingerprints in build:view and build:list commands. ([#3137](https://github.com/expo/eas-cli/pull/3137) by [@douglowder](https://github.com/douglowder)) +- Add `eas run` alias for `eas workflow:run`. Accept more inputs for workflow file input. ([#3138](https://github.com/expo/eas-cli/pull/3138) by [@sjchmiela](https://github.com/sjchmiela)) ### ๐Ÿ› Bug fixes -- Make EXPO_PUBLIC_ env vars plain text, rest sensitive ([#3121](https://github.com/expo/eas-cli/pull/3121) by [@kadikraman](https://github.com/kadikraman)) +- Make `EXPO_PUBLIC_` env vars plain text, rest sensitive ([#3121](https://github.com/expo/eas-cli/pull/3121) by [@kadikraman](https://github.com/kadikraman)) ### ๐Ÿงน Chores diff --git a/packages/eas-cli/src/commands/workflow/run.ts b/packages/eas-cli/src/commands/workflow/run.ts index c05053042f..69c6f76e8d 100644 --- a/packages/eas-cli/src/commands/workflow/run.ts +++ b/packages/eas-cli/src/commands/workflow/run.ts @@ -79,8 +79,15 @@ const EXIT_CODES = { export default class WorkflowRun extends EasCommand { static override description = 'run an EAS workflow'; + static override aliases = ['run']; - static override args = [{ name: 'file', description: 'Path to the workflow file to run' }]; + static override args = [ + { + name: 'workflow_file_input', + description: + 'Path to the workflow file to run or a workflow file basename (for a file under `.eas/workflows/[name].yml`)', + }, + ]; static override flags = { ...EASNonInteractiveFlag, @@ -109,7 +116,10 @@ export default class WorkflowRun extends EasCommand { }; async runAsync(): Promise { - const { flags, args } = await this.parse(WorkflowRun); + const { + flags, + args: { workflow_file_input: workflowFileInput }, + } = await this.parse(WorkflowRun); if (flags.json) { enableJsonOutput(); @@ -125,14 +135,15 @@ export default class WorkflowRun extends EasCommand { withServerSideEnvironment: null, }); + let filePath: string; let yamlConfig: string; + try { - const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({ + ({ yamlConfig, filePath } = await WorkflowFile.readWorkflowFileContentsAsync({ projectDir, - filePath: args.file, - }); - Log.log(`Using workflow file from ${workflowFileContents.filePath}`); - yamlConfig = workflowFileContents.yamlConfig; + filePath: workflowFileInput, + })); + Log.log(`Using workflow file from ${filePath}`); } catch (err) { Log.error('Failed to read workflow file.'); @@ -260,7 +271,7 @@ export default class WorkflowRun extends EasCommand { ({ id: workflowRunId } = await WorkflowRunMutation.createWorkflowRunAsync(graphqlClient, { appId: projectId, workflowRevisionInput: { - fileName: path.basename(args.file), + fileName: path.basename(filePath), yamlConfig, }, workflowRunInput: { diff --git a/packages/eas-cli/src/utils/__tests__/workflowFile-test.ts b/packages/eas-cli/src/utils/__tests__/workflowFile-test.ts new file mode 100644 index 0000000000..bc2bbe6305 --- /dev/null +++ b/packages/eas-cli/src/utils/__tests__/workflowFile-test.ts @@ -0,0 +1,114 @@ +import path from 'path'; +import { vol } from 'memfs'; + +import { WorkflowFile } from '../workflowFile'; + +jest.mock('fs'); + +describe('WorkflowFile.readWorkflowFileContentsAsync', () => { + beforeEach(() => { + vol.reset(); + }); + + describe('absolute paths', () => { + it('should read a file with absolute path', async () => { + const absolutePath = '/Users/test/workflow.yml'; + const yamlContent = 'name: test\njobs:\n build:\n runs-on: ubuntu-latest'; + + vol.fromJSON({ + [absolutePath]: yamlContent, + }); + + const result = await WorkflowFile.readWorkflowFileContentsAsync({ + projectDir: '/some/project', + filePath: absolutePath, + }); + + expect(result.yamlConfig).toBe(yamlContent); + expect(result.filePath).toBe(absolutePath); + }); + + it('should only try the exact absolute path when provided', async () => { + const absolutePath = '/Users/test/workflow.yml'; + const yamlContent = 'name: test\njobs:\n build:\n runs-on: ubuntu-latest'; + + vol.fromJSON({ + [absolutePath]: yamlContent, + '/Users/test/workflow.yaml': 'different content', // This should not be used + }); + + const result = await WorkflowFile.readWorkflowFileContentsAsync({ + projectDir: '/some/project', + filePath: absolutePath, + }); + + expect(result.yamlConfig).toBe(yamlContent); + expect(result.filePath).toBe(absolutePath); + }); + + it('should fail if absolute path does not exist (no extension fallback)', async () => { + const absolutePath = '/Users/test/nonexistent.yml'; + + vol.fromJSON({ + '/Users/test/nonexistent.yaml': 'some content', // This should not be tried + }); + + await expect( + WorkflowFile.readWorkflowFileContentsAsync({ + projectDir: '/some/project', + filePath: absolutePath, + }) + ).rejects.toThrow(); + }); + }); + + describe('relative paths', () => { + it('should prioritize .eas/workflows directory for relative paths', async () => { + const projectDir = '/project'; + const relativeFilePath = 'deploy'; + const yamlContent = 'name: deploy\njobs:\n deploy:\n runs-on: ubuntu-latest'; + + vol.fromJSON({ + [`${projectDir}/.eas/workflows/${relativeFilePath}.yml`]: yamlContent, + [`${projectDir}/${relativeFilePath}.yml`]: 'different content', + }); + + const result = await WorkflowFile.readWorkflowFileContentsAsync({ + projectDir, + filePath: relativeFilePath, + }); + + expect(result.yamlConfig).toBe(yamlContent); + expect(result.filePath).toBe(`${projectDir}/.eas/workflows/${relativeFilePath}.yml`); + }); + + it('should fall back to resolving relative path if not found in .eas/workflows', async () => { + const relativeFilePath = 'deploy.yml'; + const yamlContent = 'name: deploy\njobs:\n deploy:\n runs-on: ubuntu-latest'; + const resolvedPath = path.resolve(relativeFilePath); + + vol.fromJSON({ + [resolvedPath]: yamlContent, + }); + + const result = await WorkflowFile.readWorkflowFileContentsAsync({ + projectDir: '/some/project', + filePath: relativeFilePath, + }); + + expect(result.yamlConfig).toBe(yamlContent); + expect(result.filePath).toBe(resolvedPath); + }); + }); + + describe('error handling', () => { + it('should throw error if no file is found in any of the search paths', async () => { + await expect( + WorkflowFile.readWorkflowFileContentsAsync({ + projectDir: '/project', + filePath: 'nonexistent', + }) + ).rejects.toThrow(); + }); + }); +}); diff --git a/packages/eas-cli/src/utils/workflowFile.ts b/packages/eas-cli/src/utils/workflowFile.ts index 55ce9175d0..ccfbd31e4b 100644 --- a/packages/eas-cli/src/utils/workflowFile.ts +++ b/packages/eas-cli/src/utils/workflowFile.ts @@ -14,27 +14,34 @@ export namespace WorkflowFile { projectDir: string; filePath: string; }): Promise<{ yamlConfig: string; filePath: string }> { - const [yamlFromEasWorkflowsFile, yamlFromFile] = await Promise.allSettled([ - fs.promises.readFile(path.join(projectDir, '.eas', 'workflows', filePath), 'utf8'), - fs.promises.readFile(path.join(process.cwd(), filePath), 'utf8'), - ]); + // If the input is an absolute path, the user was clear about the file they wanted to run. + // We only check that file path. + if (path.isAbsolute(filePath)) { + return { yamlConfig: await fs.promises.readFile(filePath, 'utf8'), filePath }; + } + + // If the input is a relative path (which "deploy-to-production", "deploy-to-production.yml" + // and ".eas/workflows/deploy-to-production.yml" are), we try to find the file. + const pathsToSearch = [ + path.join(projectDir, '.eas', 'workflows', `${filePath}.yaml`), + path.join(projectDir, '.eas', 'workflows', `${filePath}.yml`), + path.join(projectDir, '.eas', 'workflows', filePath), + path.resolve(filePath), + ]; - // We prioritize .eas/workflows/${file} over ${file}, because - // in the worst case we'll try to read .eas/workflows/.eas/workflows/test.yml, - // which is likely not to exist. - if (yamlFromEasWorkflowsFile.status === 'fulfilled') { - return { - yamlConfig: yamlFromEasWorkflowsFile.value, - filePath: path.join(projectDir, '.eas', 'workflows', filePath), - }; - } else if (yamlFromFile.status === 'fulfilled') { - return { - yamlConfig: yamlFromFile.value, - filePath: path.join(process.cwd(), filePath), - }; + let lastError: any = null; + + for (const path of pathsToSearch) { + try { + const yamlConfig = await fs.promises.readFile(path, 'utf8'); + return { yamlConfig, filePath: path }; + } catch (err) { + lastError = err; + continue; + } } - throw yamlFromFile.reason; + throw lastError; } export function maybePrintWorkflowFileValidationErrors({