From e9208243b5e721951af9c254bafe526a422f96d2 Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Thu, 21 May 2026 10:18:34 -0700 Subject: [PATCH 1/2] [eas-cli] eas go: use SDK version from current project when available --- CHANGELOG.md | 2 + .../eas-cli/src/__tests__/commands/go-test.ts | 147 ++++++++++++++++++ packages/eas-cli/src/commands/go.ts | 36 ++++- 3 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 packages/eas-cli/src/__tests__/commands/go-test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fa6ef998a8..7c36e61d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐Ÿ› Bug fixes +- [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)) + ### ๐Ÿงน Chores ## [19.0.5](https://github.com/expo/eas-cli/releases/tag/v19.0.5) - 2026-05-20 diff --git a/packages/eas-cli/src/__tests__/commands/go-test.ts b/packages/eas-cli/src/__tests__/commands/go-test.ts new file mode 100644 index 0000000000..b2d6ba8a71 --- /dev/null +++ b/packages/eas-cli/src/__tests__/commands/go-test.ts @@ -0,0 +1,147 @@ +import { getConfigFilePaths } from '@expo/config'; + +import { getWorkflowRunUrl } from '../../build/utils/url'; +import Go from '../../commands/go'; +import { WorkflowRunStatus } from '../../graphql/generated'; +import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation'; +import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery'; +import Log from '../../log'; +import { getPrivateExpoConfigAsync } from '../../project/expoConfig'; +import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync'; +import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync'; +import { ensureActorHasPrimaryAccount } from '../../user/actions'; +import { detectProjectSdkVersionAsync } from '../../commands/go'; +import { mockTestCommand } from './utils'; + +jest.mock('@expo/config', () => ({ + ...jest.requireActual('@expo/config'), + getConfigFilePaths: jest.fn(), +})); +jest.mock('../../project/expoConfig'); +jest.mock('../../log', () => ({ + __esModule: true, + default: { + log: jest.fn(), + withTick: jest.fn(), + newLine: jest.fn(), + succeed: jest.fn(), + debug: jest.fn(), + markFreshLine: jest.fn(), + error: jest.fn(), + }, + learnMore: jest.fn().mockReturnValue(''), +})); +jest.mock('../../ora', () => ({ + ora: jest.fn().mockReturnValue({ + start: jest.fn().mockReturnThis(), + stop: jest.fn().mockReturnThis(), + fail: jest.fn().mockReturnThis(), + succeed: jest.fn().mockReturnThis(), + }), +})); +jest.mock('fs-extra', () => ({ + ensureDir: jest.fn().mockResolvedValue(undefined), + writeFile: jest.fn().mockResolvedValue(undefined), + remove: jest.fn().mockResolvedValue(undefined), +})); +jest.mock('../../user/actions'); +jest.mock('../../graphql/queries/WorkflowRunQuery'); +jest.mock('../../graphql/mutations/WorkflowRunMutation'); +jest.mock('../../project/uploadAccountScopedFileAsync'); +jest.mock('../../project/uploadAccountScopedProjectSourceAsync'); +jest.mock('../../build/utils/url'); + +const mockGetConfigFilePaths = jest.mocked(getConfigFilePaths); +const mockGetPrivateExpoConfigAsync = jest.mocked(getPrivateExpoConfigAsync); + +describe('detectProjectSdkVersionAsync', () => { + it('returns undefined when no config file exists', async () => { + mockGetConfigFilePaths.mockReturnValue({ staticConfigPath: null, dynamicConfigPath: null }); + await expect(detectProjectSdkVersionAsync('/project')).resolves.toBeUndefined(); + }); + + it('returns the sdkVersion from the project config', async () => { + mockGetConfigFilePaths.mockReturnValue({ + staticConfigPath: '/project/app.json', + dynamicConfigPath: null, + }); + mockGetPrivateExpoConfigAsync.mockResolvedValue({ sdkVersion: '55.0.0' } as any); + await expect(detectProjectSdkVersionAsync('/project')).resolves.toBe('55.0.0'); + }); + + it('returns undefined when reading the config throws', async () => { + mockGetConfigFilePaths.mockReturnValue({ + staticConfigPath: '/project/app.json', + dynamicConfigPath: null, + }); + mockGetPrivateExpoConfigAsync.mockRejectedValue(new Error('config error')); + await expect(detectProjectSdkVersionAsync('/project')).resolves.toBeUndefined(); + }); +}); + +const mockAccount = { id: 'account-id', name: 'testuser' }; +const mockActor = { + __typename: 'User' as const, + id: 'user-id', + username: 'testuser', + primaryAccount: mockAccount, +}; + +describe('Go command', () => { + beforeEach(() => { + jest.mocked(ensureActorHasPrimaryAccount).mockReturnValue(mockAccount as any); + jest.mocked(WorkflowRunQuery.expoGoRepackConfigurationAsync).mockResolvedValue({ + files: [], + sdkVersion: '55.0.0', + } as any); + jest.mocked(WorkflowRunMutation.createExpoGoRepackWorkflowRunAsync).mockResolvedValue({ + id: 'run-id', + } as any); + jest.mocked(uploadAccountScopedProjectSourceAsync).mockResolvedValue({ + projectArchiveBucketKey: 'archive-key', + }); + jest.mocked(uploadAccountScopedFileAsync).mockResolvedValue({ fileBucketKey: 'file-key' } as any); + jest.mocked(getWorkflowRunUrl).mockReturnValue('https://expo.dev/run/123'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + function makeCmd(argv: string[] = []) { + const ctx = { + loggedIn: { actor: mockActor as any, graphqlClient: {} as any }, + analytics: {} as any, + }; + const cmd = mockTestCommand(Go, ['--bundle-id', 'com.test.go', ...argv], ctx); + jest.spyOn(cmd as any, 'ensureEasProjectAsync').mockResolvedValue('project-id'); + jest.spyOn(cmd as any, 'setupCredentialsAsync').mockResolvedValue({ id: 'asc-app-id' }); + jest.spyOn(cmd as any, 'monitorWorkflowJobsAsync').mockResolvedValue(WorkflowRunStatus.Success); + return cmd; + } + + it('logs auto-selected SDK message and reports resolved version after dispatch', async () => { + mockGetConfigFilePaths.mockReturnValue({ + staticConfigPath: '/app.json', + dynamicConfigPath: null, + }); + mockGetPrivateExpoConfigAsync.mockResolvedValue({ sdkVersion: '55.0.0' } as any); + + await makeCmd().run(); + + expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('SDK 55')); + expect(Log.withTick).toHaveBeenCalledWith(expect.stringContaining('Using Expo Go SDK')); + }); + + it('skips auto-select log when --sdk-version flag is provided', async () => { + mockGetConfigFilePaths.mockReturnValue({ + staticConfigPath: '/app.json', + dynamicConfigPath: null, + }); + mockGetPrivateExpoConfigAsync.mockResolvedValue({ sdkVersion: '55.0.0' } as any); + + await makeCmd(['--sdk-version', '55.0.0']).run(); + + expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('Auto-selected')); + }); +}); diff --git a/packages/eas-cli/src/commands/go.ts b/packages/eas-cli/src/commands/go.ts index 47491bba4f..d2468f99b8 100644 --- a/packages/eas-cli/src/commands/go.ts +++ b/packages/eas-cli/src/commands/go.ts @@ -1,4 +1,4 @@ -import { ExpoConfig } from '@expo/config'; +import { ExpoConfig, getConfigFilePaths } from '@expo/config'; import { App, User, UserRole } from '@expo/apple-utils'; import { Flags } from '@oclif/core'; import chalk from 'chalk'; @@ -28,6 +28,7 @@ import { WorkflowRunQuery } from '../graphql/queries/WorkflowRunQuery'; import Log, { learnMore } from '../log'; import { confirmAsync } from '../prompts'; import { ora } from '../ora'; +import { getPrivateExpoConfigAsync } from '../project/expoConfig'; import { findProjectIdByAccountNameAndSlugNullableAsync } from '../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync'; import { uploadAccountScopedFileAsync } from '../project/uploadAccountScopedFileAsync'; import { uploadAccountScopedProjectSourceAsync } from '../project/uploadAccountScopedProjectSourceAsync'; @@ -45,6 +46,20 @@ function deriveBundleIdSlug(bundleId: string): string { return bundleId.split('.').filter(Boolean).pop()!; } +export async function detectProjectSdkVersionAsync( + projectDir: string +): Promise { + const paths = getConfigFilePaths(projectDir); + if (!paths.staticConfigPath && !paths.dynamicConfigPath) { + return; + } + try { + return (await getPrivateExpoConfigAsync(projectDir)).sdkVersion; + } catch { + return; + } +} + const TESTFLIGHT_GROUP_NAME = 'Team (Expo)'; async function setupTestFlightAsync(ascApp: App): Promise { @@ -189,7 +204,13 @@ export default class Go extends EasCommand { }); Log.withTick(`Logged in as ${chalk.cyan(getActorDisplayName(actor))}`); - const sdkVersion = flags['sdk-version']; + const detectedSdkVersion = await detectProjectSdkVersionAsync(process.cwd()); + if (detectedSdkVersion && !flags['sdk-version']) { + Log.log( + `Current project using SDK ${detectedSdkVersion.split('.')[0]}. Auto-selected same version. To use a different version, pass --sdk-version.` + ); + } + const sdkVersion = flags['sdk-version'] ?? detectedSdkVersion; const bundleId = flags['bundle-id'] ?? this.generateBundleId(actor); if (!isBundleIdentifierValid(bundleId)) { throw new Error( @@ -231,7 +252,11 @@ export default class Go extends EasCommand { } ); - const { workflowUrl, workflowRunId } = await this.dispatchWorkflowAsync( + const { + workflowUrl, + workflowRunId, + sdkVersion: resolvedSdkVersion, + } = await this.dispatchWorkflowAsync( graphqlClient, projectId, actor, @@ -242,6 +267,7 @@ export default class Go extends EasCommand { tmpDir, vcsClient ); + Log.withTick(`Using Expo Go SDK ${chalk.cyan(resolvedSdkVersion.split('.')[0])}`); Log.withTick(`Build started: ${chalk.cyan(workflowUrl)}`); const status = await this.monitorWorkflowJobsAsync(graphqlClient, workflowRunId); @@ -401,7 +427,7 @@ export default class Go extends EasCommand { sdkVersion: string | undefined, tmpDir: string, vcsClient: Client - ): Promise<{ workflowUrl: string; workflowRunId: string }> { + ): Promise<{ workflowUrl: string; workflowRunId: string; sdkVersion: string }> { const account = ensureActorHasPrimaryAccount(actor); const repackConfig = await WorkflowRunQuery.expoGoRepackConfigurationAsync(graphqlClient, { @@ -451,7 +477,7 @@ export default class Go extends EasCommand { const workflowUrl = getWorkflowRunUrl(account.name, deriveBundleIdSlug(bundleId), result.id); - return { workflowUrl, workflowRunId: result.id }; + return { workflowUrl, workflowRunId: result.id, sdkVersion: repackConfig.sdkVersion }; } private async monitorWorkflowJobsAsync( From f9f5bbbd3e022a199bb93d1f237d686ff92ea541 Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Thu, 21 May 2026 13:58:52 -0700 Subject: [PATCH 2/2] fix fmt --- packages/eas-cli/src/__tests__/commands/go-test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/eas-cli/src/__tests__/commands/go-test.ts b/packages/eas-cli/src/__tests__/commands/go-test.ts index b2d6ba8a71..ef422d5bdd 100644 --- a/packages/eas-cli/src/__tests__/commands/go-test.ts +++ b/packages/eas-cli/src/__tests__/commands/go-test.ts @@ -100,7 +100,9 @@ describe('Go command', () => { jest.mocked(uploadAccountScopedProjectSourceAsync).mockResolvedValue({ projectArchiveBucketKey: 'archive-key', }); - jest.mocked(uploadAccountScopedFileAsync).mockResolvedValue({ fileBucketKey: 'file-key' } as any); + jest + .mocked(uploadAccountScopedFileAsync) + .mockResolvedValue({ fileBucketKey: 'file-key' } as any); jest.mocked(getWorkflowRunUrl).mockReturnValue('https://expo.dev/run/123'); });