Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ This is the log of notable changes to EAS CLI and related packages.

### 🐛 Bug fixes

- [eas-cli] Use the selected EAS environment throughout `eas update`. ([#4269](https://github.com/expo/eas-cli/pull/4269) by [@ramonclaudio](https://github.com/ramonclaudio))

### 🧹 Chores

## [22.2.0](https://github.com/expo/eas-cli/releases/tag/v22.2.0) - 2026-08-20
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class DynamicPublicProjectConfigContextField extends ContextField<Dynamic
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
env: options?.env,
mode: options?.mode,
});
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
Expand Down Expand Up @@ -70,6 +71,7 @@ export class DynamicPrivateProjectConfigContextField extends ContextField<Dynami
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
env: options?.env,
mode: options?.mode,
});
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
DynamicPrivateProjectConfigContextField,
DynamicPublicProjectConfigContextField,
} from '../DynamicProjectConfigContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from '../contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from '../contextUtils/getProjectIdAsync';
import { getPrivateExpoConfigAsync, getPublicExpoConfigAsync } from '../../../project/expoConfig';

jest.mock('../contextUtils/findProjectDirAndVerifyProjectSetupAsync');
jest.mock('../contextUtils/getProjectIdAsync');
jest.mock('../../../project/expoConfig');

describe('dynamic project config context fields', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it.each([
['public config', DynamicPublicProjectConfigContextField, getPublicExpoConfigAsync],
['private config', DynamicPrivateProjectConfigContextField, getPrivateExpoConfigAsync],
])(
"uses the caller's env and mode for %s",
async (_description, ContextField, getExpoConfigAsync) => {
jest.mocked(findProjectDirAndVerifyProjectSetupAsync).mockResolvedValue('/app');
jest.mocked(getExpoConfigAsync).mockResolvedValue({ name: 'app', slug: 'app' });
jest.mocked(getProjectIdAsync).mockResolvedValue('project-id');

const getProjectConfigAsync = await new ContextField().getValueAsync({
analytics: {} as any,
nonInteractive: true,
sessionManager: {} as any,
});
const options = {
env: { APP_VARIANT: 'preview' },
mode: 'production' as const,
};

await getProjectConfigAsync(options);

expect(getExpoConfigAsync).toHaveBeenNthCalledWith(1, '/app', options);
expect(getExpoConfigAsync).toHaveBeenNthCalledWith(2, '/app', options);
expect(getProjectIdAsync).toHaveBeenCalledWith(
expect.anything(),
{ name: 'app', slug: 'app' },
{
...options,
nonInteractive: true,
}
);
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -312,17 +312,24 @@ describe(getProjectIdAsync, () => {
});

it('fetches the project ID when not in app config, and sets it in the config', async () => {
jest
.mocked(getConfig)
.mockReturnValue({ exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '2345' } },
},
const originalProcessEnv = process.env;
jest.mocked(getConfig).mockImplementation(() => {
expect(process.env.APP_VARIANT).toBe('preview');
expect(process.env.NODE_ENV).toBe('production');
return { exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any;
});
jest.mocked(modifyConfigAsync).mockImplementation(async () => {
expect(process.env.APP_VARIANT).toBe('preview');
expect(process.env.NODE_ENV).toBe('production');
return {
type: 'success',
config: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '2345' } },
},
};
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
Expand All @@ -332,11 +339,14 @@ describe(getProjectIdAsync, () => {
sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'test' },
{
env: { APP_VARIANT: 'preview' },
mode: 'production',
nonInteractive: false,
}
);

expect(projectId).toEqual('2345');
expect(process.env).toBe(originalProcessEnv);

expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
expect(modifyConfigAsync).toHaveBeenCalledWith(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ExpoConfig, getProjectConfigDescription } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import semver from 'semver';

Expand All @@ -13,6 +12,7 @@ import {
getAccountNamesWhereUserHasSufficientPermissionsToCreateApp,
} from '../../../project/accountSelection';
import {
type ExpoConfigOptions,
createOrModifyExpoConfigAsync,
getPrivateExpoConfigAsync,
} from '../../../project/expoConfig';
Expand All @@ -31,7 +31,7 @@ import { Actor, getActorUsername } from '../../../user/User';
export async function saveProjectIdToAppConfigAsync(
projectDir: string,
projectId: string,
options: { env?: Env } = {}
options: Pick<ExpoConfigOptions, 'env' | 'mode'> = {}
): Promise<void> {
// NOTE(cedric): we disable plugins to avoid writing plugin-generated content to `expo.extra`
const exp = await getPrivateExpoConfigAsync(projectDir, { skipPlugins: true, ...options });
Expand All @@ -40,7 +40,7 @@ export async function saveProjectIdToAppConfigAsync(
{
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
},
{ skipSDKVersionRequirement: true }
{ skipSDKVersionRequirement: true, ...options }
);

switch (result.type) {
Expand Down Expand Up @@ -87,7 +87,7 @@ export async function saveProjectIdToAppConfigAsync(
export async function getProjectIdAsync(
sessionManager: SessionManager,
exp: ExpoConfig,
options: { env?: Env; nonInteractive: boolean }
options: Pick<ExpoConfigOptions, 'env' | 'mode'> & { nonInteractive: boolean }
): Promise<string> {
// all codepaths in this function require a logged-in user with access to the owning account
// since they either query the app via graphql or create it, which includes getting info about
Expand All @@ -111,7 +111,7 @@ export async function validateOrSetProjectIdAsync({
exp: ExpoConfig;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
options: { env?: Env; nonInteractive: boolean };
options: Pick<ExpoConfigOptions, 'env' | 'mode'> & { nonInteractive: boolean };
cwd?: string;
}): Promise<string> {
const localProjectId = exp.extra?.eas?.projectId;
Expand Down Expand Up @@ -204,7 +204,10 @@ export async function validateOrSetProjectIdAsync({

const spinner = ora(`Linking local project to EAS project ${projectId}`).start();
try {
await saveProjectIdToAppConfigAsync(projectDir, projectId, options);
await saveProjectIdToAppConfigAsync(projectDir, projectId, {
env: options.env,
mode: options.mode,
});
spinner.succeed(`Linked local project to EAS project ${projectId}`);
} catch (e: any) {
spinner.fail();
Expand Down
130 changes: 95 additions & 35 deletions packages/eas-cli/src/commands/update/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
DynamicPublicProjectConfigContextField,
} from '../../../commandUtils/context/DynamicProjectConfigContextField';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
import { ServerSideEnvironmentVariablesContextField } from '../../../commandUtils/context/ServerSideEnvironmentVariablesContextField';
import VcsClientContextField from '../../../commandUtils/context/VcsClientContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
Expand All @@ -23,7 +22,13 @@ import { UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import { collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish';
import {
buildBundlesAsync,
collectAssetsAsync,
maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync,
uploadAssetsAsync,
} from '../../../project/publish';
import { ensureEASUpdateIsConfiguredAsync } from '../../../update/configure';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { selectAsync } from '../../../prompts';
import { resolveVcsClient } from '../../../vcs';
Expand Down Expand Up @@ -67,6 +72,13 @@ jest.mock('../../../project/publish', () => ({
...jest.requireActual('../../../project/publish'),
buildBundlesAsync: jest.fn(),
collectAssetsAsync: jest.fn(),
maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync: jest.fn(
async (args: any) =>
args.runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map((info: any) => ({
...info,
fingerprintInfoGroup: {},
}))
),
resolveInputDirectoryAsync: jest.fn((inputDir = 'dist') => path.join(projectRoot, inputDir)),
uploadAssetsAsync: jest.fn(),
}));
Expand Down Expand Up @@ -113,6 +125,9 @@ describe(UpdatePublish.name, () => {
await new UpdatePublish(flags, commandOptions).run();

expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
expect(buildBundlesAsync).toHaveBeenCalledWith(
expect.objectContaining({ extraEnv: { NODE_ENV: 'production' } })
);
});

it('creates a new update with --non-interactive, --channel, and --message', async () => {
Expand Down Expand Up @@ -154,10 +169,11 @@ describe(UpdatePublish.name, () => {
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});

it('prompts for environment when SDK >= 55 and --environment is not provided', async () => {
it('uses the prompted environment for app config, export, and Fingerprint', async () => {
const flags = ['--branch=branch123', '--message=abc'];

mockTestProject({ expoConfig: { sdkVersion: '55.0.0' } });
const { getDynamicPrivateProjectConfigAsync, getDynamicPublicProjectConfigAsync, projectId } =
mockTestProject({ expoConfig: { sdkVersion: '55.0.0' } });
const { platforms, runtimeVersion } = mockTestExport();

jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
Expand All @@ -176,10 +192,13 @@ describe(UpdatePublish.name, () => {
}))
);

jest.mocked(selectAsync).mockResolvedValue('production');
jest.mocked(selectAsync).mockResolvedValue('preview');
jest
.mocked(EnvironmentVariablesQuery.environmentVariableEnvironmentsAsync)
.mockResolvedValue([]);
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValue([{ name: 'APP_VARIANT', value: 'from-eas' }] as any);

const ciValue = process.env.CI;
try {
Expand All @@ -194,6 +213,40 @@ describe(UpdatePublish.name, () => {
}

expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
expect.anything(),
{
appId: projectId,
environment: 'preview',
}
);
const updateEnv = {
APP_VARIANT: 'from-eas',
EXPO_NO_DOTENV: '1',
};
expect(getDynamicPublicProjectConfigAsync).toHaveBeenCalledWith({ mode: 'production' });
expect(getDynamicPublicProjectConfigAsync).toHaveBeenCalledWith({
env: updateEnv,
mode: 'production',
});
expect(getDynamicPrivateProjectConfigAsync).toHaveBeenCalledWith({
env: updateEnv,
mode: 'production',
});
expect(ensureEASUpdateIsConfiguredAsync).toHaveBeenCalledWith(
expect.objectContaining({ env: updateEnv })
);
expect(buildBundlesAsync).toHaveBeenCalledWith(
expect.objectContaining({
extraEnv: {
...updateEnv,
NODE_ENV: 'production',
},
})
);
expect(
maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync
).toHaveBeenCalledWith(expect.objectContaining({ env: updateEnv }));
});

it('errors when SDK >= 55, --environment is not provided, and --non-interactive is set', async () => {
Expand Down Expand Up @@ -402,7 +455,12 @@ function mockTestProject({
}: {
configuredProjectId?: string;
expoConfig?: Partial<ExpoConfig>;
} = {}): { projectId: string; appJson: AppJSONConfig } {
} = {}): {
projectId: string;
appJson: AppJSONConfig;
getDynamicPrivateProjectConfigAsync: jest.Mock;
getDynamicPublicProjectConfigAsync: jest.Mock;
} {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
Expand Down Expand Up @@ -438,38 +496,35 @@ function mockTestProject({
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));

jest.mocked(getConfig).mockReturnValue(mockManifest as any);
const getDynamicPrivateProjectConfigAsync = jest.fn(async () => {
const exp = { ...mockManifest.exp };
return {
exp,
projectDir: projectRoot,
projectId: configuredProjectId,
};
});
jest
.spyOn(DynamicPrivateProjectConfigContextField.prototype, 'getValueAsync')
.mockResolvedValue(async () => {
const exp = { ...mockManifest.exp };
return {
exp,
projectDir: projectRoot,
projectId: configuredProjectId,
};
});
jest
.spyOn(ServerSideEnvironmentVariablesContextField.prototype, 'getValueAsync')
.mockResolvedValue(async () => {
return {};
});
.mockResolvedValue(getDynamicPrivateProjectConfigAsync);
const getDynamicPublicProjectConfigAsync = jest.fn(async () => {
const exp = {
name: mockManifest.exp.name,
version: mockManifest.exp.version,
slug: mockManifest.exp.slug,
sdkVersion: mockManifest.exp.sdkVersion,
owner: mockManifest.exp.owner,
extra: mockManifest.exp.extra,
};
return {
exp,
projectDir: projectRoot,
projectId: configuredProjectId,
};
});
jest
.spyOn(DynamicPublicProjectConfigContextField.prototype, 'getValueAsync')
.mockResolvedValue(async () => {
const exp = {
name: mockManifest.exp.name,
version: mockManifest.exp.version,
slug: mockManifest.exp.slug,
sdkVersion: mockManifest.exp.sdkVersion,
owner: mockManifest.exp.owner,
extra: mockManifest.exp.extra,
};
return {
exp,
projectDir: projectRoot,
projectId: configuredProjectId,
};
});
.mockResolvedValue(getDynamicPublicProjectConfigAsync);

jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
Expand All @@ -489,7 +544,12 @@ function mockTestProject({
ownerAccount: jester.accounts[0],
});

return { projectId: configuredProjectId, appJson: appJSON };
return {
projectId: configuredProjectId,
appJson: appJSON,
getDynamicPrivateProjectConfigAsync,
getDynamicPublicProjectConfigAsync,
};
}

/** Create a new in-memory export of the project */
Expand Down
Loading
Loading