Skip to content

Commit ea2cd4f

Browse files
authored
[eas-cli] eas go: use SDK version from current project when available (#3776)
* [eas-cli] eas go: use SDK version from current project when available * fix fmt
1 parent 639a7cc commit ea2cd4f

3 files changed

Lines changed: 182 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This is the log of notable changes to EAS CLI and related packages.
1010

1111
### 🐛 Bug fixes
1212

13+
- [eas-cli] `eas go` now pre-selects the SDK version from the current project's `app.json` or `app.config.js` when available. ([#3776](https://github.com/expo/eas-cli/pull/3776) by [@gwdp](https://github.com/gwdp))
14+
1315
### 🧹 Chores
1416

1517
## [19.0.7](https://github.com/expo/eas-cli/releases/tag/v19.0.7) - 2026-05-21
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { getConfigFilePaths } from '@expo/config';
2+
3+
import { getWorkflowRunUrl } from '../../build/utils/url';
4+
import Go from '../../commands/go';
5+
import { WorkflowRunStatus } from '../../graphql/generated';
6+
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
7+
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
8+
import Log from '../../log';
9+
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
10+
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
11+
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
12+
import { ensureActorHasPrimaryAccount } from '../../user/actions';
13+
import { detectProjectSdkVersionAsync } from '../../commands/go';
14+
import { mockTestCommand } from './utils';
15+
16+
jest.mock('@expo/config', () => ({
17+
...jest.requireActual('@expo/config'),
18+
getConfigFilePaths: jest.fn(),
19+
}));
20+
jest.mock('../../project/expoConfig');
21+
jest.mock('../../log', () => ({
22+
__esModule: true,
23+
default: {
24+
log: jest.fn(),
25+
withTick: jest.fn(),
26+
newLine: jest.fn(),
27+
succeed: jest.fn(),
28+
debug: jest.fn(),
29+
markFreshLine: jest.fn(),
30+
error: jest.fn(),
31+
},
32+
learnMore: jest.fn().mockReturnValue(''),
33+
}));
34+
jest.mock('../../ora', () => ({
35+
ora: jest.fn().mockReturnValue({
36+
start: jest.fn().mockReturnThis(),
37+
stop: jest.fn().mockReturnThis(),
38+
fail: jest.fn().mockReturnThis(),
39+
succeed: jest.fn().mockReturnThis(),
40+
}),
41+
}));
42+
jest.mock('fs-extra', () => ({
43+
ensureDir: jest.fn().mockResolvedValue(undefined),
44+
writeFile: jest.fn().mockResolvedValue(undefined),
45+
remove: jest.fn().mockResolvedValue(undefined),
46+
}));
47+
jest.mock('../../user/actions');
48+
jest.mock('../../graphql/queries/WorkflowRunQuery');
49+
jest.mock('../../graphql/mutations/WorkflowRunMutation');
50+
jest.mock('../../project/uploadAccountScopedFileAsync');
51+
jest.mock('../../project/uploadAccountScopedProjectSourceAsync');
52+
jest.mock('../../build/utils/url');
53+
54+
const mockGetConfigFilePaths = jest.mocked(getConfigFilePaths);
55+
const mockGetPrivateExpoConfigAsync = jest.mocked(getPrivateExpoConfigAsync);
56+
57+
describe('detectProjectSdkVersionAsync', () => {
58+
it('returns undefined when no config file exists', async () => {
59+
mockGetConfigFilePaths.mockReturnValue({ staticConfigPath: null, dynamicConfigPath: null });
60+
await expect(detectProjectSdkVersionAsync('/project')).resolves.toBeUndefined();
61+
});
62+
63+
it('returns the sdkVersion from the project config', async () => {
64+
mockGetConfigFilePaths.mockReturnValue({
65+
staticConfigPath: '/project/app.json',
66+
dynamicConfigPath: null,
67+
});
68+
mockGetPrivateExpoConfigAsync.mockResolvedValue({ sdkVersion: '55.0.0' } as any);
69+
await expect(detectProjectSdkVersionAsync('/project')).resolves.toBe('55.0.0');
70+
});
71+
72+
it('returns undefined when reading the config throws', async () => {
73+
mockGetConfigFilePaths.mockReturnValue({
74+
staticConfigPath: '/project/app.json',
75+
dynamicConfigPath: null,
76+
});
77+
mockGetPrivateExpoConfigAsync.mockRejectedValue(new Error('config error'));
78+
await expect(detectProjectSdkVersionAsync('/project')).resolves.toBeUndefined();
79+
});
80+
});
81+
82+
const mockAccount = { id: 'account-id', name: 'testuser' };
83+
const mockActor = {
84+
__typename: 'User' as const,
85+
id: 'user-id',
86+
username: 'testuser',
87+
primaryAccount: mockAccount,
88+
};
89+
90+
describe('Go command', () => {
91+
beforeEach(() => {
92+
jest.mocked(ensureActorHasPrimaryAccount).mockReturnValue(mockAccount as any);
93+
jest.mocked(WorkflowRunQuery.expoGoRepackConfigurationAsync).mockResolvedValue({
94+
files: [],
95+
sdkVersion: '55.0.0',
96+
} as any);
97+
jest.mocked(WorkflowRunMutation.createExpoGoRepackWorkflowRunAsync).mockResolvedValue({
98+
id: 'run-id',
99+
} as any);
100+
jest.mocked(uploadAccountScopedProjectSourceAsync).mockResolvedValue({
101+
projectArchiveBucketKey: 'archive-key',
102+
});
103+
jest
104+
.mocked(uploadAccountScopedFileAsync)
105+
.mockResolvedValue({ fileBucketKey: 'file-key' } as any);
106+
jest.mocked(getWorkflowRunUrl).mockReturnValue('https://expo.dev/run/123');
107+
});
108+
109+
afterEach(() => {
110+
jest.clearAllMocks();
111+
});
112+
113+
function makeCmd(argv: string[] = []) {
114+
const ctx = {
115+
loggedIn: { actor: mockActor as any, graphqlClient: {} as any },
116+
analytics: {} as any,
117+
};
118+
const cmd = mockTestCommand(Go, ['--bundle-id', 'com.test.go', ...argv], ctx);
119+
jest.spyOn(cmd as any, 'ensureEasProjectAsync').mockResolvedValue('project-id');
120+
jest.spyOn(cmd as any, 'setupCredentialsAsync').mockResolvedValue({ id: 'asc-app-id' });
121+
jest.spyOn(cmd as any, 'monitorWorkflowJobsAsync').mockResolvedValue(WorkflowRunStatus.Success);
122+
return cmd;
123+
}
124+
125+
it('logs auto-selected SDK message and reports resolved version after dispatch', async () => {
126+
mockGetConfigFilePaths.mockReturnValue({
127+
staticConfigPath: '/app.json',
128+
dynamicConfigPath: null,
129+
});
130+
mockGetPrivateExpoConfigAsync.mockResolvedValue({ sdkVersion: '55.0.0' } as any);
131+
132+
await makeCmd().run();
133+
134+
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('SDK 55'));
135+
expect(Log.withTick).toHaveBeenCalledWith(expect.stringContaining('Using Expo Go SDK'));
136+
});
137+
138+
it('skips auto-select log when --sdk-version flag is provided', async () => {
139+
mockGetConfigFilePaths.mockReturnValue({
140+
staticConfigPath: '/app.json',
141+
dynamicConfigPath: null,
142+
});
143+
mockGetPrivateExpoConfigAsync.mockResolvedValue({ sdkVersion: '55.0.0' } as any);
144+
145+
await makeCmd(['--sdk-version', '55.0.0']).run();
146+
147+
expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('Auto-selected'));
148+
});
149+
});

