Compare commits

...
4 Commits
Author SHA1 Message Date
Stanisław Chmiela 43cb6df841 Fixup 2025-08-10 11:57:06 +02:00
Stanisław Chmiela f7841c2988 Add changelog 2025-08-10 11:55:07 +02:00
Stanisław Chmiela d94a0f8055 Switch extension preference 2025-08-10 11:53:14 +02:00
Stanisław Chmiela 312fe7ddab Add "eas run deploy-to-production" 2025-08-10 11:50:00 +02:00
4 changed files with 161 additions and 28 deletions

No files matched your search

+2 -1
View File
@@ -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
+19 -8
View File
@@ -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<void> {
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: {
@@ -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();
});
});
});
+26 -19
View File
@@ -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'),
]);
// 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),
};
// 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 };
}
throw yamlFromFile.reason;
// 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),
];
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 lastError;
}
export function maybePrintWorkflowFileValidationErrors({