diff --git a/packages/worker/src/CacheManager.ts b/packages/worker/src/CacheManager.ts index 3b1a37d4bc..2446add087 100644 --- a/packages/worker/src/CacheManager.ts +++ b/packages/worker/src/CacheManager.ts @@ -12,6 +12,8 @@ import nullthrows from 'nullthrows'; import path from 'path'; import * as tar from 'tar'; +import Sentry from './sentry'; + export class GCSCacheManager implements CacheManager { private skipCacheUpdate = false; @@ -39,14 +41,8 @@ export class GCSCacheManager implements CacheManager { }, paths ); - } catch (err: any) { - ctx.logger.error({ err }, 'Failed to create cache archive'); - return; - } - - const archiveSize = (await fs.stat(archivePath)).size; + const archiveSize = (await fs.stat(archivePath)).size; - try { await uploadCacheAsync({ logger: ctx.logger, jobId: nullthrows(ctx.env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set'), @@ -63,7 +59,8 @@ export class GCSCacheManager implements CacheManager { force: ctx.job.cache?.clear, }); } catch (err: any) { - ctx.logger.error({ err }); + ctx.logger.error({ err }, 'Failed to save cache'); + Sentry.capture('Failed to save legacy build cache', err, { level: 'warning' }); } } @@ -99,7 +96,8 @@ export class GCSCacheManager implements CacheManager { if (err?.response?.status === 404) { ctx.logger.info('No cache found for this key'); } else { - ctx.logger.error({ err }); + ctx.logger.error({ err }, 'Failed to download cache'); + Sentry.capture('Failed to download legacy build cache', err, { level: 'warning' }); this.skipCacheUpdate = true; // if restore failed we don't want to update cache with new values } return; @@ -123,6 +121,7 @@ export class GCSCacheManager implements CacheManager { ); } catch (err: any) { ctx.logger.error({ err }, 'Failed to extract cache archive'); + Sentry.capture('Failed to extract legacy build cache', err, { level: 'warning' }); this.skipCacheUpdate = true; } } diff --git a/packages/worker/src/__unit__/CacheManager.test.ts b/packages/worker/src/__unit__/CacheManager.test.ts index 08cb98e8d4..4257c889a2 100644 --- a/packages/worker/src/__unit__/CacheManager.test.ts +++ b/packages/worker/src/__unit__/CacheManager.test.ts @@ -4,6 +4,8 @@ import fs from 'fs-extra'; import os from 'os'; import path from 'path'; +import Sentry from '../sentry'; + // These tests use real fs and directories in os.tmpdir(). // Something about tar v7 makes mocking fs with memfs problematic. // Mocking all variants of fs/node:fs/fs/promises does not help. @@ -31,6 +33,7 @@ describe(GCSCacheManager, () => { await fs.rm(outsideDir, { recursive: true, force: true }); jest.mocked(uploadCacheAsync).mockReset(); jest.mocked(downloadCacheAsync).mockReset(); + jest.mocked(Sentry.capture).mockReset(); }); function createMockCtx(cacheConfig: Partial) { @@ -250,6 +253,7 @@ describe(GCSCacheManager, () => { await manager.saveCache(mockCtx); expect(mockCtx.logger.info).toHaveBeenCalledWith('No cache found for this key'); + expect(Sentry.capture).not.toHaveBeenCalled(); const requestedKey = jest.mocked(downloadCacheAsync).mock.calls[0][0].key; expect(uploadCacheAsync).toHaveBeenCalledWith( expect.objectContaining({ @@ -269,6 +273,52 @@ describe(GCSCacheManager, () => { await manager.saveCache(mockCtx); expect(uploadCacheAsync).not.toHaveBeenCalled(); + expect(Sentry.capture).toHaveBeenCalledWith( + 'Failed to download legacy build cache', + expect.any(Error), + { level: 'warning' } + ); + }); + + test('does not fail the build when saving cache fails', async () => { + const manager = new GCSCacheManager(); + const mockCtx = createMockCtx({ paths: ['index.ts'] }); + await fs.outputFile(path.join(tmpDir, 'build', 'index.ts'), 'index.ts'); + const statSpy = jest.spyOn(fs, 'stat').mockImplementationOnce(() => { + throw new Error('stat failed'); + }); + + await expect(manager.saveCache(mockCtx)).resolves.toBeUndefined(); + + expect(uploadCacheAsync).not.toHaveBeenCalled(); + expect(Sentry.capture).toHaveBeenCalledWith( + 'Failed to save legacy build cache', + expect.any(Error), + { level: 'warning' } + ); + statSpy.mockRestore(); + }); + + test('does not save when cache extraction fails', async () => { + const manager = new GCSCacheManager(); + const mockCtx = createMockCtx({ paths: ['index.ts'] }); + const downloadDir = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cache-download-')); + const archivePath = path.join(downloadDir, 'cache.tar.gz'); + await fs.writeFile(archivePath, 'invalid archive'); + jest.mocked(downloadCacheAsync).mockResolvedValue({ + archivePath, + matchedKey: 'matched-cache-key', + }); + + await manager.restoreCache(mockCtx); + await manager.saveCache(mockCtx); + + expect(uploadCacheAsync).not.toHaveBeenCalled(); + expect(Sentry.capture).toHaveBeenCalledWith( + 'Failed to extract legacy build cache', + expect.any(Error), + { level: 'warning' } + ); }); test('cache key is stable when clear changes and job cache is not mutated', async () => {