packages/eas-cli/src/commands/go.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExpoConfig } from '@expo/config';
1+
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
22
import { App, User, UserRole } from '@expo/apple-utils';
33
import { Flags } from '@oclif/core';
44
import chalk from 'chalk';
@@ -28,6 +28,7 @@ import { WorkflowRunQuery } from '../graphql/queries/WorkflowRunQuery';
2828
import Log, { learnMore } from '../log';
2929
import { confirmAsync } from '../prompts';
3030
import { ora } from '../ora';
31+
import { getPrivateExpoConfigAsync } from '../project/expoConfig';
3132
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
3233
import { uploadAccountScopedFileAsync } from '../project/uploadAccountScopedFileAsync';
3334
import { uploadAccountScopedProjectSourceAsync } from '../project/uploadAccountScopedProjectSourceAsync';
@@ -45,6 +46,20 @@ function deriveBundleIdSlug(bundleId: string): string {
4546
return bundleId.split('.').filter(Boolean).pop()!;
4647
}
4748

49+
export async function detectProjectSdkVersionAsync(
50+
projectDir: string
51+
): Promise<string | undefined> {
52+
const paths = getConfigFilePaths(projectDir);
53+
if (!paths.staticConfigPath && !paths.dynamicConfigPath) {
54+
return;
55+
}
56+
try {
57+
return (await getPrivateExpoConfigAsync(projectDir)).sdkVersion;
58+
} catch {
59+
return;
60+
}
61+
}
62+
4863
const TESTFLIGHT_GROUP_NAME = 'Team (Expo)';
4964

