diff --git a/lib/util/cache/repository/common.ts b/lib/util/cache/repository/common.ts index ce88e1a1af56e7d68ec801512c9c4bb1a9292b31..11cb26a26ae614ec1d30e68d49309b4e60bee8db 100644 --- a/lib/util/cache/repository/common.ts +++ b/lib/util/cache/repository/common.ts @@ -2,30 +2,46 @@ import is from '@sindresorhus/is'; import type { RepoCacheData, RepoCacheRecord } from './types'; // Increment this whenever there could be incompatibilities between old and new cache structure -export const CACHE_REVISION = 11; +export const CACHE_REVISION = 12; -export function isValidCacheRecord( +export function isValidRev10( input: unknown, repo?: string -): input is RepoCacheRecord { +): input is RepoCacheData & { repository?: string; revision?: number } { return ( is.plainObject(input) && - is.string(input.repository) && is.safeInteger(input.revision) && - (!repo || repo === input.repository) && - input.revision === CACHE_REVISION + input.revision === 10 && + is.string(input.repository) && + (!repo || repo === input.repository) ); } -export function canBeMigratedToV11( +export function isValidRev11( input: unknown, repo?: string -): input is RepoCacheData & { repository?: string; revision?: number } { +): input is { repository: string; revision: number; data: RepoCacheData } { return ( is.plainObject(input) && + is.safeInteger(input.revision) && + input.revision === 11 && is.string(input.repository) && + is.plainObject(input.data) && + (!repo || repo === input.repository) + ); +} + +export function isValidRev12( + input: unknown, + repo?: string +): input is RepoCacheRecord { + return ( + is.plainObject(input) && is.safeInteger(input.revision) && + input.revision === CACHE_REVISION && + is.string(input.repository) && (!repo || repo === input.repository) && - input.revision === 10 + is.string(input.payload) && + is.string(input.hash) ); } diff --git a/lib/util/cache/repository/impl/local.spec.ts b/lib/util/cache/repository/impl/local.spec.ts index 2e7158d3e91824a15db904fb7d79799a79eebf86..0d1ad4e4dd5ac316c6d956553c74f222ea03a3d3 100644 --- a/lib/util/cache/repository/impl/local.spec.ts +++ b/lib/util/cache/repository/impl/local.spec.ts @@ -1,3 +1,6 @@ +import { promisify } from 'util'; +import zlib from 'zlib'; +import hasha from 'hasha'; import { fs } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; import { CACHE_REVISION } from '../common'; @@ -6,6 +9,21 @@ import { LocalRepoCache } from './local'; jest.mock('../../../fs'); +const compress = promisify(zlib.brotliCompress); + +async function createCacheRecord( + data: RepoCacheData, + repository = 'some/repo' +): Promise<RepoCacheRecord> { + const revision = CACHE_REVISION; + const jsonStr = JSON.stringify(data); + const hash = hasha(jsonStr, { algorithm: 'sha256' }); + const compressed = await compress(jsonStr); + const payload = compressed.toString('base64'); + const record: RepoCacheRecord = { revision, repository, payload, hash }; + return record; +} + describe('util/cache/repository/impl/local', () => { beforeEach(() => { GlobalConfig.set({ cacheDir: '/tmp/cache' }); @@ -16,14 +34,10 @@ describe('util/cache/repository/impl/local', () => { expect(localRepoCache.getData()).toBeEmpty(); }); - it('loads valid cache from disk', async () => { + it('loads previously stored cache from disk', async () => { const data: RepoCacheData = { semanticCommits: 'enabled' }; - const cache: RepoCacheRecord = { - repository: 'some/repo', - revision: CACHE_REVISION, - data, - }; - fs.readFile.mockResolvedValue(JSON.stringify(cache)); + const cacheRecord = await createCacheRecord(data); + fs.readFile.mockResolvedValue(JSON.stringify(cacheRecord)); const localRepoCache = new LocalRepoCache('github', 'some/repo'); await localRepoCache.load(); @@ -31,7 +45,7 @@ describe('util/cache/repository/impl/local', () => { expect(localRepoCache.getData()).toEqual(data); }); - it('migrates revision from 10 to 11', async () => { + it('migrates revision from 10 to 12', async () => { fs.readFile.mockResolvedValue( JSON.stringify({ revision: 10, @@ -40,18 +54,35 @@ describe('util/cache/repository/impl/local', () => { }) ); const localRepoCache = new LocalRepoCache('github', 'some/repo'); - await localRepoCache.load(); + await localRepoCache.load(); await localRepoCache.save(); + const cacheRecord = await createCacheRecord({ semanticCommits: 'enabled' }); expect(fs.outputFile).toHaveBeenCalledWith( '/tmp/cache/renovate/repository/github/some/repo.json', + JSON.stringify(cacheRecord) + ); + }); + + it('migrates revision from 11 to 12', async () => { + fs.readFile.mockResolvedValue( JSON.stringify({ - revision: CACHE_REVISION, + revision: 11, repository: 'some/repo', data: { semanticCommits: 'enabled' }, }) ); + const localRepoCache = new LocalRepoCache('github', 'some/repo'); + + await localRepoCache.load(); + await localRepoCache.save(); + + const cacheRecord = await createCacheRecord({ semanticCommits: 'enabled' }); + expect(fs.outputFile).toHaveBeenCalledWith( + '/tmp/cache/renovate/repository/github/some/repo.json', + JSON.stringify(cacheRecord) + ); }); it('does not migrate from older revisions to 11', async () => { @@ -89,13 +120,8 @@ describe('util/cache/repository/impl/local', () => { }); it('resets if repository does not match', async () => { - fs.readFile.mockResolvedValueOnce( - JSON.stringify({ - revision: CACHE_REVISION, - repository: 'foo/bar', - data: { semanticCommits: 'enabled' }, - }) as never - ); + const cacheRecord = createCacheRecord({ semanticCommits: 'enabled' }); + fs.readFile.mockResolvedValueOnce(JSON.stringify(cacheRecord) as never); const localRepoCache = new LocalRepoCache('github', 'some/repo'); await localRepoCache.load(); @@ -103,28 +129,22 @@ describe('util/cache/repository/impl/local', () => { expect(localRepoCache.getData()).toEqual({}); }); - it('saves cache data to file', async () => { - fs.readFile.mockResolvedValueOnce( - JSON.stringify({ - revision: CACHE_REVISION, - repository: 'some/repo', - data: { semanticCommits: 'enabled' }, - }) - ); + it('saves modified cache data to file', async () => { + const oldCacheRecord = createCacheRecord({ semanticCommits: 'enabled' }); + fs.readFile.mockResolvedValueOnce(JSON.stringify(oldCacheRecord)); const localRepoCache = new LocalRepoCache('github', 'some/repo'); - await localRepoCache.load(); + await localRepoCache.load(); const data = localRepoCache.getData(); data.semanticCommits = 'disabled'; await localRepoCache.save(); + const newCacheRecord = await createCacheRecord({ + semanticCommits: 'disabled', + }); expect(fs.outputFile).toHaveBeenCalledWith( '/tmp/cache/renovate/repository/github/some/repo.json', - JSON.stringify({ - revision: CACHE_REVISION, - repository: 'some/repo', - data: { semanticCommits: 'disabled' }, - }) + JSON.stringify(newCacheRecord) ); }); }); diff --git a/lib/util/cache/repository/impl/local.ts b/lib/util/cache/repository/impl/local.ts index 3b41aa4245fb68a390a4d3333a140a64b50f0e2d..ce9a546515d56bce26366bd0c7581eaa1dd659dc 100644 --- a/lib/util/cache/repository/impl/local.ts +++ b/lib/util/cache/repository/impl/local.ts @@ -1,21 +1,30 @@ +import { promisify } from 'util'; +import zlib from 'zlib'; +import hasha from 'hasha'; import upath from 'upath'; import { GlobalConfig } from '../../../../config/global'; import { logger } from '../../../../logger'; import { outputFile, readFile } from '../../../fs'; import { CACHE_REVISION, - canBeMigratedToV11, - isValidCacheRecord, + isValidRev10, + isValidRev11, + isValidRev12, } from '../common'; import type { RepoCacheRecord } from '../types'; import { RepoCacheBase } from './base'; +const compress = promisify(zlib.brotliCompress); +const decompress = promisify(zlib.brotliDecompress); + export class LocalRepoCache extends RepoCacheBase { + private oldHash: string | null = null; + constructor(private platform: string, private repository: string) { super(); } - private getCacheFileName(): string { + public getCacheFileName(): string { const cacheDir = GlobalConfig.get('cacheDir'); const repoCachePath = '/renovate/repository/'; const platform = this.platform; @@ -29,17 +38,32 @@ export class LocalRepoCache extends RepoCacheBase { const cacheFileName = this.getCacheFileName(); const rawCache = await readFile(cacheFileName, 'utf8'); const oldCache = JSON.parse(rawCache); - if (isValidCacheRecord(oldCache, this.repository)) { - this.data = oldCache.data; + + if (isValidRev12(oldCache, this.repository)) { + const compressed = Buffer.from(oldCache.payload, 'base64'); + const uncompressed = await decompress(compressed); + const jsonStr = uncompressed.toString('utf8'); + this.data = JSON.parse(jsonStr); + this.oldHash = oldCache.hash; logger.debug('Repository cache is valid'); - } else if (canBeMigratedToV11(oldCache, this.repository)) { + return; + } + + if (isValidRev11(oldCache, this.repository)) { + this.data = oldCache.data; + logger.debug('Repository cache is migrated from 11 revision'); + return; + } + + if (isValidRev10(oldCache, this.repository)) { delete oldCache.repository; delete oldCache.revision; this.data = oldCache; - logger.debug('Repository cache is migrated'); - } else { - logger.debug('Repository cache is invalid'); + logger.debug('Repository cache is migrated from 10 revision'); + return; } + + logger.debug('Repository cache is invalid'); } catch (err) { logger.debug({ cacheFileName }, 'Repository cache not found'); } @@ -50,7 +74,13 @@ export class LocalRepoCache extends RepoCacheBase { const revision = CACHE_REVISION; const repository = this.repository; const data = this.getData(); - const record: RepoCacheRecord = { revision, repository, data }; - await outputFile(cacheFileName, JSON.stringify(record)); + const jsonStr = JSON.stringify(data); + const hash = await hasha.async(jsonStr, { algorithm: 'sha256' }); + if (hash !== this.oldHash) { + const compressed = await compress(jsonStr); + const payload = compressed.toString('base64'); + const record: RepoCacheRecord = { revision, repository, payload, hash }; + await outputFile(cacheFileName, JSON.stringify(record)); + } } } diff --git a/lib/util/cache/repository/types.ts b/lib/util/cache/repository/types.ts index ec0f12ffc9a4dc903c55c7ea7c8c2cbfd9b7bda7..79f8cf40b59d43da39647cbea562386a7ba93a40 100644 --- a/lib/util/cache/repository/types.ts +++ b/lib/util/cache/repository/types.ts @@ -49,7 +49,8 @@ export interface RepoCacheData { export interface RepoCacheRecord { repository: string; revision: number; - data: RepoCacheData; + payload: string; + hash: string; } export interface RepoCache {