From 93052ec9178a3ae57912703cdae2e9f959f36431 Mon Sep 17 00:00:00 2001 From: Rhys Arkins Date: Wed, 10 May 2023 06:30:12 +0200 Subject: [PATCH] feat: platform=local (#22010) Co-authored-by: Jamie Magee --- lib/config/presets/local/index.ts | 1 + lib/constants/platforms.ts | 3 +- lib/modules/platform/api.ts | 2 + lib/modules/platform/local/index.spec.ts | 128 +++++++++++++++++++++++ lib/modules/platform/local/index.ts | 123 ++++++++++++++++++++++ lib/modules/platform/local/scm.spec.ts | 68 ++++++++++++ lib/modules/platform/local/scm.ts | 51 +++++++++ lib/modules/platform/scm.ts | 2 + lib/util/fs/index.spec.ts | 5 + lib/util/fs/index.ts | 4 + lib/util/git/index.ts | 4 + lib/workers/global/autodiscover.spec.ts | 13 +++ lib/workers/global/autodiscover.ts | 13 +++ lib/workers/global/index.spec.ts | 9 ++ lib/workers/global/index.ts | 11 +- lib/workers/repository/process/index.ts | 2 +- package.json | 2 +- 17 files changed, 434 insertions(+), 7 deletions(-) create mode 100644 lib/modules/platform/local/index.spec.ts create mode 100644 lib/modules/platform/local/index.ts create mode 100644 lib/modules/platform/local/scm.spec.ts create mode 100644 lib/modules/platform/local/scm.ts diff --git a/lib/config/presets/local/index.ts b/lib/config/presets/local/index.ts index ac8e2a69703049..6f99390a6c676d 100644 --- a/lib/config/presets/local/index.ts +++ b/lib/config/presets/local/index.ts @@ -24,6 +24,7 @@ const resolvers = { gitea, github, gitlab, + local: null, } satisfies Record; export function getPreset({ diff --git a/lib/constants/platforms.ts b/lib/constants/platforms.ts index f3a1e71ac8b7ba..049155bd3ba43c 100644 --- a/lib/constants/platforms.ts +++ b/lib/constants/platforms.ts @@ -5,7 +5,8 @@ export type PlatformId = | 'bitbucket-server' | 'gitea' | 'github' - | 'gitlab'; + | 'gitlab' + | 'local'; export const GITHUB_API_USING_HOST_TYPES = [ 'github', diff --git a/lib/modules/platform/api.ts b/lib/modules/platform/api.ts index bb67f2c39432a4..973f6b75d39db6 100644 --- a/lib/modules/platform/api.ts +++ b/lib/modules/platform/api.ts @@ -6,6 +6,7 @@ import * as codecommit from './codecommit'; import * as gitea from './gitea'; import * as github from './github'; import * as gitlab from './gitlab'; +import * as local from './local'; import type { Platform } from './types'; const api = new Map(); @@ -18,3 +19,4 @@ api.set(codecommit.id, codecommit); api.set(gitea.id, gitea); api.set(github.id, github); api.set(gitlab.id, gitlab); +api.set(local.id, local); diff --git a/lib/modules/platform/local/index.spec.ts b/lib/modules/platform/local/index.spec.ts new file mode 100644 index 00000000000000..c226d334d79cbe --- /dev/null +++ b/lib/modules/platform/local/index.spec.ts @@ -0,0 +1,128 @@ +import * as platform from './index'; + +describe('modules/platform/local/index', () => { + describe('initPlatform', () => { + it('returns input', async () => { + expect(await platform.initPlatform({})).toMatchInlineSnapshot(` + { + "dryRun": "lookup", + "endpoint": "local", + "persistRepoData": true, + "requireConfig": "optional", + } + `); + }); + }); + + describe('getRepos', () => { + it('returns empty array', async () => { + expect(await platform.getRepos()).toEqual([]); + }); + }); + + describe('initRepo', () => { + it('returns object', async () => { + expect(await platform.initRepo()).toMatchInlineSnapshot(` + { + "defaultBranch": "", + "isFork": false, + "repoFingerprint": "", + } + `); + }); + }); + + describe('dummy functions', () => { + it('getRepoForceRebase', async () => { + expect(await platform.getRepoForceRebase()).toBe(false); + }); + + it('findIssue', async () => { + expect(await platform.findIssue()).toBeNull(); + }); + + it('getIssueList', async () => { + expect(await platform.getIssueList()).toEqual([]); + }); + + it('getRawFile', async () => { + expect(await platform.getRawFile()).toBeNull(); + }); + + it('getJsonFile', async () => { + expect(await platform.getJsonFile()).toBeNull(); + }); + + it('getPrList', async () => { + expect(await platform.getPrList()).toEqual([]); + }); + + it('ensureIssueClosing', async () => { + expect(await platform.ensureIssueClosing()).toBeUndefined(); + }); + + it('ensureIssue', async () => { + expect(await platform.ensureIssue()).toBeNull(); + }); + + it('massageMarkdown', () => { + expect(platform.massageMarkdown('foo')).toBe('foo'); + }); + + it('updatePr', async () => { + expect(await platform.updatePr()).toBeUndefined(); + }); + + it('mergePr', async () => { + expect(await platform.mergePr()).toBe(false); + }); + + it('addReviewers', async () => { + expect(await platform.addReviewers()).toBeUndefined(); + }); + + it('addAssignees', async () => { + expect(await platform.addAssignees()).toBeUndefined(); + }); + + it('createPr', async () => { + expect(await platform.createPr()).toBeNull(); + }); + + it('deleteLabel', async () => { + expect(await platform.deleteLabel()).toBeUndefined(); + }); + + it('setBranchStatus', async () => { + expect(await platform.setBranchStatus()).toBeUndefined(); + }); + + it('getBranchStatus', async () => { + expect(await platform.getBranchStatus()).toBe('red'); + }); + + it('getBranchStatusCheck', async () => { + expect(await platform.getBranchStatusCheck()).toBeNull(); + }); + + it('ensureCommentRemoval', async () => { + expect(await platform.ensureCommentRemoval()).toBeUndefined(); + }); + + it('ensureComment', async () => { + expect(await platform.ensureComment()).toBeFalse(); + }); + + it('getPr', async () => { + expect(await platform.getPr()).toBeNull(); + }); + + it('findPr', async () => { + expect(await platform.findPr()).toBeNull(); + }); + + it('getBranchPr', async () => { + expect(await platform.getBranchPr()).toBeNull(); + }); + }); +}); diff --git a/lib/modules/platform/local/index.ts b/lib/modules/platform/local/index.ts new file mode 100644 index 00000000000000..1c88d787371daf --- /dev/null +++ b/lib/modules/platform/local/index.ts @@ -0,0 +1,123 @@ +import type { BranchStatus } from '../../../types'; +import type { + Issue, + PlatformParams, + PlatformResult, + Pr, + RepoResult, +} from '../types'; + +export const id = 'local'; + +export function initPlatform(params: PlatformParams): Promise { + return Promise.resolve({ + dryRun: 'lookup', + endpoint: 'local', + persistRepoData: true, + requireConfig: 'optional', + }); +} + +export function getRepos(): Promise { + return Promise.resolve([]); +} + +export function initRepo(): Promise { + return Promise.resolve({ + defaultBranch: '', + isFork: false, + repoFingerprint: '', + }); +} + +export function getRepoForceRebase(): Promise { + return Promise.resolve(false); +} + +export function findIssue(): Promise { + return Promise.resolve(null); +} + +export function getIssueList(): Promise { + return Promise.resolve([]); +} + +export function getRawFile(): Promise { + return Promise.resolve(null); +} + +export function getJsonFile(): Promise | null> { + return Promise.resolve(null); +} + +export function getPrList(): Promise { + return Promise.resolve([]); +} + +export function ensureIssueClosing(): Promise { + return Promise.resolve(); +} + +export function ensureIssue(): Promise { + return Promise.resolve(null); +} + +export function massageMarkdown(input: string): string { + return input; +} + +export function updatePr(): Promise { + return Promise.resolve(); +} + +export function mergePr(): Promise { + return Promise.resolve(false); +} + +export function addReviewers(): Promise { + return Promise.resolve(); +} + +export function addAssignees(): Promise { + return Promise.resolve(); +} + +export function createPr(): Promise { + return Promise.resolve(null); +} + +export function deleteLabel(): Promise { + return Promise.resolve(); +} + +export function setBranchStatus(): Promise { + return Promise.resolve(); +} + +export function getBranchStatus(): Promise { + return Promise.resolve('red'); +} + +export function getBranchStatusCheck(): Promise { + return Promise.resolve(null); +} + +export function ensureCommentRemoval(): Promise { + return Promise.resolve(); +} + +export function ensureComment(): Promise { + return Promise.resolve(false); +} + +export function getPr(): Promise { + return Promise.resolve(null); +} + +export function findPr(): Promise { + return Promise.resolve(null); +} + +export function getBranchPr(): Promise { + return Promise.resolve(null); +} diff --git a/lib/modules/platform/local/scm.spec.ts b/lib/modules/platform/local/scm.spec.ts new file mode 100644 index 00000000000000..6549475ad55d93 --- /dev/null +++ b/lib/modules/platform/local/scm.spec.ts @@ -0,0 +1,68 @@ +import { execSync as _execSync } from 'node:child_process'; +import { mockedFunction } from '../../../../test/util'; +import { LocalFs } from './scm'; + +jest.mock('node:child_process'); +const execSync = mockedFunction(_execSync); + +describe('modules/platform/local/scm', () => { + let localFs: LocalFs; + + beforeEach(() => { + localFs = new LocalFs(); + }); + + describe('dummy functions', () => { + it('behindBaseBranch', async () => { + expect(await localFs.isBranchBehindBase('', '')).toBe(false); + }); + + it('isBranchModified', async () => { + expect(await localFs.isBranchModified('')).toBe(false); + }); + + it('isBranchConflicted', async () => { + expect(await localFs.isBranchConflicted('', '')).toBe(false); + }); + + it('branchExists', async () => { + expect(await localFs.branchExists('')).toBe(true); + }); + + it('getBranchCommit', async () => { + expect(await localFs.getBranchCommit('')).toBeNull(); + }); + + it('deleteBranch', async () => { + expect(await localFs.deleteBranch('')).toBeUndefined(); + }); + + it('commitAndPush', async () => { + expect(await localFs.commitAndPush({} as any)).toBeNull(); + }); + + it('checkoutBranch', async () => { + expect(await localFs.checkoutBranch('')).toBe(''); + }); + }); + + describe('getFileList', () => { + it('should return file list using git', async () => { + execSync.mockReturnValueOnce('file1\nfile2'); + expect(await localFs.getFileList()).toHaveLength(2); + }); + + it('should return file list using glob', async () => { + execSync.mockImplementationOnce(() => { + throw new Error(); + }); + jest.mock('glob', () => ({ + glob: jest + .fn() + .mockImplementation(() => Promise.resolve(['file1', 'file2'])), + })); + + expect(await localFs.getFileList()).toHaveLength(2); + }); + }); +}); diff --git a/lib/modules/platform/local/scm.ts b/lib/modules/platform/local/scm.ts new file mode 100644 index 00000000000000..5b01391572c110 --- /dev/null +++ b/lib/modules/platform/local/scm.ts @@ -0,0 +1,51 @@ +import { execSync } from 'node:child_process'; +import { glob } from 'glob'; +import { logger } from '../../../logger'; +import type { CommitFilesConfig, CommitSha } from '../../../util/git/types'; +import type { PlatformScm } from '../types'; + +let fileList: string[] | undefined; +export class LocalFs implements PlatformScm { + isBranchBehindBase(branchName: string, baseBranch: string): Promise { + return Promise.resolve(false); + } + isBranchModified(branchName: string): Promise { + return Promise.resolve(false); + } + isBranchConflicted(baseBranch: string, branch: string): Promise { + return Promise.resolve(false); + } + branchExists(branchName: string): Promise { + return Promise.resolve(true); + } + getBranchCommit(branchName: string): Promise { + return Promise.resolve(null); + } + deleteBranch(branchName: string): Promise { + return Promise.resolve(); + } + commitAndPush(commitConfig: CommitFilesConfig): Promise { + return Promise.resolve(null); + } + + async getFileList(): Promise { + try { + // fetch file list using git + const stdout = execSync('git ls-files', { encoding: 'utf-8' }); + logger.debug('Got file list using git'); + fileList = stdout.split('\n'); + } catch (err) { + logger.debug('Could not get file list using git, using glob instead'); + fileList ??= await glob('**', { + dot: true, + nodir: true, + }); + } + + return fileList; + } + + checkoutBranch(branchName: string): Promise { + return Promise.resolve(''); + } +} diff --git a/lib/modules/platform/scm.ts b/lib/modules/platform/scm.ts index 9a26a35594401d..8f80217ec36da5 100644 --- a/lib/modules/platform/scm.ts +++ b/lib/modules/platform/scm.ts @@ -3,6 +3,7 @@ import type { PlatformId } from '../../constants'; import { PLATFORM_NOT_FOUND } from '../../constants/error-messages'; import { DefaultGitScm } from './default-scm'; import { GithubScm } from './github/scm'; +import { LocalFs } from './local/scm'; import type { PlatformScm } from './types'; export const platformScmImpls = new Map>(); @@ -13,6 +14,7 @@ platformScmImpls.set('bitbucket-server', DefaultGitScm); platformScmImpls.set('gitea', DefaultGitScm); platformScmImpls.set('github', GithubScm); platformScmImpls.set('gitlab', DefaultGitScm); +platformScmImpls.set('local', LocalFs); let _scm: PlatformScm | undefined; diff --git a/lib/util/fs/index.spec.ts b/lib/util/fs/index.spec.ts index f200818843e2d7..361f4eff62dda9 100644 --- a/lib/util/fs/index.spec.ts +++ b/lib/util/fs/index.spec.ts @@ -134,6 +134,11 @@ describe('util/fs/index', () => { }); describe('deleteLocalFile', () => { + it('throws if platform is local', async () => { + GlobalConfig.set({ platform: 'local' }); + await expect(deleteLocalFile('foo/bar/file.txt')).rejects.toThrow(); + }); + it('deletes file', async () => { const filePath = `${localDir}/foo/bar/file.txt`; await fs.outputFile(filePath, 'foobar'); diff --git a/lib/util/fs/index.ts b/lib/util/fs/index.ts index 5493a6397f8493..2a9b2793975755 100644 --- a/lib/util/fs/index.ts +++ b/lib/util/fs/index.ts @@ -65,6 +65,10 @@ export async function writeLocalFile( } export async function deleteLocalFile(fileName: string): Promise { + // This a failsafe and hopefully will never be triggered + if (GlobalConfig.get('platform') === 'local') { + throw new Error('Cannot delete file when platform=local'); + } const localDir = GlobalConfig.get('localDir'); if (localDir) { const localFileName = ensureLocalPath(fileName); diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts index 1ab24e34acf71f..5d086bef8612ca 100644 --- a/lib/util/git/index.ts +++ b/lib/util/git/index.ts @@ -383,6 +383,10 @@ export async function syncGit(): Promise { } return; } + // istanbul ignore if: failsafe + if (GlobalConfig.get('platform') === 'local') { + throw new Error('Cannot sync git when platform=local'); + } gitInitialized = true; const localDir = GlobalConfig.get('localDir')!; logger.debug(`Initializing git repository into ${localDir}`); diff --git a/lib/workers/global/autodiscover.spec.ts b/lib/workers/global/autodiscover.spec.ts index e401268d6fd148..ae75305ad695a9 100644 --- a/lib/workers/global/autodiscover.spec.ts +++ b/lib/workers/global/autodiscover.spec.ts @@ -25,6 +25,19 @@ describe('workers/global/autodiscover', () => { }); }); + it('throws if local and repositories defined', async () => { + config.platform = 'local'; + config.repositories = ['a']; + await expect(autodiscoverRepositories(config)).rejects.toThrow(); + }); + + it('returns local', async () => { + config.platform = 'local'; + expect((await autodiscoverRepositories(config)).repositories).toEqual([ + 'local', + ]); + }); + it('returns if not autodiscovering', async () => { expect(await autodiscoverRepositories(config)).toEqual(config); }); diff --git a/lib/workers/global/autodiscover.ts b/lib/workers/global/autodiscover.ts index 332961b82e1506..943f739d67f247 100644 --- a/lib/workers/global/autodiscover.ts +++ b/lib/workers/global/autodiscover.ts @@ -13,6 +13,19 @@ function repoName(value: string | { repository: string }): string { export async function autodiscoverRepositories( config: AllConfig ): Promise { + if (config.platform === 'local') { + if (config.repositories?.length) { + logger.debug( + { repositories: config.repositories }, + 'Found repositories when in local mode' + ); + throw new Error( + 'Invalid configuration: repositories list not supported when platform=local' + ); + } + config.repositories = ['local']; + return config; + } if (!config.autodiscover) { if (!config.repositories?.length) { logger.warn( diff --git a/lib/workers/global/index.spec.ts b/lib/workers/global/index.spec.ts index 8c371725b1569a..0bc6aec1add7c0 100644 --- a/lib/workers/global/index.spec.ts +++ b/lib/workers/global/index.spec.ts @@ -95,6 +95,15 @@ describe('workers/global/index', () => { expect(repositoryWorker.renovateRepository).not.toHaveBeenCalled(); }); + it('handles local', async () => { + parseConfigs.mockResolvedValueOnce({ + platform: 'local', + }); + await expect(globalWorker.start()).resolves.toBe(0); + expect(parseConfigs).toHaveBeenCalledTimes(1); + expect(repositoryWorker.renovateRepository).toHaveBeenCalledTimes(1); + }); + it('processes repositories', async () => { parseConfigs.mockResolvedValueOnce({ gitAuthor: 'a@b.com', diff --git a/lib/workers/global/index.ts b/lib/workers/global/index.ts index 52e7beb5ea9a67..80869a7426c87e 100644 --- a/lib/workers/global/index.ts +++ b/lib/workers/global/index.ts @@ -37,10 +37,13 @@ export async function getRepositoryConfig( ); // TODO: types (#7154) const platform = GlobalConfig.get('platform')!; - repoConfig.localDir = upath.join( - repoConfig.baseDir, - `./repos/${platform}/${repoConfig.repository}` - ); + repoConfig.localDir = + platform === 'local' + ? process.cwd() + : upath.join( + repoConfig.baseDir, + `./repos/${platform}/${repoConfig.repository}` + ); await fs.ensureDir(repoConfig.localDir); delete repoConfig.baseDir; return configParser.filterConfig(repoConfig, 'repository'); diff --git a/lib/workers/repository/process/index.ts b/lib/workers/repository/process/index.ts index 0593f539bc42bb..cced4e1702d921 100644 --- a/lib/workers/repository/process/index.ts +++ b/lib/workers/repository/process/index.ts @@ -108,7 +108,7 @@ export async function extractDependencies( branchList: [], packageFiles: null!, }; - if (config.baseBranches?.length) { + if (GlobalConfig.get('platform') !== 'local' && config.baseBranches?.length) { config.baseBranches = unfoldBaseBranches(config.baseBranches); logger.debug({ baseBranches: config.baseBranches }, 'baseBranches'); const extracted: Record> = {}; diff --git a/package.json b/package.json index 0fe3870314db92..78aa5863febcc9 100644 --- a/package.json +++ b/package.json @@ -196,6 +196,7 @@ "global-agent": "3.0.0", "good-enough-parser": "1.1.22", "got": "11.8.6", + "glob": "10.2.2", "graph-data-structure": "3.3.0", "handlebars": "4.7.7", "hasha": "5.2.2", @@ -307,7 +308,6 @@ "eslint-plugin-promise": "6.1.1", "eslint-plugin-typescript-enum": "2.1.0", "expect-more-jest": "5.5.0", - "glob": "10.2.2", "graphql": "16.6.0", "husky": "8.0.3", "jest": "29.5.0",