5065
async function setupTestFlightAsync(ascApp: App): Promise<void> {
@@ -189,7 +204,13 @@ export default class Go extends EasCommand {
189204
});
190205
Log.withTick(`Logged in as ${chalk.cyan(getActorDisplayName(actor))}`);
191206

192-
const sdkVersion = flags['sdk-version'];
207+
const detectedSdkVersion = await detectProjectSdkVersionAsync(process.cwd());
208+
if (detectedSdkVersion && !flags['sdk-version']) {
209+
Log.log(
210+
`Current project using SDK ${detectedSdkVersion.split('.')[0]}. Auto-selected same version. To use a different version, pass --sdk-version.`
211+
);
212+
}
213+
const sdkVersion = flags['sdk-version'] ?? detectedSdkVersion;
193214
const bundleId = flags['bundle-id'] ?? this.generateBundleId(actor);
194215
if (!isBundleIdentifierValid(bundleId)) {
195216
throw new Error(
@@ -231,7 +252,11 @@ export default class Go extends EasCommand {
231252
}
232253
);
233254

234-
const { workflowUrl, workflowRunId } = await this.dispatchWorkflowAsync(
255+
const {
256+
workflowUrl,
257+
workflowRunId,
258+
sdkVersion: resolvedSdkVersion,
259+
} = await this.dispatchWorkflowAsync(
235260
graphqlClient,
236261
projectId,
237262
actor,
@@ -242,6 +267,7 @@ export default class Go extends EasCommand {
242267
tmpDir,
243268
vcsClient
244269
);
270+
Log.withTick(`Using Expo Go SDK ${chalk.cyan(resolvedSdkVersion.split('.')[0])}`);
245271
Log.withTick(`Build started: ${chalk.cyan(workflowUrl)}`);
246272

247273
const status = await this.monitorWorkflowJobsAsync(graphqlClient, workflowRunId);
@@ -401,7 +427,7 @@ export default class Go extends EasCommand {
401427
sdkVersion: string | undefined,
402428
tmpDir: string,
403429
vcsClient: Client
404-
): Promise<{ workflowUrl: string; workflowRunId: string }> {
430+
): Promise<{ workflowUrl: string; workflowRunId: string; sdkVersion: string }> {
405431
const account = ensureActorHasPrimaryAccount(actor);
406432

407433
const repackConfig = await WorkflowRunQuery.expoGoRepackConfigurationAsync(graphqlClient, {
@@ -451,7 +477,7 @@ export default class Go extends EasCommand {
451477

452478
const workflowUrl = getWorkflowRunUrl(account.name, deriveBundleIdSlug(bundleId), result.id);
453479

454-
return { workflowUrl, workflowRunId: result.id };
480+
return { workflowUrl, workflowRunId: result.id, sdkVersion: repackConfig.sdkVersion };
455481
}
456482

457483
private async monitorWorkflowJobsAsync(

0 commit comments

Comments
 (0)