diff --git a/lib/util/cache/repository/types.ts b/lib/util/cache/repository/types.ts index 3146e6e06385c49f6444d62116bd44d4df58bfa4..6f5cbfe32601da14e78db24a26dfe25c2cd39fa2 100644 --- a/lib/util/cache/repository/types.ts +++ b/lib/util/cache/repository/types.ts @@ -26,6 +26,12 @@ export interface BranchUpgradeCache { sourceUrl?: string; } +export interface OnboardingBranchCache { + onboardingBranch: string; + defaultBranchSha: string; + onboardingBranchSha: string; +} + export interface PrCache { fingerprint: string; /** @@ -100,6 +106,7 @@ export interface RepoCacheData { github?: Record<string, unknown>; }; prComments?: Record<number, Record<string, string>>; + onboardingBranchCache?: OnboardingBranchCache; } export interface RepoCache { diff --git a/lib/workers/repository/onboarding/branch/check.spec.ts b/lib/workers/repository/onboarding/branch/check.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..add73cf44a00765903da6e1cc23d00b6ac24b83e --- /dev/null +++ b/lib/workers/repository/onboarding/branch/check.spec.ts @@ -0,0 +1,66 @@ +import { + RenovateConfig, + git, + mocked, + partial, + platform, +} from '../../../../../test/util'; +import { REPOSITORY_CLOSED_ONBOARDING } from '../../../../constants/error-messages'; +import { logger } from '../../../../logger'; +import type { Pr } from '../../../../modules/platform/types'; +import * as _cache from '../../../../util/cache/repository'; +import { isOnboarded } from './check'; + +jest.mock('../../../../util/cache/repository'); +jest.mock('../../../../util/git'); + +const cache = mocked(_cache); + +describe('workers/repository/onboarding/branch/check', () => { + const config = partial<RenovateConfig>({ + requireConfig: 'required', + suppressNotifications: [], + }); + + it('skips normal onboarding check if onboardingCache is valid', async () => { + cache.getCache.mockReturnValueOnce({ + onboardingBranchCache: { + onboardingBranch: 'configure/renovate', + defaultBranchSha: 'default-sha', + onboardingBranchSha: 'onboarding-sha', + }, + }); + git.getBranchCommit + .mockReturnValueOnce('default-sha') + .mockReturnValueOnce('onboarding-sha'); + const res = await isOnboarded(config); + expect(res).toBeFalse(); + expect(logger.debug).toHaveBeenCalledWith( + 'Onboarding cache is valid. Repo is not onboarded' + ); + }); + + it('continues with normal logic if onboardingCache is invalid', async () => { + cache.getCache.mockReturnValueOnce({ + onboardingBranchCache: { + onboardingBranch: 'configure/renovate', + defaultBranchSha: 'default-sha', + onboardingBranchSha: 'onboarding-sha', + }, + }); + git.getFileList.mockResolvedValue([]); + await isOnboarded(config); + expect(logger.debug).not.toHaveBeenCalledWith( + 'Onboarding cache is valid. Repo is not onboarded' + ); + }); + + it('continues with normal logic if closedPr exists', async () => { + cache.getCache.mockReturnValue({}); + platform.findPr.mockResolvedValue(partial<Pr>()); + git.getFileList.mockResolvedValue([]); + await expect(isOnboarded(config)).rejects.toThrow( + REPOSITORY_CLOSED_ONBOARDING + ); + }); +}); diff --git a/lib/workers/repository/onboarding/branch/check.ts b/lib/workers/repository/onboarding/branch/check.ts index 7f77ded5485d6db00487674c009aa89a5163c189..25f7d21a229c54361d6f316d8f9f48ea220c9b54 100644 --- a/lib/workers/repository/onboarding/branch/check.ts +++ b/lib/workers/repository/onboarding/branch/check.ts @@ -9,7 +9,7 @@ import { Pr, platform } from '../../../../modules/platform'; import { ensureComment } from '../../../../modules/platform/comment'; import { getCache } from '../../../../util/cache/repository'; import { readLocalFile } from '../../../../util/fs'; -import { getFileList } from '../../../../util/git'; +import { getBranchCommit, getFileList } from '../../../../util/git'; async function findFile(fileName: string): Promise<boolean> { logger.debug(`findFile(${fileName})`); @@ -61,7 +61,24 @@ export async function isOnboarded(config: RenovateConfig): Promise<boolean> { logger.debug('Config file will be ignored'); return true; } + + const pr = await closedPrExists(config); const cache = getCache(); + const onboardingBranchCache = cache?.onboardingBranchCache; + // if onboarding cache is present and base branch has not been updated branch is not onboarded + // if closed pr exists then presence of onboarding cache doesn't matter as we need to skip onboarding + if ( + !pr && + onboardingBranchCache && + onboardingBranchCache.defaultBranchSha === + getBranchCommit(config.defaultBranch!) && + onboardingBranchCache.onboardingBranchSha === + getBranchCommit(config.onboardingBranch!) + ) { + logger.debug('Onboarding cache is valid. Repo is not onboarded'); + return false; + } + if (cache.configFileName) { logger.debug('Checking cached config file name'); try { @@ -104,7 +121,6 @@ export async function isOnboarded(config: RenovateConfig): Promise<boolean> { throw new Error(REPOSITORY_NO_CONFIG); } - const pr = await closedPrExists(config); if (!pr) { logger.debug('Found no closed onboarding PR'); return false; diff --git a/lib/workers/repository/onboarding/branch/index.spec.ts b/lib/workers/repository/onboarding/branch/index.spec.ts index 1569fab4c451c3a08775ffe87a717d8cafce435d..04fc5ed1856d67db72e99746efe2efbf42a473f1 100644 --- a/lib/workers/repository/onboarding/branch/index.spec.ts +++ b/lib/workers/repository/onboarding/branch/index.spec.ts @@ -21,6 +21,7 @@ import * as _cache from '../../../../util/cache/repository'; import type { FileAddition } from '../../../../util/git/types'; import { OnboardingState } from '../common'; import * as _config from './config'; +import * as _onboardingCache from './onboarding-branch-cache'; import * as _rebase from './rebase'; import { checkOnboardingBranch } from '.'; @@ -32,12 +33,21 @@ jest.mock('../../../../util/cache/repository'); jest.mock('../../../../util/fs'); jest.mock('../../../../util/git'); jest.mock('./config'); +jest.mock('./onboarding-branch-cache'); const cache = mocked(_cache); +const onboardingCache = mocked(_onboardingCache); describe('workers/repository/onboarding/branch/index', () => { describe('checkOnboardingBranch', () => { let config: RenovateConfig; + const dummyCache = { + onboardingBranchCache: { + onboardingBranch: 'configure/renovate', + defaultBranchSha: 'default-sha', + onboardingBranchSha: 'onboarding-sha', + }, + }; beforeEach(() => { memCache.init(); @@ -168,9 +178,11 @@ describe('workers/repository/onboarding/branch/index', () => { }); it('detects repo is onboarded via file', async () => { + cache.getCache.mockReturnValue(dummyCache); git.getFileList.mockResolvedValueOnce(['renovate.json']); const res = await checkOnboardingBranch(config); expect(res.repoIsOnboarded).toBeTrue(); + expect(onboardingCache.deleteOnboardingCache).toHaveBeenCalledTimes(1); // removes onboarding cache when repo is onboarded }); it('handles removed cached file name', async () => { @@ -251,6 +263,7 @@ describe('workers/repository/onboarding/branch/index', () => { }); it('updates onboarding branch', async () => { + cache.getCache.mockReturnValue(dummyCache); git.getFileList.mockResolvedValue(['package.json']); platform.findPr.mockResolvedValue(null); platform.getBranchPr.mockResolvedValueOnce(mock<Pr>()); @@ -259,6 +272,7 @@ describe('workers/repository/onboarding/branch/index', () => { expect(res.repoIsOnboarded).toBeFalse(); expect(res.branchList).toEqual(['renovate/configure']); expect(git.checkoutBranch).toHaveBeenCalledTimes(1); + expect(onboardingCache.setOnboardingCache).toHaveBeenCalledTimes(1); // update onboarding cache expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); diff --git a/lib/workers/repository/onboarding/branch/index.ts b/lib/workers/repository/onboarding/branch/index.ts index 2add4def7375e7874ff47e861f00f5dfe0dcebd6..c8f50ecefa8575b9bb71f235039901293d7fff9a 100644 --- a/lib/workers/repository/onboarding/branch/index.ts +++ b/lib/workers/repository/onboarding/branch/index.ts @@ -8,13 +8,21 @@ import { } from '../../../../constants/error-messages'; import { logger } from '../../../../logger'; import { Pr, platform } from '../../../../modules/platform'; -import { checkoutBranch, setGitAuthor } from '../../../../util/git'; +import { + checkoutBranch, + getBranchCommit, + setGitAuthor, +} from '../../../../util/git'; import { extractAllDependencies } from '../../extract'; import { mergeRenovateConfig } from '../../init/merge'; import { OnboardingState } from '../common'; import { getOnboardingPr, isOnboarded } from './check'; import { getOnboardingConfig } from './config'; import { createOnboardingBranch } from './create'; +import { + deleteOnboardingCache, + setOnboardingCache, +} from './onboarding-branch-cache'; import { rebaseOnboardingBranch } from './rebase'; export async function checkOnboardingBranch( @@ -26,6 +34,9 @@ export async function checkOnboardingBranch( const repoIsOnboarded = await isOnboarded(config); if (repoIsOnboarded) { logger.debug('Repo is onboarded'); + + // delete onboarding cache + deleteOnboardingCache(); return { ...config, repoIsOnboarded }; } if (config.isFork && config.forkProcessing !== 'enabled') { @@ -47,6 +58,13 @@ export async function checkOnboardingBranch( { branch: config.onboardingBranch, commit, onboarding: true }, 'Branch updated' ); + + // update onboarding cache + setOnboardingCache( + config.onboardingBranch!, + getBranchCommit(config.defaultBranch!)!, + commit + ); } // istanbul ignore if if (platform.refreshPr) { @@ -78,6 +96,13 @@ export async function checkOnboardingBranch( { branch: onboardingBranch, commit, onboarding: true }, 'Branch created' ); + + // set onboarding branch cache + setOnboardingCache( + config.onboardingBranch!, + getBranchCommit(config.defaultBranch!)!, + commit + ); } } if (!GlobalConfig.get('dryRun')) { diff --git a/lib/workers/repository/onboarding/branch/onboarding-branch-cache.spec.ts b/lib/workers/repository/onboarding/branch/onboarding-branch-cache.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..10307e5a3ec37424a7199fed37f4751c35ff6c1c --- /dev/null +++ b/lib/workers/repository/onboarding/branch/onboarding-branch-cache.spec.ts @@ -0,0 +1,61 @@ +import { mocked } from '../../../../../test/util'; +import * as _cache from '../../../../util/cache/repository'; +import type { RepoCacheData } from '../../../../util/cache/repository/types'; +import { + deleteOnboardingCache, + setOnboardingCache, +} from './onboarding-branch-cache'; + +jest.mock('../../../../util/cache/repository'); +const cache = mocked(_cache); + +describe('workers/repository/onboarding/branch/onboarding-branch-cache', () => { + it('sets new cache', () => { + const dummyCache = {} satisfies RepoCacheData; + cache.getCache.mockReturnValueOnce(dummyCache); + setOnboardingCache('configure/renovate', 'default-sha', 'onboarding-sha'); + expect(dummyCache).toEqual({ + onboardingBranchCache: { + onboardingBranch: 'configure/renovate', + defaultBranchSha: 'default-sha', + onboardingBranchSha: 'onboarding-sha', + }, + }); + }); + + it('updates old cache', () => { + const dummyCache = { + onboardingBranchCache: { + onboardingBranch: 'configure/renovate', + defaultBranchSha: 'default-sha', + onboardingBranchSha: 'onboarding-sha', + }, + } satisfies RepoCacheData; + cache.getCache.mockReturnValueOnce(dummyCache); + setOnboardingCache( + 'configure/renovate', + 'default-sha-1', + 'onboarding-sha-1' + ); + expect(dummyCache).toEqual({ + onboardingBranchCache: { + onboardingBranch: 'configure/renovate', + defaultBranchSha: 'default-sha-1', + onboardingBranchSha: 'onboarding-sha-1', + }, + }); + }); + + it('deletes cache', () => { + const dummyCache = { + onboardingBranchCache: { + onboardingBranch: 'configure/renovate', + defaultBranchSha: 'default-sha', + onboardingBranchSha: 'onboarding-sha', + }, + } satisfies RepoCacheData; + cache.getCache.mockReturnValueOnce(dummyCache); + deleteOnboardingCache(); + expect(dummyCache.onboardingBranchCache).toBeUndefined(); + }); +}); diff --git a/lib/workers/repository/onboarding/branch/onboarding-branch-cache.ts b/lib/workers/repository/onboarding/branch/onboarding-branch-cache.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6ecfcb29677b2f81315e7dad4415505f9274c7f --- /dev/null +++ b/lib/workers/repository/onboarding/branch/onboarding-branch-cache.ts @@ -0,0 +1,30 @@ +import { logger } from '../../../../logger'; +import { getCache } from '../../../../util/cache/repository'; + +export function setOnboardingCache( + onboardingBranch: string, + defaultBranchSha: string, + onboardingBranchSha: string +): void { + const cache = getCache(); + const onboardingCache = { + onboardingBranch, + defaultBranchSha, + onboardingBranchSha, + }; + if (cache.onboardingBranchCache) { + logger.debug('Update Onboarding Cache'); + } else { + logger.debug('Create Onboarding Cache'); + } + cache.onboardingBranchCache = onboardingCache; +} + +export function deleteOnboardingCache(): void { + const cache = getCache(); + + if (cache?.onboardingBranchCache) { + logger.debug('Delete Onboarding Cache'); + delete cache.onboardingBranchCache; + } +}