Skip to content
Merged
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
17 changes: 8 additions & 9 deletions packages/worker/src/CacheManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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'),
Expand All @@ -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' });
}
}

Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand Down
50 changes: 50 additions & 0 deletions packages/worker/src/__unit__/CacheManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Cache>) {
Expand Down Expand Up @@ -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({
Expand All @@ -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 () => {
Expand Down
Loading