Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 19 additions & 8 deletions packages/eas-cli/src/commands/workflow/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,15 @@ const EXIT_CODES = {

export default class WorkflowRun extends EasCommand {
static override description = 'run an EAS workflow';
static override aliases = ['run'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have build:run, so I am a little concerned about customer confusion with adding the alias.


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,
Expand Down Expand Up @@ -109,7 +116,10 @@ export default class WorkflowRun extends EasCommand {
};

async runAsync(): Promise<void> {
const { flags, args } = await this.parse(WorkflowRun);
const {
flags,
args: { workflow_file_input: workflowFileInput },
} = await this.parse(WorkflowRun);

if (flags.json) {
enableJsonOutput();
Expand All @@ -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.');

Expand Down Expand Up @@ -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: {
Expand Down
114 changes: 114 additions & 0 deletions packages/eas-cli/src/utils/__tests__/workflowFile-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import path from 'path';
import { vol } from 'memfs';

Check warning on line 2 in packages/eas-cli/src/utils/__tests__/workflowFile-test.ts

View workflow job for this annotation

GitHub Actions / Test with Node 22

`memfs` import should occur before import of `path`
Comment on lines +1 to +2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import path from 'path';
import { vol } from 'memfs';
import { vol } from 'memfs';
import path from 'path';


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();
});
});
});
43 changes: 25 additions & 18 deletions packages/eas-cli/src/utils/workflowFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading