diff --git a/lib/util/cache/repository/types.ts b/lib/util/cache/repository/types.ts index 5410621bad911674ef354c3b55aa35dd50b2254c..d4753ae4929e49dca9804a7f51d018e24de95bac 100644 --- a/lib/util/cache/repository/types.ts +++ b/lib/util/cache/repository/types.ts @@ -38,6 +38,8 @@ export interface OnboardingBranchCache { onboardingBranchSha: string; isConflicted: boolean; isModified: boolean; + configFileName?: string; + configFileParsed?: string; } export interface PrCache { diff --git a/lib/workers/repository/index.ts b/lib/workers/repository/index.ts index 45103753917f34b5617adbd59ff9a14883c7e421..e19cad910c0eee7240d2c8f27568d97b9e941009 100644 --- a/lib/workers/repository/index.ts +++ b/lib/workers/repository/index.ts @@ -55,7 +55,7 @@ export async function renovateRepository( addSplit('init'); const performExtract = config.repoIsOnboarded! || - !config.onboardingRebaseCheckbox || + !OnboardingState.onboardingCacheValid || OnboardingState.prUpdateRequested; const { branches, branchList, packageFiles } = performExtract ? await instrument('extract', () => extractDependencies(config)) diff --git a/lib/workers/repository/init/merge.spec.ts b/lib/workers/repository/init/merge.spec.ts index af798bf329a2bbff3d9e6f0d27406ca35aa70a40..28676bd9ef23b058fcac688232eb700fbd813361 100644 --- a/lib/workers/repository/init/merge.spec.ts +++ b/lib/workers/repository/init/merge.spec.ts @@ -10,9 +10,12 @@ import { import { getConfig } from '../../../config/defaults'; import * as _migrateAndValidate from '../../../config/migrate-validate'; import * as _migrate from '../../../config/migration'; +import * as memCache from '../../../util/cache/memory'; import * as repoCache from '../../../util/cache/repository'; import { initRepoCache } from '../../../util/cache/repository/init'; import type { RepoCacheData } from '../../../util/cache/repository/types'; +import * as _onboardingCache from '../onboarding/branch/onboarding-branch-cache'; +import { OnboardingState } from '../onboarding/common'; import { checkForRepoConfigError, detectRepoFileConfig, @@ -21,13 +24,16 @@ import { jest.mock('../../../util/fs'); jest.mock('../../../util/git'); +jest.mock('../onboarding/branch/onboarding-branch-cache'); const migrate = mocked(_migrate); const migrateAndValidate = mocked(_migrateAndValidate); +const onboardingCache = mocked(_onboardingCache); let config: RenovateConfig; beforeEach(() => { + memCache.init(); jest.resetAllMocks(); config = getConfig(); config.errors = []; @@ -64,6 +70,64 @@ describe('workers/repository/init/merge', () => { ); }); + it('returns cache config from onboarding cache - package.json', async () => { + const pJson = JSON.stringify({ + schema: 'https://docs.renovate.com', + }); + OnboardingState.onboardingCacheValid = true; + onboardingCache.getOnboardingFileNameFromCache.mockReturnValueOnce( + 'package.json' + ); + onboardingCache.getOnboardingConfigFromCache.mockReturnValueOnce(pJson); + expect(await detectRepoFileConfig()).toEqual({ + configFileName: 'package.json', + configFileParsed: { schema: 'https://docs.renovate.com' }, + }); + }); + + it('clones, if onboarding cache is valid but parsed config is undefined', async () => { + OnboardingState.onboardingCacheValid = true; + onboardingCache.getOnboardingFileNameFromCache.mockReturnValueOnce( + 'package.json' + ); + onboardingCache.getOnboardingConfigFromCache.mockReturnValueOnce( + undefined as never + ); + scm.getFileList.mockResolvedValueOnce(['package.json']); + const pJson = JSON.stringify({ + name: 'something', + renovate: { + prHourlyLimit: 10, + }, + }); + fs.readLocalFile.mockResolvedValueOnce(pJson); + platform.getRawFile.mockResolvedValueOnce(pJson); + expect(await detectRepoFileConfig()).toEqual({ + configFileName: 'package.json', + configFileParsed: { prHourlyLimit: 10 }, + }); + }); + + it('returns cache config from onboarding cache - renovate.json', async () => { + const configParsed = JSON.stringify({ + schema: 'https://docs.renovate.com', + }); + OnboardingState.onboardingCacheValid = true; + onboardingCache.getOnboardingFileNameFromCache.mockReturnValueOnce( + 'renovate.json' + ); + onboardingCache.getOnboardingConfigFromCache.mockReturnValueOnce( + configParsed + ); + expect(await detectRepoFileConfig()).toEqual({ + configFileName: 'renovate.json', + configFileParsed: { + schema: 'https://docs.renovate.com', + }, + configFileRaw: undefined, + }); + }); + it('uses package.json config if found', async () => { scm.getFileList.mockResolvedValue(['package.json']); const pJson = JSON.stringify({ diff --git a/lib/workers/repository/init/merge.ts b/lib/workers/repository/init/merge.ts index 38d6a2ba0d6188a9e0443a09e08ce68ff02717cd..e26570c6382cb133225f2cb0caba172749b108b4 100644 --- a/lib/workers/repository/init/merge.ts +++ b/lib/workers/repository/init/merge.ts @@ -24,6 +24,12 @@ import { readLocalFile } from '../../../util/fs'; import * as hostRules from '../../../util/host-rules'; import * as queue from '../../../util/http/queue'; import * as throttle from '../../../util/http/throttle'; +import { + getOnboardingConfigFromCache, + getOnboardingFileNameFromCache, + setOnboardingConfigDetails, +} from '../onboarding/branch/onboarding-branch-cache'; +import { OnboardingState } from '../onboarding/common'; import type { RepoFileConfig } from './types'; async function detectConfigFile(): Promise<string | null> { @@ -74,7 +80,13 @@ export async function detectRepoFileConfig(): Promise<RepoFileConfig> { delete cache.configFileName; } } - configFileName = (await detectConfigFile()) ?? undefined; + + if (OnboardingState.onboardingCacheValid) { + configFileName = getOnboardingFileNameFromCache(); + } else { + configFileName = (await detectConfigFile()) ?? undefined; + } + if (!configFileName) { logger.debug('No renovate config file found'); return {}; @@ -84,6 +96,16 @@ export async function detectRepoFileConfig(): Promise<RepoFileConfig> { // TODO #7154 let configFileParsed: any; let configFileRaw: string | undefined | null; + + if (OnboardingState.onboardingCacheValid) { + const cachedConfig = getOnboardingConfigFromCache(); + const parsedConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined; + if (parsedConfig) { + setOnboardingConfigDetails(configFileName, JSON.stringify(parsedConfig)); + return { configFileName, configFileRaw, configFileParsed: parsedConfig }; + } + } + if (configFileName === 'package.json') { // We already know it parses configFileParsed = JSON.parse( @@ -171,6 +193,8 @@ export async function detectRepoFileConfig(): Promise<RepoFileConfig> { 'Repository config' ); } + + setOnboardingConfigDetails(configFileName, JSON.stringify(configFileParsed)); return { configFileName, configFileRaw, configFileParsed }; } diff --git a/lib/workers/repository/onboarding/branch/index.spec.ts b/lib/workers/repository/onboarding/branch/index.spec.ts index 1c30ac8a2c8a6a9386cd77a2835f1968e2a1ebf6..926ab508f7f17e048ad5560f2d1531fa27160443 100644 --- a/lib/workers/repository/onboarding/branch/index.spec.ts +++ b/lib/workers/repository/onboarding/branch/index.spec.ts @@ -263,6 +263,33 @@ describe('workers/repository/onboarding/branch/index', () => { expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); + it('skips processing onboarding branch when main/onboarding SHAs have not changed', async () => { + GlobalConfig.set({ platform: 'github' }); + const dummyCache = { + onboardingBranchCache: { + defaultBranchSha: 'default-sha', + onboardingBranchSha: 'onboarding-sha', + isConflicted: false, + isModified: false, + configFileParsed: 'raw', + configFileName: 'renovate.json', + }, + } satisfies RepoCacheData; + cache.getCache.mockReturnValue(dummyCache); + scm.getFileList.mockResolvedValue(['package.json']); + platform.findPr.mockResolvedValue(null); // finds closed onboarding pr + platform.getBranchPr.mockResolvedValueOnce( + mock<Pr>({ bodyStruct: { rebaseRequested: false } }) + ); // finds open onboarding pr + git.getBranchCommit + .mockReturnValueOnce('default-sha') + .mockReturnValueOnce('default-sha') + .mockReturnValueOnce('onboarding-sha'); + config.onboardingRebaseCheckbox = true; + await checkOnboardingBranch(config); + expect(git.mergeBranch).not.toHaveBeenCalled(); + }); + it('processes modified onboarding branch and invalidates extract cache', async () => { const dummyCache = { scan: { diff --git a/lib/workers/repository/onboarding/branch/index.ts b/lib/workers/repository/onboarding/branch/index.ts index a4853d1fec8ecaa64b371d252eb68d5986373899..fb881e3525edb5483a2479deb9e18e34a23e8415 100644 --- a/lib/workers/repository/onboarding/branch/index.ts +++ b/lib/workers/repository/onboarding/branch/index.ts @@ -51,12 +51,26 @@ export async function checkOnboardingBranch( // global gitAuthor will need to be used setGitAuthor(config.gitAuthor); const onboardingPr = await getOnboardingPr(config); + // TODO #7154 + const branchList = [onboardingBranch!]; if (onboardingPr) { if (config.onboardingRebaseCheckbox) { handleOnboardingManualRebase(onboardingPr); } logger.debug('Onboarding PR already exists'); + if ( + isOnboardingCacheValid(config.defaultBranch!, config.onboardingBranch!) && + !(config.onboardingRebaseCheckbox && OnboardingState.prUpdateRequested) + ) { + logger.debug( + 'Skip processing since the onboarding branch is up to date and default branch has not changed' + ); + OnboardingState.onboardingCacheValid = true; + return { ...config, repoIsOnboarded, onboardingBranch, branchList }; + } + OnboardingState.onboardingCacheValid = false; + isModified = await isOnboardingBranchModified(config.onboardingBranch!); if (isModified) { if (hasOnboardingBranchChanged(config.onboardingBranch!)) { @@ -109,8 +123,6 @@ export async function checkOnboardingBranch( isModified ); - // TODO #7154 - const branchList = [onboardingBranch!]; return { ...config, repoIsOnboarded, onboardingBranch, branchList }; } @@ -137,3 +149,19 @@ function invalidateExtractCache(baseBranch: string): void { delete cache.scan[baseBranch]; } } + +function isOnboardingCacheValid( + defaultBranch: string, + onboardingBranch: string +): boolean { + const cache = getCache(); + const onboardingBranchCache = cache?.onboardingBranchCache; + return !!( + onboardingBranchCache && + onboardingBranchCache.defaultBranchSha === getBranchCommit(defaultBranch) && + onboardingBranchCache.onboardingBranchSha === + getBranchCommit(onboardingBranch) && + onboardingBranchCache.configFileName && + onboardingBranchCache.configFileParsed + ); +} diff --git a/lib/workers/repository/onboarding/branch/onboarding-branch-cache.spec.ts b/lib/workers/repository/onboarding/branch/onboarding-branch-cache.spec.ts index 40cb301774145499bebbc90201c353566d8f17e1..b1e64d0a57c203f924448a85e37dc52c16594b07 100644 --- a/lib/workers/repository/onboarding/branch/onboarding-branch-cache.spec.ts +++ b/lib/workers/repository/onboarding/branch/onboarding-branch-cache.spec.ts @@ -1,12 +1,18 @@ -import { git, mocked, scm } from '../../../../../test/util'; +import { git, mocked, partial, scm } from '../../../../../test/util'; import * as _cache from '../../../../util/cache/repository'; -import type { RepoCacheData } from '../../../../util/cache/repository/types'; +import type { + OnboardingBranchCache, + RepoCacheData, +} from '../../../../util/cache/repository/types'; import { deleteOnboardingCache, + getOnboardingConfigFromCache, + getOnboardingFileNameFromCache, hasOnboardingBranchChanged, isOnboardingBranchConflicted, isOnboardingBranchModified, setOnboardingCache, + setOnboardingConfigDetails, } from './onboarding-branch-cache'; jest.mock('../../../../util/cache/repository'); @@ -232,4 +238,61 @@ describe('workers/repository/onboarding/branch/onboarding-branch-cache', () => { ).toBeTrue(); }); }); + + describe('getOnboardingFileNameFromCache()', () => { + it('returns cached value', () => { + const dummyCache = { + onboardingBranchCache: partial<OnboardingBranchCache>({ + configFileName: 'renovate.json', + }), + } satisfies RepoCacheData; + cache.getCache.mockReturnValueOnce(dummyCache); + expect(getOnboardingFileNameFromCache()).toBe('renovate.json'); + }); + + it('returns undefined', () => { + expect(getOnboardingFileNameFromCache()).toBeUndefined(); + }); + }); + + describe('getOnboardingConfigFromCache()', () => { + it('returns cached value', () => { + const dummyCache = { + onboardingBranchCache: partial<OnboardingBranchCache>({ + configFileParsed: 'parsed', + }), + } satisfies RepoCacheData; + cache.getCache.mockReturnValueOnce(dummyCache); + expect(getOnboardingConfigFromCache()).toBe('parsed'); + }); + + it('returns undefined', () => { + expect(getOnboardingConfigFromCache()).toBeUndefined(); + }); + }); + + describe('setOnboardingConfigDetails()', () => { + it('returns cached value', () => { + const dummyCache = { + onboardingBranchCache: { + defaultBranchSha: 'default-sha', + onboardingBranchSha: 'onboarding-sha', + isConflicted: true, + isModified: true, + }, + } satisfies RepoCacheData; + cache.getCache.mockReturnValueOnce(dummyCache); + setOnboardingConfigDetails('renovate.json', 'parsed'); + expect(dummyCache).toEqual({ + onboardingBranchCache: { + defaultBranchSha: 'default-sha', + onboardingBranchSha: 'onboarding-sha', + isConflicted: true, + isModified: true, + configFileName: 'renovate.json', + configFileParsed: 'parsed', + }, + }); + }); + }); }); diff --git a/lib/workers/repository/onboarding/branch/onboarding-branch-cache.ts b/lib/workers/repository/onboarding/branch/onboarding-branch-cache.ts index 9f7ebd1b47cf1ba1865edd4842ce24b8ee86bc20..7917b124ab05508c21323ffa7f333481f06a5d25 100644 --- a/lib/workers/repository/onboarding/branch/onboarding-branch-cache.ts +++ b/lib/workers/repository/onboarding/branch/onboarding-branch-cache.ts @@ -80,6 +80,27 @@ export async function isOnboardingBranchModified( return isModified; } +export function getOnboardingFileNameFromCache(): string | undefined { + const cache = getCache(); + return cache.onboardingBranchCache?.configFileName; +} + +export function getOnboardingConfigFromCache(): string | undefined { + const cache = getCache(); + return cache.onboardingBranchCache?.configFileParsed; +} + +export function setOnboardingConfigDetails( + configFileName: string, + configFileParsed: string +): void { + const cache = getCache(); + if (cache.onboardingBranchCache) { + cache.onboardingBranchCache.configFileName = configFileName; + cache.onboardingBranchCache.configFileParsed = configFileParsed; + } +} + export async function isOnboardingBranchConflicted( defaultBranch: string, onboardingBranch: string diff --git a/lib/workers/repository/onboarding/common.ts b/lib/workers/repository/onboarding/common.ts index 5738c3d09d33afa835b241c6c714f56cdb4d7d8e..d19da1ca0a492644312db6bed7f1e2a91a6fab65 100644 --- a/lib/workers/repository/onboarding/common.ts +++ b/lib/workers/repository/onboarding/common.ts @@ -11,6 +11,7 @@ export function defaultConfigFile(config: RenovateConfig): string { export class OnboardingState { private static readonly cacheKey = 'OnboardingState'; + private static readonly skipKey = 'OnboardingStateValid'; static get prUpdateRequested(): boolean { const updateRequested = !!memCache.get<boolean | undefined>( @@ -27,4 +28,20 @@ export class OnboardingState { logger.trace({ value }, 'Set OnboardingState.prUpdateRequested'); memCache.set(OnboardingState.cacheKey, value); } + + static get onboardingCacheValid(): boolean { + const cacheValid = !!memCache.get<boolean | undefined>( + OnboardingState.skipKey + ); + logger.trace( + { value: cacheValid }, + 'Get OnboardingState.onboardingCacheValid' + ); + return cacheValid; + } + + static set onboardingCacheValid(value: boolean) { + logger.trace({ value }, 'Set OnboardingState.onboardingCacheValid'); + memCache.set(OnboardingState.skipKey, value); + } } diff --git a/lib/workers/repository/onboarding/pr/index.ts b/lib/workers/repository/onboarding/pr/index.ts index e391fc8dd008621ec701c79e9855b099e8743a5c..08b58ea760b2012a4f4de3c44e0982daf5af06b6 100644 --- a/lib/workers/repository/onboarding/pr/index.ts +++ b/lib/workers/repository/onboarding/pr/index.ts @@ -33,6 +33,7 @@ export async function ensureOnboardingPr( ): Promise<void> { if ( config.repoIsOnboarded || + OnboardingState.onboardingCacheValid || (config.onboardingRebaseCheckbox && !OnboardingState.prUpdateRequested) ) { return;