import type { RenovateConfig } from '~test/util.ts'; import { git, partial, platform, scm } from '~test/util.ts'; import { GlobalConfig } from '../../../config/global.ts'; import type { Pr } from '../../../modules/platform/types.ts'; import * as cleanup from './prune.ts'; let config: RenovateConfig; beforeEach(() => { config = partial({ repoIsOnboarded: true, defaultBranch: 'main', branchPrefix: `renovate/`, pruneStaleBranches: true, }); }); describe('workers/repository/finalize/prune', () => { describe('pruneStaleBranches()', () => { beforeEach(() => { GlobalConfig.reset(); }); it('returns if no branchList', async () => { delete config.branchList; await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(0); }); it('ignores reconfigure branch', async () => { delete config.branchList; await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(0); }); it('returns if no defaultBranch', async () => { delete config.defaultBranch; config.branchList = []; await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(0); }); it('returns if branchPrefix is empty', async () => { config.branchPrefix = ''; config.branchList = ['main/quality/renovate-a']; await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(0); expect(scm.deleteBranch).toHaveBeenCalledTimes(0); }); it('returns if no renovate branches', async () => { config.branchList = []; git.getBranchList.mockReturnValueOnce([]); await expect( cleanup.pruneStaleBranches(config, config.branchList), ).resolves.not.toThrow(); }); it('returns if no remaining branches', async () => { config.branchList = ['renovate/a', 'renovate/b']; git.getBranchList.mockReturnValueOnce(config.branchList); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(0); }); it('renames deletes remaining branch', async () => { config.branchList = ['renovate/a', 'renovate/b']; git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); platform.findPr.mockResolvedValueOnce(partial({ title: 'foo' })); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(1); expect(platform.updatePr).toHaveBeenCalledTimes(1); }); it('skips rename but still deletes branch', async () => { config.branchList = ['renovate/a', 'renovate/b']; git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); platform.findPr.mockResolvedValueOnce( partial({ title: 'foo - autoclosed', }), ); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(1); expect(platform.updatePr).toHaveBeenCalledTimes(1); }); it('deletes with base branches', async () => { config.branchList = ['renovate/main-a']; config.baseBranchPatterns = ['/main.*/']; config.baseBranches = ['main', 'maint/v7']; git.getBranchList.mockReturnValueOnce( config.branchList.concat([ 'renovate/main-b', 'renovate/maint/v7-a', 'renovate/maint/v7-b', ]), ); scm.isBranchModified.mockResolvedValueOnce(true); scm.isBranchModified.mockResolvedValueOnce(false); scm.isBranchModified.mockResolvedValueOnce(true); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledExactlyOnceWith( 'renovate/maint/v7-a', ); expect(scm.isBranchModified).toHaveBeenCalledTimes(3); expect(scm.isBranchModified).toHaveBeenCalledWith( 'renovate/main-b', 'main', ); expect(scm.isBranchModified).toHaveBeenCalledWith( 'renovate/maint/v7-a', 'maint/v7', ); expect(scm.isBranchModified).toHaveBeenCalledWith( 'renovate/maint/v7-b', 'maint/v7', ); }); it('uses single configured base branch instead of defaultBranch', async () => { config.branchList = []; config.baseBranchPatterns = ['renovate-updates']; config.baseBranches = ['renovate-updates']; config.defaultBranch = 'main'; git.getBranchList.mockReturnValueOnce(['renovate/dayjs-1.x']); platform.findPr.mockResolvedValueOnce(null); await cleanup.pruneStaleBranches(config, config.branchList); expect(platform.findPr).toHaveBeenCalledExactlyOnceWith({ branchName: 'renovate/dayjs-1.x', state: 'open', targetBranch: 'renovate-updates', }); expect(scm.isBranchModified).toHaveBeenCalledExactlyOnceWith( 'renovate/dayjs-1.x', 'renovate-updates', ); }); it('uses defaultBranch when baseBranchPatterns exist but baseBranches are not computed yet', async () => { config.branchList = []; config.baseBranchPatterns = ['/^release\\/.*/']; config.defaultBranch = 'main'; git.getBranchList.mockReturnValueOnce([ 'renovate/release/1.x-dependency', ]); platform.findPr.mockResolvedValueOnce(null); await expect( cleanup.pruneStaleBranches(config, config.branchList), ).resolves.not.toThrow(); expect(platform.findPr).toHaveBeenCalledExactlyOnceWith({ branchName: 'renovate/release/1.x-dependency', state: 'open', targetBranch: 'main', }); expect(scm.isBranchModified).toHaveBeenCalledExactlyOnceWith( 'renovate/release/1.x-dependency', 'main', ); expect(scm.deleteBranch).toHaveBeenCalledExactlyOnceWith( 'renovate/release/1.x-dependency', ); }); it('does nothing on dryRun', async () => { config.branchList = ['renovate/a', 'renovate/b']; GlobalConfig.set({ dryRun: 'full' }); git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); platform.findPr.mockResolvedValueOnce(partial({ title: 'foo' })); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(0); expect(platform.updatePr).toHaveBeenCalledTimes(0); }); it('does nothing on prune stale branches disabled', async () => { config.branchList = ['renovate/a', 'renovate/b']; config.pruneStaleBranches = false; git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); platform.findPr.mockResolvedValueOnce(partial({ title: 'foo' })); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(0); expect(platform.updatePr).toHaveBeenCalledTimes(0); }); it('notifies via PR changes if someone pushed to PR', async () => { config.branchList = ['renovate/a', 'renovate/b']; git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); platform.getBranchPr.mockResolvedValueOnce(partial()); scm.isBranchModified.mockResolvedValueOnce(true); platform.findPr.mockResolvedValueOnce(partial({ title: 'foo' })); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(0); expect(platform.updatePr).toHaveBeenCalledTimes(1); expect(platform.ensureComment).toHaveBeenCalledTimes(1); }); it('skips appending - abandoned to PR title if already present', async () => { config.branchList = ['renovate/a', 'renovate/b']; git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); platform.getBranchPr.mockResolvedValueOnce(partial()); scm.isBranchModified.mockResolvedValueOnce(true); platform.findPr.mockResolvedValueOnce( partial({ title: 'foo - abandoned' }), ); await cleanup.pruneStaleBranches(config, config.branchList); expect(platform.updatePr).toHaveBeenCalledTimes(0); }); it('skips changes to PR if dry run', async () => { config.branchList = ['renovate/a', 'renovate/b']; GlobalConfig.set({ dryRun: 'full' }); git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); platform.getBranchPr.mockResolvedValueOnce(partial()); scm.isBranchModified.mockResolvedValueOnce(true); platform.findPr.mockResolvedValueOnce(partial({ title: 'foo' })); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(0); expect(platform.updatePr).toHaveBeenCalledTimes(0); expect(platform.ensureComment).toHaveBeenCalledTimes(0); }); it('dry run delete branch no PR', async () => { config.branchList = ['renovate/a', 'renovate/b']; GlobalConfig.set({ dryRun: 'full' }); git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); platform.findPr.mockResolvedValueOnce(null); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(0); expect(platform.updatePr).toHaveBeenCalledTimes(0); }); it('delete branch no PR', async () => { config.branchList = ['renovate/a', 'renovate/b']; git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); platform.findPr.mockResolvedValueOnce(null); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(1); expect(platform.updatePr).toHaveBeenCalledTimes(0); }); it('does not delete modified orphan branch', async () => { config.branchList = ['renovate/a', 'renovate/b']; git.getBranchList.mockReturnValueOnce( config.branchList.concat(['renovate/c']), ); scm.isBranchModified.mockResolvedValueOnce(true); platform.findPr.mockResolvedValueOnce(null); await cleanup.pruneStaleBranches(config, config.branchList); expect(git.getBranchList).toHaveBeenCalledTimes(1); expect(scm.deleteBranch).toHaveBeenCalledTimes(0); expect(platform.updatePr).toHaveBeenCalledTimes(0); }); }); });