From 6d6f44840ecd0b2a50745d79a0c60c5c6e7e444e Mon Sep 17 00:00:00 2001 From: szdziedzic Date: Tue, 7 Jul 2026 13:25:46 +0200 Subject: [PATCH 1/3] Clean up build inspect temp directory --- .../commands/build/__tests__/inspect.test.ts | 94 +++++++++++++++++++ .../eas-cli/src/commands/build/inspect.ts | 10 ++ 2 files changed, 104 insertions(+) create mode 100644 packages/eas-cli/src/commands/build/__tests__/inspect.test.ts diff --git a/packages/eas-cli/src/commands/build/__tests__/inspect.test.ts b/packages/eas-cli/src/commands/build/__tests__/inspect.test.ts new file mode 100644 index 0000000000..d9ff9f650c --- /dev/null +++ b/packages/eas-cli/src/commands/build/__tests__/inspect.test.ts @@ -0,0 +1,94 @@ +import fs from 'fs-extra'; +import path from 'path'; + +import { getMockOclifConfig } from '../../../__tests__/commands/utils'; +import { runBuildAndSubmitAsync } from '../../../build/runBuildAndSubmit'; +import BuildInspect from '../inspect'; + +jest.mock('fs-extra', () => ({ + pathExists: jest.fn(), + remove: jest.fn(), + mkdirp: jest.fn(), + copy: jest.fn(), +})); +jest.mock('../../../build/runBuildAndSubmit', () => ({ + runBuildAndSubmitAsync: jest.fn(), +})); +jest.mock('../../../log'); +jest.mock('../../../ora', () => ({ + ora: jest.fn(() => ({ + start: jest.fn(() => ({ + succeed: jest.fn(), + fail: jest.fn(), + })), + })), +})); +jest.mock('../../../utils/paths', () => ({ + ...jest.requireActual('../../../utils/paths'), + getTmpDirectory: jest.fn(() => '/tmp/eas-cli'), +})); +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'tmp-id'), +})); + +describe(BuildInspect, () => { + const mockConfig = getMockOclifConfig(); + const tmpWorkingdir = '/tmp/eas-cli/tmp-id'; + const tmpBuildDir = path.join(tmpWorkingdir, 'build'); + const outputDirectory = path.join(process.cwd(), 'inspect-output'); + + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(fs.pathExists).mockResolvedValue(false); + jest.mocked(fs.mkdirp).mockResolvedValue(undefined); + jest.mocked(fs.copy).mockResolvedValue(undefined); + jest.mocked(fs.remove).mockResolvedValue(undefined); + jest.mocked(runBuildAndSubmitAsync).mockResolvedValue({ buildIds: [] }); + }); + + function createCommand(): BuildInspect { + const command = new BuildInspect( + ['--platform', 'ios', '--stage', 'pre-build', '--output', 'inspect-output'], + mockConfig + ); + jest.spyOn(command, 'getContextAsync').mockResolvedValue({ + loggedIn: { + actor: {}, + graphqlClient: {}, + }, + getDynamicPrivateProjectConfigAsync: jest.fn(), + projectDir: '/project', + analytics: {}, + vcsClient: {}, + } as any); + return command; + } + + it('removes the temporary working directory after copying output when the build fails', async () => { + jest.mocked(runBuildAndSubmitAsync).mockRejectedValue(new Error('build failed')); + jest + .mocked(fs.pathExists) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + + await createCommand().runAsync(); + + expect(fs.copy).toHaveBeenCalledWith(tmpBuildDir, outputDirectory); + expect(fs.remove).toHaveBeenCalledWith(tmpBuildDir); + expect(fs.remove).toHaveBeenCalledWith(tmpWorkingdir); + }); + + it('keeps the temporary working directory when copying output fails', async () => { + jest.mocked(runBuildAndSubmitAsync).mockRejectedValue(new Error('build failed')); + jest + .mocked(fs.pathExists) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + jest.mocked(fs.copy).mockRejectedValue(new Error('copy failed')); + + await expect(createCommand().runAsync()).rejects.toThrow('copy failed'); + + expect(fs.remove).not.toHaveBeenCalledWith(tmpBuildDir); + expect(fs.remove).not.toHaveBeenCalledWith(tmpWorkingdir); + }); +}); diff --git a/packages/eas-cli/src/commands/build/inspect.ts b/packages/eas-cli/src/commands/build/inspect.ts index 5e805e2bda..6b9c883196 100644 --- a/packages/eas-cli/src/commands/build/inspect.ts +++ b/packages/eas-cli/src/commands/build/inspect.ts @@ -135,6 +135,7 @@ export default class BuildInspect extends EasCommand { } } finally { await this.copyToOutputDirAsync(path.join(tmpWorkingdir, 'build'), outputDirectory); + await this.cleanUpTmpWorkingdirAsync(tmpWorkingdir); } } } @@ -163,4 +164,13 @@ export default class BuildInspect extends EasCommand { throw err; } } + + private async cleanUpTmpWorkingdirAsync(tmpWorkingdir: string): Promise { + try { + await fs.remove(tmpWorkingdir); + } catch (err) { + Log.warn(`Failed to remove temporary directory ${tmpWorkingdir}`); + Log.debug(err); + } + } } From b4032cd5692ac12b460cb449616a3c853e23bf9b Mon Sep 17 00:00:00 2001 From: szdziedzic Date: Tue, 7 Jul 2026 13:31:06 +0200 Subject: [PATCH 2/3] [eas-cli] Add build inspect cleanup changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fbff8ec5c..8d14e79920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This is the log of notable changes to EAS CLI and related packages. ### 🐛 Bug fixes +- [eas-cli] Remove temporary working directories after `eas build:inspect` copies inspect output, avoiding leftover disk usage after failed builds. ([#3981](https://github.com/expo/eas-cli/pull/3981) by [@szdziedzic](https://github.com/szdziedzic)) - [build-tools] Skip embedded bundle upload for development client builds instead of warning that the bundle is missing. ([#3940](https://github.com/expo/eas-cli/pull/3940) by [@gwdp](https://github.com/gwdp)) - [eas-cli] Retry uploading assets that don't finish processing during `eas update`, instead of failing the update. ([#3918](https://github.com/expo/eas-cli/pull/3918) by [@gwdp](https://github.com/gwdp)) From 91904224014242ae43596bc8c308178f8582bfa1 Mon Sep 17 00:00:00 2001 From: szdziedzic Date: Tue, 7 Jul 2026 13:36:23 +0200 Subject: [PATCH 3/3] [eas-cli] Fix build inspect test types --- .../commands/build/__tests__/inspect.test.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/eas-cli/src/commands/build/__tests__/inspect.test.ts b/packages/eas-cli/src/commands/build/__tests__/inspect.test.ts index d9ff9f650c..ac9da381d8 100644 --- a/packages/eas-cli/src/commands/build/__tests__/inspect.test.ts +++ b/packages/eas-cli/src/commands/build/__tests__/inspect.test.ts @@ -36,13 +36,17 @@ describe(BuildInspect, () => { const tmpWorkingdir = '/tmp/eas-cli/tmp-id'; const tmpBuildDir = path.join(tmpWorkingdir, 'build'); const outputDirectory = path.join(process.cwd(), 'inspect-output'); + const mockPathExists = fs.pathExists as jest.Mock; + const mockMkdirp = fs.mkdirp as jest.Mock; + const mockCopy = fs.copy as jest.Mock; + const mockRemove = fs.remove as jest.Mock; beforeEach(() => { jest.clearAllMocks(); - jest.mocked(fs.pathExists).mockResolvedValue(false); - jest.mocked(fs.mkdirp).mockResolvedValue(undefined); - jest.mocked(fs.copy).mockResolvedValue(undefined); - jest.mocked(fs.remove).mockResolvedValue(undefined); + mockPathExists.mockResolvedValue(false); + mockMkdirp.mockResolvedValue(undefined); + mockCopy.mockResolvedValue(undefined); + mockRemove.mockResolvedValue(undefined); jest.mocked(runBuildAndSubmitAsync).mockResolvedValue({ buildIds: [] }); }); @@ -51,7 +55,7 @@ describe(BuildInspect, () => { ['--platform', 'ios', '--stage', 'pre-build', '--output', 'inspect-output'], mockConfig ); - jest.spyOn(command, 'getContextAsync').mockResolvedValue({ + jest.spyOn(command as any, 'getContextAsync').mockResolvedValue({ loggedIn: { actor: {}, graphqlClient: {}, @@ -66,10 +70,7 @@ describe(BuildInspect, () => { it('removes the temporary working directory after copying output when the build fails', async () => { jest.mocked(runBuildAndSubmitAsync).mockRejectedValue(new Error('build failed')); - jest - .mocked(fs.pathExists) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(true); + mockPathExists.mockResolvedValueOnce(false).mockResolvedValueOnce(true); await createCommand().runAsync(); @@ -80,11 +81,8 @@ describe(BuildInspect, () => { it('keeps the temporary working directory when copying output fails', async () => { jest.mocked(runBuildAndSubmitAsync).mockRejectedValue(new Error('build failed')); - jest - .mocked(fs.pathExists) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(true); - jest.mocked(fs.copy).mockRejectedValue(new Error('copy failed')); + mockPathExists.mockResolvedValueOnce(false).mockResolvedValueOnce(true); + mockCopy.mockRejectedValue(new Error('copy failed')); await expect(createCommand().runAsync()).rejects.toThrow('copy failed');