diff --git a/CHANGELOG.md b/CHANGELOG.md index c4d0eb1a58..64bf84bf71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This is the log of notable changes to EAS CLI and related packages. ### 🎉 New features - Better description for update:rollback. ([#3154](https://github.com/expo/eas-cli/pull/3154) by [@douglowder](https://github.com/douglowder)) +- Support native server deployments in EAS Update ([#3155](https://github.com/expo/eas-cli/pull/3155) by [@krystofwoldrich](https://github.com/krystofwoldrich)) ### 🐛 Bug fixes diff --git a/packages/eas-cli/src/commands/update/__tests__/index.test.ts b/packages/eas-cli/src/commands/update/__tests__/index.test.ts index ce175341ab..2a547501cb 100644 --- a/packages/eas-cli/src/commands/update/__tests__/index.test.ts +++ b/packages/eas-cli/src/commands/update/__tests__/index.test.ts @@ -21,8 +21,9 @@ import { jester } from '../../../credentials/__tests__/fixtures-constants'; import { UpdateFragment } from '../../../graphql/generated'; import { PublishMutation } from '../../../graphql/mutations/PublishMutation'; import { AppQuery } from '../../../graphql/queries/AppQuery'; -import { collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish'; +import { buildBundlesAsync, collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish'; import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync'; +import { getTemporaryPath, safelyDeletePathAsync } from '../../../utils/temporaryPath'; import { resolveVcsClient } from '../../../vcs'; const projectRoot = '/test-project'; @@ -65,6 +66,7 @@ jest.mock('../../../project/publish', () => ({ resolveInputDirectoryAsync: jest.fn((inputDir = 'dist') => path.join(projectRoot, inputDir)), uploadAssetsAsync: jest.fn(), })); +jest.mock('../../../utils/temporaryPath.ts'); describe(UpdatePublish.name, () => { afterEach(() => { @@ -177,6 +179,60 @@ describe(UpdatePublish.name, () => { // Ensure non-public config is not present expect(expoConfig).not.toHaveProperty('hooks'); }); + + it('creates a new update with a native deployment', async () => { + const flags = ['--non-interactive', '--branch=branch123', '--message=abc']; + + const { appJson } = mockTestProject(); + const { platforms, runtimeVersion } = mockTestExport(); + jest.mocked(buildBundlesAsync).mockImplementation(() => { + // Mock config changing during the build + // e.g. server url deployment + appJson.expo.extra = { + ...appJson.expo.extra, + afterBuild: 'mocked', + }; + + return Promise.resolve(); + }); + + // Mock the temporary path for the generated config + const mockedGeneratedConfigTmpPath = `/tmp/test-${Math.random().toString(36).substring(2)}`; + jest.mocked(getTemporaryPath).mockReturnValueOnce(mockedGeneratedConfigTmpPath); + + // Mock an existing branch, so we don't create a new one + jest.mocked(ensureBranchExistsAsync).mockResolvedValue({ + branch: { + id: 'branch123', + name: 'wat', + }, + createdBranch: false, + }); + + jest + .mocked(PublishMutation.publishUpdateGroupAsync) + .mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion }))); + + await new UpdatePublish(flags, commandOptions).run(); + + // Pull the publish data from the mocked publish function + const publishData = jest.mocked(PublishMutation.publishUpdateGroupAsync).mock.calls[0][1][0]; + // Pull the Expo config from the publish data + const expoConfig = nullthrows(publishData.updateInfoGroup).ios!.extra.expoClient; + + expect(getTemporaryPath).toHaveBeenCalledTimes(1); + expect(safelyDeletePathAsync).toHaveBeenCalledTimes(1); + expect(safelyDeletePathAsync).toHaveBeenCalledWith(mockedGeneratedConfigTmpPath); + + expect(buildBundlesAsync).toHaveBeenCalledWith( + expect.objectContaining({ + extraEnv: expect.objectContaining({ + __EXPO_GENERATED_CONFIG_PATH: mockedGeneratedConfigTmpPath, + }), + }) + ); + expect(expoConfig?.extra?.afterBuild).toEqual('mocked'); + }); }); /** Create a new in-memory project, copied from src/commands/update/__tests__/republish.test.ts */ diff --git a/packages/eas-cli/src/commands/update/index.ts b/packages/eas-cli/src/commands/update/index.ts index 061f5fc61e..9a57c35dcd 100644 --- a/packages/eas-cli/src/commands/update/index.ts +++ b/packages/eas-cli/src/commands/update/index.ts @@ -65,6 +65,7 @@ import uniqBy from '../../utils/expodash/uniqBy'; import formatFields from '../../utils/formatFields'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService'; +import { getTemporaryPath, safelyDeletePathAsync } from '../../utils/temporaryPath'; type RawUpdateFlags = { auto: boolean; @@ -167,6 +168,15 @@ export default class UpdatePublish extends EasCommand { }; async runAsync(): Promise { + const generatedConfigPath = getTemporaryPath(); + try { + await this.runUnsafeAsync(generatedConfigPath); + } finally { + await safelyDeletePathAsync(generatedConfigPath); + } + } + + private async runUnsafeAsync(generatedConfigPath: string): Promise { const { flags: rawFlags } = await this.parse(UpdatePublish); const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags); const { @@ -262,7 +272,10 @@ export default class UpdatePublish extends EasCommand { exp, platformFlag: requestedPlatform, clearCache, - extraEnv: maybeServerEnv, + extraEnv: { + ...maybeServerEnv, + __EXPO_GENERATED_CONFIG_PATH: generatedConfigPath, + }, }); bundleSpinner.succeed('Exported bundle(s)'); } catch (e) { @@ -351,7 +364,13 @@ export default class UpdatePublish extends EasCommand { uploadedAssetCount = uploadResults.uniqueUploadedAssetCount; assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup; - unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp); + + const { exp: expAfterBuild } = await getDynamicPublicProjectConfigAsync({ + env: { + __EXPO_GENERATED_CONFIG_PATH: generatedConfigPath, + }, + }); + unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, expAfterBuild); // NOTE(cedric): we assume that bundles are always uploaded, and always are part of // `uploadedAssetCount`, perferably we don't assume. For that, we need to refactor the diff --git a/packages/eas-cli/src/project/publish.ts b/packages/eas-cli/src/project/publish.ts index 48369edd85..68eb23ae82 100644 --- a/packages/eas-cli/src/project/publish.ts +++ b/packages/eas-cli/src/project/publish.ts @@ -42,6 +42,7 @@ import { truncateString as truncateUpdateMessage, } from '../update/utils'; import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads'; +import { easCliBin } from '../utils/easCli'; import { expoCommandAsync, shouldUseVersionedExpoCLI, @@ -223,6 +224,11 @@ export async function buildBundlesAsync({ throw new Error('Could not locate package.json'); } + const extendedEnv = { + ...extraEnv, + __EAS_BIN: easCliBin, + }; + // Legacy global Expo CLI if (!shouldUseVersionedExpoCLI(projectDir, exp)) { await expoCommandAsync( @@ -239,7 +245,7 @@ export async function buildBundlesAsync({ ...(clearCache ? ['--clear'] : []), ], { - extraEnv, + extraEnv: extendedEnv, } ); return; @@ -265,7 +271,7 @@ export async function buildBundlesAsync({ ...(clearCache ? ['--clear'] : []), ], { - extraEnv, + extraEnv: extendedEnv, } ); return; @@ -293,7 +299,7 @@ export async function buildBundlesAsync({ ...(clearCache ? ['--clear'] : []), ], { - extraEnv, + extraEnv: extendedEnv, } ); } diff --git a/packages/eas-cli/src/utils/easCli.ts b/packages/eas-cli/src/utils/easCli.ts index 17844a946e..84bab07285 100644 --- a/packages/eas-cli/src/utils/easCli.ts +++ b/packages/eas-cli/src/utils/easCli.ts @@ -1,3 +1,6 @@ +import { argv } from 'node:process'; + const packageJSON = require('../../package.json'); export const easCliVersion: string = packageJSON.version; +export const easCliBin: string = argv[1]; diff --git a/packages/eas-cli/src/utils/temporaryPath.ts b/packages/eas-cli/src/utils/temporaryPath.ts new file mode 100644 index 0000000000..3629d67972 --- /dev/null +++ b/packages/eas-cli/src/utils/temporaryPath.ts @@ -0,0 +1,13 @@ +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +export function getTemporaryPath(): string { + return path.join(os.tmpdir(), Math.random().toString(36).substring(2)); +} + +export async function safelyDeletePathAsync(value: string): Promise { + try { + await fs.rm(value, { recursive: true, force: true }); + } catch {} +}