From e3780f7cf89a130ef864307669369b632f89cbb8 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 1 Aug 2023 10:06:05 +0200 Subject: [PATCH 01/61] - Added Renovate support for the [SCM-Manager](https://scm-manager.org/) - SCM-Manager is a repository management tool like GitHub - It is maintained as an open source project by the company [Cloudogu](https://cloudogu.com/en/) - The SCM-Manager support is not feature complete yet, features like auto merging are still missing - The SCM-Manager also got added to the documentation as another platform author Thomas Zerr 1690877165 +0200 committer Eduard Heimbuch 1706264263 +0100 --- docs/usage/bot-comparison.md | 2 +- docs/usage/faq.md | 8 +- docs/usage/getting-started/running.md | 1 + docs/usage/getting-started/use-cases.md | 4 +- lib/config/options/index.ts | 6 +- lib/config/presets/local/index.ts | 1 + lib/constants/platforms.ts | 3 +- lib/modules/platform/api.ts | 2 + lib/modules/platform/scm.ts | 1 + lib/modules/platform/scmm/index.spec.ts | 411 +++++++++++++++++++ lib/modules/platform/scmm/index.ts | 349 ++++++++++++++++ lib/modules/platform/scmm/mapper.spec.ts | 44 ++ lib/modules/platform/scmm/mapper.ts | 20 + lib/modules/platform/scmm/readme.md | 13 + lib/modules/platform/scmm/scm-client.spec.ts | 332 +++++++++++++++ lib/modules/platform/scmm/scm-client.ts | 130 ++++++ lib/modules/platform/scmm/types.ts | 144 +++++++ lib/modules/platform/scmm/utils.spec.ts | 175 ++++++++ lib/modules/platform/scmm/utils.ts | 101 +++++ package.json | 1 + pnpm-lock.yaml | 64 +++ readme.md | 1 + 22 files changed, 1802 insertions(+), 11 deletions(-) create mode 100644 lib/modules/platform/scmm/index.spec.ts create mode 100644 lib/modules/platform/scmm/index.ts create mode 100644 lib/modules/platform/scmm/mapper.spec.ts create mode 100644 lib/modules/platform/scmm/mapper.ts create mode 100644 lib/modules/platform/scmm/readme.md create mode 100644 lib/modules/platform/scmm/scm-client.spec.ts create mode 100644 lib/modules/platform/scmm/scm-client.ts create mode 100644 lib/modules/platform/scmm/types.ts create mode 100644 lib/modules/platform/scmm/utils.spec.ts create mode 100644 lib/modules/platform/scmm/utils.ts diff --git a/docs/usage/bot-comparison.md b/docs/usage/bot-comparison.md index 227542e02c..e53993d5c5 100644 --- a/docs/usage/bot-comparison.md +++ b/docs/usage/bot-comparison.md @@ -12,7 +12,7 @@ If you see anything wrong on this page, please let us know by creating a [Discus | Dependency Dashboard | Yes | No | | Grouped updates | Yes, use community-provided groups, or create your own | Yes, create [`groups`](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups) manually | | Upgrades common monorepo packages at once | Yes | No | -| Officially supported platforms | GitHub, GitLab, Bitbucket, Azure, Gitea, see [full list](./index.md#supported-platforms) | GitHub only | +| Officially supported platforms | GitHub, GitLab, Bitbucket, Azure, Gitea, SCM-Manager, see [full list](./index.md#supported-platforms) | GitHub only | | Supported languages | [List for Renovate](./modules/manager/index.md) | [List for Dependabot](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/about-dependabot-version-updates#supported-repositories-and-ecosystems) | | Show changelogs | Yes | Yes | | Compatibility score badges | Four badges showing: Age, Adoption, Passing, Confidence | One badge with overall compatibility score | diff --git a/docs/usage/faq.md b/docs/usage/faq.md index 940aa16db1..bf1f61eb64 100644 --- a/docs/usage/faq.md +++ b/docs/usage/faq.md @@ -38,10 +38,10 @@ Major releases of Renovate are held back until the maintainers are reasonably ce ## Renovate core features not supported on all platforms -| Feature | Platforms which lack feature | See Renovate issue(s) | -| --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------ | -| Dependency Dashboard | Azure, Bitbucket, Bitbucket Server | [#9592](https://github.com/renovatebot/renovate/issues/9592) | -| The Mend Renovate App | Azure, Bitbucket, Bitbucket Server, Forgejo, Gitea, GitLab | | +| Feature | Platforms which lack feature | See Renovate issue(s) | +| --------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------ | +| Dependency Dashboard | Azure, Bitbucket, Bitbucket Server, SCM-Manager | [#9592](https://github.com/renovatebot/renovate/issues/9592) | +| The Mend Renovate App | Azure, Bitbucket, Bitbucket Server, Forgejo, Gitea, GitLab, SCM-Manager | | ## Major platform features not supported by Renovate diff --git a/docs/usage/getting-started/running.md b/docs/usage/getting-started/running.md index 5abf351cd7..a132daef73 100644 --- a/docs/usage/getting-started/running.md +++ b/docs/usage/getting-started/running.md @@ -208,6 +208,7 @@ Read the platform-specific docs to learn how to setup authentication on your pla - [Gitea and Forgejo](../modules/platform/gitea/index.md) - [github.com and GitHub Enterprise Server](../modules/platform/github/index.md) - [GitLab](../modules/platform/gitlab/index.md) +- [SCM-Manager](../modules/platform/scmm/index.md) ### GitHub.com token for changelogs diff --git a/docs/usage/getting-started/use-cases.md b/docs/usage/getting-started/use-cases.md index ffcdd260bc..f863a72435 100644 --- a/docs/usage/getting-started/use-cases.md +++ b/docs/usage/getting-started/use-cases.md @@ -22,8 +22,8 @@ Example package files include: Renovate: 1. Scans your repositories to find package files and their dependencies -1. Checks if any newer versions exist -1. Raises Pull Requests for available updates +2. Checks if any newer versions exist +3. Raises Pull Requests for available updates The Pull Requests patch the package files directly, and include changelogs for the newer versions (if they are available). diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 17a556a8c6..71a519c03d 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -316,7 +316,7 @@ const options: RenovateOptions[] = [ 'If set to `true` then Renovate creates draft PRs, instead of normal status PRs.', type: 'boolean', default: false, - supportedPlatforms: ['azure', 'gitea', 'github', 'gitlab'], + supportedPlatforms: ['azure', 'gitea', 'github', 'gitlab', 'scmm'], }, { name: 'dryRun', @@ -780,7 +780,7 @@ const options: RenovateOptions[] = [ description: 'Username for authentication.', stage: 'repository', type: 'string', - supportedPlatforms: ['azure', 'bitbucket', 'bitbucket-server'], + supportedPlatforms: ['azure', 'bitbucket', 'bitbucket-server', 'scmm'], globalOnly: true, }, { @@ -2729,7 +2729,7 @@ const options: RenovateOptions[] = [ description: 'Overrides the default resolution for Git remote, e.g. to switch GitLab from HTTPS to SSH-based.', type: 'string', - supportedPlatforms: ['gitlab', 'bitbucket-server'], + supportedPlatforms: ['gitlab', 'bitbucket-server', 'scmm'], allowedValues: ['default', 'ssh', 'endpoint'], default: 'default', stage: 'repository', diff --git a/lib/config/presets/local/index.ts b/lib/config/presets/local/index.ts index 6b7e39a373..ff78212a9e 100644 --- a/lib/config/presets/local/index.ts +++ b/lib/config/presets/local/index.ts @@ -26,6 +26,7 @@ const resolvers = { github, gitlab, local: null, + scmm: null, } satisfies Record; export function getPreset({ diff --git a/lib/constants/platforms.ts b/lib/constants/platforms.ts index d1ee815631..f91756f58c 100644 --- a/lib/constants/platforms.ts +++ b/lib/constants/platforms.ts @@ -7,7 +7,8 @@ export type PlatformId = | 'gitea' | 'github' | 'gitlab' - | 'local'; + | 'local' + | 'scmm'; export const GITEA_API_USING_HOST_TYPES = [ 'gitea', diff --git a/lib/modules/platform/api.ts b/lib/modules/platform/api.ts index 7d0ab19cee..514d2effac 100644 --- a/lib/modules/platform/api.ts +++ b/lib/modules/platform/api.ts @@ -8,6 +8,7 @@ import * as gitea from './gitea'; import * as github from './github'; import * as gitlab from './gitlab'; import * as local from './local'; +import * as scmm from './scmm'; import type { Platform } from './types'; const api = new Map(); @@ -22,3 +23,4 @@ api.set(gitea.id, gitea); api.set(github.id, github); api.set(gitlab.id, gitlab); api.set(local.id, local); +api.set(scmm.id, scmm); diff --git a/lib/modules/platform/scm.ts b/lib/modules/platform/scm.ts index 7adc03b7fa..f62617238a 100644 --- a/lib/modules/platform/scm.ts +++ b/lib/modules/platform/scm.ts @@ -17,6 +17,7 @@ platformScmImpls.set('gitea', DefaultGitScm); platformScmImpls.set('github', GithubScm); platformScmImpls.set('gitlab', DefaultGitScm); platformScmImpls.set('local', LocalFs); +platformScmImpls.set('scmm', DefaultGitScm); let _scm: PlatformScm | undefined; diff --git a/lib/modules/platform/scmm/index.spec.ts b/lib/modules/platform/scmm/index.spec.ts new file mode 100644 index 0000000000..3941a33a68 --- /dev/null +++ b/lib/modules/platform/scmm/index.spec.ts @@ -0,0 +1,411 @@ +import { mocked } from '../../../../test/util'; +import * as _git from '../../../util/git'; +import * as _hostRules from '../../../util/host-rules'; +import type { Pr } from '../types'; +import * as _util from '../util'; +import { mapPrFromScmToRenovate } from './mapper'; +import ScmClient from './scm-client'; +import type { + PrFilterByState, + PullRequest, + PullRequestCreateParams, + Repo, + User, +} from './types'; +import { + createPr, + findPr, + getBranchPr, + getPr, + getPrList, + getRepos, + initPlatform, + initRepo, + invalidatePrCache, + updatePr, +} from './index'; + +jest.mock('../../../util/host-rules'); +const hostRules: jest.Mocked = mocked(_hostRules); + +jest.mock('../../../util/git'); +const git: jest.Mocked = mocked(_git); + +jest.mock('../util'); +const util: jest.Mocked = mocked(_util); + +const endpoint = 'http://localhost:1337/scm/api/v2'; +const token = 'TEST_TOKEN'; + +const user: User = { + mail: 'test@user.de', + displayName: 'Test User', + username: 'testUser1337', +}; + +const repo: Repo = { + contact: 'test@test.com', + creationDate: '2023-08-02T10:48:24.762Z', + description: 'Default Repo', + lastModified: '2023-08-10T10:48:24.762Z', + namespace: 'default', + name: 'repo', + type: 'git', + archived: false, + exporting: false, + healthCheckRunning: false, + _links: { + protocol: [ + { name: 'http', href: 'http://localhost:8080/scm/default/repo' }, + ], + }, +}; + +const pullRequest: PullRequest = { + id: '1', + author: { displayName: 'Thomas Zerr', username: 'tzerr' }, + source: 'feature/test', + target: 'develop', + title: 'The PullRequest', + description: 'Another PullRequest', + creationDate: '2023-08-02T10:48:24.762Z', + status: 'OPEN', + labels: [], + tasks: { todo: 2, done: 4 }, + _links: {}, + _embedded: { + defaultConfig: { + mergeStrategy: 'SQUASH', + deleteBranchOnMerge: true, + }, + }, +}; + +const renovatePr: Pr = mapPrFromScmToRenovate(pullRequest); + +describe('modules/platform/scmm/index', () => { + beforeEach(() => { + jest.resetAllMocks(); + invalidatePrCache(); + }); + + describe(initPlatform, () => { + it('should throw error, because endpoint is not configured', async () => { + await expect(initPlatform({ token })).rejects.toThrow( + 'SCM-Manager endpoint not configured', + ); + }); + + it('should throw error, because token is not configured', async () => { + await expect(initPlatform({ endpoint })).rejects.toThrow( + 'SCM-Manager api token not configured', + ); + }); + + it('should init platform', async () => { + jest + .spyOn(ScmClient.prototype, 'getCurrentUser') + .mockResolvedValueOnce(user); + + expect(await initPlatform({ endpoint, token })).toEqual({ + endpoint, + gitAuthor: 'Test User ', + }); + }); + }); + + describe(initRepo, () => { + it('should init repo', async () => { + const repository = `${repo.namespace}/${repo.name}`; + const expectedFingerprint = 'expectedFingerprint'; + const expectedDefaultBranch = 'expectedDefaultBranch'; + + jest.spyOn(ScmClient.prototype, 'getRepo').mockResolvedValueOnce(repo); + jest + .spyOn(ScmClient.prototype, 'getDefaultBranch') + .mockResolvedValueOnce(expectedDefaultBranch); + + hostRules.find.mockReturnValueOnce({ username: user.username }); + git.initRepo.mockImplementationOnce(() => { + return Promise.resolve(); + }); + util.repoFingerprint.mockReturnValueOnce(expectedFingerprint); + + expect( + await initRepo({ repository: `${repo.namespace}/${repo.name}` }), + ).toEqual({ + defaultBranch: expectedDefaultBranch, + isFork: false, + repoFingerprint: expectedFingerprint, + }); + + expect(git.initRepo).toHaveBeenCalledWith({ + url: `http://${user.username}@localhost:8080/scm/default/repo`, + repository, + defaultBranch: expectedDefaultBranch, + }); + }); + }); + + describe(getRepos, () => { + it('should return all available repos', async () => { + jest + .spyOn(ScmClient.prototype, 'getAllRepos') + .mockResolvedValueOnce([ + repo, + { ...repo, namespace: 'other', name: 'repository' }, + { ...repo, namespace: 'other', name: 'mercurial', type: 'hg' }, + { ...repo, namespace: 'other', name: 'subversion', type: 'svn' }, + ]); + + expect(await getRepos()).toEqual(['default/repo', 'other/repository']); + }); + }); + + describe(getPrList, () => { + it('should return empty array, because no pr could be found', async () => { + jest + .spyOn(ScmClient.prototype, 'getAllRepoPrs') + .mockRejectedValue(new Error()); + + expect(await getPrList()).toIncludeAllMembers([]); + }); + + it('should return all prs of a repo', async () => { + const expectedResult: Pr[] = [ + { + sourceBranch: pullRequest.source, + createdAt: pullRequest.creationDate, + labels: pullRequest.labels, + number: parseInt(pullRequest.id), + state: pullRequest.status, + targetBranch: pullRequest.target, + title: pullRequest.title, + hasAssignees: false, + isDraft: false, + reviewers: [], + }, + ]; + + jest + .spyOn(ScmClient.prototype, 'getAllRepoPrs') + .mockResolvedValueOnce([pullRequest]); + + //Fetching from client + expect(await getPrList()).toIncludeAllMembers(expectedResult); + //Fetching from cache + expect(await getPrList()).toIncludeAllMembers(expectedResult); + }); + }); + + describe(findPr, () => { + it('search in pull request without explicitly setting the state as argument', async () => { + jest + .spyOn(ScmClient.prototype, 'getAllRepoPrs') + .mockResolvedValueOnce([pullRequest]); + + expect( + await findPr({ + branchName: pullRequest.source, + prTitle: pullRequest.title, + }), + ).toEqual(renovatePr); + }); + + it.each([ + [[], pullRequest.source, pullRequest.title, 'all', null], + [[pullRequest], 'invalid branchName', pullRequest.title, 'all', null], + [[pullRequest], pullRequest.source, 'invalid title', 'all', null], + [[pullRequest], pullRequest.source, null, 'all', renovatePr], + [[pullRequest], pullRequest.source, undefined, 'all', renovatePr], + [[pullRequest], pullRequest.source, pullRequest.title, 'all', renovatePr], + [ + [pullRequest], + pullRequest.source, + pullRequest.title, + 'open', + renovatePr, + ], + [[pullRequest], pullRequest.source, pullRequest.title, '!open', null], + [[pullRequest], pullRequest.source, pullRequest.title, 'closed', null], + ])( + 'search within %p for %p, %p, %p with result %p', + async ( + availablePullRequest: PullRequest[], + branchName: string, + prTitle: string | null | undefined, + state: string, + result: Pr | null, + ) => { + jest + .spyOn(ScmClient.prototype, 'getAllRepoPrs') + .mockResolvedValueOnce(availablePullRequest); + + expect( + await findPr({ + branchName, + prTitle, + state: state as PrFilterByState, + }), + ).toEqual(result); + }, + ); + }); + + describe(getBranchPr, () => { + it.each([ + [[], pullRequest.source, null], + [[pullRequest], 'invalid branchName', null], + [[pullRequest], pullRequest.source, renovatePr], + ])( + 'search within %p for %p with result %p', + async ( + availablePullRequest: PullRequest[], + branchName: string, + result: Pr | null, + ) => { + jest + .spyOn(ScmClient.prototype, 'getAllRepoPrs') + .mockResolvedValueOnce(availablePullRequest); + + expect(await getBranchPr(branchName)).toEqual(result); + }, + ); + }); + + describe(getPr, () => { + it('should return null, because pr was not found', async () => { + jest + .spyOn(ScmClient.prototype, 'getAllRepoPrs') + .mockResolvedValueOnce([]); + + jest + .spyOn(ScmClient.prototype, 'getRepoPr') + .mockRejectedValue(new Error('Not found')); + + expect(await getPr(1)).toBeNull(); + }); + + it.each([ + [[], pullRequest, 1, renovatePr], + [[pullRequest], pullRequest, 1, renovatePr], + ])( + 'search within %p for %p with result %p', + async ( + availablePullRequest: PullRequest[], + pullRequestById: PullRequest, + prId: number, + result: Pr | null, + ) => { + jest + .spyOn(ScmClient.prototype, 'getAllRepoPrs') + .mockResolvedValueOnce(availablePullRequest); + + jest + .spyOn(ScmClient.prototype, 'getRepoPr') + .mockResolvedValueOnce(pullRequestById); + + expect(await getPr(prId)).toEqual(result); + }, + ); + }); + + describe(createPr, () => { + it.each([ + [undefined, 'OPEN', false], + [false, 'OPEN', false], + [true, 'DRAFT', true], + ])( + 'it should create the pr with isDraft %p and state %p', + async ( + draftPR: boolean | undefined, + expectedState: string, + expectedIsDraft: boolean, + ) => { + jest + .spyOn(ScmClient.prototype, 'createPr') + .mockImplementationOnce( + (_repoPath: string, createParams: PullRequestCreateParams) => { + return Promise.resolve({ + id: '1337', + source: createParams.source, + target: createParams.target, + title: createParams.title, + description: createParams.description ?? '', + creationDate: '2023-01-01T13:37:00.000Z', + status: createParams.status ?? 'OPEN', + labels: [], + tasks: { todo: 0, done: 0 }, + _links: {}, + _embedded: { + defaultConfig: { + mergeStrategy: 'FAST_FORWARD_IF_POSSIBLE', + deleteBranchOnMerge: false, + }, + }, + }); + }, + ); + + expect( + await createPr({ + sourceBranch: 'feature/test', + targetBranch: 'develop', + prTitle: 'PR Title', + prBody: 'PR Body', + draftPR, + }), + ).toEqual({ + sourceBranch: 'feature/test', + targetBranch: 'develop', + title: 'PR Title', + createdAt: '2023-01-01T13:37:00.000Z', + hasAssignees: false, + isDraft: expectedIsDraft, + labels: [], + number: 1337, + reviewers: [], + state: expectedState, + }); + }, + ); + }); + + describe(updatePr, () => { + it.each([ + ['open', 'OPEN', 'prBody', 'prBody'], + ['closed', 'REJECTED', 'prBody', 'prBody'], + [undefined, undefined, 'prBody', 'prBody'], + ['open', 'OPEN', undefined, undefined], + ])( + 'it should update the pr with state %p and prBody %p', + async ( + actualState: string | undefined, + expectedState: string | undefined, + actualPrBody: string | undefined, + expectedPrBody: string | undefined, + ) => { + jest + .spyOn(ScmClient.prototype, 'updatePr') + .mockImplementationOnce(() => Promise.resolve()); + + await updatePr({ + number: 1, + prTitle: 'PR Title', + prBody: actualPrBody, + state: actualState as 'open' | 'closed' | undefined, + targetBranch: 'Target/Branch', + }); + + expect( + jest.spyOn(ScmClient.prototype, 'updatePr'), + ).toHaveBeenCalledWith('default/repo', 1, { + description: expectedPrBody, + status: expectedState, + target: 'Target/Branch', + title: 'PR Title', + }); + }, + ); + }); +}); diff --git a/lib/modules/platform/scmm/index.ts b/lib/modules/platform/scmm/index.ts new file mode 100644 index 0000000000..5da76f5f31 --- /dev/null +++ b/lib/modules/platform/scmm/index.ts @@ -0,0 +1,349 @@ +import { logger } from '../../../logger'; +import type { BranchStatus } from '../../../types'; +import * as git from '../../../util/git'; +import * as hostRules from '../../../util/host-rules'; +import { sanitize } from '../../../util/sanitize'; +import type { + BranchStatusConfig, + CreatePRConfig, + EnsureCommentConfig, + EnsureCommentRemovalConfigByContent, + EnsureCommentRemovalConfigByTopic, + EnsureIssueConfig, + FindPRConfig, + Issue, + MergePRConfig, + PlatformParams, + PlatformResult, + Pr, + RepoParams, + RepoResult, + UpdatePrConfig, +} from '../types'; +import { repoFingerprint } from '../util'; +import { smartTruncate } from '../utils/pr-body'; +import { mapPrFromScmToRenovate } from './mapper'; +import ScmClient from './scm-client'; +import { getRepoUrl, mapPrState, matchPrState, smartLinks } from './utils'; + +interface SCMMRepoConfig { + repository: string; + prList: Pr[] | null; + defaultBranch: string; +} + +export const id = 'scmm'; + +let config: SCMMRepoConfig = {} as any; +let scmmClient: ScmClient; + +export async function initPlatform({ + endpoint, + token, +}: PlatformParams): Promise { + if (!endpoint) { + throw new Error('SCM-Manager endpoint not configured'); + } + + if (!token) { + throw new Error('SCM-Manager api token not configured'); + } + + scmmClient = new ScmClient(endpoint, token); + + const me = await scmmClient.getCurrentUser(); + const gitAuthor = `${me.displayName} <${me.mail}>`; + const result = { endpoint, gitAuthor }; + + logger.info(`Plattform initialized ${JSON.stringify(result)}`); + + return result; +} + +export async function initRepo({ + repository, + gitUrl, +}: RepoParams): Promise { + const repo = await scmmClient.getRepo(repository); + const defaultBranch = await scmmClient.getDefaultBranch(repo); + const url = getRepoUrl( + repo, + gitUrl, + /* istanbul ignore next */ + hostRules.find({ hostType: id, url: scmmClient.getEndpoint() }).username ?? + '', + process.env.RENOVATE_TOKEN ?? '', + ); + + config = {} as any; + config.repository = repository; + config.defaultBranch = defaultBranch; + + await git.initRepo({ + ...config, + url, + }); + + // Reset cached resources + invalidatePrCache(); + + const result = { + defaultBranch: config.defaultBranch, + isFork: false, + repoFingerprint: repoFingerprint( + config.repository, + scmmClient.getEndpoint(), + ), + }; + + logger.info(`Repo initialized: ${JSON.stringify(result)}`); + + return result; +} + +export async function getRepos(): Promise { + const repos = (await scmmClient.getAllRepos()).filter( + (repo) => repo.type === 'git', + ); + const result = repos.map((repo) => `${repo.namespace}/${repo.name}`); + logger.info(`Discovered ${repos.length} repos`); + + return result; +} + +export async function getBranchPr(branchName: string): Promise { + return await findPr({ branchName, state: 'open' }); +} + +export async function findPr({ + branchName, + prTitle, + state = 'all', +}: FindPRConfig): Promise { + const inProgressPrs = await getPrList(); + const result = inProgressPrs.find( + (pr) => + branchName === pr.sourceBranch && + (!prTitle || prTitle === pr.title) && + matchPrState(pr, state), + ); + + if (result) { + logger.info(`Found PR ${JSON.stringify(result)}`); + return result; + } + + logger.debug( + `Could not find PR with source branch ${branchName} and title ${ + prTitle ?? '' + } and state ${state}`, + ); + + return null; +} + +export async function getPr(number: number): Promise { + const inProgressPrs = await getPrList(); + const cachedPr = inProgressPrs.find((pr) => pr.number === number); + + if (cachedPr) { + logger.info(`Returning from cached PRs, ${JSON.stringify(cachedPr)}`); + return cachedPr; + } + + try { + const result = await scmmClient.getRepoPr(config.repository, number); + logger.info(`Returning PR from API, ${JSON.stringify(result)}`); + return mapPrFromScmToRenovate(result); + } catch (error) { + logger.info(`Not found PR with id ${number}`); + return null; + } +} + +export async function getPrList(): Promise { + if (config.prList === null) { + try { + config.prList = (await scmmClient.getAllRepoPrs(config.repository)).map( + (pr) => mapPrFromScmToRenovate(pr), + ); + } catch (error) { + logger.error(error); + } + } + + return config.prList ?? []; +} + +export async function createPr({ + sourceBranch, + targetBranch, + prTitle, + prBody, + draftPR, +}: CreatePRConfig): Promise { + const createdPr = await scmmClient.createPr(config.repository, { + source: sourceBranch, + target: targetBranch, + title: prTitle, + description: sanitize(prBody), + status: draftPR ? 'DRAFT' : 'OPEN', + }); + + logger.info( + `Pr Created with title '${createdPr.title}' from source '${createdPr.source}' to target '${createdPr.target}'`, + ); + logger.debug(`Pr Created ${JSON.stringify(createdPr)}`); + + return mapPrFromScmToRenovate(createdPr); +} + +export async function updatePr({ + number, + prTitle, + prBody, + state, + targetBranch, +}: UpdatePrConfig): Promise { + await scmmClient.updatePr(config.repository, number, { + title: prTitle, + description: sanitize(prBody) ?? undefined, + target: targetBranch, + status: mapPrState(state), + }); + + logger.info(`Updated Pr #${number} with title ${prTitle}`); +} + +/* istanbul ignore next */ +export function mergePr(config: MergePRConfig): Promise { + logger.debug('NO-OP mergePr'); + return Promise.resolve(false); +} + +/* istanbul ignore next */ +export function getBranchStatus( + branchName: string, + internalChecksAsSuccess: boolean, +): Promise { + logger.debug('NO-OP getBranchStatus'); + return Promise.resolve('red'); +} + +/* istanbul ignore next */ +export function setBranchStatus( + branchStatusConfig: BranchStatusConfig, +): Promise { + logger.debug('NO-OP setBranchStatus'); + return Promise.resolve(); +} + +/* istanbul ignore next */ +export function getBranchStatusCheck( + branchName: string, + context: string | null | undefined, +): Promise { + logger.debug('NO-OP setBranchStatus'); + return Promise.resolve(null); +} + +/* istanbul ignore next */ +export function addReviewers( + number: number, + reviewers: string[], +): Promise { + logger.debug('NO-OP addReviewers'); + return Promise.resolve(); +} + +/* istanbul ignore next */ +export function addAssignees( + number: number, + assignees: string[], +): Promise { + logger.debug('NO-OP addAssignees'); + return Promise.resolve(); +} + +/* istanbul ignore next */ +export function deleteLabel(number: number, label: string): Promise { + logger.debug('NO-OP deleteLabel'); + return Promise.resolve(); +} + +/* istanbul ignore next */ +export function getIssueList(): Promise { + logger.debug('NO-OP getIssueList'); + return Promise.resolve([]); +} + +/* istanbul ignore next */ +export function findIssue(title: string): Promise { + logger.debug('NO-OP findIssue'); + return Promise.resolve(null); +} + +/* istanbul ignore next */ +export function ensureIssue( + config: EnsureIssueConfig, +): Promise<'updated' | 'created' | null> { + logger.debug('NO-OP ensureIssue'); + return Promise.resolve(null); +} + +/* istanbul ignore next */ +export function ensureIssueClosing(title: string): Promise { + logger.debug('NO-OP ensureIssueClosing'); + return Promise.resolve(); +} + +/* istanbul ignore next */ +export function ensureComment(config: EnsureCommentConfig): Promise { + logger.debug('NO-OP ensureComment'); + return Promise.resolve(false); +} + +/* istanbul ignore next */ +export function ensureCommentRemoval( + ensureCommentRemoval: + | EnsureCommentRemovalConfigByTopic + | EnsureCommentRemovalConfigByContent, +): Promise { + logger.debug('NO-OP ensureCommentRemoval'); + return Promise.resolve(); +} + +/* istanbul ignore next */ +export function massageMarkdown(prBody: string): string { + return smartTruncate(smartLinks(prBody), 10000); +} + +/* istanbul ignore next */ +export function getRepoForceRebase(): Promise { + return Promise.resolve(false); +} + +/* istanbul ignore next */ +export function getRawFile( + fileName: string, + repoName?: string, + branchOrTag?: string, +): Promise { + logger.debug('NO-OP getRawFile'); + return Promise.resolve(null); +} + +/* istanbul ignore next */ +export function getJsonFile( + fileName: string, + repoName?: string, + branchOrTag?: string, +): Promise { + logger.debug('NO-OP getJsonFile'); + return Promise.resolve(undefined); +} + +/* istanbul ignore next */ +export function invalidatePrCache(): void { + config.prList = null; +} diff --git a/lib/modules/platform/scmm/mapper.spec.ts b/lib/modules/platform/scmm/mapper.spec.ts new file mode 100644 index 0000000000..925e826ea9 --- /dev/null +++ b/lib/modules/platform/scmm/mapper.spec.ts @@ -0,0 +1,44 @@ +import { mapPrFromScmToRenovate } from './mapper'; +import type { PullRequest as SCMPullRequest } from './types'; + +describe('modules/platform/scmm/mapper', () => { + it('should correctly map the scm type of a pr to the renovate pr type', () => { + const scmPr: SCMPullRequest = { + source: 'feat/new', + target: 'develop', + creationDate: '2024-12-24T18:21Z', + closeDate: '2024-12-25T18:21Z', + reviewer: [ + { id: 'id', displayName: 'user', mail: 'user@user.de', approved: true }, + ], + labels: ['label'], + id: '1', + status: 'OPEN', + title: 'Merge please', + description: 'Description', + tasks: { todo: 0, done: 0 }, + _links: {}, + _embedded: { + defaultConfig: { + mergeStrategy: 'SQUASH', + deleteBranchOnMerge: true, + }, + }, + }; + + const result = mapPrFromScmToRenovate(scmPr); + expect(result).toEqual({ + sourceBranch: 'feat/new', + targetBranch: 'develop', + createdAt: '2024-12-24T18:21Z', + closedAt: '2024-12-25T18:21Z', + hasAssignees: true, + labels: ['label'], + number: 1, + reviewers: ['user'], + state: 'OPEN', + title: 'Merge please', + isDraft: false, + }); + }); +}); diff --git a/lib/modules/platform/scmm/mapper.ts b/lib/modules/platform/scmm/mapper.ts new file mode 100644 index 0000000000..a6adceb0ca --- /dev/null +++ b/lib/modules/platform/scmm/mapper.ts @@ -0,0 +1,20 @@ +import type { Pr as RenovatePr } from '../types'; +import type { PullRequest as SCMPullRequest } from './types'; + +export function mapPrFromScmToRenovate(pr: SCMPullRequest): RenovatePr { + return { + sourceBranch: pr.source, + targetBranch: pr.target, + createdAt: pr.creationDate, + closedAt: pr.closeDate, + hasAssignees: pr.reviewer !== undefined && pr.reviewer.length > 0, + labels: pr.labels, + number: parseInt(pr.id), + reviewers: pr.reviewer + ? pr.reviewer.map((review) => review.displayName) + : [], + state: pr.status, + title: pr.title, + isDraft: pr.status === 'DRAFT', + }; +} diff --git a/lib/modules/platform/scmm/readme.md b/lib/modules/platform/scmm/readme.md new file mode 100644 index 0000000000..ba8617e77a --- /dev/null +++ b/lib/modules/platform/scmm/readme.md @@ -0,0 +1,13 @@ +# SCM-Manager + +Renovate supports [SCM-Manager](https://scm-manager.org). + +## Authentication + +First, create an API Key for your technical Renovate user in SCM-Manager. +The technical user should be configured properly with name and email address. +Then let Renovate use your API Key by setting the `RENOVATE_TOKEN` environment variable with your key. + +You must set `platform=scmm` in your Renovate config file. + +The technical user needs at least the permissions to read your repository read and create pull request. This can be achieved by granting the permission role "OWNER" to your technical Renovate user. diff --git a/lib/modules/platform/scmm/scm-client.spec.ts b/lib/modules/platform/scmm/scm-client.spec.ts new file mode 100644 index 0000000000..7e61edbd97 --- /dev/null +++ b/lib/modules/platform/scmm/scm-client.spec.ts @@ -0,0 +1,332 @@ +import * as httpMock from '../../../../test/http-mock'; +import ScmClient from './scm-client'; +import type { + PullRequest, + PullRequestCreateParams, + PullRequestUpdateParams, + Repo, + User, +} from './types'; + +describe('modules/platform/scmm/scm-client', () => { + const endpoint = 'http://localhost:8080/scm/api/v2'; + const token = 'validApiToken'; + + const scmClient = new ScmClient(endpoint, token); + + const repo: Repo = { + contact: 'test@test.com', + creationDate: '2023-08-02T10:48:24.762Z', + description: 'Default Repo', + lastModified: '2023-08-10T10:48:24.762Z', + namespace: 'default', + name: 'repo', + type: 'git', + archived: false, + exporting: false, + healthCheckRunning: false, + _links: { + protocol: [ + { name: 'http', href: 'http://localhost:8080/scm/default/repo' }, + ], + defaultBranch: { + href: `${endpoint}/config/git/default/repo/default-branch`, + }, + }, + }; + + const pullRequest: PullRequest = { + id: '1337', + author: { displayName: 'Thomas Zerr', username: 'tzerr' }, + source: 'feature/test', + target: 'develop', + title: 'The PullRequest', + description: 'Another PullRequest', + creationDate: '2023-08-02T10:48:24.762Z', + status: 'OPEN', + labels: [], + tasks: { todo: 2, done: 4 }, + _links: {}, + _embedded: { + defaultConfig: { + mergeStrategy: 'SQUASH', + deleteBranchOnMerge: true, + }, + }, + }; + + describe(scmClient.getEndpoint, () => { + it('should return the endpoint', () => { + expect(scmClient.getEndpoint()).toEqual(endpoint); + }); + }); + + describe(scmClient.getCurrentUser, () => { + it('should return the current user', async () => { + const expectedUser: User = { + mail: 'test@test.de', + displayName: 'Test User', + username: 'test', + }; + + httpMock.scope(endpoint).get('/me').reply(200, expectedUser); + + expect(await scmClient.getCurrentUser()).toEqual(expectedUser); + }); + + it.each([[401, 500]])( + 'should throw %p response', + async (response: number) => { + httpMock.scope(endpoint).get('/me').reply(response); + await expect(scmClient.getCurrentUser()).rejects.toThrow(); + }, + ); + }); + + describe(scmClient.getRepo, () => { + it('should return the repo', async () => { + httpMock + .scope(endpoint) + .get(`/repositories/${repo.namespace}/${repo.name}`) + .reply(200, repo); + + expect(await scmClient.getRepo(`${repo.namespace}/${repo.name}`)).toEqual( + repo, + ); + }); + + it.each([[401], [403], [404], [500]])( + 'should throw %p response', + async (response: number) => { + httpMock + .scope(endpoint) + .get(`/repositories/${repo.namespace}/${repo.name}`) + .reply(response); + + await expect( + scmClient.getRepo(`${repo.namespace}/${repo.name}`), + ).rejects.toThrow(); + }, + ); + }); + + describe(scmClient.getAllRepos, () => { + it('should return all repos', async () => { + httpMock + .scope(endpoint) + .get('/repositories?pageSize=1000000') + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { repositories: [repo] }, + }); + + expect(await scmClient.getAllRepos()).toEqual([repo]); + }); + + it.each([[401], [403], [500]])( + 'should throw %p response', + async (response: number) => { + httpMock + .scope(endpoint) + .get('/repositories?pageSize=1000000') + .reply(response); + + await expect(scmClient.getAllRepos()).rejects.toThrow(); + }, + ); + }); + + describe(scmClient.getDefaultBranch, () => { + it('should return the default branch', async () => { + httpMock + .scope(endpoint) + .get('/config/git/default/repo/default-branch') + .reply(200, { + defaultBranch: 'develop', + }); + + expect(await scmClient.getDefaultBranch(repo)).toBe('develop'); + }); + + it.each([[401], [403], [404], [500]])( + 'should throw %p response', + async (response: number) => { + httpMock + .scope(endpoint) + .get('/config/git/default/repo/default-branch') + .reply(response); + + await expect(scmClient.getDefaultBranch(repo)).rejects.toThrow(); + }, + ); + }); + + describe(scmClient.getAllRepoPrs, () => { + it('should return all repo prs', async () => { + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + pullRequests: [pullRequest], + }, + }); + + expect( + await scmClient.getAllRepoPrs(`${repo.namespace}/${repo.name}`), + ).toEqual([pullRequest]); + }); + + it.each([[401], [403], [404], [500]])( + 'should throw %p response', + async (response: number) => { + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(response); + + await expect( + scmClient.getAllRepoPrs(`${repo.namespace}/${repo.name}`), + ).rejects.toThrow(); + }, + ); + }); + + describe(scmClient.getRepoPr, () => { + it('should return the repo pr', async () => { + httpMock + .scope(endpoint) + .get(`/pull-requests/${repo.namespace}/${repo.name}/${pullRequest.id}`) + .reply(200, pullRequest); + + expect( + await scmClient.getRepoPr(`${repo.namespace}/${repo.name}`, 1337), + ).toEqual(pullRequest); + }); + + it.each([[401], [403], [404], [500]])( + 'should throw %p response', + async (response: number) => { + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}/${pullRequest.id}`, + ) + .reply(response); + + await expect( + scmClient.getRepoPr(`${repo.namespace}/${repo.name}`, 1337), + ).rejects.toThrow(); + }, + ); + }); + + describe(scmClient.createPr, () => { + it('should create pr for a repo', async () => { + const expectedCreateParams: PullRequestCreateParams = { + source: 'feature/test', + target: 'develop', + title: 'Test Title', + description: 'PR description', + assignees: ['Test assignee'], + status: 'OPEN', + }; + + const expectedPrId = 1337; + + httpMock + .scope(endpoint) + .post(`/pull-requests/${repo.namespace}/${repo.name}`) + .reply(201, undefined, { + location: `${endpoint}/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`, + }); + + httpMock + .scope(endpoint) + .get(`/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`) + .reply(200, pullRequest); + + expect( + await scmClient.createPr( + `${repo.namespace}/${repo.name}`, + expectedCreateParams, + ), + ).toEqual(pullRequest); + }); + + it.each([[400], [401], [403], [404], [500]])( + 'should throw %p response', + async (response: number) => { + httpMock + .scope(endpoint) + .post(`/pull-requests/${repo.namespace}/${repo.name}`) + .reply(response); + + await expect( + scmClient.createPr(`${repo.namespace}/${repo.name}`, { + source: 'feature/test', + target: 'develop', + title: 'Test Title', + description: 'PR description', + assignees: ['Test assignee'], + status: 'OPEN', + }), + ).rejects.toThrow(); + }, + ); + }); + + describe(scmClient.updatePr, () => { + it('should update pr for a repo', async () => { + const expectedUpdateParams: PullRequestUpdateParams = { + title: 'Test Title', + description: 'PR description', + assignees: ['Test assignee'], + status: 'OPEN', + }; + + const expectedPrId = 1337; + + httpMock + .scope(endpoint) + .put(`/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`) + .reply(204); + + await expect( + scmClient.updatePr( + `${repo.namespace}/${repo.name}`, + expectedPrId, + expectedUpdateParams, + ), + ).resolves.not.toThrow(); + }); + + it.each([[400], [401], [403], [404], [500]])( + 'should throw %p response', + async (response: number) => { + const expectedPrId = 1337; + + httpMock + .scope(endpoint) + .put(`/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`) + .reply(response); + + await expect( + scmClient.updatePr(`${repo.namespace}/${repo.name}`, expectedPrId, { + title: 'Test Title', + description: 'PR description', + assignees: ['Test assignee'], + status: 'OPEN', + }), + ).rejects.toThrow(); + }, + ); + }); +}); diff --git a/lib/modules/platform/scmm/scm-client.ts b/lib/modules/platform/scmm/scm-client.ts new file mode 100644 index 0000000000..775d9efd9f --- /dev/null +++ b/lib/modules/platform/scmm/scm-client.ts @@ -0,0 +1,130 @@ +import type { AxiosInstance } from 'axios'; +import axios from 'axios'; +import type { + Link, + Page, + PullRequest, + PullRequestCreateParams, + PullRequestPage, + PullRequestUpdateParams, + Repo, + RepoPage, + User, +} from './types'; + +const URLS = { + ME: 'me', + ALLREPOS: 'repositories', + REPO: (repoPath: string) => `repositories/${repoPath}`, + PULLREQUESTS: (repoPath: string) => `pull-requests/${repoPath}`, + PULLREQUESTBYID: (repoPath: string, id: number) => + `pull-requests/${repoPath}/${id}`, +}; + +const CONTENT_TYPES = { + PULLREQUESTS: 'application/vnd.scmm-pullrequest+json;v=2', +}; + +export default class ScmClient { + private httpClient: AxiosInstance; + + constructor(endpoint: string, token: string) { + this.httpClient = axios.create({ + baseURL: endpoint, + headers: { + Authorization: `Bearer ${token}`, + Accept: '*', + 'X-Scm-Client': 'WUI', + }, + }); + } + + public getEndpoint(): string { + /* istanbul ignore next */ + if (!this.httpClient.defaults.baseURL) { + throw new Error('BaseURL is not defined'); + } + + return this.httpClient.defaults.baseURL; + } + + public async getCurrentUser(): Promise { + const response = await this.httpClient.get(URLS.ME); + return response.data; + } + + public async getRepo(repoPath: string): Promise { + const response = await this.httpClient.get(URLS.REPO(repoPath)); + return response.data; + } + + public async getAllRepos(): Promise { + const response = await this.httpClient.get>(URLS.ALLREPOS, { + params: { pageSize: 1000000 }, + }); + + return response.data._embedded.repositories; + } + + public async getDefaultBranch(repo: Repo): Promise { + const defaultBranchUrl = repo._links['defaultBranch'] as Link; + const response = await this.httpClient.get<{ defaultBranch: string }>( + defaultBranchUrl.href, + { baseURL: undefined }, + ); + + return response.data.defaultBranch; + } + + public async getAllRepoPrs(repoPath: string): Promise { + const response = await this.httpClient.get>( + URLS.PULLREQUESTS(repoPath), + { + params: { status: 'ALL', pageSize: 1000000 }, + }, + ); + return response.data._embedded.pullRequests; + } + + public async getRepoPr(repoPath: string, id: number): Promise { + const response = await this.httpClient.get( + URLS.PULLREQUESTBYID(repoPath, id), + ); + + return response.data; + } + + public async createPr( + repoPath: string, + params: PullRequestCreateParams, + ): Promise { + const createPrResponse = await this.httpClient.post( + URLS.PULLREQUESTS(repoPath), + params, + { + headers: { + 'Content-Type': CONTENT_TYPES.PULLREQUESTS, + }, + }, + ); + + const getCreatedPrResponse = await this.httpClient.get( + createPrResponse.headers.location, + { baseURL: undefined }, + ); + + return getCreatedPrResponse.data; + } + + public async updatePr( + repoPath: string, + id: number, + params: PullRequestUpdateParams, + ): Promise { + await this.httpClient.put(URLS.PULLREQUESTBYID(repoPath, id), params, { + headers: { + 'Content-Type': CONTENT_TYPES.PULLREQUESTS, + }, + }); + } +} diff --git a/lib/modules/platform/scmm/types.ts b/lib/modules/platform/scmm/types.ts new file mode 100644 index 0000000000..3c5665fac7 --- /dev/null +++ b/lib/modules/platform/scmm/types.ts @@ -0,0 +1,144 @@ +export type Page = { + page: number; + pageTotal: number; + _embedded: T; +}; + +export type Links = { + [link: string]: Link | Link[] | undefined; +}; + +export type Link = { + href: string; + name?: string; + templated?: boolean; +}; + +export type PullRequestPage = { + pullRequests: PullRequest[]; +}; + +export interface PullRequestCreateParams extends PullRequestUpdateParams { + source: string; + target: string; +} + +export interface PullRequestUpdateParams { + title: string; + description?: string; + assignees?: string[]; + status?: PRState; + target?: string; +} + +export interface PullRequest { + id: string; + author?: User; + reviser?: Reviser; + closeDate?: string; + source: string; + target: string; + title: string; + description: string; + creationDate: string; + lastModified?: string; + status: PRState; + reviewer?: Reviewer[]; + labels: string[]; + tasks: Tasks; + _links: Links; + _embedded: { + defaultConfig: { + mergeStrategy: PRMergeMethod; + deleteBranchOnMerge: boolean; + }; + }; +} + +export interface User { + mail?: string; + displayName: string; + username: string; +} + +export interface Reviser { + id?: string; + displayName?: string; +} + +export type PRState = 'DRAFT' | 'OPEN' | 'REJECTED' | 'MERGED'; + +export interface Reviewer { + id: string; + displayName: string; + mail?: string; + approved: boolean; +} + +export interface Tasks { + todo: number; + done: number; +} + +export type PRMergeMethod = + | 'MERGE_COMMIT' + | 'REBASE' + | 'FAST_FORWARD_IF_POSSIBLE' + | 'SQUASH'; + +export type CommitStatusType = + | 'pending' + | 'success' + | 'error' + | 'failure' + | 'warning' + | 'unknown'; + +export interface RepoPage { + repositories: Repo[]; +} + +export interface Repo { + contact: string; + creationDate: string; + description: string; + lastModified?: string; + namespace: string; + name: string; + type: RepoType; + archived: boolean; + exporting: boolean; + healthCheckRunning: boolean; + _links: Links; +} + +export type RepoType = 'git' | 'svn' | 'hg'; + +export interface Comment { + id: number; + body: string; +} + +export interface Label { + id: number; + name: string; + description: string; + color: string; +} + +export interface Branch { + name: string; + commit: Commit; +} + +export interface Commit { + id: string; + author: User; +} + +export interface CommitStatus { + id: number; + description: string; +} + +export type PrFilterByState = 'open' | 'closed' | '!open' | 'all'; diff --git a/lib/modules/platform/scmm/utils.spec.ts b/lib/modules/platform/scmm/utils.spec.ts new file mode 100644 index 0000000000..34fec84fc7 --- /dev/null +++ b/lib/modules/platform/scmm/utils.spec.ts @@ -0,0 +1,175 @@ +import type { MergeStrategy } from '../../../config/types'; +import type { GitUrlOption, Pr } from '../types'; +import type { PrFilterByState, Repo } from './types'; +import { getMergeMethod, getRepoUrl, matchPrState, smartLinks } from './utils'; + +describe('modules/platform/scmm/utils', () => { + describe(getMergeMethod, () => { + it.each([ + [undefined, null], + ['auto', null], + ['fast-forward', 'FAST_FORWARD_IF_POSSIBLE'], + ['merge-commit', 'MERGE_COMMIT'], + ['rebase', 'REBASE'], + ['squash', 'SQUASH'], + ])( + 'map merge strategy %p on pr merge method %p', + (strategy: string | undefined, method: string | null) => { + expect(getMergeMethod(strategy as MergeStrategy)).toEqual(method); + }, + ); + }); + + describe(smartLinks, () => { + it.each([ + ['', ''], + ['](../pull/', '](pulls/'], + ])('adjust %p to smart link %p', (body: string, result: string) => { + expect(smartLinks(body)).toEqual(result); + }); + }); + + describe(matchPrState, () => { + const defaultPr: Pr = { + sourceBranch: 'feature/test', + createdAt: '2023-08-02T10:48:24.762Z', + number: 1, + state: '', + title: 'Feature Test PR', + isDraft: false, + }; + + it.each([ + [{ ...defaultPr, state: 'OPEN' }, 'all', true], + [{ ...defaultPr, state: 'DRAFT' }, 'all', true], + [{ ...defaultPr, state: 'MERGED' }, 'all', true], + [{ ...defaultPr, state: 'REJECTED' }, 'all', true], + [{ ...defaultPr, state: 'OPEN' }, 'open', true], + [{ ...defaultPr, state: 'DRAFT' }, 'open', true], + [{ ...defaultPr, state: 'MERGED' }, 'open', false], + [{ ...defaultPr, state: 'REJECTED' }, 'open', false], + [{ ...defaultPr, state: 'OPEN' }, '!open', false], + [{ ...defaultPr, state: 'DRAFT' }, '!open', false], + [{ ...defaultPr, state: 'MERGED' }, '!open', true], + [{ ...defaultPr, state: 'REJECTED' }, '!open', true], + [{ ...defaultPr, state: 'OPEN' }, 'closed', false], + [{ ...defaultPr, state: 'DRAFT' }, 'closed', false], + [{ ...defaultPr, state: 'MERGED' }, 'closed', true], + [{ ...defaultPr, state: 'REJECTED' }, 'closed', true], + ])( + 'match scm pr %p state to pr filter by state %p', + (pr: Pr, state: string, result: boolean) => { + expect(matchPrState(pr, state as PrFilterByState)).toEqual(result); + }, + ); + }); + + describe(getRepoUrl, () => { + const repo: Repo = { + contact: 'test@test.com', + creationDate: '2023-08-02T10:48:24.762Z', + description: 'Default Repo', + lastModified: '2023-08-10T10:48:24.762Z', + namespace: 'default', + name: 'repo', + type: 'git', + archived: false, + exporting: false, + healthCheckRunning: false, + _links: {}, + }; + + const username = 'tzerr'; + const password = 'password'; + const gitHttpEndpoint = 'http://localhost:8081/scm/repo/default/repo'; + const gitSshEndpoint = 'ssh://localhost:2222/scm/repo/default/repo'; + + it.each([['ssh'], ['default'], ['endpoint'], [undefined]])( + 'should throw error for option %p, because protocol links are missing', + (gitUrl: string | undefined) => { + expect(() => + getRepoUrl(repo, gitUrl as GitUrlOption, username, password), + ).toThrow('MISSING_PROTOCOL_LINKS'); + }, + ); + + it('should throw error because of missing ssh link', () => { + expect(() => + getRepoUrl( + { + ...repo, + _links: { protocol: [{ name: 'http', href: gitHttpEndpoint }] }, + }, + 'ssh', + username, + password, + ), + ).toThrow('MISSING_SSH_LINK'); + }); + + it('should use the provided ssh link', () => { + expect( + getRepoUrl( + { + ...repo, + _links: { protocol: [{ name: 'ssh', href: gitSshEndpoint }] }, + }, + 'ssh', + username, + password, + ), + ).toEqual(gitSshEndpoint); + }); + + it.each([['endpoint'], ['default'], [undefined]])( + 'should throw error because of missing http link, for option %p', + (gitUrl: string | undefined) => { + expect(() => + getRepoUrl( + { + ...repo, + _links: { protocol: [{ name: 'ssh', href: gitSshEndpoint }] }, + }, + gitUrl as GitUrlOption | undefined, + username, + password, + ), + ).toThrow('MISSING_HTTP_LINK'); + }, + ); + + it.each([['endpoint'], ['default'], [undefined]])( + 'should throw error because of malformed http link, with option %p', + (gitUrl: string | undefined) => { + expect(() => + getRepoUrl( + { + ...repo, + _links: { protocol: [{ name: 'http', href: 'invalid url' }] }, + }, + gitUrl as GitUrlOption | undefined, + username, + password, + ), + ).toThrow('MALFORMED_HTTP_LINK'); + }, + ); + + it.each([['endpoint'], ['default'], [undefined]])( + 'should provide the http link with username, for option %p', + (gitUrl: string | undefined) => { + expect( + getRepoUrl( + { + ...repo, + _links: { protocol: [{ name: 'http', href: gitHttpEndpoint }] }, + }, + gitUrl as GitUrlOption | undefined, + username, + password, + ), + ).toBe('http://tzerr:password@localhost:8081/scm/repo/default/repo'); + }, + ); + }); +}); diff --git a/lib/modules/platform/scmm/utils.ts b/lib/modules/platform/scmm/utils.ts new file mode 100644 index 0000000000..df94fa872e --- /dev/null +++ b/lib/modules/platform/scmm/utils.ts @@ -0,0 +1,101 @@ +import type { MergeStrategy } from '../../../config/types'; +import { logger } from '../../../logger'; +import { regEx } from '../../../util/regex'; +import { parseUrl } from '../../../util/url'; +import type { GitUrlOption, Pr } from '../types'; +import type { Link, PRMergeMethod, PrFilterByState, Repo } from './types'; + +export function mapPrState( + state: 'open' | 'closed' | undefined, +): 'OPEN' | 'REJECTED' | undefined { + switch (state) { + case 'open': + return 'OPEN'; + case 'closed': + return 'REJECTED'; + default: + return undefined; + } +} + +export function matchPrState(pr: Pr, state: PrFilterByState): boolean { + if (state === 'all') { + return true; + } + + if (state === 'open' && (pr.state === 'OPEN' || pr.state === 'DRAFT')) { + return true; + } + + if (state === '!open' && (pr.state === 'MERGED' || pr.state === 'REJECTED')) { + return true; + } + + if ( + state === 'closed' && + (pr.state === 'MERGED' || pr.state === 'REJECTED') + ) { + return true; + } + + return false; +} + +export function smartLinks(body: string): string { + return body.replace(regEx(/\]\(\.\.\/pull\//g), '](pulls/'); +} + +export function getRepoUrl( + repo: Repo, + gitUrl: GitUrlOption | undefined, + username: string, + password: string, +): string { + const protocolLinks = repo._links.protocol as Link[] | undefined; + if (!protocolLinks) { + throw new Error('MISSING_PROTOCOL_LINKS'); + } + + if (gitUrl === 'ssh') { + const sshUrl = protocolLinks.find((l) => l.name === 'ssh')?.href; + if (!sshUrl) { + throw new Error('MISSING_SSH_LINKS'); + } + + logger.debug(`Using SSH URL: ${sshUrl}`); + return sshUrl; + } + + const httpUrl = protocolLinks.find((l) => l.name === 'http')?.href; + if (!httpUrl) { + throw new Error('MISSING_HTTP_LINK'); + } + + logger.debug(`Using HTTP URL: ${httpUrl}`); + + const repoUrl = parseUrl(httpUrl); + if (!repoUrl) { + throw new Error('MALFORMED_HTTP_LINK'); + } + + repoUrl.username = username; + repoUrl.password = password; + return repoUrl.toString(); +} + +export function getMergeMethod( + strategy: MergeStrategy | undefined, +): PRMergeMethod | null { + switch (strategy) { + case 'fast-forward': + return 'FAST_FORWARD_IF_POSSIBLE'; + case 'merge-commit': + return 'MERGE_COMMIT'; + case 'rebase': + return 'REBASE'; + case 'squash': + return 'SQUASH'; + default: + return null; + } +} diff --git a/package.json b/package.json index 41d638da99..00ecb1ed2f 100644 --- a/package.json +++ b/package.json @@ -176,6 +176,7 @@ "aggregate-error": "3.1.0", "auth-header": "1.0.0", "aws4": "1.12.0", + "axios": "1.6.5", "azure-devops-node-api": "12.3.0", "bunyan": "1.8.15", "cacache": "18.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ee46cb293..fc7c5d759f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: aws4: specifier: 1.12.0 version: 1.12.0 + axios: + specifier: 1.6.5 + version: 1.6.5 azure-devops-node-api: specifier: 12.3.0 version: 12.3.0 @@ -4526,6 +4529,10 @@ packages: safer-buffer: 2.1.2 dev: false + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + /auth-header@1.0.0: resolution: {integrity: sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA==} dev: false @@ -4546,6 +4553,16 @@ packages: resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} dev: false + /axios@1.6.5: + resolution: {integrity: sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==} + dependencies: + follow-redirects: 1.15.5 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /azure-devops-node-api@12.3.0: resolution: {integrity: sha512-5HDhBFIXJxiFhcJ+A3hN87gwo92PrDNLJvcvRHjr+p7AsuUSF64yQU+M6wcBDczkIDVV7m+MrraKQ2tqUxSbCA==} dependencies: @@ -4973,6 +4990,13 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + /commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -5257,6 +5281,11 @@ packages: has-property-descriptors: 1.0.1 object-keys: 1.1.1 + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + /deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} @@ -6098,6 +6127,16 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /follow-redirects@1.15.5: + resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -6118,6 +6157,15 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} dependencies: @@ -8134,6 +8182,18 @@ packages: braces: 3.0.2 picomatch: 2.3.1 + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + /mime@4.0.1: resolution: {integrity: sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==} engines: {node: '>=16'} @@ -9103,6 +9163,10 @@ packages: resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} dev: false + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: diff --git a/readme.md b/readme.md index 2d08611e81..2e7bfd953c 100644 --- a/readme.md +++ b/readme.md @@ -40,6 +40,7 @@ Renovate works on these platforms: - [AWS CodeCommit](https://docs.renovatebot.com/modules/platform/codecommit/) - [Gitea and Forgejo](https://docs.renovatebot.com/modules/platform/gitea/) - [Gerrit (experimental)](https://docs.renovatebot.com/modules/platform/gerrit/) +- [SCM-Manager](https://scm-manager.org/) ## Who Uses Renovate? From f0a024e2ffc8937614ea6f1e77b213060785a983 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 29 Jan 2024 09:08:07 +0100 Subject: [PATCH 02/61] Reverted numbering change in markdown docs --- docs/usage/getting-started/use-cases.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage/getting-started/use-cases.md b/docs/usage/getting-started/use-cases.md index f863a72435..ffcdd260bc 100644 --- a/docs/usage/getting-started/use-cases.md +++ b/docs/usage/getting-started/use-cases.md @@ -22,8 +22,8 @@ Example package files include: Renovate: 1. Scans your repositories to find package files and their dependencies -2. Checks if any newer versions exist -3. Raises Pull Requests for available updates +1. Checks if any newer versions exist +1. Raises Pull Requests for available updates The Pull Requests patch the package files directly, and include changelogs for the newer versions (if they are available). From 01b68fb7ee0462e9a28d3577d8119756d309e8f2 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 29 Jan 2024 09:32:33 +0100 Subject: [PATCH 03/61] Changed from type to interface, if possible --- lib/modules/platform/scmm/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/modules/platform/scmm/types.ts b/lib/modules/platform/scmm/types.ts index 3c5665fac7..63bf09fdc3 100644 --- a/lib/modules/platform/scmm/types.ts +++ b/lib/modules/platform/scmm/types.ts @@ -4,17 +4,17 @@ export type Page = { _embedded: T; }; -export type Links = { +export interface Links { [link: string]: Link | Link[] | undefined; }; -export type Link = { +export interface Link { href: string; name?: string; templated?: boolean; }; -export type PullRequestPage = { +export interface PullRequestPage { pullRequests: PullRequest[]; }; From acb5d94c321b572a4b6e27a2e27559fa5210c33d Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 29 Jan 2024 12:58:11 +0100 Subject: [PATCH 04/61] Removed axios dependency and using the http layer of renovate instead --- lib/modules/platform/scmm/scm-client.ts | 102 ++++++++++++------------ lib/modules/platform/scmm/types.ts | 6 +- package.json | 1 - pnpm-lock.yaml | 64 --------------- 4 files changed, 55 insertions(+), 118 deletions(-) diff --git a/lib/modules/platform/scmm/scm-client.ts b/lib/modules/platform/scmm/scm-client.ts index 775d9efd9f..3883af220c 100644 --- a/lib/modules/platform/scmm/scm-client.ts +++ b/lib/modules/platform/scmm/scm-client.ts @@ -1,5 +1,6 @@ -import type { AxiosInstance } from 'axios'; -import axios from 'axios'; +import { Http } from '../../../util/http'; +import type { HttpOptions } from '../../../util/http/types'; +import { resolveBaseUrl } from '../../../util/url'; import type { Link, Page, @@ -13,11 +14,13 @@ import type { } from './types'; const URLS = { - ME: 'me', - ALLREPOS: 'repositories', + ME: '/me', + ALL_REPOS: 'repositories?pageSize=1000000', REPO: (repoPath: string) => `repositories/${repoPath}`, PULLREQUESTS: (repoPath: string) => `pull-requests/${repoPath}`, - PULLREQUESTBYID: (repoPath: string, id: number) => + PULLREQUESTS_WITH_PAGINATION: (repoPath: string) => + `pull-requests/${repoPath}?status=ALL&pageSize=1000000`, + PULLREQUEST_BY_ID: (repoPath: string, id: number) => `pull-requests/${repoPath}/${id}`, }; @@ -26,94 +29,89 @@ const CONTENT_TYPES = { }; export default class ScmClient { - private httpClient: AxiosInstance; + private readonly httpClient: Http; + private readonly endpoint: string; constructor(endpoint: string, token: string) { - this.httpClient = axios.create({ - baseURL: endpoint, - headers: { - Authorization: `Bearer ${token}`, - Accept: '*', - 'X-Scm-Client': 'WUI', - }, + this.endpoint = endpoint; + this.httpClient = new Http('scmm', { + throwHttpErrors: true, }); } public getEndpoint(): string { - /* istanbul ignore next */ - if (!this.httpClient.defaults.baseURL) { - throw new Error('BaseURL is not defined'); - } - - return this.httpClient.defaults.baseURL; + return this.endpoint; } public async getCurrentUser(): Promise { - const response = await this.httpClient.get(URLS.ME); - return response.data; + const response = await this.httpClient.getJson( + resolveBaseUrl(this.endpoint, URLS.ME), + ); + return response.body; } public async getRepo(repoPath: string): Promise { - const response = await this.httpClient.get(URLS.REPO(repoPath)); - return response.data; + const response = await this.httpClient.getJson( + resolveBaseUrl(this.endpoint, URLS.REPO(repoPath)), + ); + return response.body; } public async getAllRepos(): Promise { - const response = await this.httpClient.get>(URLS.ALLREPOS, { - params: { pageSize: 1000000 }, - }); + const response = await this.httpClient.getJson>( + resolveBaseUrl(this.endpoint, URLS.ALL_REPOS), + ); - return response.data._embedded.repositories; + return response.body._embedded.repositories; } public async getDefaultBranch(repo: Repo): Promise { const defaultBranchUrl = repo._links['defaultBranch'] as Link; - const response = await this.httpClient.get<{ defaultBranch: string }>( + const response = await this.httpClient.getJson<{ defaultBranch: string }>( defaultBranchUrl.href, - { baseURL: undefined }, ); - return response.data.defaultBranch; + return response.body.defaultBranch; } public async getAllRepoPrs(repoPath: string): Promise { - const response = await this.httpClient.get>( - URLS.PULLREQUESTS(repoPath), - { - params: { status: 'ALL', pageSize: 1000000 }, - }, + const response = await this.httpClient.getJson>( + resolveBaseUrl( + this.endpoint, + URLS.PULLREQUESTS_WITH_PAGINATION(repoPath), + ), ); - return response.data._embedded.pullRequests; + return response.body._embedded.pullRequests; } public async getRepoPr(repoPath: string, id: number): Promise { - const response = await this.httpClient.get( - URLS.PULLREQUESTBYID(repoPath, id), + const response = await this.httpClient.getJson( + resolveBaseUrl(this.endpoint, URLS.PULLREQUEST_BY_ID(repoPath, id)), ); - return response.data; + return response.body; } public async createPr( repoPath: string, params: PullRequestCreateParams, ): Promise { - const createPrResponse = await this.httpClient.post( - URLS.PULLREQUESTS(repoPath), - params, + const createPrResponse = await this.httpClient.postJson( + resolveBaseUrl(this.endpoint, URLS.PULLREQUESTS(repoPath)), { + body: params, headers: { 'Content-Type': CONTENT_TYPES.PULLREQUESTS, }, }, ); - const getCreatedPrResponse = await this.httpClient.get( - createPrResponse.headers.location, - { baseURL: undefined }, + const getCreatedPrResponse = await this.httpClient.getJson( + /* istanbul ignore next: Just to please the compiler, location would never be undefined */ + createPrResponse.headers.location ?? '', ); - return getCreatedPrResponse.data; + return getCreatedPrResponse.body; } public async updatePr( @@ -121,10 +119,14 @@ export default class ScmClient { id: number, params: PullRequestUpdateParams, ): Promise { - await this.httpClient.put(URLS.PULLREQUESTBYID(repoPath, id), params, { - headers: { - 'Content-Type': CONTENT_TYPES.PULLREQUESTS, + await this.httpClient.putJson( + resolveBaseUrl(this.endpoint, URLS.PULLREQUEST_BY_ID(repoPath, id)), + { + body: params, + headers: { + 'Content-Type': CONTENT_TYPES.PULLREQUESTS, + }, }, - }); + ); } } diff --git a/lib/modules/platform/scmm/types.ts b/lib/modules/platform/scmm/types.ts index 63bf09fdc3..a2139716de 100644 --- a/lib/modules/platform/scmm/types.ts +++ b/lib/modules/platform/scmm/types.ts @@ -6,17 +6,17 @@ export type Page = { export interface Links { [link: string]: Link | Link[] | undefined; -}; +} export interface Link { href: string; name?: string; templated?: boolean; -}; +} export interface PullRequestPage { pullRequests: PullRequest[]; -}; +} export interface PullRequestCreateParams extends PullRequestUpdateParams { source: string; diff --git a/package.json b/package.json index 00ecb1ed2f..41d638da99 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,6 @@ "aggregate-error": "3.1.0", "auth-header": "1.0.0", "aws4": "1.12.0", - "axios": "1.6.5", "azure-devops-node-api": "12.3.0", "bunyan": "1.8.15", "cacache": "18.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc7c5d759f..6ee46cb293 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,9 +101,6 @@ importers: aws4: specifier: 1.12.0 version: 1.12.0 - axios: - specifier: 1.6.5 - version: 1.6.5 azure-devops-node-api: specifier: 12.3.0 version: 12.3.0 @@ -4529,10 +4526,6 @@ packages: safer-buffer: 2.1.2 dev: false - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false - /auth-header@1.0.0: resolution: {integrity: sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA==} dev: false @@ -4553,16 +4546,6 @@ packages: resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} dev: false - /axios@1.6.5: - resolution: {integrity: sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==} - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - dev: false - /azure-devops-node-api@12.3.0: resolution: {integrity: sha512-5HDhBFIXJxiFhcJ+A3hN87gwo92PrDNLJvcvRHjr+p7AsuUSF64yQU+M6wcBDczkIDVV7m+MrraKQ2tqUxSbCA==} dependencies: @@ -4990,13 +4973,6 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - dev: false - /commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -5281,11 +5257,6 @@ packages: has-property-descriptors: 1.0.1 object-keys: 1.1.1 - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: false - /deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} @@ -6127,16 +6098,6 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true - /follow-redirects@1.15.5: - resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false - /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -6157,15 +6118,6 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 - /form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - dev: false - /from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} dependencies: @@ -8182,18 +8134,6 @@ packages: braces: 3.0.2 picomatch: 2.3.1 - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: false - - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: false - /mime@4.0.1: resolution: {integrity: sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==} engines: {node: '>=16'} @@ -9163,10 +9103,6 @@ packages: resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} dev: false - /proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: false - /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: From ca1e24ed9dcc4e25577292d03d4187a7660ba323 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Tue, 30 Jan 2024 10:33:45 +0100 Subject: [PATCH 05/61] Fixed not using scmm specific mime types for accept header --- lib/modules/platform/scmm/scm-client.ts | 104 ++++++++++++++---------- lib/modules/platform/scmm/types.ts | 6 ++ 2 files changed, 69 insertions(+), 41 deletions(-) diff --git a/lib/modules/platform/scmm/scm-client.ts b/lib/modules/platform/scmm/scm-client.ts index 3883af220c..7c08549aa8 100644 --- a/lib/modules/platform/scmm/scm-client.ts +++ b/lib/modules/platform/scmm/scm-client.ts @@ -1,5 +1,9 @@ import { Http } from '../../../util/http'; -import type { HttpOptions } from '../../../util/http/types'; +import type { + HttpRequestOptions, + HttpResponse, + InternalHttpOptions, +} from '../../../util/http/types'; import { resolveBaseUrl } from '../../../util/url'; import type { Link, @@ -10,6 +14,7 @@ import type { PullRequestUpdateParams, Repo, RepoPage, + ScmmHttpOptions, User, } from './types'; @@ -25,18 +30,34 @@ const URLS = { }; const CONTENT_TYPES = { - PULLREQUESTS: 'application/vnd.scmm-pullrequest+json;v=2', + ME: 'application/vnd.scmm-me+json;v=2', + REPOSITORY: 'application/vnd.scmm-repository+json;v=2', + REPOSITORIES: 'application/vnd.scmm-repositoryCollection+json;v=2', + GIT_CONFIG: 'application/vnd.scmm-gitDefaultBranch+json;v=2', + PULLREQUEST: 'application/vnd.scmm-pullRequest+json;v=2', + PULLREQUESTS: 'application/vnd.scmm-pullRequestCollection+json;v=2', }; -export default class ScmClient { - private readonly httpClient: Http; +export default class ScmClient extends Http { private readonly endpoint: string; constructor(endpoint: string, token: string) { + super('scmm', { throwHttpErrors: true, token }); this.endpoint = endpoint; - this.httpClient = new Http('scmm', { - throwHttpErrors: true, - }); + } + + protected override async request( + requestUrl: string | URL, + options?: InternalHttpOptions & ScmmHttpOptions & HttpRequestOptions, + ): Promise> { + const opts = { + ...options, + headers: { + ...options?.headers, + accept: options?.scmmContentType, + }, + }; + return await super.request(resolveBaseUrl(this.endpoint, requestUrl), opts); } public getEndpoint(): string { @@ -44,49 +65,55 @@ export default class ScmClient { } public async getCurrentUser(): Promise { - const response = await this.httpClient.getJson( - resolveBaseUrl(this.endpoint, URLS.ME), - ); + const response = await this.getJson(URLS.ME, { + scmmContentType: CONTENT_TYPES.ME, + }); return response.body; } public async getRepo(repoPath: string): Promise { - const response = await this.httpClient.getJson( - resolveBaseUrl(this.endpoint, URLS.REPO(repoPath)), - ); + const response = await this.getJson(URLS.REPO(repoPath), { + scmmContentType: CONTENT_TYPES.REPOSITORY, + }); return response.body; } public async getAllRepos(): Promise { - const response = await this.httpClient.getJson>( - resolveBaseUrl(this.endpoint, URLS.ALL_REPOS), - ); + const response = await this.getJson>(URLS.ALL_REPOS, { + scmmContentType: CONTENT_TYPES.REPOSITORIES, + }); return response.body._embedded.repositories; } public async getDefaultBranch(repo: Repo): Promise { const defaultBranchUrl = repo._links['defaultBranch'] as Link; - const response = await this.httpClient.getJson<{ defaultBranch: string }>( + const response = await this.getJson<{ defaultBranch: string }>( defaultBranchUrl.href, + { + scmmContentType: CONTENT_TYPES.GIT_CONFIG, + }, ); return response.body.defaultBranch; } public async getAllRepoPrs(repoPath: string): Promise { - const response = await this.httpClient.getJson>( - resolveBaseUrl( - this.endpoint, - URLS.PULLREQUESTS_WITH_PAGINATION(repoPath), - ), + const response = await this.getJson>( + URLS.PULLREQUESTS_WITH_PAGINATION(repoPath), + { + scmmContentType: CONTENT_TYPES.PULLREQUESTS, + }, ); return response.body._embedded.pullRequests; } public async getRepoPr(repoPath: string, id: number): Promise { - const response = await this.httpClient.getJson( - resolveBaseUrl(this.endpoint, URLS.PULLREQUEST_BY_ID(repoPath, id)), + const response = await this.getJson( + URLS.PULLREQUEST_BY_ID(repoPath, id), + { + scmmContentType: CONTENT_TYPES.PULLREQUEST, + }, ); return response.body; @@ -96,17 +123,15 @@ export default class ScmClient { repoPath: string, params: PullRequestCreateParams, ): Promise { - const createPrResponse = await this.httpClient.postJson( - resolveBaseUrl(this.endpoint, URLS.PULLREQUESTS(repoPath)), - { - body: params, - headers: { - 'Content-Type': CONTENT_TYPES.PULLREQUESTS, - }, + const createPrResponse = await this.postJson(URLS.PULLREQUESTS(repoPath), { + scmmContentType: CONTENT_TYPES.PULLREQUEST, + body: params, + headers: { + 'Content-Type': CONTENT_TYPES.PULLREQUEST, }, - ); + }); - const getCreatedPrResponse = await this.httpClient.getJson( + const getCreatedPrResponse = await this.getJson( /* istanbul ignore next: Just to please the compiler, location would never be undefined */ createPrResponse.headers.location ?? '', ); @@ -119,14 +144,11 @@ export default class ScmClient { id: number, params: PullRequestUpdateParams, ): Promise { - await this.httpClient.putJson( - resolveBaseUrl(this.endpoint, URLS.PULLREQUEST_BY_ID(repoPath, id)), - { - body: params, - headers: { - 'Content-Type': CONTENT_TYPES.PULLREQUESTS, - }, + await this.putJson(URLS.PULLREQUEST_BY_ID(repoPath, id), { + body: params, + headers: { + 'Content-Type': CONTENT_TYPES.PULLREQUEST, }, - ); + }); } } diff --git a/lib/modules/platform/scmm/types.ts b/lib/modules/platform/scmm/types.ts index a2139716de..f15774af5e 100644 --- a/lib/modules/platform/scmm/types.ts +++ b/lib/modules/platform/scmm/types.ts @@ -1,9 +1,15 @@ +import type { HttpOptions } from '../../../util/http/types'; + export type Page = { page: number; pageTotal: number; _embedded: T; }; +export interface ScmmHttpOptions extends HttpOptions { + scmmContentType?: string; +} + export interface Links { [link: string]: Link | Link[] | undefined; } From 46071f52d011cabc19385e6423ed8d2491086ece Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 15 Feb 2024 09:51:20 +0100 Subject: [PATCH 06/61] Use git from test/util instead --- lib/modules/platform/scmm/index.spec.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/modules/platform/scmm/index.spec.ts b/lib/modules/platform/scmm/index.spec.ts index 3941a33a68..a1e122f90f 100644 --- a/lib/modules/platform/scmm/index.spec.ts +++ b/lib/modules/platform/scmm/index.spec.ts @@ -1,5 +1,4 @@ -import { mocked } from '../../../../test/util'; -import * as _git from '../../../util/git'; +import { git, mocked } from '../../../../test/util'; import * as _hostRules from '../../../util/host-rules'; import type { Pr } from '../types'; import * as _util from '../util'; @@ -25,16 +24,14 @@ import { updatePr, } from './index'; +jest.mock('../../../util/git'); jest.mock('../../../util/host-rules'); const hostRules: jest.Mocked = mocked(_hostRules); -jest.mock('../../../util/git'); -const git: jest.Mocked = mocked(_git); - jest.mock('../util'); const util: jest.Mocked = mocked(_util); -const endpoint = 'http://localhost:1337/scm/api/v2'; +const endpoint = 'https://localhost:8080/scm/api/v2'; const token = 'TEST_TOKEN'; const user: User = { @@ -56,7 +53,7 @@ const repo: Repo = { healthCheckRunning: false, _links: { protocol: [ - { name: 'http', href: 'http://localhost:8080/scm/default/repo' }, + { name: 'http', href: 'https://localhost:8080/scm/default/repo' }, ], }, }; @@ -126,9 +123,6 @@ describe('modules/platform/scmm/index', () => { .mockResolvedValueOnce(expectedDefaultBranch); hostRules.find.mockReturnValueOnce({ username: user.username }); - git.initRepo.mockImplementationOnce(() => { - return Promise.resolve(); - }); util.repoFingerprint.mockReturnValueOnce(expectedFingerprint); expect( @@ -140,7 +134,7 @@ describe('modules/platform/scmm/index', () => { }); expect(git.initRepo).toHaveBeenCalledWith({ - url: `http://${user.username}@localhost:8080/scm/default/repo`, + url: `https://${user.username}@localhost:8080/scm/default/repo`, repository, defaultBranch: expectedDefaultBranch, }); From f92e243fafd012f0d7b6cabcf98277fdef738267 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 15 Feb 2024 11:01:12 +0100 Subject: [PATCH 07/61] Add dummy tests for no ops --- lib/modules/platform/scmm/index.spec.ts | 135 ++++++++++++++++++++++++ lib/modules/platform/scmm/index.ts | 16 --- 2 files changed, 135 insertions(+), 16 deletions(-) diff --git a/lib/modules/platform/scmm/index.spec.ts b/lib/modules/platform/scmm/index.spec.ts index a1e122f90f..bcad91829d 100644 --- a/lib/modules/platform/scmm/index.spec.ts +++ b/lib/modules/platform/scmm/index.spec.ts @@ -12,15 +12,31 @@ import type { User, } from './types'; import { + addAssignees, + addReviewers, createPr, + deleteLabel, + ensureCommentRemoval, + ensureIssue, + ensureIssueClosing, + findIssue, findPr, getBranchPr, + getBranchStatus, + getBranchStatusCheck, + getIssueList, + getJsonFile, getPr, getPrList, + getRawFile, + getRepoForceRebase, getRepos, initPlatform, initRepo, invalidatePrCache, + massageMarkdown, + mergePr, + setBranchStatus, updatePr, } from './index'; @@ -402,4 +418,123 @@ describe('modules/platform/scmm/index', () => { }, ); }); + + describe(mergePr, () => { + it('should no-op and return false', async () => { + const result = await mergePr({ id: 1 }); + expect(result).toBeFalse(); + }); + }); + + describe(getBranchStatus, () => { + it('should no-op and return red', async () => { + const result = await getBranchStatus('test/branch', false); + expect(result).toBe('red'); + }); + }); + + describe(setBranchStatus, () => { + it('should no-op', async () => { + await expect( + setBranchStatus({ + branchName: 'test/branch', + context: 'context', + description: 'description', + state: 'red', + }), + ).resolves.not.toThrow(); + }); + }); + + describe(getBranchStatusCheck, () => { + it('should no-op and return null', async () => { + const result = await getBranchStatusCheck('test/branch', null); + expect(result).toBeNull(); + }); + }); + + describe(addReviewers, () => { + it('should no-op', async () => { + await expect(addReviewers(1, ['reviewer'])).resolves.not.toThrow(); + }); + }); + + describe(addAssignees, () => { + it('should no-op', async () => { + await expect(addAssignees(1, ['assignee'])).resolves.not.toThrow(); + }); + }); + + describe(deleteLabel, () => { + it('should no-op', async () => { + await expect(deleteLabel(1, 'label')).resolves.not.toThrow(); + }); + }); + + describe(getIssueList, () => { + it('should no-op and return empty list', async () => { + const result = await getIssueList(); + expect(result).toEqual([]); + }); + }); + + describe(findIssue, () => { + it('should no-op and return null', async () => { + const result = await findIssue('issue'); + expect(result).toBeNull(); + }); + }); + + describe(ensureIssue, () => { + it('should no-op and return null', async () => { + const result = await ensureIssue({ title: 'issue', body: 'body' }); + expect(result).toBeNull(); + }); + }); + + describe(ensureIssueClosing, () => { + it('should no-op', async () => { + await expect(ensureIssueClosing('issue')).resolves.not.toThrow(); + }); + }); + + describe(ensureCommentRemoval, () => { + it('should no-op', async () => { + await expect( + ensureCommentRemoval({ + type: 'by-content', + number: 1, + content: 'content', + }), + ).resolves.not.toThrow(); + }); + }); + + describe(massageMarkdown, () => { + it('should adjust smart link for pull requests', () => { + const result = massageMarkdown('[PR](../pull/1)'); + expect(result).toBe('[PR](pulls/1)'); + }); + }); + + describe(getRepoForceRebase, () => { + it('should no-op and return false', async () => { + const result = await getRepoForceRebase(); + expect(result).toBeFalse(); + }); + }); + + describe(getRawFile, () => { + it('should no-op and return null', async () => { + const result = await getRawFile('file'); + expect(result).toBeNull(); + }); + }); + + describe(getJsonFile, () => { + it('should no-op and return undefined', async () => { + const result = await getJsonFile('package.json'); + expect(result).toBeUndefined(); + }); + }); }); diff --git a/lib/modules/platform/scmm/index.ts b/lib/modules/platform/scmm/index.ts index 5da76f5f31..89155029fc 100644 --- a/lib/modules/platform/scmm/index.ts +++ b/lib/modules/platform/scmm/index.ts @@ -215,13 +215,11 @@ export async function updatePr({ logger.info(`Updated Pr #${number} with title ${prTitle}`); } -/* istanbul ignore next */ export function mergePr(config: MergePRConfig): Promise { logger.debug('NO-OP mergePr'); return Promise.resolve(false); } -/* istanbul ignore next */ export function getBranchStatus( branchName: string, internalChecksAsSuccess: boolean, @@ -230,7 +228,6 @@ export function getBranchStatus( return Promise.resolve('red'); } -/* istanbul ignore next */ export function setBranchStatus( branchStatusConfig: BranchStatusConfig, ): Promise { @@ -238,7 +235,6 @@ export function setBranchStatus( return Promise.resolve(); } -/* istanbul ignore next */ export function getBranchStatusCheck( branchName: string, context: string | null | undefined, @@ -247,7 +243,6 @@ export function getBranchStatusCheck( return Promise.resolve(null); } -/* istanbul ignore next */ export function addReviewers( number: number, reviewers: string[], @@ -256,7 +251,6 @@ export function addReviewers( return Promise.resolve(); } -/* istanbul ignore next */ export function addAssignees( number: number, assignees: string[], @@ -265,25 +259,21 @@ export function addAssignees( return Promise.resolve(); } -/* istanbul ignore next */ export function deleteLabel(number: number, label: string): Promise { logger.debug('NO-OP deleteLabel'); return Promise.resolve(); } -/* istanbul ignore next */ export function getIssueList(): Promise { logger.debug('NO-OP getIssueList'); return Promise.resolve([]); } -/* istanbul ignore next */ export function findIssue(title: string): Promise { logger.debug('NO-OP findIssue'); return Promise.resolve(null); } -/* istanbul ignore next */ export function ensureIssue( config: EnsureIssueConfig, ): Promise<'updated' | 'created' | null> { @@ -291,7 +281,6 @@ export function ensureIssue( return Promise.resolve(null); } -/* istanbul ignore next */ export function ensureIssueClosing(title: string): Promise { logger.debug('NO-OP ensureIssueClosing'); return Promise.resolve(); @@ -303,7 +292,6 @@ export function ensureComment(config: EnsureCommentConfig): Promise { return Promise.resolve(false); } -/* istanbul ignore next */ export function ensureCommentRemoval( ensureCommentRemoval: | EnsureCommentRemovalConfigByTopic @@ -313,17 +301,14 @@ export function ensureCommentRemoval( return Promise.resolve(); } -/* istanbul ignore next */ export function massageMarkdown(prBody: string): string { return smartTruncate(smartLinks(prBody), 10000); } -/* istanbul ignore next */ export function getRepoForceRebase(): Promise { return Promise.resolve(false); } -/* istanbul ignore next */ export function getRawFile( fileName: string, repoName?: string, @@ -333,7 +318,6 @@ export function getRawFile( return Promise.resolve(null); } -/* istanbul ignore next */ export function getJsonFile( fileName: string, repoName?: string, From bf656b96cf290c491078eebc016ad8f67f1af368 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 15 Feb 2024 11:26:18 +0100 Subject: [PATCH 08/61] Fix some phrasing and spelling issues --- lib/modules/platform/scmm/index.spec.ts | 16 ++++++++-------- lib/modules/platform/scmm/index.ts | 8 ++++---- lib/modules/platform/scmm/mapper.spec.ts | 2 +- lib/modules/platform/scmm/readme.md | 10 +++++----- lib/modules/platform/scmm/scm-client.spec.ts | 8 ++++---- lib/modules/platform/scmm/utils.spec.ts | 10 +++++----- lib/modules/platform/scmm/utils.ts | 2 +- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/modules/platform/scmm/index.spec.ts b/lib/modules/platform/scmm/index.spec.ts index bcad91829d..b772cf7ff5 100644 --- a/lib/modules/platform/scmm/index.spec.ts +++ b/lib/modules/platform/scmm/index.spec.ts @@ -103,15 +103,15 @@ describe('modules/platform/scmm/index', () => { }); describe(initPlatform, () => { - it('should throw error, because endpoint is not configured', async () => { + it('should throw error, when endpoint is not configured', async () => { await expect(initPlatform({ token })).rejects.toThrow( 'SCM-Manager endpoint not configured', ); }); - it('should throw error, because token is not configured', async () => { + it('should throw error, when token is not configured', async () => { await expect(initPlatform({ endpoint })).rejects.toThrow( - 'SCM-Manager api token not configured', + 'SCM-Manager API token not configured', ); }); @@ -173,7 +173,7 @@ describe('modules/platform/scmm/index', () => { }); describe(getPrList, () => { - it('should return empty array, because no pr could be found', async () => { + it('should return empty array, because no PR could be found', async () => { jest .spyOn(ScmClient.prototype, 'getAllRepoPrs') .mockRejectedValue(new Error()); @@ -181,7 +181,7 @@ describe('modules/platform/scmm/index', () => { expect(await getPrList()).toIncludeAllMembers([]); }); - it('should return all prs of a repo', async () => { + it('should return all PRs of a repo', async () => { const expectedResult: Pr[] = [ { sourceBranch: pullRequest.source, @@ -284,7 +284,7 @@ describe('modules/platform/scmm/index', () => { }); describe(getPr, () => { - it('should return null, because pr was not found', async () => { + it('should return null, because PR was not found', async () => { jest .spyOn(ScmClient.prototype, 'getAllRepoPrs') .mockResolvedValueOnce([]); @@ -326,7 +326,7 @@ describe('modules/platform/scmm/index', () => { [false, 'OPEN', false], [true, 'DRAFT', true], ])( - 'it should create the pr with isDraft %p and state %p', + 'it should create the PR with isDraft %p and state %p', async ( draftPR: boolean | undefined, expectedState: string, @@ -388,7 +388,7 @@ describe('modules/platform/scmm/index', () => { [undefined, undefined, 'prBody', 'prBody'], ['open', 'OPEN', undefined, undefined], ])( - 'it should update the pr with state %p and prBody %p', + 'it should update the PR with state %p and prBody %p', async ( actualState: string | undefined, expectedState: string | undefined, diff --git a/lib/modules/platform/scmm/index.ts b/lib/modules/platform/scmm/index.ts index 89155029fc..25074e484d 100644 --- a/lib/modules/platform/scmm/index.ts +++ b/lib/modules/platform/scmm/index.ts @@ -46,7 +46,7 @@ export async function initPlatform({ } if (!token) { - throw new Error('SCM-Manager api token not configured'); + throw new Error('SCM-Manager API token not configured'); } scmmClient = new ScmClient(endpoint, token); @@ -191,9 +191,9 @@ export async function createPr({ }); logger.info( - `Pr Created with title '${createdPr.title}' from source '${createdPr.source}' to target '${createdPr.target}'`, + `PR created with title '${createdPr.title}' from source '${createdPr.source}' to target '${createdPr.target}'`, ); - logger.debug(`Pr Created ${JSON.stringify(createdPr)}`); + logger.debug(`PR created ${JSON.stringify(createdPr)}`); return mapPrFromScmToRenovate(createdPr); } @@ -212,7 +212,7 @@ export async function updatePr({ status: mapPrState(state), }); - logger.info(`Updated Pr #${number} with title ${prTitle}`); + logger.info(`Updated PR #${number} with title ${prTitle}`); } export function mergePr(config: MergePRConfig): Promise { diff --git a/lib/modules/platform/scmm/mapper.spec.ts b/lib/modules/platform/scmm/mapper.spec.ts index 925e826ea9..34043465e9 100644 --- a/lib/modules/platform/scmm/mapper.spec.ts +++ b/lib/modules/platform/scmm/mapper.spec.ts @@ -2,7 +2,7 @@ import { mapPrFromScmToRenovate } from './mapper'; import type { PullRequest as SCMPullRequest } from './types'; describe('modules/platform/scmm/mapper', () => { - it('should correctly map the scm type of a pr to the renovate pr type', () => { + it('should correctly map the scm type of a PR to the Renovate PR type', () => { const scmPr: SCMPullRequest = { source: 'feat/new', target: 'develop', diff --git a/lib/modules/platform/scmm/readme.md b/lib/modules/platform/scmm/readme.md index ba8617e77a..fbee648a72 100644 --- a/lib/modules/platform/scmm/readme.md +++ b/lib/modules/platform/scmm/readme.md @@ -1,13 +1,13 @@ # SCM-Manager -Renovate supports [SCM-Manager](https://scm-manager.org). +Renovate supports the [SCM-Manager](https://scm-manager.org) platform. ## Authentication -First, create an API Key for your technical Renovate user in SCM-Manager. -The technical user should be configured properly with name and email address. -Then let Renovate use your API Key by setting the `RENOVATE_TOKEN` environment variable with your key. +1. Create an API Key for your technical Renovate user in SCM-Manager. +2. The technical user _must_ have the correct name and email address. +3. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it. -You must set `platform=scmm` in your Renovate config file. +You must set the [`platform`](https://docs.renovatebot.com/self-hosted-configuration/#platform) to `scmm` in your your Renovate config file. The technical user needs at least the permissions to read your repository read and create pull request. This can be achieved by granting the permission role "OWNER" to your technical Renovate user. diff --git a/lib/modules/platform/scmm/scm-client.spec.ts b/lib/modules/platform/scmm/scm-client.spec.ts index 7e61edbd97..07185eca2b 100644 --- a/lib/modules/platform/scmm/scm-client.spec.ts +++ b/lib/modules/platform/scmm/scm-client.spec.ts @@ -163,7 +163,7 @@ describe('modules/platform/scmm/scm-client', () => { }); describe(scmClient.getAllRepoPrs, () => { - it('should return all repo prs', async () => { + it('should return all repo PRs', async () => { httpMock .scope(endpoint) .get( @@ -200,7 +200,7 @@ describe('modules/platform/scmm/scm-client', () => { }); describe(scmClient.getRepoPr, () => { - it('should return the repo pr', async () => { + it('should return the repo PR', async () => { httpMock .scope(endpoint) .get(`/pull-requests/${repo.namespace}/${repo.name}/${pullRequest.id}`) @@ -229,7 +229,7 @@ describe('modules/platform/scmm/scm-client', () => { }); describe(scmClient.createPr, () => { - it('should create pr for a repo', async () => { + it('should create PR for a repo', async () => { const expectedCreateParams: PullRequestCreateParams = { source: 'feature/test', target: 'develop', @@ -284,7 +284,7 @@ describe('modules/platform/scmm/scm-client', () => { }); describe(scmClient.updatePr, () => { - it('should update pr for a repo', async () => { + it('should update PR for a repo', async () => { const expectedUpdateParams: PullRequestUpdateParams = { title: 'Test Title', description: 'PR description', diff --git a/lib/modules/platform/scmm/utils.spec.ts b/lib/modules/platform/scmm/utils.spec.ts index 34fec84fc7..c693482284 100644 --- a/lib/modules/platform/scmm/utils.spec.ts +++ b/lib/modules/platform/scmm/utils.spec.ts @@ -13,7 +13,7 @@ describe('modules/platform/scmm/utils', () => { ['rebase', 'REBASE'], ['squash', 'SQUASH'], ])( - 'map merge strategy %p on pr merge method %p', + 'map merge strategy %p on PR merge method %p', (strategy: string | undefined, method: string | null) => { expect(getMergeMethod(strategy as MergeStrategy)).toEqual(method); }, @@ -89,11 +89,11 @@ describe('modules/platform/scmm/utils', () => { (gitUrl: string | undefined) => { expect(() => getRepoUrl(repo, gitUrl as GitUrlOption, username, password), - ).toThrow('MISSING_PROTOCOL_LINKS'); + ).toThrow('Missing protocol links.'); }, ); - it('should throw error because of missing ssh link', () => { + it('should throw error because of missing SSH link', () => { expect(() => getRepoUrl( { @@ -122,7 +122,7 @@ describe('modules/platform/scmm/utils', () => { }); it.each([['endpoint'], ['default'], [undefined]])( - 'should throw error because of missing http link, for option %p', + 'should throw error because of missing HTTP link, for option %p', (gitUrl: string | undefined) => { expect(() => getRepoUrl( @@ -139,7 +139,7 @@ describe('modules/platform/scmm/utils', () => { ); it.each([['endpoint'], ['default'], [undefined]])( - 'should throw error because of malformed http link, with option %p', + 'should throw error because of malformed HTTP link, with option %p', (gitUrl: string | undefined) => { expect(() => getRepoUrl( diff --git a/lib/modules/platform/scmm/utils.ts b/lib/modules/platform/scmm/utils.ts index df94fa872e..6b6a5a5bc6 100644 --- a/lib/modules/platform/scmm/utils.ts +++ b/lib/modules/platform/scmm/utils.ts @@ -53,7 +53,7 @@ export function getRepoUrl( ): string { const protocolLinks = repo._links.protocol as Link[] | undefined; if (!protocolLinks) { - throw new Error('MISSING_PROTOCOL_LINKS'); + throw new Error('Missing protocol links.'); } if (gitUrl === 'ssh') { From fca81b1815a052854c2f70cd89da78840ff7c760 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 19 Feb 2024 15:26:23 +0100 Subject: [PATCH 09/61] Change scmm to scm-manager and fix SCM-Manager specific content type --- lib/config/options/index.ts | 11 ++++++++--- lib/config/presets/local/index.ts | 2 +- lib/constants/platforms.ts | 2 +- lib/modules/platform/api.ts | 2 +- .../platform/{scmm => scm-manager}/index.spec.ts | 2 +- lib/modules/platform/{scmm => scm-manager}/index.ts | 2 +- .../platform/{scmm => scm-manager}/mapper.spec.ts | 2 +- lib/modules/platform/{scmm => scm-manager}/mapper.ts | 0 lib/modules/platform/{scmm => scm-manager}/readme.md | 0 .../platform/{scmm => scm-manager}/scm-client.spec.ts | 2 +- .../platform/{scmm => scm-manager}/scm-client.ts | 5 ++++- lib/modules/platform/{scmm => scm-manager}/types.ts | 0 .../platform/{scmm => scm-manager}/utils.spec.ts | 2 +- lib/modules/platform/{scmm => scm-manager}/utils.ts | 0 lib/modules/platform/scm.ts | 2 +- 15 files changed, 21 insertions(+), 13 deletions(-) rename lib/modules/platform/{scmm => scm-manager}/index.spec.ts (99%) rename lib/modules/platform/{scmm => scm-manager}/index.ts (99%) rename lib/modules/platform/{scmm => scm-manager}/mapper.spec.ts (95%) rename lib/modules/platform/{scmm => scm-manager}/mapper.ts (100%) rename lib/modules/platform/{scmm => scm-manager}/readme.md (100%) rename lib/modules/platform/{scmm => scm-manager}/scm-client.spec.ts (99%) rename lib/modules/platform/{scmm => scm-manager}/scm-client.ts (97%) rename lib/modules/platform/{scmm => scm-manager}/types.ts (100%) rename lib/modules/platform/{scmm => scm-manager}/utils.spec.ts (99%) rename lib/modules/platform/{scmm => scm-manager}/utils.ts (100%) diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 71a519c03d..342b864dfc 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -316,7 +316,7 @@ const options: RenovateOptions[] = [ 'If set to `true` then Renovate creates draft PRs, instead of normal status PRs.', type: 'boolean', default: false, - supportedPlatforms: ['azure', 'gitea', 'github', 'gitlab', 'scmm'], + supportedPlatforms: ['azure', 'gitea', 'github', 'gitlab', 'scm-manager'], }, { name: 'dryRun', @@ -780,7 +780,12 @@ const options: RenovateOptions[] = [ description: 'Username for authentication.', stage: 'repository', type: 'string', - supportedPlatforms: ['azure', 'bitbucket', 'bitbucket-server', 'scmm'], + supportedPlatforms: [ + 'azure', + 'bitbucket', + 'bitbucket-server', + 'scm-manager', + ], globalOnly: true, }, { @@ -2729,7 +2734,7 @@ const options: RenovateOptions[] = [ description: 'Overrides the default resolution for Git remote, e.g. to switch GitLab from HTTPS to SSH-based.', type: 'string', - supportedPlatforms: ['gitlab', 'bitbucket-server', 'scmm'], + supportedPlatforms: ['gitlab', 'bitbucket-server', 'scm-manager'], allowedValues: ['default', 'ssh', 'endpoint'], default: 'default', stage: 'repository', diff --git a/lib/config/presets/local/index.ts b/lib/config/presets/local/index.ts index ff78212a9e..ca865ef9ee 100644 --- a/lib/config/presets/local/index.ts +++ b/lib/config/presets/local/index.ts @@ -26,7 +26,7 @@ const resolvers = { github, gitlab, local: null, - scmm: null, + 'scm-manager': null, } satisfies Record; export function getPreset({ diff --git a/lib/constants/platforms.ts b/lib/constants/platforms.ts index f91756f58c..10f9451e66 100644 --- a/lib/constants/platforms.ts +++ b/lib/constants/platforms.ts @@ -8,7 +8,7 @@ export type PlatformId = | 'github' | 'gitlab' | 'local' - | 'scmm'; + | 'scm-manager'; export const GITEA_API_USING_HOST_TYPES = [ 'gitea', diff --git a/lib/modules/platform/api.ts b/lib/modules/platform/api.ts index 514d2effac..0a8211ef64 100644 --- a/lib/modules/platform/api.ts +++ b/lib/modules/platform/api.ts @@ -8,7 +8,7 @@ import * as gitea from './gitea'; import * as github from './github'; import * as gitlab from './gitlab'; import * as local from './local'; -import * as scmm from './scmm'; +import * as scmm from './scm-manager'; import type { Platform } from './types'; const api = new Map(); diff --git a/lib/modules/platform/scmm/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts similarity index 99% rename from lib/modules/platform/scmm/index.spec.ts rename to lib/modules/platform/scm-manager/index.spec.ts index b772cf7ff5..bc38c6c6c6 100644 --- a/lib/modules/platform/scmm/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -96,7 +96,7 @@ const pullRequest: PullRequest = { const renovatePr: Pr = mapPrFromScmToRenovate(pullRequest); -describe('modules/platform/scmm/index', () => { +describe('modules/platform/scm-manager/index', () => { beforeEach(() => { jest.resetAllMocks(); invalidatePrCache(); diff --git a/lib/modules/platform/scmm/index.ts b/lib/modules/platform/scm-manager/index.ts similarity index 99% rename from lib/modules/platform/scmm/index.ts rename to lib/modules/platform/scm-manager/index.ts index 25074e484d..00df590ffe 100644 --- a/lib/modules/platform/scmm/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -32,7 +32,7 @@ interface SCMMRepoConfig { defaultBranch: string; } -export const id = 'scmm'; +export const id = 'scm-manager'; let config: SCMMRepoConfig = {} as any; let scmmClient: ScmClient; diff --git a/lib/modules/platform/scmm/mapper.spec.ts b/lib/modules/platform/scm-manager/mapper.spec.ts similarity index 95% rename from lib/modules/platform/scmm/mapper.spec.ts rename to lib/modules/platform/scm-manager/mapper.spec.ts index 34043465e9..ef15721e36 100644 --- a/lib/modules/platform/scmm/mapper.spec.ts +++ b/lib/modules/platform/scm-manager/mapper.spec.ts @@ -1,7 +1,7 @@ import { mapPrFromScmToRenovate } from './mapper'; import type { PullRequest as SCMPullRequest } from './types'; -describe('modules/platform/scmm/mapper', () => { +describe('modules/platform/scm-manager/mapper', () => { it('should correctly map the scm type of a PR to the Renovate PR type', () => { const scmPr: SCMPullRequest = { source: 'feat/new', diff --git a/lib/modules/platform/scmm/mapper.ts b/lib/modules/platform/scm-manager/mapper.ts similarity index 100% rename from lib/modules/platform/scmm/mapper.ts rename to lib/modules/platform/scm-manager/mapper.ts diff --git a/lib/modules/platform/scmm/readme.md b/lib/modules/platform/scm-manager/readme.md similarity index 100% rename from lib/modules/platform/scmm/readme.md rename to lib/modules/platform/scm-manager/readme.md diff --git a/lib/modules/platform/scmm/scm-client.spec.ts b/lib/modules/platform/scm-manager/scm-client.spec.ts similarity index 99% rename from lib/modules/platform/scmm/scm-client.spec.ts rename to lib/modules/platform/scm-manager/scm-client.spec.ts index 07185eca2b..8d0b4c699b 100644 --- a/lib/modules/platform/scmm/scm-client.spec.ts +++ b/lib/modules/platform/scm-manager/scm-client.spec.ts @@ -8,7 +8,7 @@ import type { User, } from './types'; -describe('modules/platform/scmm/scm-client', () => { +describe('modules/platform/scm-manager/scm-client', () => { const endpoint = 'http://localhost:8080/scm/api/v2'; const token = 'validApiToken'; diff --git a/lib/modules/platform/scmm/scm-client.ts b/lib/modules/platform/scm-manager/scm-client.ts similarity index 97% rename from lib/modules/platform/scmm/scm-client.ts rename to lib/modules/platform/scm-manager/scm-client.ts index 7c08549aa8..061dbde5a1 100644 --- a/lib/modules/platform/scmm/scm-client.ts +++ b/lib/modules/platform/scm-manager/scm-client.ts @@ -42,7 +42,7 @@ export default class ScmClient extends Http { private readonly endpoint: string; constructor(endpoint: string, token: string) { - super('scmm', { throwHttpErrors: true, token }); + super('scm-manager', { throwHttpErrors: true, token }); this.endpoint = endpoint; } @@ -134,6 +134,9 @@ export default class ScmClient extends Http { const getCreatedPrResponse = await this.getJson( /* istanbul ignore next: Just to please the compiler, location would never be undefined */ createPrResponse.headers.location ?? '', + { + scmmContentType: CONTENT_TYPES.PULLREQUEST + } ); return getCreatedPrResponse.body; diff --git a/lib/modules/platform/scmm/types.ts b/lib/modules/platform/scm-manager/types.ts similarity index 100% rename from lib/modules/platform/scmm/types.ts rename to lib/modules/platform/scm-manager/types.ts diff --git a/lib/modules/platform/scmm/utils.spec.ts b/lib/modules/platform/scm-manager/utils.spec.ts similarity index 99% rename from lib/modules/platform/scmm/utils.spec.ts rename to lib/modules/platform/scm-manager/utils.spec.ts index c693482284..90de4bfc8b 100644 --- a/lib/modules/platform/scmm/utils.spec.ts +++ b/lib/modules/platform/scm-manager/utils.spec.ts @@ -3,7 +3,7 @@ import type { GitUrlOption, Pr } from '../types'; import type { PrFilterByState, Repo } from './types'; import { getMergeMethod, getRepoUrl, matchPrState, smartLinks } from './utils'; -describe('modules/platform/scmm/utils', () => { +describe('modules/platform/scm-manager/utils', () => { describe(getMergeMethod, () => { it.each([ [undefined, null], diff --git a/lib/modules/platform/scmm/utils.ts b/lib/modules/platform/scm-manager/utils.ts similarity index 100% rename from lib/modules/platform/scmm/utils.ts rename to lib/modules/platform/scm-manager/utils.ts diff --git a/lib/modules/platform/scm.ts b/lib/modules/platform/scm.ts index f62617238a..e11d0c6309 100644 --- a/lib/modules/platform/scm.ts +++ b/lib/modules/platform/scm.ts @@ -17,7 +17,7 @@ platformScmImpls.set('gitea', DefaultGitScm); platformScmImpls.set('github', GithubScm); platformScmImpls.set('gitlab', DefaultGitScm); platformScmImpls.set('local', LocalFs); -platformScmImpls.set('scmm', DefaultGitScm); +platformScmImpls.set('scm-manager', DefaultGitScm); let _scm: PlatformScm | undefined; From 26afe5db12798410c49fdab0a59872156330e1a8 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 19 Feb 2024 15:34:13 +0100 Subject: [PATCH 10/61] Add supported major versions, review plugin requirement, write permissions is needed --- lib/modules/platform/scm-manager/readme.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index fbee648a72..909bd101ea 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -5,9 +5,17 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. ## Authentication 1. Create an API Key for your technical Renovate user in SCM-Manager. -2. The technical user _must_ have the correct name and email address. +2. The technical user _must_ have valid name and email address. 3. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it. -You must set the [`platform`](https://docs.renovatebot.com/self-hosted-configuration/#platform) to `scmm` in your your Renovate config file. +You must set the [`platform`](https://docs.renovatebot.com/self-hosted-configuration/#platform) to `scm-manager` in your Renovate config file. -The technical user needs at least the permissions to read your repository read and create pull request. This can be achieved by granting the permission role "OWNER" to your technical Renovate user. +The technical user needs the permissions to read and write your repository. +This can be achieved by granting the permission role "OWNER" to your technical Renovate user. +Additionally, the Review Plugin needs to be installed. +Otherwise, the pull request API will not be available. +Plugins can be installed under Administration -> Plugins -> Available. + +Renovate supports SCM-Manager major version 2.x and 3.x. +The 2.x is supported since 2.48.0. +The 3.x is supported since 3.0.0. From dc6517874f387e12237fa3f27df52d40b2f159bc Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 19 Feb 2024 15:50:11 +0100 Subject: [PATCH 11/61] Change using actual host rules instead of mocking it --- lib/modules/platform/scm-manager/index.spec.ts | 7 ++----- lib/modules/platform/scm-manager/scm-client.ts | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index bc38c6c6c6..cd12795a2f 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -1,5 +1,5 @@ import { git, mocked } from '../../../../test/util'; -import * as _hostRules from '../../../util/host-rules'; +import * as hostRules from '../../../util/host-rules'; import type { Pr } from '../types'; import * as _util from '../util'; import { mapPrFromScmToRenovate } from './mapper'; @@ -41,9 +41,6 @@ import { } from './index'; jest.mock('../../../util/git'); -jest.mock('../../../util/host-rules'); -const hostRules: jest.Mocked = mocked(_hostRules); - jest.mock('../util'); const util: jest.Mocked = mocked(_util); @@ -99,6 +96,7 @@ const renovatePr: Pr = mapPrFromScmToRenovate(pullRequest); describe('modules/platform/scm-manager/index', () => { beforeEach(() => { jest.resetAllMocks(); + hostRules.add({ token, username: user.username }); invalidatePrCache(); }); @@ -138,7 +136,6 @@ describe('modules/platform/scm-manager/index', () => { .spyOn(ScmClient.prototype, 'getDefaultBranch') .mockResolvedValueOnce(expectedDefaultBranch); - hostRules.find.mockReturnValueOnce({ username: user.username }); util.repoFingerprint.mockReturnValueOnce(expectedFingerprint); expect( diff --git a/lib/modules/platform/scm-manager/scm-client.ts b/lib/modules/platform/scm-manager/scm-client.ts index 061dbde5a1..8409b7a7f2 100644 --- a/lib/modules/platform/scm-manager/scm-client.ts +++ b/lib/modules/platform/scm-manager/scm-client.ts @@ -135,8 +135,8 @@ export default class ScmClient extends Http { /* istanbul ignore next: Just to please the compiler, location would never be undefined */ createPrResponse.headers.location ?? '', { - scmmContentType: CONTENT_TYPES.PULLREQUEST - } + scmmContentType: CONTENT_TYPES.PULLREQUEST, + }, ); return getCreatedPrResponse.body; From b6bf89a63d81c81b61f5ee45e28a3d4d61c7a3ad Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Fri, 23 Feb 2024 14:04:48 +0100 Subject: [PATCH 12/61] Change use http mocks for testing --- .../platform/scm-manager/index.spec.ts | 257 ++++++++++++------ .../platform/scm-manager/scm-client.ts | 2 +- 2 files changed, 168 insertions(+), 91 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index cd12795a2f..584c47615d 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -1,3 +1,4 @@ +import * as httpMock from '../../../../test/http-mock'; import { git, mocked } from '../../../../test/util'; import * as hostRules from '../../../util/host-rules'; import type { Pr } from '../types'; @@ -68,6 +69,9 @@ const repo: Repo = { protocol: [ { name: 'http', href: 'https://localhost:8080/scm/default/repo' }, ], + defaultBranch: { + href: 'https://localhost:8080/scm/api/v2/config/git/default/repo/default-branch', + }, }, }; @@ -114,10 +118,7 @@ describe('modules/platform/scm-manager/index', () => { }); it('should init platform', async () => { - jest - .spyOn(ScmClient.prototype, 'getCurrentUser') - .mockResolvedValueOnce(user); - + httpMock.scope(endpoint).get('/me').reply(200, user); expect(await initPlatform({ endpoint, token })).toEqual({ endpoint, gitAuthor: 'Test User ', @@ -131,10 +132,14 @@ describe('modules/platform/scm-manager/index', () => { const expectedFingerprint = 'expectedFingerprint'; const expectedDefaultBranch = 'expectedDefaultBranch'; - jest.spyOn(ScmClient.prototype, 'getRepo').mockResolvedValueOnce(repo); - jest - .spyOn(ScmClient.prototype, 'getDefaultBranch') - .mockResolvedValueOnce(expectedDefaultBranch); + httpMock + .scope(endpoint) + .get(`/repositories/${repository}`) + .reply(200, repo); + httpMock + .scope(endpoint) + .get(`/config/git/${repository}/default-branch`) + .reply(200, { defaultBranch: expectedDefaultBranch }); util.repoFingerprint.mockReturnValueOnce(expectedFingerprint); @@ -156,14 +161,21 @@ describe('modules/platform/scm-manager/index', () => { describe(getRepos, () => { it('should return all available repos', async () => { - jest - .spyOn(ScmClient.prototype, 'getAllRepos') - .mockResolvedValueOnce([ - repo, - { ...repo, namespace: 'other', name: 'repository' }, - { ...repo, namespace: 'other', name: 'mercurial', type: 'hg' }, - { ...repo, namespace: 'other', name: 'subversion', type: 'svn' }, - ]); + httpMock + .scope(endpoint) + .get(`/repositories?pageSize=1000000`) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + repositories: [ + repo, + { ...repo, namespace: 'other', name: 'repository' }, + { ...repo, namespace: 'other', name: 'mercurial', type: 'hg' }, + { ...repo, namespace: 'other', name: 'subversion', type: 'svn' }, + ], + }, + }); expect(await getRepos()).toEqual(['default/repo', 'other/repository']); }); @@ -171,9 +183,18 @@ describe('modules/platform/scm-manager/index', () => { describe(getPrList, () => { it('should return empty array, because no PR could be found', async () => { - jest - .spyOn(ScmClient.prototype, 'getAllRepoPrs') - .mockRejectedValue(new Error()); + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + pullRequests: [], + }, + }); expect(await getPrList()).toIncludeAllMembers([]); }); @@ -194,9 +215,18 @@ describe('modules/platform/scm-manager/index', () => { }, ]; - jest - .spyOn(ScmClient.prototype, 'getAllRepoPrs') - .mockResolvedValueOnce([pullRequest]); + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + pullRequests: [pullRequest], + }, + }); //Fetching from client expect(await getPrList()).toIncludeAllMembers(expectedResult); @@ -207,9 +237,18 @@ describe('modules/platform/scm-manager/index', () => { describe(findPr, () => { it('search in pull request without explicitly setting the state as argument', async () => { - jest - .spyOn(ScmClient.prototype, 'getAllRepoPrs') - .mockResolvedValueOnce([pullRequest]); + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + pullRequests: [pullRequest], + }, + }); expect( await findPr({ @@ -244,9 +283,18 @@ describe('modules/platform/scm-manager/index', () => { state: string, result: Pr | null, ) => { - jest - .spyOn(ScmClient.prototype, 'getAllRepoPrs') - .mockResolvedValueOnce(availablePullRequest); + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + pullRequests: availablePullRequest, + }, + }); expect( await findPr({ @@ -271,9 +319,18 @@ describe('modules/platform/scm-manager/index', () => { branchName: string, result: Pr | null, ) => { - jest - .spyOn(ScmClient.prototype, 'getAllRepoPrs') - .mockResolvedValueOnce(availablePullRequest); + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + pullRequests: availablePullRequest, + }, + }); expect(await getBranchPr(branchName)).toEqual(result); }, @@ -282,39 +339,65 @@ describe('modules/platform/scm-manager/index', () => { describe(getPr, () => { it('should return null, because PR was not found', async () => { - jest - .spyOn(ScmClient.prototype, 'getAllRepoPrs') - .mockResolvedValueOnce([]); + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + pullRequests: [], + }, + }); - jest - .spyOn(ScmClient.prototype, 'getRepoPr') - .mockRejectedValue(new Error('Not found')); + httpMock + .scope(endpoint) + .get(`/pull-requests/${repo.namespace}/${repo.name}/${pullRequest.id}`) + .reply(404); expect(await getPr(1)).toBeNull(); }); - it.each([ - [[], pullRequest, 1, renovatePr], - [[pullRequest], pullRequest, 1, renovatePr], - ])( - 'search within %p for %p with result %p', - async ( - availablePullRequest: PullRequest[], - pullRequestById: PullRequest, - prId: number, - result: Pr | null, - ) => { - jest - .spyOn(ScmClient.prototype, 'getAllRepoPrs') - .mockResolvedValueOnce(availablePullRequest); + it('should return pr from cache', async () => { + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + pullRequests: [pullRequest], + }, + }); - jest - .spyOn(ScmClient.prototype, 'getRepoPr') - .mockResolvedValueOnce(pullRequestById); + expect(await getPr(parseInt(pullRequest.id))).toEqual(renovatePr); + }); - expect(await getPr(prId)).toEqual(result); - }, - ); + it('should return fetched pr', async () => { + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(200, { + page: 0, + pageTotal: 1, + _embedded: { + pullRequests: [], + }, + }); + + httpMock + .scope(endpoint) + .get(`/pull-requests/${repo.namespace}/${repo.name}/${pullRequest.id}`) + .reply(200, pullRequest); + + expect(await getPr(parseInt(pullRequest.id))).toEqual(renovatePr); + }); }); describe(createPr, () => { @@ -329,30 +412,28 @@ describe('modules/platform/scm-manager/index', () => { expectedState: string, expectedIsDraft: boolean, ) => { - jest - .spyOn(ScmClient.prototype, 'createPr') - .mockImplementationOnce( - (_repoPath: string, createParams: PullRequestCreateParams) => { - return Promise.resolve({ - id: '1337', - source: createParams.source, - target: createParams.target, - title: createParams.title, - description: createParams.description ?? '', - creationDate: '2023-01-01T13:37:00.000Z', - status: createParams.status ?? 'OPEN', - labels: [], - tasks: { todo: 0, done: 0 }, - _links: {}, - _embedded: { - defaultConfig: { - mergeStrategy: 'FAST_FORWARD_IF_POSSIBLE', - deleteBranchOnMerge: false, - }, - }, - }); + httpMock.scope(endpoint).post(`/pull-requests/${repo.namespace}/${repo.name}`).reply(201, undefined, { + location: `${endpoint}/pull-requests/${repo.namespace}/${repo.name}/1337`, + }); + + httpMock.scope(endpoint).get(`/pull-requests/${repo.namespace}/${repo.name}/1337`).reply(200, { + id: '1337', + source: 'feature/test', + target: 'develop', + title: 'PR Title', + description: 'PR Body', + creationDate: '2023-01-01T13:37:00.000Z', + status: draftPR ? 'DRAFT' : 'OPEN', + labels: [], + tasks: { todo: 0, done: 0 }, + _links: {}, + _embedded: { + defaultConfig: { + mergeStrategy: 'FAST_FORWARD_IF_POSSIBLE', + deleteBranchOnMerge: false, }, - ); + }, + }); expect( await createPr({ @@ -392,9 +473,10 @@ describe('modules/platform/scm-manager/index', () => { actualPrBody: string | undefined, expectedPrBody: string | undefined, ) => { - jest - .spyOn(ScmClient.prototype, 'updatePr') - .mockImplementationOnce(() => Promise.resolve()); + httpMock + .scope(endpoint) + .put(`/pull-requests/${repo.namespace}/${repo.name}/1`) + .reply(204) await updatePr({ number: 1, @@ -405,13 +487,8 @@ describe('modules/platform/scm-manager/index', () => { }); expect( - jest.spyOn(ScmClient.prototype, 'updatePr'), - ).toHaveBeenCalledWith('default/repo', 1, { - description: expectedPrBody, - status: expectedState, - target: 'Target/Branch', - title: 'PR Title', - }); + httpMock.allUsed() + ).toBeTrue(); }, ); }); diff --git a/lib/modules/platform/scm-manager/scm-client.ts b/lib/modules/platform/scm-manager/scm-client.ts index 8409b7a7f2..88d6576ec6 100644 --- a/lib/modules/platform/scm-manager/scm-client.ts +++ b/lib/modules/platform/scm-manager/scm-client.ts @@ -19,7 +19,7 @@ import type { } from './types'; const URLS = { - ME: '/me', + ME: 'me', ALL_REPOS: 'repositories?pageSize=1000000', REPO: (repoPath: string) => `repositories/${repoPath}`, PULLREQUESTS: (repoPath: string) => `pull-requests/${repoPath}`, From eb0a6a84ad95e4aace51940c1d0483c18d79030b Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Fri, 23 Feb 2024 14:08:38 +0100 Subject: [PATCH 13/61] Update docs/usage/getting-started/running.md Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- docs/usage/getting-started/running.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/getting-started/running.md b/docs/usage/getting-started/running.md index a132daef73..7e6607c281 100644 --- a/docs/usage/getting-started/running.md +++ b/docs/usage/getting-started/running.md @@ -208,7 +208,7 @@ Read the platform-specific docs to learn how to setup authentication on your pla - [Gitea and Forgejo](../modules/platform/gitea/index.md) - [github.com and GitHub Enterprise Server](../modules/platform/github/index.md) - [GitLab](../modules/platform/gitlab/index.md) -- [SCM-Manager](../modules/platform/scmm/index.md) +- [SCM-Manager](../modules/platform/scm-manager/index.md) ### GitHub.com token for changelogs From c1cab0426d9f2457bfa3c626a302af3ae680b6f5 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Fri, 23 Feb 2024 14:10:49 +0100 Subject: [PATCH 14/61] Update lib/modules/platform/scm-manager/index.ts Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 00df590ffe..8ef3b225b7 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -156,7 +156,7 @@ export async function getPr(number: number): Promise { logger.info(`Returning PR from API, ${JSON.stringify(result)}`); return mapPrFromScmToRenovate(result); } catch (error) { - logger.info(`Not found PR with id ${number}`); + logger.info(`Can not find a PR with id ${number}`); return null; } } From 62ab7bcebd1be74bdc8ba964d8b7e9b63119ea08 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Fri, 23 Feb 2024 14:13:48 +0100 Subject: [PATCH 15/61] Update lib/modules/platform/scm-manager/mapper.spec.ts Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/mapper.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/mapper.spec.ts b/lib/modules/platform/scm-manager/mapper.spec.ts index ef15721e36..ac0116eaf2 100644 --- a/lib/modules/platform/scm-manager/mapper.spec.ts +++ b/lib/modules/platform/scm-manager/mapper.spec.ts @@ -2,7 +2,7 @@ import { mapPrFromScmToRenovate } from './mapper'; import type { PullRequest as SCMPullRequest } from './types'; describe('modules/platform/scm-manager/mapper', () => { - it('should correctly map the scm type of a PR to the Renovate PR type', () => { + it('should correctly map the scm-manager type of a PR to the Renovate PR type', () => { const scmPr: SCMPullRequest = { source: 'feat/new', target: 'develop', From cba62ca18111d91ff19938ff7365e8d2186d869a Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Fri, 23 Feb 2024 14:14:03 +0100 Subject: [PATCH 16/61] Update lib/modules/platform/scm-manager/readme.md Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index 909bd101ea..6027a29707 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -4,9 +4,9 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. ## Authentication -1. Create an API Key for your technical Renovate user in SCM-Manager. -2. The technical user _must_ have valid name and email address. -3. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it. +1. Create an API Key for your technical Renovate user in SCM-Manager +1. The technical user _must_ have a valid name and email address +1. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it You must set the [`platform`](https://docs.renovatebot.com/self-hosted-configuration/#platform) to `scm-manager` in your Renovate config file. From 5b99a0608de2d3eb4f2d5e8d0d4c007ae4bec958 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Fri, 23 Feb 2024 14:14:32 +0100 Subject: [PATCH 17/61] Update lib/modules/platform/scm-manager/readme.md Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index 6027a29707..fa391a4834 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -8,7 +8,7 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. 1. The technical user _must_ have a valid name and email address 1. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it -You must set the [`platform`](https://docs.renovatebot.com/self-hosted-configuration/#platform) to `scm-manager` in your Renovate config file. +You must set the [`platform` config option](https://docs.renovatebot.com/self-hosted-configuration/#platform) to `scm-manager` in your Renovate config file. The technical user needs the permissions to read and write your repository. This can be achieved by granting the permission role "OWNER" to your technical Renovate user. From 22b3bc84309b9a3921f24ce91217fb2072954d7f Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Fri, 23 Feb 2024 14:14:52 +0100 Subject: [PATCH 18/61] Update lib/modules/platform/scm-manager/readme.md Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index fa391a4834..28b2f66dfc 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -10,7 +10,7 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. You must set the [`platform` config option](https://docs.renovatebot.com/self-hosted-configuration/#platform) to `scm-manager` in your Renovate config file. -The technical user needs the permissions to read and write your repository. +The technical user must have permission to read and write to your repository. This can be achieved by granting the permission role "OWNER" to your technical Renovate user. Additionally, the Review Plugin needs to be installed. Otherwise, the pull request API will not be available. From 53bdd87859dfe901f882d9901e2cb86e0481232b Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Fri, 23 Feb 2024 14:15:08 +0100 Subject: [PATCH 19/61] Update lib/modules/platform/scm-manager/readme.md Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index 28b2f66dfc..91ad550bcf 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -11,7 +11,7 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. You must set the [`platform` config option](https://docs.renovatebot.com/self-hosted-configuration/#platform) to `scm-manager` in your Renovate config file. The technical user must have permission to read and write to your repository. -This can be achieved by granting the permission role "OWNER" to your technical Renovate user. +You can do this by granting the permission role "OWNER" to the technical Renovate user. Additionally, the Review Plugin needs to be installed. Otherwise, the pull request API will not be available. Plugins can be installed under Administration -> Plugins -> Available. From 376c79ac28b3d6886c763fd95be65ed1a7e57e26 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Fri, 23 Feb 2024 14:15:26 +0100 Subject: [PATCH 20/61] Update lib/modules/platform/scm-manager/readme.md Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index 91ad550bcf..f56e055568 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -16,6 +16,6 @@ Additionally, the Review Plugin needs to be installed. Otherwise, the pull request API will not be available. Plugins can be installed under Administration -> Plugins -> Available. -Renovate supports SCM-Manager major version 2.x and 3.x. -The 2.x is supported since 2.48.0. -The 3.x is supported since 3.0.0. +Renovate supports SCM-Manager major version `2.x` and `3.x`. +The minimum version for the `2.x` range is `2.48.0`. +The minimum version for the `3.x` range is `3.0.0`. From ae532315e0fbc3e650d3c03fd73b05c3bcc45668 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Fri, 23 Feb 2024 14:18:34 +0100 Subject: [PATCH 21/61] Fix scm-manager docs link --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 2e7bfd953c..bcf592e807 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ Renovate works on these platforms: - [AWS CodeCommit](https://docs.renovatebot.com/modules/platform/codecommit/) - [Gitea and Forgejo](https://docs.renovatebot.com/modules/platform/gitea/) - [Gerrit (experimental)](https://docs.renovatebot.com/modules/platform/gerrit/) -- [SCM-Manager](https://scm-manager.org/) +- [SCM-Manager](https://docs.renovatebot.com/modules/platform/scm-manager/) ## Who Uses Renovate? From 5431ffc310e71724257fce7527d9198b50858e96 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 2 May 2024 11:08:17 +0200 Subject: [PATCH 22/61] Fix linting --- .../platform/scm-manager/index.spec.ts | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 584c47615d..d716577413 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -412,28 +412,34 @@ describe('modules/platform/scm-manager/index', () => { expectedState: string, expectedIsDraft: boolean, ) => { - httpMock.scope(endpoint).post(`/pull-requests/${repo.namespace}/${repo.name}`).reply(201, undefined, { - location: `${endpoint}/pull-requests/${repo.namespace}/${repo.name}/1337`, - }); + httpMock + .scope(endpoint) + .post(`/pull-requests/${repo.namespace}/${repo.name}`) + .reply(201, undefined, { + location: `${endpoint}/pull-requests/${repo.namespace}/${repo.name}/1337`, + }); - httpMock.scope(endpoint).get(`/pull-requests/${repo.namespace}/${repo.name}/1337`).reply(200, { - id: '1337', - source: 'feature/test', - target: 'develop', - title: 'PR Title', - description: 'PR Body', - creationDate: '2023-01-01T13:37:00.000Z', - status: draftPR ? 'DRAFT' : 'OPEN', - labels: [], - tasks: { todo: 0, done: 0 }, - _links: {}, - _embedded: { - defaultConfig: { - mergeStrategy: 'FAST_FORWARD_IF_POSSIBLE', - deleteBranchOnMerge: false, + httpMock + .scope(endpoint) + .get(`/pull-requests/${repo.namespace}/${repo.name}/1337`) + .reply(200, { + id: '1337', + source: 'feature/test', + target: 'develop', + title: 'PR Title', + description: 'PR Body', + creationDate: '2023-01-01T13:37:00.000Z', + status: draftPR ? 'DRAFT' : 'OPEN', + labels: [], + tasks: { todo: 0, done: 0 }, + _links: {}, + _embedded: { + defaultConfig: { + mergeStrategy: 'FAST_FORWARD_IF_POSSIBLE', + deleteBranchOnMerge: false, + }, }, - }, - }); + }); expect( await createPr({ @@ -476,7 +482,7 @@ describe('modules/platform/scm-manager/index', () => { httpMock .scope(endpoint) .put(`/pull-requests/${repo.namespace}/${repo.name}/1`) - .reply(204) + .reply(204); await updatePr({ number: 1, @@ -486,9 +492,7 @@ describe('modules/platform/scm-manager/index', () => { targetBranch: 'Target/Branch', }); - expect( - httpMock.allUsed() - ).toBeTrue(); + expect(httpMock.allUsed()).toBeTrue(); }, ); }); From 404f77d4242657d476b1764f5036d9936e6a6e1b Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 2 May 2024 11:12:32 +0200 Subject: [PATCH 23/61] Fix linting --- lib/modules/platform/scm-manager/index.spec.ts | 2 -- lib/modules/platform/scm-manager/scm-client.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index d716577413..6f712ef881 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -4,11 +4,9 @@ import * as hostRules from '../../../util/host-rules'; import type { Pr } from '../types'; import * as _util from '../util'; import { mapPrFromScmToRenovate } from './mapper'; -import ScmClient from './scm-client'; import type { PrFilterByState, PullRequest, - PullRequestCreateParams, Repo, User, } from './types'; diff --git a/lib/modules/platform/scm-manager/scm-client.ts b/lib/modules/platform/scm-manager/scm-client.ts index 88d6576ec6..2ff1196453 100644 --- a/lib/modules/platform/scm-manager/scm-client.ts +++ b/lib/modules/platform/scm-manager/scm-client.ts @@ -1,6 +1,5 @@ import { Http } from '../../../util/http'; import type { - HttpRequestOptions, HttpResponse, InternalHttpOptions, } from '../../../util/http/types'; @@ -48,7 +47,7 @@ export default class ScmClient extends Http { protected override async request( requestUrl: string | URL, - options?: InternalHttpOptions & ScmmHttpOptions & HttpRequestOptions, + options?: InternalHttpOptions & ScmmHttpOptions, ): Promise> { const opts = { ...options, From 3e1a72138b7a7f6b6660a93718fdbc9a766e483b Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 2 May 2024 13:35:52 +0200 Subject: [PATCH 24/61] Throw errors if username or token is not provided by the host rules --- .../platform/scm-manager/index.spec.ts | 35 +++++++++++++++---- lib/modules/platform/scm-manager/index.ts | 16 ++++++--- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 6f712ef881..76321dbb7f 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -4,12 +4,7 @@ import * as hostRules from '../../../util/host-rules'; import type { Pr } from '../types'; import * as _util from '../util'; import { mapPrFromScmToRenovate } from './mapper'; -import type { - PrFilterByState, - PullRequest, - Repo, - User, -} from './types'; +import type { PrFilterByState, PullRequest, Repo, User } from './types'; import { addAssignees, addReviewers, @@ -150,11 +145,26 @@ describe('modules/platform/scm-manager/index', () => { }); expect(git.initRepo).toHaveBeenCalledWith({ - url: `https://${user.username}@localhost:8080/scm/default/repo`, + url: `https://${user.username}:${token}@localhost:8080/scm/default/repo`, repository, defaultBranch: expectedDefaultBranch, }); }); + + it('should throw error, because username is not provided in host rules', async () => { + hostRules.clear(); + await expect( + initRepo({ repository: `${repo.namespace}/${repo.name}` }), + ).rejects.toThrow('Username is not provided'); + }); + + it('should throw error, because token is not provided in host rules', async () => { + hostRules.clear(); + hostRules.add({username: user.username}); + await expect( + initRepo({ repository: `${repo.namespace}/${repo.name}` }), + ).rejects.toThrow('Token is not provided'); + }); }); describe(getRepos, () => { @@ -197,6 +207,17 @@ describe('modules/platform/scm-manager/index', () => { expect(await getPrList()).toIncludeAllMembers([]); }); + it('should return empty array, because api request failed', async () => { + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, + ) + .reply(400); + + expect(await getPrList()).toIncludeAllMembers([]); + }); + it('should return all PRs of a repo', async () => { const expectedResult: Pr[] = [ { diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 8ef3b225b7..e2771cbb6d 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -64,15 +64,23 @@ export async function initRepo({ repository, gitUrl, }: RepoParams): Promise { + const hostOptions = hostRules.find({hostType: id, url: scmmClient.getEndpoint()}); + + if (!hostOptions.username) { + throw new Error('Username is not provided'); + } + + if (!hostOptions.token) { + throw new Error('Token is not provided'); + } + const repo = await scmmClient.getRepo(repository); const defaultBranch = await scmmClient.getDefaultBranch(repo); const url = getRepoUrl( repo, gitUrl, - /* istanbul ignore next */ - hostRules.find({ hostType: id, url: scmmClient.getEndpoint() }).username ?? - '', - process.env.RENOVATE_TOKEN ?? '', + hostOptions.username, + hostOptions.token ); config = {} as any; From 808c7d4207dbb9cdb97656303e52a77451c01229 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 2 May 2024 13:37:41 +0200 Subject: [PATCH 25/61] Replace NO-OP with Not implemented --- .../platform/scm-manager/index.spec.ts | 30 +++++++++---------- lib/modules/platform/scm-manager/index.ts | 30 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 76321dbb7f..bd93717d0b 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -517,21 +517,21 @@ describe('modules/platform/scm-manager/index', () => { }); describe(mergePr, () => { - it('should no-op and return false', async () => { + it('should Not implemented and return false', async () => { const result = await mergePr({ id: 1 }); expect(result).toBeFalse(); }); }); describe(getBranchStatus, () => { - it('should no-op and return red', async () => { + it('should Not implemented and return red', async () => { const result = await getBranchStatus('test/branch', false); expect(result).toBe('red'); }); }); describe(setBranchStatus, () => { - it('should no-op', async () => { + it('should Not implemented', async () => { await expect( setBranchStatus({ branchName: 'test/branch', @@ -544,59 +544,59 @@ describe('modules/platform/scm-manager/index', () => { }); describe(getBranchStatusCheck, () => { - it('should no-op and return null', async () => { + it('should Not implemented and return null', async () => { const result = await getBranchStatusCheck('test/branch', null); expect(result).toBeNull(); }); }); describe(addReviewers, () => { - it('should no-op', async () => { + it('should Not implemented', async () => { await expect(addReviewers(1, ['reviewer'])).resolves.not.toThrow(); }); }); describe(addAssignees, () => { - it('should no-op', async () => { + it('should Not implemented', async () => { await expect(addAssignees(1, ['assignee'])).resolves.not.toThrow(); }); }); describe(deleteLabel, () => { - it('should no-op', async () => { + it('should Not implemented', async () => { await expect(deleteLabel(1, 'label')).resolves.not.toThrow(); }); }); describe(getIssueList, () => { - it('should no-op and return empty list', async () => { + it('should Not implemented and return empty list', async () => { const result = await getIssueList(); expect(result).toEqual([]); }); }); describe(findIssue, () => { - it('should no-op and return null', async () => { + it('should Not implemented and return null', async () => { const result = await findIssue('issue'); expect(result).toBeNull(); }); }); describe(ensureIssue, () => { - it('should no-op and return null', async () => { + it('should Not implemented and return null', async () => { const result = await ensureIssue({ title: 'issue', body: 'body' }); expect(result).toBeNull(); }); }); describe(ensureIssueClosing, () => { - it('should no-op', async () => { + it('should Not implemented', async () => { await expect(ensureIssueClosing('issue')).resolves.not.toThrow(); }); }); describe(ensureCommentRemoval, () => { - it('should no-op', async () => { + it('should Not implemented', async () => { await expect( ensureCommentRemoval({ type: 'by-content', @@ -615,21 +615,21 @@ describe('modules/platform/scm-manager/index', () => { }); describe(getRepoForceRebase, () => { - it('should no-op and return false', async () => { + it('should Not implemented and return false', async () => { const result = await getRepoForceRebase(); expect(result).toBeFalse(); }); }); describe(getRawFile, () => { - it('should no-op and return null', async () => { + it('should Not implemented and return null', async () => { const result = await getRawFile('file'); expect(result).toBeNull(); }); }); describe(getJsonFile, () => { - it('should no-op and return undefined', async () => { + it('should Not implemented and return undefined', async () => { const result = await getJsonFile('package.json'); expect(result).toBeUndefined(); }); diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index e2771cbb6d..57ac7ff85f 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -224,7 +224,7 @@ export async function updatePr({ } export function mergePr(config: MergePRConfig): Promise { - logger.debug('NO-OP mergePr'); + logger.debug('Not implemented mergePr'); return Promise.resolve(false); } @@ -232,14 +232,14 @@ export function getBranchStatus( branchName: string, internalChecksAsSuccess: boolean, ): Promise { - logger.debug('NO-OP getBranchStatus'); + logger.debug('Not implemented getBranchStatus'); return Promise.resolve('red'); } export function setBranchStatus( branchStatusConfig: BranchStatusConfig, ): Promise { - logger.debug('NO-OP setBranchStatus'); + logger.debug('Not implemented setBranchStatus'); return Promise.resolve(); } @@ -247,7 +247,7 @@ export function getBranchStatusCheck( branchName: string, context: string | null | undefined, ): Promise { - logger.debug('NO-OP setBranchStatus'); + logger.debug('Not implemented setBranchStatus'); return Promise.resolve(null); } @@ -255,7 +255,7 @@ export function addReviewers( number: number, reviewers: string[], ): Promise { - logger.debug('NO-OP addReviewers'); + logger.debug('Not implemented addReviewers'); return Promise.resolve(); } @@ -263,40 +263,40 @@ export function addAssignees( number: number, assignees: string[], ): Promise { - logger.debug('NO-OP addAssignees'); + logger.debug('Not implemented addAssignees'); return Promise.resolve(); } export function deleteLabel(number: number, label: string): Promise { - logger.debug('NO-OP deleteLabel'); + logger.debug('Not implemented deleteLabel'); return Promise.resolve(); } export function getIssueList(): Promise { - logger.debug('NO-OP getIssueList'); + logger.debug('Not implemented getIssueList'); return Promise.resolve([]); } export function findIssue(title: string): Promise { - logger.debug('NO-OP findIssue'); + logger.debug('Not implemented findIssue'); return Promise.resolve(null); } export function ensureIssue( config: EnsureIssueConfig, ): Promise<'updated' | 'created' | null> { - logger.debug('NO-OP ensureIssue'); + logger.debug('Not implemented ensureIssue'); return Promise.resolve(null); } export function ensureIssueClosing(title: string): Promise { - logger.debug('NO-OP ensureIssueClosing'); + logger.debug('Not implemented ensureIssueClosing'); return Promise.resolve(); } /* istanbul ignore next */ export function ensureComment(config: EnsureCommentConfig): Promise { - logger.debug('NO-OP ensureComment'); + logger.debug('Not implemented ensureComment'); return Promise.resolve(false); } @@ -305,7 +305,7 @@ export function ensureCommentRemoval( | EnsureCommentRemovalConfigByTopic | EnsureCommentRemovalConfigByContent, ): Promise { - logger.debug('NO-OP ensureCommentRemoval'); + logger.debug('Not implemented ensureCommentRemoval'); return Promise.resolve(); } @@ -322,7 +322,7 @@ export function getRawFile( repoName?: string, branchOrTag?: string, ): Promise { - logger.debug('NO-OP getRawFile'); + logger.debug('Not implemented getRawFile'); return Promise.resolve(null); } @@ -331,7 +331,7 @@ export function getJsonFile( repoName?: string, branchOrTag?: string, ): Promise { - logger.debug('NO-OP getJsonFile'); + logger.debug('Not implemented getJsonFile'); return Promise.resolve(undefined); } From af6d95940a9008125a9726df065f1e4cc78c500a Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 2 May 2024 13:40:49 +0200 Subject: [PATCH 26/61] Remove objects from log messages --- lib/modules/platform/scm-manager/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 57ac7ff85f..91381dea66 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -155,16 +155,16 @@ export async function getPr(number: number): Promise { const cachedPr = inProgressPrs.find((pr) => pr.number === number); if (cachedPr) { - logger.info(`Returning from cached PRs, ${JSON.stringify(cachedPr)}`); + logger.info('Returning from cached PRs'); return cachedPr; } try { const result = await scmmClient.getRepoPr(config.repository, number); - logger.info(`Returning PR from API, ${JSON.stringify(result)}`); + logger.info('Returning PR from API'); return mapPrFromScmToRenovate(result); } catch (error) { - logger.info(`Can not find a PR with id ${number}`); + logger.error(`Can not find a PR with id ${number}`); return null; } } @@ -201,7 +201,6 @@ export async function createPr({ logger.info( `PR created with title '${createdPr.title}' from source '${createdPr.source}' to target '${createdPr.target}'`, ); - logger.debug(`PR created ${JSON.stringify(createdPr)}`); return mapPrFromScmToRenovate(createdPr); } From a733bdb7d5a51fbdf1931302aa1377bf5ce28d84 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 2 May 2024 13:44:00 +0200 Subject: [PATCH 27/61] Fix linting --- lib/modules/platform/scm-manager/index.spec.ts | 2 +- lib/modules/platform/scm-manager/index.ts | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index bd93717d0b..50ae2620a5 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -160,7 +160,7 @@ describe('modules/platform/scm-manager/index', () => { it('should throw error, because token is not provided in host rules', async () => { hostRules.clear(); - hostRules.add({username: user.username}); + hostRules.add({ username: user.username }); await expect( initRepo({ repository: `${repo.namespace}/${repo.name}` }), ).rejects.toThrow('Token is not provided'); diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 91381dea66..803ce692a1 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -64,7 +64,10 @@ export async function initRepo({ repository, gitUrl, }: RepoParams): Promise { - const hostOptions = hostRules.find({hostType: id, url: scmmClient.getEndpoint()}); + const hostOptions = hostRules.find({ + hostType: id, + url: scmmClient.getEndpoint(), + }); if (!hostOptions.username) { throw new Error('Username is not provided'); @@ -76,12 +79,7 @@ export async function initRepo({ const repo = await scmmClient.getRepo(repository); const defaultBranch = await scmmClient.getDefaultBranch(repo); - const url = getRepoUrl( - repo, - gitUrl, - hostOptions.username, - hostOptions.token - ); + const url = getRepoUrl(repo, gitUrl, hostOptions.username, hostOptions.token); config = {} as any; config.repository = repository; From 0f11d6bf0a271e063451d045f037ac2accaf00e7 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 6 May 2024 08:36:27 +0200 Subject: [PATCH 28/61] Fix path to platform config options --- lib/modules/platform/scm-manager/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index f56e055568..0d28bfa3dc 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -8,7 +8,7 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. 1. The technical user _must_ have a valid name and email address 1. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it -You must set the [`platform` config option](https://docs.renovatebot.com/self-hosted-configuration/#platform) to `scm-manager` in your Renovate config file. +You must set the [`platform` config option](../../../../docs/usage/self-hosted-configuration#platform) to `scm-manager` in your Renovate config file. The technical user must have permission to read and write to your repository. You can do this by granting the permission role "OWNER" to the technical Renovate user. From f598a6069c8f16443736c8e89191584b825727a9 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Tue, 7 May 2024 13:28:14 +0200 Subject: [PATCH 29/61] Fix link to self-host-configuration docs --- lib/modules/platform/scm-manager/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index 0d28bfa3dc..0f8532d829 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -8,7 +8,7 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. 1. The technical user _must_ have a valid name and email address 1. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it -You must set the [`platform` config option](../../../../docs/usage/self-hosted-configuration#platform) to `scm-manager` in your Renovate config file. +You must set the [`platform` config option](/usage/self-hosted-configuration#platform) to `scm-manager` in your Renovate config file. The technical user must have permission to read and write to your repository. You can do this by granting the permission role "OWNER" to the technical Renovate user. From 4f89f29ff2cd90c221a60ba704aa0f2685b01708 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Tue, 7 May 2024 13:29:11 +0200 Subject: [PATCH 30/61] Fix link to self-host-configuration docs --- lib/modules/platform/scm-manager/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index 0f8532d829..ff0751d6f9 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -8,7 +8,7 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. 1. The technical user _must_ have a valid name and email address 1. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it -You must set the [`platform` config option](/usage/self-hosted-configuration#platform) to `scm-manager` in your Renovate config file. +You must set the [`platform` config option](/usage/self-hosted-configuration) to `scm-manager` in your Renovate config file. The technical user must have permission to read and write to your repository. You can do this by granting the permission role "OWNER" to the technical Renovate user. From 6d736cf8191cfda0e573a7b8fac58d5b3814b139 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Tue, 7 May 2024 15:37:10 +0200 Subject: [PATCH 31/61] Remove the link to the platform config option, because the tests keep failing --- lib/modules/platform/scm-manager/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index ff0751d6f9..7c95cc96c1 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -8,7 +8,7 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. 1. The technical user _must_ have a valid name and email address 1. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it -You must set the [`platform` config option](/usage/self-hosted-configuration) to `scm-manager` in your Renovate config file. +You must set the `platform` config option to `scm-manager` in your Renovate config file. The technical user must have permission to read and write to your repository. You can do this by granting the permission role "OWNER" to the technical Renovate user. From 2c92044108c9ffa516297fa7c8a41d0dc517f8d7 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 24 Jul 2024 10:16:09 +0200 Subject: [PATCH 32/61] Fix link to self-host-configuration docs --- lib/modules/platform/scm-manager/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index 7c95cc96c1..75a8cacba6 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -8,7 +8,7 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. 1. The technical user _must_ have a valid name and email address 1. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it -You must set the `platform` config option to `scm-manager` in your Renovate config file. +You must set the [`platform`](../../../self-hosted-configuration.md#platform) config option to `scm-manager` in your Renovate config file. The technical user must have permission to read and write to your repository. You can do this by granting the permission role "OWNER" to the technical Renovate user. From 19edcb32d065b597eadb236ac99cd4128dda41b5 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Wed, 24 Jul 2024 10:18:03 +0200 Subject: [PATCH 33/61] Remove not needed type cast Co-authored-by: Michael Kriese --- lib/modules/platform/scm-manager/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/utils.ts b/lib/modules/platform/scm-manager/utils.ts index 6b6a5a5bc6..22f13216c5 100644 --- a/lib/modules/platform/scm-manager/utils.ts +++ b/lib/modules/platform/scm-manager/utils.ts @@ -51,7 +51,7 @@ export function getRepoUrl( username: string, password: string, ): string { - const protocolLinks = repo._links.protocol as Link[] | undefined; + const protocolLinks = repo._links.protocol; if (!protocolLinks) { throw new Error('Missing protocol links.'); } From bd0e4f4d10f371baeecfe8fe717b0f8bc5c5ee98 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 24 Jul 2024 10:23:03 +0200 Subject: [PATCH 34/61] Fix type check of protocol links --- lib/modules/platform/scm-manager/utils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/modules/platform/scm-manager/utils.ts b/lib/modules/platform/scm-manager/utils.ts index 22f13216c5..80e9003ad4 100644 --- a/lib/modules/platform/scm-manager/utils.ts +++ b/lib/modules/platform/scm-manager/utils.ts @@ -52,10 +52,15 @@ export function getRepoUrl( password: string, ): string { const protocolLinks = repo._links.protocol; + if (!protocolLinks) { throw new Error('Missing protocol links.'); } + if (!Array.isArray(protocolLinks)) { + throw new Error('Expected protocol links to be an array of links.') + } + if (gitUrl === 'ssh') { const sshUrl = protocolLinks.find((l) => l.name === 'ssh')?.href; if (!sshUrl) { From 2b20b20501ba37e304b351d7f9bc163c431b6864 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 24 Jul 2024 10:43:43 +0200 Subject: [PATCH 35/61] Move scm-manager into util http folder --- lib/modules/platform/scm-manager/index.ts | 26 ++++---- lib/modules/platform/scm-manager/types.ts | 6 -- lib/modules/platform/scm-manager/utils.ts | 2 +- .../http/scm-manager.spec.ts} | 62 +++++++++---------- .../http/scm-manager.ts} | 22 ++++--- 5 files changed, 58 insertions(+), 60 deletions(-) rename lib/{modules/platform/scm-manager/scm-client.spec.ts => util/http/scm-manager.spec.ts} (79%) rename lib/{modules/platform/scm-manager/scm-client.ts => util/http/scm-manager.ts} (91%) diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 803ce692a1..e88104c096 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -23,7 +23,7 @@ import type { import { repoFingerprint } from '../util'; import { smartTruncate } from '../utils/pr-body'; import { mapPrFromScmToRenovate } from './mapper'; -import ScmClient from './scm-client'; +import ScmManagerHttp from '../../../util/http/scm-manager'; import { getRepoUrl, mapPrState, matchPrState, smartLinks } from './utils'; interface SCMMRepoConfig { @@ -35,7 +35,7 @@ interface SCMMRepoConfig { export const id = 'scm-manager'; let config: SCMMRepoConfig = {} as any; -let scmmClient: ScmClient; +let scmManagerHttp: ScmManagerHttp; export async function initPlatform({ endpoint, @@ -49,9 +49,9 @@ export async function initPlatform({ throw new Error('SCM-Manager API token not configured'); } - scmmClient = new ScmClient(endpoint, token); + scmManagerHttp = new ScmManagerHttp(endpoint, token); - const me = await scmmClient.getCurrentUser(); + const me = await scmManagerHttp.getCurrentUser(); const gitAuthor = `${me.displayName} <${me.mail}>`; const result = { endpoint, gitAuthor }; @@ -66,7 +66,7 @@ export async function initRepo({ }: RepoParams): Promise { const hostOptions = hostRules.find({ hostType: id, - url: scmmClient.getEndpoint(), + url: scmManagerHttp.getEndpoint(), }); if (!hostOptions.username) { @@ -77,8 +77,8 @@ export async function initRepo({ throw new Error('Token is not provided'); } - const repo = await scmmClient.getRepo(repository); - const defaultBranch = await scmmClient.getDefaultBranch(repo); + const repo = await scmManagerHttp.getRepo(repository); + const defaultBranch = await scmManagerHttp.getDefaultBranch(repo); const url = getRepoUrl(repo, gitUrl, hostOptions.username, hostOptions.token); config = {} as any; @@ -98,7 +98,7 @@ export async function initRepo({ isFork: false, repoFingerprint: repoFingerprint( config.repository, - scmmClient.getEndpoint(), + scmManagerHttp.getEndpoint(), ), }; @@ -108,7 +108,7 @@ export async function initRepo({ } export async function getRepos(): Promise { - const repos = (await scmmClient.getAllRepos()).filter( + const repos = (await scmManagerHttp.getAllRepos()).filter( (repo) => repo.type === 'git', ); const result = repos.map((repo) => `${repo.namespace}/${repo.name}`); @@ -158,7 +158,7 @@ export async function getPr(number: number): Promise { } try { - const result = await scmmClient.getRepoPr(config.repository, number); + const result = await scmManagerHttp.getRepoPr(config.repository, number); logger.info('Returning PR from API'); return mapPrFromScmToRenovate(result); } catch (error) { @@ -170,7 +170,7 @@ export async function getPr(number: number): Promise { export async function getPrList(): Promise { if (config.prList === null) { try { - config.prList = (await scmmClient.getAllRepoPrs(config.repository)).map( + config.prList = (await scmManagerHttp.getAllRepoPrs(config.repository)).map( (pr) => mapPrFromScmToRenovate(pr), ); } catch (error) { @@ -188,7 +188,7 @@ export async function createPr({ prBody, draftPR, }: CreatePRConfig): Promise { - const createdPr = await scmmClient.createPr(config.repository, { + const createdPr = await scmManagerHttp.createPr(config.repository, { source: sourceBranch, target: targetBranch, title: prTitle, @@ -210,7 +210,7 @@ export async function updatePr({ state, targetBranch, }: UpdatePrConfig): Promise { - await scmmClient.updatePr(config.repository, number, { + await scmManagerHttp.updatePr(config.repository, number, { title: prTitle, description: sanitize(prBody) ?? undefined, target: targetBranch, diff --git a/lib/modules/platform/scm-manager/types.ts b/lib/modules/platform/scm-manager/types.ts index f15774af5e..a2139716de 100644 --- a/lib/modules/platform/scm-manager/types.ts +++ b/lib/modules/platform/scm-manager/types.ts @@ -1,15 +1,9 @@ -import type { HttpOptions } from '../../../util/http/types'; - export type Page = { page: number; pageTotal: number; _embedded: T; }; -export interface ScmmHttpOptions extends HttpOptions { - scmmContentType?: string; -} - export interface Links { [link: string]: Link | Link[] | undefined; } diff --git a/lib/modules/platform/scm-manager/utils.ts b/lib/modules/platform/scm-manager/utils.ts index 80e9003ad4..084a1541ab 100644 --- a/lib/modules/platform/scm-manager/utils.ts +++ b/lib/modules/platform/scm-manager/utils.ts @@ -3,7 +3,7 @@ import { logger } from '../../../logger'; import { regEx } from '../../../util/regex'; import { parseUrl } from '../../../util/url'; import type { GitUrlOption, Pr } from '../types'; -import type { Link, PRMergeMethod, PrFilterByState, Repo } from './types'; +import type { PRMergeMethod, PrFilterByState, Repo } from './types'; export function mapPrState( state: 'open' | 'closed' | undefined, diff --git a/lib/modules/platform/scm-manager/scm-client.spec.ts b/lib/util/http/scm-manager.spec.ts similarity index 79% rename from lib/modules/platform/scm-manager/scm-client.spec.ts rename to lib/util/http/scm-manager.spec.ts index 8d0b4c699b..002987b6eb 100644 --- a/lib/modules/platform/scm-manager/scm-client.spec.ts +++ b/lib/util/http/scm-manager.spec.ts @@ -1,18 +1,18 @@ -import * as httpMock from '../../../../test/http-mock'; -import ScmClient from './scm-client'; +import * as httpMock from '../../../test/http-mock'; import type { PullRequest, PullRequestCreateParams, PullRequestUpdateParams, Repo, User, -} from './types'; +} from '../../modules/platform/scm-manager/types'; +import ScmManagerHttp from './scm-manager'; -describe('modules/platform/scm-manager/scm-client', () => { +describe('util/http/scm-manager', () => { const endpoint = 'http://localhost:8080/scm/api/v2'; const token = 'validApiToken'; - const scmClient = new ScmClient(endpoint, token); + const scmManagerHttp = new ScmManagerHttp(endpoint, token); const repo: Repo = { contact: 'test@test.com', @@ -55,13 +55,13 @@ describe('modules/platform/scm-manager/scm-client', () => { }, }; - describe(scmClient.getEndpoint, () => { + describe(scmManagerHttp.getEndpoint, () => { it('should return the endpoint', () => { - expect(scmClient.getEndpoint()).toEqual(endpoint); + expect(scmManagerHttp.getEndpoint()).toEqual(endpoint); }); }); - describe(scmClient.getCurrentUser, () => { + describe(scmManagerHttp.getCurrentUser, () => { it('should return the current user', async () => { const expectedUser: User = { mail: 'test@test.de', @@ -71,26 +71,26 @@ describe('modules/platform/scm-manager/scm-client', () => { httpMock.scope(endpoint).get('/me').reply(200, expectedUser); - expect(await scmClient.getCurrentUser()).toEqual(expectedUser); + expect(await scmManagerHttp.getCurrentUser()).toEqual(expectedUser); }); it.each([[401, 500]])( 'should throw %p response', async (response: number) => { httpMock.scope(endpoint).get('/me').reply(response); - await expect(scmClient.getCurrentUser()).rejects.toThrow(); + await expect(scmManagerHttp.getCurrentUser()).rejects.toThrow(); }, ); }); - describe(scmClient.getRepo, () => { + describe(scmManagerHttp.getRepo, () => { it('should return the repo', async () => { httpMock .scope(endpoint) .get(`/repositories/${repo.namespace}/${repo.name}`) .reply(200, repo); - expect(await scmClient.getRepo(`${repo.namespace}/${repo.name}`)).toEqual( + expect(await scmManagerHttp.getRepo(`${repo.namespace}/${repo.name}`)).toEqual( repo, ); }); @@ -104,13 +104,13 @@ describe('modules/platform/scm-manager/scm-client', () => { .reply(response); await expect( - scmClient.getRepo(`${repo.namespace}/${repo.name}`), + scmManagerHttp.getRepo(`${repo.namespace}/${repo.name}`), ).rejects.toThrow(); }, ); }); - describe(scmClient.getAllRepos, () => { + describe(scmManagerHttp.getAllRepos, () => { it('should return all repos', async () => { httpMock .scope(endpoint) @@ -121,7 +121,7 @@ describe('modules/platform/scm-manager/scm-client', () => { _embedded: { repositories: [repo] }, }); - expect(await scmClient.getAllRepos()).toEqual([repo]); + expect(await scmManagerHttp.getAllRepos()).toEqual([repo]); }); it.each([[401], [403], [500]])( @@ -132,12 +132,12 @@ describe('modules/platform/scm-manager/scm-client', () => { .get('/repositories?pageSize=1000000') .reply(response); - await expect(scmClient.getAllRepos()).rejects.toThrow(); + await expect(scmManagerHttp.getAllRepos()).rejects.toThrow(); }, ); }); - describe(scmClient.getDefaultBranch, () => { + describe(scmManagerHttp.getDefaultBranch, () => { it('should return the default branch', async () => { httpMock .scope(endpoint) @@ -146,7 +146,7 @@ describe('modules/platform/scm-manager/scm-client', () => { defaultBranch: 'develop', }); - expect(await scmClient.getDefaultBranch(repo)).toBe('develop'); + expect(await scmManagerHttp.getDefaultBranch(repo)).toBe('develop'); }); it.each([[401], [403], [404], [500]])( @@ -157,12 +157,12 @@ describe('modules/platform/scm-manager/scm-client', () => { .get('/config/git/default/repo/default-branch') .reply(response); - await expect(scmClient.getDefaultBranch(repo)).rejects.toThrow(); + await expect(scmManagerHttp.getDefaultBranch(repo)).rejects.toThrow(); }, ); }); - describe(scmClient.getAllRepoPrs, () => { + describe(scmManagerHttp.getAllRepoPrs, () => { it('should return all repo PRs', async () => { httpMock .scope(endpoint) @@ -178,7 +178,7 @@ describe('modules/platform/scm-manager/scm-client', () => { }); expect( - await scmClient.getAllRepoPrs(`${repo.namespace}/${repo.name}`), + await scmManagerHttp.getAllRepoPrs(`${repo.namespace}/${repo.name}`), ).toEqual([pullRequest]); }); @@ -193,13 +193,13 @@ describe('modules/platform/scm-manager/scm-client', () => { .reply(response); await expect( - scmClient.getAllRepoPrs(`${repo.namespace}/${repo.name}`), + scmManagerHttp.getAllRepoPrs(`${repo.namespace}/${repo.name}`), ).rejects.toThrow(); }, ); }); - describe(scmClient.getRepoPr, () => { + describe(scmManagerHttp.getRepoPr, () => { it('should return the repo PR', async () => { httpMock .scope(endpoint) @@ -207,7 +207,7 @@ describe('modules/platform/scm-manager/scm-client', () => { .reply(200, pullRequest); expect( - await scmClient.getRepoPr(`${repo.namespace}/${repo.name}`, 1337), + await scmManagerHttp.getRepoPr(`${repo.namespace}/${repo.name}`, 1337), ).toEqual(pullRequest); }); @@ -222,13 +222,13 @@ describe('modules/platform/scm-manager/scm-client', () => { .reply(response); await expect( - scmClient.getRepoPr(`${repo.namespace}/${repo.name}`, 1337), + scmManagerHttp.getRepoPr(`${repo.namespace}/${repo.name}`, 1337), ).rejects.toThrow(); }, ); }); - describe(scmClient.createPr, () => { + describe(scmManagerHttp.createPr, () => { it('should create PR for a repo', async () => { const expectedCreateParams: PullRequestCreateParams = { source: 'feature/test', @@ -254,7 +254,7 @@ describe('modules/platform/scm-manager/scm-client', () => { .reply(200, pullRequest); expect( - await scmClient.createPr( + await scmManagerHttp.createPr( `${repo.namespace}/${repo.name}`, expectedCreateParams, ), @@ -270,7 +270,7 @@ describe('modules/platform/scm-manager/scm-client', () => { .reply(response); await expect( - scmClient.createPr(`${repo.namespace}/${repo.name}`, { + scmManagerHttp.createPr(`${repo.namespace}/${repo.name}`, { source: 'feature/test', target: 'develop', title: 'Test Title', @@ -283,7 +283,7 @@ describe('modules/platform/scm-manager/scm-client', () => { ); }); - describe(scmClient.updatePr, () => { + describe(scmManagerHttp.updatePr, () => { it('should update PR for a repo', async () => { const expectedUpdateParams: PullRequestUpdateParams = { title: 'Test Title', @@ -300,7 +300,7 @@ describe('modules/platform/scm-manager/scm-client', () => { .reply(204); await expect( - scmClient.updatePr( + scmManagerHttp.updatePr( `${repo.namespace}/${repo.name}`, expectedPrId, expectedUpdateParams, @@ -319,7 +319,7 @@ describe('modules/platform/scm-manager/scm-client', () => { .reply(response); await expect( - scmClient.updatePr(`${repo.namespace}/${repo.name}`, expectedPrId, { + scmManagerHttp.updatePr(`${repo.namespace}/${repo.name}`, expectedPrId, { title: 'Test Title', description: 'PR description', assignees: ['Test assignee'], diff --git a/lib/modules/platform/scm-manager/scm-client.ts b/lib/util/http/scm-manager.ts similarity index 91% rename from lib/modules/platform/scm-manager/scm-client.ts rename to lib/util/http/scm-manager.ts index 2ff1196453..fe78b8fda8 100644 --- a/lib/modules/platform/scm-manager/scm-client.ts +++ b/lib/util/http/scm-manager.ts @@ -1,9 +1,3 @@ -import { Http } from '../../../util/http'; -import type { - HttpResponse, - InternalHttpOptions, -} from '../../../util/http/types'; -import { resolveBaseUrl } from '../../../util/url'; import type { Link, Page, @@ -13,9 +7,15 @@ import type { PullRequestUpdateParams, Repo, RepoPage, - ScmmHttpOptions, User, +} from '../../modules/platform/scm-manager/types'; +import { resolveBaseUrl } from '../url'; +import type { + HttpOptions, + HttpResponse, + InternalHttpOptions, } from './types'; +import { Http } from './index'; const URLS = { ME: 'me', @@ -37,7 +37,11 @@ const CONTENT_TYPES = { PULLREQUESTS: 'application/vnd.scmm-pullRequestCollection+json;v=2', }; -export default class ScmClient extends Http { +export interface ScmManagerHttpOptions extends HttpOptions { + scmmContentType?: string; +} + +export default class ScmManagerHttp extends Http { private readonly endpoint: string; constructor(endpoint: string, token: string) { @@ -47,7 +51,7 @@ export default class ScmClient extends Http { protected override async request( requestUrl: string | URL, - options?: InternalHttpOptions & ScmmHttpOptions, + options?: InternalHttpOptions & ScmManagerHttpOptions, ): Promise> { const opts = { ...options, From 5647da2749e6da1c31101bb1318578d72f5c3eb6 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 24 Jul 2024 10:48:56 +0200 Subject: [PATCH 36/61] Apply linting --- docs/usage/faq.md | 2 +- lib/modules/platform/scm-manager/index.ts | 8 ++++---- lib/modules/platform/scm-manager/utils.ts | 2 +- lib/util/http/scm-manager.spec.ts | 22 +++++++++++++--------- lib/util/http/scm-manager.ts | 6 +----- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/usage/faq.md b/docs/usage/faq.md index 87bb85b81b..8f70785a7e 100644 --- a/docs/usage/faq.md +++ b/docs/usage/faq.md @@ -62,7 +62,7 @@ Follow these steps to see which version the Mend Renovate app is on: ## Renovate core features not supported on all platforms | Feature | Platforms which lack feature | See Renovate issue(s) | -| --------------------- |--------------------------------------------------------------| ------------------------------------------------------------ | +| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | Dependency Dashboard | Azure, Bitbucket, Bitbucket Server, Gerrit, SCM-Manager | [#9592](https://github.com/renovatebot/renovate/issues/9592) | | The Mend Renovate App | Azure, Bitbucket Server, Forgejo, Gitea, GitLab, SCM-Manager | | diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index e88104c096..985a2655ed 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -2,6 +2,7 @@ import { logger } from '../../../logger'; import type { BranchStatus } from '../../../types'; import * as git from '../../../util/git'; import * as hostRules from '../../../util/host-rules'; +import ScmManagerHttp from '../../../util/http/scm-manager'; import { sanitize } from '../../../util/sanitize'; import type { BranchStatusConfig, @@ -23,7 +24,6 @@ import type { import { repoFingerprint } from '../util'; import { smartTruncate } from '../utils/pr-body'; import { mapPrFromScmToRenovate } from './mapper'; -import ScmManagerHttp from '../../../util/http/scm-manager'; import { getRepoUrl, mapPrState, matchPrState, smartLinks } from './utils'; interface SCMMRepoConfig { @@ -170,9 +170,9 @@ export async function getPr(number: number): Promise { export async function getPrList(): Promise { if (config.prList === null) { try { - config.prList = (await scmManagerHttp.getAllRepoPrs(config.repository)).map( - (pr) => mapPrFromScmToRenovate(pr), - ); + config.prList = ( + await scmManagerHttp.getAllRepoPrs(config.repository) + ).map((pr) => mapPrFromScmToRenovate(pr)); } catch (error) { logger.error(error); } diff --git a/lib/modules/platform/scm-manager/utils.ts b/lib/modules/platform/scm-manager/utils.ts index 084a1541ab..6586c4f03b 100644 --- a/lib/modules/platform/scm-manager/utils.ts +++ b/lib/modules/platform/scm-manager/utils.ts @@ -58,7 +58,7 @@ export function getRepoUrl( } if (!Array.isArray(protocolLinks)) { - throw new Error('Expected protocol links to be an array of links.') + throw new Error('Expected protocol links to be an array of links.'); } if (gitUrl === 'ssh') { diff --git a/lib/util/http/scm-manager.spec.ts b/lib/util/http/scm-manager.spec.ts index 002987b6eb..34ca808c3f 100644 --- a/lib/util/http/scm-manager.spec.ts +++ b/lib/util/http/scm-manager.spec.ts @@ -90,9 +90,9 @@ describe('util/http/scm-manager', () => { .get(`/repositories/${repo.namespace}/${repo.name}`) .reply(200, repo); - expect(await scmManagerHttp.getRepo(`${repo.namespace}/${repo.name}`)).toEqual( - repo, - ); + expect( + await scmManagerHttp.getRepo(`${repo.namespace}/${repo.name}`), + ).toEqual(repo); }); it.each([[401], [403], [404], [500]])( @@ -319,12 +319,16 @@ describe('util/http/scm-manager', () => { .reply(response); await expect( - scmManagerHttp.updatePr(`${repo.namespace}/${repo.name}`, expectedPrId, { - title: 'Test Title', - description: 'PR description', - assignees: ['Test assignee'], - status: 'OPEN', - }), + scmManagerHttp.updatePr( + `${repo.namespace}/${repo.name}`, + expectedPrId, + { + title: 'Test Title', + description: 'PR description', + assignees: ['Test assignee'], + status: 'OPEN', + }, + ), ).rejects.toThrow(); }, ); diff --git a/lib/util/http/scm-manager.ts b/lib/util/http/scm-manager.ts index fe78b8fda8..68e337d08a 100644 --- a/lib/util/http/scm-manager.ts +++ b/lib/util/http/scm-manager.ts @@ -10,11 +10,7 @@ import type { User, } from '../../modules/platform/scm-manager/types'; import { resolveBaseUrl } from '../url'; -import type { - HttpOptions, - HttpResponse, - InternalHttpOptions, -} from './types'; +import type { HttpOptions, HttpResponse, InternalHttpOptions } from './types'; import { Http } from './index'; const URLS = { From 15e547d8647b6d61bed41699ed9a0bfe487e93a8 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 24 Jul 2024 14:07:52 +0200 Subject: [PATCH 37/61] Add maxBodyLength function and its test --- lib/modules/platform/scm-manager/index.spec.ts | 8 +++++++- lib/modules/platform/scm-manager/index.ts | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 50ae2620a5..eac8bd6058 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -28,7 +28,7 @@ import { initPlatform, initRepo, invalidatePrCache, - massageMarkdown, + massageMarkdown, maxBodyLength, mergePr, setBranchStatus, updatePr, @@ -634,4 +634,10 @@ describe('modules/platform/scm-manager/index', () => { expect(result).toBeUndefined(); }); }); + + describe(maxBodyLength, () => { + it('should return the max body length allowed for an SCM-Manager request body', () => { + expect(maxBodyLength()).toBe(200000); + }) + }) }); diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 985a2655ed..6c722fd3bf 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -307,7 +307,7 @@ export function ensureCommentRemoval( } export function massageMarkdown(prBody: string): string { - return smartTruncate(smartLinks(prBody), 10000); + return smartTruncate(smartLinks(prBody), maxBodyLength()); } export function getRepoForceRebase(): Promise { @@ -332,6 +332,10 @@ export function getJsonFile( return Promise.resolve(undefined); } +export function maxBodyLength(): number { + return 200000; +} + /* istanbul ignore next */ export function invalidatePrCache(): void { config.prList = null; From 76f797153985c4e310ebab1adffe09a18b179e72 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 24 Jul 2024 14:08:08 +0200 Subject: [PATCH 38/61] Add test case if protocol links are not an array of links --- lib/modules/platform/scm-manager/utils.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/modules/platform/scm-manager/utils.spec.ts b/lib/modules/platform/scm-manager/utils.spec.ts index 90de4bfc8b..044e6c4dc6 100644 --- a/lib/modules/platform/scm-manager/utils.spec.ts +++ b/lib/modules/platform/scm-manager/utils.spec.ts @@ -107,6 +107,20 @@ describe('modules/platform/scm-manager/utils', () => { ).toThrow('MISSING_SSH_LINK'); }); + it('should throw error because protocol links are not an array', () => { + expect(() => + getRepoUrl( + { + ...repo, + _links: { protocol: { name: 'http', href: gitHttpEndpoint }}, + }, + 'ssh', + username, + password, + ), + ).toThrow('Expected protocol links to be an array of links.'); + }); + it('should use the provided ssh link', () => { expect( getRepoUrl( From 55b3b91b996463a044d254627c40a3b691694741 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 24 Jul 2024 15:04:43 +0200 Subject: [PATCH 39/61] Use zod schema for pagination --- lib/modules/platform/scm-manager/schema.ts | 103 +++++++++++++++++++++ lib/modules/platform/scm-manager/types.ts | 14 --- lib/util/http/scm-manager.ts | 20 ++-- 3 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 lib/modules/platform/scm-manager/schema.ts diff --git a/lib/modules/platform/scm-manager/schema.ts b/lib/modules/platform/scm-manager/schema.ts new file mode 100644 index 0000000000..315c56e542 --- /dev/null +++ b/lib/modules/platform/scm-manager/schema.ts @@ -0,0 +1,103 @@ +import { z } from 'zod'; + +const UserSchema = z.object({ + mail: z.string().optional(), + displayName: z.string(), + username: z.string(), +}); + +const ReviserSchema = z.object({ + id: z.string().optional(), + displayName: z.string().optional(), +}); + +const PrStateSchema = z.enum(['DRAFT', 'OPEN', 'REJECTED', 'MERGED']); + +const ReviewerSchema = z.object({ + id: z.string(), + displayName: z.string(), + mail: z.string().optional(), + approved: z.boolean(), +}); + +const TasksSchema = z.object({ + todo: z.number(), + done: z.number(), +}); + +const LinkSchema = z.object({ + href: z.string(), + name: z.string().optional(), + templated: z.boolean().optional(), +}); + +const LinksSchema = z.record( + z.string(), + z.union([LinkSchema, z.array(LinkSchema)]), +); + +const PrMergeMethodSchema = z.enum([ + 'MERGE_COMMIT', + 'REBASE', + 'FAST_FORWARD_IF_POSSIBLE', + 'SQUASH', +]); + +const PrConfigSchema = z.object({ + defaultConfig: z.object({ + mergeStrategy: PrMergeMethodSchema, + deleteBranchOnMerge: z.boolean(), + }), +}); + +const PullRequestSchema = z.object({ + id: z.string(), + author: z.optional(UserSchema), + reviser: z.optional(ReviserSchema), + closeDate: z.string().optional(), + source: z.string(), + target: z.string(), + title: z.string(), + description: z.string(), + creationDate: z.string(), + lastModified: z.string().optional(), + status: PrStateSchema, + reviewer: z.array(ReviewerSchema).optional(), + labels: z.string().array(), + tasks: TasksSchema, + _links: LinksSchema, + _embedded: PrConfigSchema, +}); + +const RepoTypeSchema = z.enum(['git', 'svn', 'hg']); + +const RepoSchema = z.object({ + contact: z.string(), + creationDate: z.string(), + description: z.string(), + lastModified: z.string().optional(), + namespace: z.string(), + name: z.string(), + type: RepoTypeSchema, + archived: z.boolean(), + exporting: z.boolean(), + healthCheckRunning: z.boolean(), + _links: LinksSchema, +}); + +const PagedSchema = z.object({ + page: z.number(), + pageTotal: z.number(), +}); + +export const PagedPullRequestSchema = PagedSchema.extend({ + _embedded: z.object({ + pullRequests: z.array(PullRequestSchema), + }), +}); + +export const PagedRepoSchema = PagedSchema.extend({ + _embedded: z.object({ + repositories: z.array(RepoSchema), + }), +}); diff --git a/lib/modules/platform/scm-manager/types.ts b/lib/modules/platform/scm-manager/types.ts index a2139716de..7bc24d49ea 100644 --- a/lib/modules/platform/scm-manager/types.ts +++ b/lib/modules/platform/scm-manager/types.ts @@ -1,9 +1,3 @@ -export type Page = { - page: number; - pageTotal: number; - _embedded: T; -}; - export interface Links { [link: string]: Link | Link[] | undefined; } @@ -14,10 +8,6 @@ export interface Link { templated?: boolean; } -export interface PullRequestPage { - pullRequests: PullRequest[]; -} - export interface PullRequestCreateParams extends PullRequestUpdateParams { source: string; target: string; @@ -94,10 +84,6 @@ export type CommitStatusType = | 'warning' | 'unknown'; -export interface RepoPage { - repositories: Repo[]; -} - export interface Repo { contact: string; creationDate: string; diff --git a/lib/util/http/scm-manager.ts b/lib/util/http/scm-manager.ts index 68e337d08a..8708e8e3ba 100644 --- a/lib/util/http/scm-manager.ts +++ b/lib/util/http/scm-manager.ts @@ -1,12 +1,13 @@ +import { + PagedPullRequestSchema, + PagedRepoSchema, +} from '../../modules/platform/scm-manager/schema'; import type { Link, - Page, PullRequest, PullRequestCreateParams, - PullRequestPage, PullRequestUpdateParams, Repo, - RepoPage, User, } from '../../modules/platform/scm-manager/types'; import { resolveBaseUrl } from '../url'; @@ -78,9 +79,13 @@ export default class ScmManagerHttp extends Http { } public async getAllRepos(): Promise { - const response = await this.getJson>(URLS.ALL_REPOS, { - scmmContentType: CONTENT_TYPES.REPOSITORIES, - }); + const response = await this.getJson( + URLS.ALL_REPOS, + { + scmmContentType: CONTENT_TYPES.REPOSITORIES, + }, + PagedRepoSchema, + ); return response.body._embedded.repositories; } @@ -98,11 +103,12 @@ export default class ScmManagerHttp extends Http { } public async getAllRepoPrs(repoPath: string): Promise { - const response = await this.getJson>( + const response = await this.getJson( URLS.PULLREQUESTS_WITH_PAGINATION(repoPath), { scmmContentType: CONTENT_TYPES.PULLREQUESTS, }, + PagedPullRequestSchema, ); return response.body._embedded.pullRequests; } From 62db5dc8197ef3501ec9266adf8dd75c3fcffc69 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 24 Jul 2024 15:04:57 +0200 Subject: [PATCH 40/61] Apply linting --- lib/modules/platform/scm-manager/index.spec.ts | 11 ++++++----- lib/modules/platform/scm-manager/utils.spec.ts | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index eac8bd6058..777000cf9f 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -28,7 +28,8 @@ import { initPlatform, initRepo, invalidatePrCache, - massageMarkdown, maxBodyLength, + massageMarkdown, + maxBodyLength, mergePr, setBranchStatus, updatePr, @@ -636,8 +637,8 @@ describe('modules/platform/scm-manager/index', () => { }); describe(maxBodyLength, () => { - it('should return the max body length allowed for an SCM-Manager request body', () => { - expect(maxBodyLength()).toBe(200000); - }) - }) + it('should return the max body length allowed for an SCM-Manager request body', () => { + expect(maxBodyLength()).toBe(200000); + }); + }); }); diff --git a/lib/modules/platform/scm-manager/utils.spec.ts b/lib/modules/platform/scm-manager/utils.spec.ts index 044e6c4dc6..244c16f70d 100644 --- a/lib/modules/platform/scm-manager/utils.spec.ts +++ b/lib/modules/platform/scm-manager/utils.spec.ts @@ -112,7 +112,7 @@ describe('modules/platform/scm-manager/utils', () => { getRepoUrl( { ...repo, - _links: { protocol: { name: 'http', href: gitHttpEndpoint }}, + _links: { protocol: { name: 'http', href: gitHttpEndpoint } }, }, 'ssh', username, From 5f4b6fdd42ecb149fa567ae7a3b7a42456495af5 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 24 Jul 2024 16:35:19 +0200 Subject: [PATCH 41/61] Refactor typing with zod schema --- .../platform/scm-manager/index.spec.ts | 10 +- lib/modules/platform/scm-manager/mapper.ts | 7 +- lib/modules/platform/scm-manager/schema.ts | 95 +++++++------ lib/modules/platform/scm-manager/types.ts | 130 +++--------------- lib/modules/platform/scm-manager/utils.ts | 4 +- lib/util/http/scm-manager.spec.ts | 4 +- lib/util/http/scm-manager.ts | 33 +++-- 7 files changed, 110 insertions(+), 173 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 777000cf9f..7d329e8fd1 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -45,7 +45,7 @@ const token = 'TEST_TOKEN'; const user: User = { mail: 'test@user.de', displayName: 'Test User', - username: 'testUser1337', + name: 'testUser1337', }; const repo: Repo = { @@ -71,7 +71,7 @@ const repo: Repo = { const pullRequest: PullRequest = { id: '1', - author: { displayName: 'Thomas Zerr', username: 'tzerr' }, + author: { displayName: 'Thomas Zerr', id: 'tzerr' }, source: 'feature/test', target: 'develop', title: 'The PullRequest', @@ -94,7 +94,7 @@ const renovatePr: Pr = mapPrFromScmToRenovate(pullRequest); describe('modules/platform/scm-manager/index', () => { beforeEach(() => { jest.resetAllMocks(); - hostRules.add({ token, username: user.username }); + hostRules.add({ token, username: user.name }); invalidatePrCache(); }); @@ -146,7 +146,7 @@ describe('modules/platform/scm-manager/index', () => { }); expect(git.initRepo).toHaveBeenCalledWith({ - url: `https://${user.username}:${token}@localhost:8080/scm/default/repo`, + url: `https://${user.name}:${token}@localhost:8080/scm/default/repo`, repository, defaultBranch: expectedDefaultBranch, }); @@ -161,7 +161,7 @@ describe('modules/platform/scm-manager/index', () => { it('should throw error, because token is not provided in host rules', async () => { hostRules.clear(); - hostRules.add({ username: user.username }); + hostRules.add({ username: user.name }); await expect( initRepo({ repository: `${repo.namespace}/${repo.name}` }), ).rejects.toThrow('Token is not provided'); diff --git a/lib/modules/platform/scm-manager/mapper.ts b/lib/modules/platform/scm-manager/mapper.ts index a6adceb0ca..dedf9a11d0 100644 --- a/lib/modules/platform/scm-manager/mapper.ts +++ b/lib/modules/platform/scm-manager/mapper.ts @@ -6,8 +6,11 @@ export function mapPrFromScmToRenovate(pr: SCMPullRequest): RenovatePr { sourceBranch: pr.source, targetBranch: pr.target, createdAt: pr.creationDate, - closedAt: pr.closeDate, - hasAssignees: pr.reviewer !== undefined && pr.reviewer.length > 0, + closedAt: pr.closeDate ? pr.closeDate : undefined, + hasAssignees: + pr.reviewer !== undefined && + pr.reviewer !== null && + pr.reviewer.length > 0, labels: pr.labels, number: parseInt(pr.id), reviewers: pr.reviewer diff --git a/lib/modules/platform/scm-manager/schema.ts b/lib/modules/platform/scm-manager/schema.ts index 315c56e542..8a036e82e4 100644 --- a/lib/modules/platform/scm-manager/schema.ts +++ b/lib/modules/platform/scm-manager/schema.ts @@ -1,81 +1,92 @@ import { z } from 'zod'; -const UserSchema = z.object({ - mail: z.string().optional(), +export const UserSchema = z.object({ + mail: z.string().optional().nullable(), displayName: z.string(), - username: z.string(), + name: z.string(), }); -const ReviserSchema = z.object({ - id: z.string().optional(), - displayName: z.string().optional(), +export const DefaultBranchSchema = z.object({ + defaultBranch: z.string(), }); -const PrStateSchema = z.enum(['DRAFT', 'OPEN', 'REJECTED', 'MERGED']); - -const ReviewerSchema = z.object({ - id: z.string(), - displayName: z.string(), - mail: z.string().optional(), - approved: z.boolean(), -}); - -const TasksSchema = z.object({ - todo: z.number(), - done: z.number(), -}); - -const LinkSchema = z.object({ +export const LinkSchema = z.object({ href: z.string(), - name: z.string().optional(), - templated: z.boolean().optional(), + name: z.string().optional().nullable(), + templated: z.boolean().optional().nullable(), }); -const LinksSchema = z.record( +export const LinksSchema = z.record( z.string(), z.union([LinkSchema, z.array(LinkSchema)]), ); -const PrMergeMethodSchema = z.enum([ +export const PrStateSchema = z.enum(['DRAFT', 'OPEN', 'REJECTED', 'MERGED']); + +export const PrMergeMethodSchema = z.enum([ 'MERGE_COMMIT', 'REBASE', 'FAST_FORWARD_IF_POSSIBLE', 'SQUASH', ]); -const PrConfigSchema = z.object({ - defaultConfig: z.object({ - mergeStrategy: PrMergeMethodSchema, - deleteBranchOnMerge: z.boolean(), - }), -}); - -const PullRequestSchema = z.object({ +export const PullRequestSchema = z.object({ id: z.string(), - author: z.optional(UserSchema), - reviser: z.optional(ReviserSchema), - closeDate: z.string().optional(), + author: z + .object({ + mail: z.string().optional().nullable(), + displayName: z.string(), + id: z.string(), + }) + .optional() + .nullable(), + reviser: z + .object({ + id: z.string().optional().nullable(), + displayName: z.string().optional().nullable(), + }) + .optional() + .nullable(), + closeDate: z.string().optional().nullable(), source: z.string(), target: z.string(), title: z.string(), description: z.string(), creationDate: z.string(), - lastModified: z.string().optional(), + lastModified: z.string().optional().nullable(), status: PrStateSchema, - reviewer: z.array(ReviewerSchema).optional(), + reviewer: z + .array( + z.object({ + id: z.string(), + displayName: z.string(), + mail: z.string().optional().nullable(), + approved: z.boolean(), + }), + ) + .optional() + .nullable(), labels: z.string().array(), - tasks: TasksSchema, + tasks: z.object({ + todo: z.number(), + done: z.number(), + }), _links: LinksSchema, - _embedded: PrConfigSchema, + _embedded: z.object({ + defaultConfig: z.object({ + mergeStrategy: PrMergeMethodSchema, + deleteBranchOnMerge: z.boolean(), + }), + }), }); const RepoTypeSchema = z.enum(['git', 'svn', 'hg']); -const RepoSchema = z.object({ +export const RepoSchema = z.object({ contact: z.string(), creationDate: z.string(), description: z.string(), - lastModified: z.string().optional(), + lastModified: z.string().optional().nullable(), namespace: z.string(), name: z.string(), type: RepoTypeSchema, diff --git a/lib/modules/platform/scm-manager/types.ts b/lib/modules/platform/scm-manager/types.ts index 7bc24d49ea..63e5980818 100644 --- a/lib/modules/platform/scm-manager/types.ts +++ b/lib/modules/platform/scm-manager/types.ts @@ -1,12 +1,18 @@ -export interface Links { - [link: string]: Link | Link[] | undefined; -} +import { z } from 'zod'; +import { + LinkSchema, + LinksSchema, + PrMergeMethodSchema, + PrStateSchema, + PullRequestSchema, + RepoSchema, + UserSchema, +} from './schema'; -export interface Link { - href: string; - name?: string; - templated?: boolean; -} +export type Link = z.infer; +export type Links = z.infer; + +export type User = z.infer; export interface PullRequestCreateParams extends PullRequestUpdateParams { source: string; @@ -17,114 +23,16 @@ export interface PullRequestUpdateParams { title: string; description?: string; assignees?: string[]; - status?: PRState; + status?: PrState; target?: string; } -export interface PullRequest { - id: string; - author?: User; - reviser?: Reviser; - closeDate?: string; - source: string; - target: string; - title: string; - description: string; - creationDate: string; - lastModified?: string; - status: PRState; - reviewer?: Reviewer[]; - labels: string[]; - tasks: Tasks; - _links: Links; - _embedded: { - defaultConfig: { - mergeStrategy: PRMergeMethod; - deleteBranchOnMerge: boolean; - }; - }; -} +export type PullRequest = z.infer; -export interface User { - mail?: string; - displayName: string; - username: string; -} +type PrState = z.infer; -export interface Reviser { - id?: string; - displayName?: string; -} +export type PrMergeMethod = z.infer; -export type PRState = 'DRAFT' | 'OPEN' | 'REJECTED' | 'MERGED'; - -export interface Reviewer { - id: string; - displayName: string; - mail?: string; - approved: boolean; -} - -export interface Tasks { - todo: number; - done: number; -} - -export type PRMergeMethod = - | 'MERGE_COMMIT' - | 'REBASE' - | 'FAST_FORWARD_IF_POSSIBLE' - | 'SQUASH'; - -export type CommitStatusType = - | 'pending' - | 'success' - | 'error' - | 'failure' - | 'warning' - | 'unknown'; - -export interface Repo { - contact: string; - creationDate: string; - description: string; - lastModified?: string; - namespace: string; - name: string; - type: RepoType; - archived: boolean; - exporting: boolean; - healthCheckRunning: boolean; - _links: Links; -} - -export type RepoType = 'git' | 'svn' | 'hg'; - -export interface Comment { - id: number; - body: string; -} - -export interface Label { - id: number; - name: string; - description: string; - color: string; -} - -export interface Branch { - name: string; - commit: Commit; -} - -export interface Commit { - id: string; - author: User; -} - -export interface CommitStatus { - id: number; - description: string; -} +export type Repo = z.infer; export type PrFilterByState = 'open' | 'closed' | '!open' | 'all'; diff --git a/lib/modules/platform/scm-manager/utils.ts b/lib/modules/platform/scm-manager/utils.ts index 6586c4f03b..0b03c32175 100644 --- a/lib/modules/platform/scm-manager/utils.ts +++ b/lib/modules/platform/scm-manager/utils.ts @@ -3,7 +3,7 @@ import { logger } from '../../../logger'; import { regEx } from '../../../util/regex'; import { parseUrl } from '../../../util/url'; import type { GitUrlOption, Pr } from '../types'; -import type { PRMergeMethod, PrFilterByState, Repo } from './types'; +import type { PrFilterByState, PrMergeMethod, Repo } from './types'; export function mapPrState( state: 'open' | 'closed' | undefined, @@ -90,7 +90,7 @@ export function getRepoUrl( export function getMergeMethod( strategy: MergeStrategy | undefined, -): PRMergeMethod | null { +): PrMergeMethod | null { switch (strategy) { case 'fast-forward': return 'FAST_FORWARD_IF_POSSIBLE'; diff --git a/lib/util/http/scm-manager.spec.ts b/lib/util/http/scm-manager.spec.ts index 34ca808c3f..c5c20f0ab3 100644 --- a/lib/util/http/scm-manager.spec.ts +++ b/lib/util/http/scm-manager.spec.ts @@ -37,7 +37,7 @@ describe('util/http/scm-manager', () => { const pullRequest: PullRequest = { id: '1337', - author: { displayName: 'Thomas Zerr', username: 'tzerr' }, + author: { displayName: 'Thomas Zerr', id: 'tzerr' }, source: 'feature/test', target: 'develop', title: 'The PullRequest', @@ -66,7 +66,7 @@ describe('util/http/scm-manager', () => { const expectedUser: User = { mail: 'test@test.de', displayName: 'Test User', - username: 'test', + name: 'test', }; httpMock.scope(endpoint).get('/me').reply(200, expectedUser); diff --git a/lib/util/http/scm-manager.ts b/lib/util/http/scm-manager.ts index 8708e8e3ba..58b7de82f0 100644 --- a/lib/util/http/scm-manager.ts +++ b/lib/util/http/scm-manager.ts @@ -1,6 +1,10 @@ import { + DefaultBranchSchema, PagedPullRequestSchema, PagedRepoSchema, + PullRequestSchema, + RepoSchema, + UserSchema, } from '../../modules/platform/scm-manager/schema'; import type { Link, @@ -65,16 +69,24 @@ export default class ScmManagerHttp extends Http { } public async getCurrentUser(): Promise { - const response = await this.getJson(URLS.ME, { - scmmContentType: CONTENT_TYPES.ME, - }); + const response = await this.getJson( + URLS.ME, + { + scmmContentType: CONTENT_TYPES.ME, + }, + UserSchema, + ); return response.body; } public async getRepo(repoPath: string): Promise { - const response = await this.getJson(URLS.REPO(repoPath), { - scmmContentType: CONTENT_TYPES.REPOSITORY, - }); + const response = await this.getJson( + URLS.REPO(repoPath), + { + scmmContentType: CONTENT_TYPES.REPOSITORY, + }, + RepoSchema, + ); return response.body; } @@ -92,11 +104,12 @@ export default class ScmManagerHttp extends Http { public async getDefaultBranch(repo: Repo): Promise { const defaultBranchUrl = repo._links['defaultBranch'] as Link; - const response = await this.getJson<{ defaultBranch: string }>( + const response = await this.getJson( defaultBranchUrl.href, { scmmContentType: CONTENT_TYPES.GIT_CONFIG, }, + DefaultBranchSchema, ); return response.body.defaultBranch; @@ -114,11 +127,12 @@ export default class ScmManagerHttp extends Http { } public async getRepoPr(repoPath: string, id: number): Promise { - const response = await this.getJson( + const response = await this.getJson( URLS.PULLREQUEST_BY_ID(repoPath, id), { scmmContentType: CONTENT_TYPES.PULLREQUEST, }, + PullRequestSchema, ); return response.body; @@ -136,12 +150,13 @@ export default class ScmManagerHttp extends Http { }, }); - const getCreatedPrResponse = await this.getJson( + const getCreatedPrResponse = await this.getJson( /* istanbul ignore next: Just to please the compiler, location would never be undefined */ createPrResponse.headers.location ?? '', { scmmContentType: CONTENT_TYPES.PULLREQUEST, }, + PullRequestSchema, ); return getCreatedPrResponse.body; From 79ebdec382c16bed8b7ed9db0149721e82a3f1ea Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Mon, 5 Aug 2024 08:51:12 +0200 Subject: [PATCH 42/61] Fix test description Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 7d329e8fd1..3d626dff15 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -380,7 +380,7 @@ describe('modules/platform/scm-manager/index', () => { expect(await getPr(1)).toBeNull(); }); - it('should return pr from cache', async () => { + it('should return PR from cache', async () => { httpMock .scope(endpoint) .get( From 15405ffd9e09edd72cd21f14026194877923e7b8 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Mon, 5 Aug 2024 08:51:27 +0200 Subject: [PATCH 43/61] Fix test description Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 3d626dff15..29463780b3 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -208,7 +208,7 @@ describe('modules/platform/scm-manager/index', () => { expect(await getPrList()).toIncludeAllMembers([]); }); - it('should return empty array, because api request failed', async () => { + it('should return empty array, because API request failed', async () => { httpMock .scope(endpoint) .get( From b27708678d283ba58a2280c73420337a57f69b54 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Mon, 5 Aug 2024 08:51:47 +0200 Subject: [PATCH 44/61] Fix test description Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 29463780b3..841ac75cde 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -256,7 +256,7 @@ describe('modules/platform/scm-manager/index', () => { }); describe(findPr, () => { - it('search in pull request without explicitly setting the state as argument', async () => { + it('search in Pull Request without explicitly setting the state as argument', async () => { httpMock .scope(endpoint) .get( From 25d20bcd7bccb8ba0b1c297ffe9f82c6e573633d Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Mon, 5 Aug 2024 08:58:32 +0200 Subject: [PATCH 45/61] Fix test description Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 841ac75cde..4f2b348eb3 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -609,7 +609,7 @@ describe('modules/platform/scm-manager/index', () => { }); describe(massageMarkdown, () => { - it('should adjust smart link for pull requests', () => { + it('should adjust smart link for Pull Requests', () => { const result = massageMarkdown('[PR](../pull/1)'); expect(result).toBe('[PR](pulls/1)'); }); From c721319c287d7e9f47f32da24eec5b17e82a4f28 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Mon, 5 Aug 2024 09:23:55 +0200 Subject: [PATCH 46/61] Update readme Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/platform/scm-manager/readme.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/modules/platform/scm-manager/readme.md b/lib/modules/platform/scm-manager/readme.md index 75a8cacba6..02f341ea0b 100644 --- a/lib/modules/platform/scm-manager/readme.md +++ b/lib/modules/platform/scm-manager/readme.md @@ -8,14 +8,23 @@ Renovate supports the [SCM-Manager](https://scm-manager.org) platform. 1. The technical user _must_ have a valid name and email address 1. Put the API key in the `RENOVATE_TOKEN` environment variable, so that Renovate can use it +## Set correct platform + You must set the [`platform`](../../../self-hosted-configuration.md#platform) config option to `scm-manager` in your Renovate config file. +## Set permissions + The technical user must have permission to read and write to your repository. You can do this by granting the permission role "OWNER" to the technical Renovate user. -Additionally, the Review Plugin needs to be installed. -Otherwise, the pull request API will not be available. -Plugins can be installed under Administration -> Plugins -> Available. + +## Install Review Plugin + +To let Renovate access the Pull Request API, you must install the Review Plugin. +Find the list of available plugins by going to to Administration -> Plugins -> Available. + +## Supported versions of SCM-Manager Renovate supports SCM-Manager major version `2.x` and `3.x`. + The minimum version for the `2.x` range is `2.48.0`. The minimum version for the `3.x` range is `3.0.0`. From fbab830431c261183f5054f53eb9582447ebfa26 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 15 Aug 2024 13:55:56 +0200 Subject: [PATCH 47/61] Add try catch around fetching the current user --- lib/modules/platform/scm-manager/index.spec.ts | 8 ++++++++ lib/modules/platform/scm-manager/index.ts | 18 +++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 4f2b348eb3..555789e4ad 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -111,6 +111,14 @@ describe('modules/platform/scm-manager/index', () => { ); }); + it('should throw error, when token is invalid', async () => { + httpMock.scope(endpoint).get(`/me`).reply(401); + + await expect( + initPlatform({ endpoint, token: 'invalid' }), + ).rejects.toThrow('Init: Authentication failure'); + }); + it('should init platform', async () => { httpMock.scope(endpoint).get('/me').reply(200, user); expect(await initPlatform({ endpoint, token })).toEqual({ diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 6c722fd3bf..5ac28dd31c 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -51,13 +51,21 @@ export async function initPlatform({ scmManagerHttp = new ScmManagerHttp(endpoint, token); - const me = await scmManagerHttp.getCurrentUser(); - const gitAuthor = `${me.displayName} <${me.mail}>`; - const result = { endpoint, gitAuthor }; + try { + const me = await scmManagerHttp.getCurrentUser(); + const gitAuthor = `${me.displayName} <${me.mail}>`; + const result = { endpoint, gitAuthor }; - logger.info(`Plattform initialized ${JSON.stringify(result)}`); + logger.info(`Plattform initialized ${JSON.stringify(result)}`); - return result; + return result; + } catch (err) { + logger.debug( + { err }, + 'Error authenticating with SCM-Manager. Check your token', + ); + throw new Error('Init: Authentication failure'); + } } export async function initRepo({ From c1a65913b2fbd0fd2b39a2a7e3bc622aa5d98df4 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Thu, 15 Aug 2024 14:02:20 +0200 Subject: [PATCH 48/61] Switch log statement from info to debug Co-authored-by: Michael Kriese --- lib/modules/platform/scm-manager/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 5ac28dd31c..ff1cc6a2fb 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -120,7 +120,7 @@ export async function getRepos(): Promise { (repo) => repo.type === 'git', ); const result = repos.map((repo) => `${repo.namespace}/${repo.name}`); - logger.info(`Discovered ${repos.length} repos`); + logger.debug(`Discovered ${repos.length} repos`); return result; } From 01a8c4f1b2c441c3f304f3122fbb9791ef060505 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Thu, 15 Aug 2024 14:02:56 +0200 Subject: [PATCH 49/61] Switch log statement from info to trace Co-authored-by: Michael Kriese --- lib/modules/platform/scm-manager/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index ff1cc6a2fb..dfa04bc3c6 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -110,7 +110,7 @@ export async function initRepo({ ), }; - logger.info(`Repo initialized: ${JSON.stringify(result)}`); + logger.trace({ result }, `Repo initialized`); return result; } From aa96216164c131b5e3af7e9753e55a9d8ef80bd8 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Thu, 15 Aug 2024 14:03:16 +0200 Subject: [PATCH 50/61] Switch log statement from debug to trace Co-authored-by: Michael Kriese --- lib/modules/platform/scm-manager/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index dfa04bc3c6..10a66fe940 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -147,7 +147,7 @@ export async function findPr({ return result; } - logger.debug( + logger.trace( `Could not find PR with source branch ${branchName} and title ${ prTitle ?? '' } and state ${state}`, From d3e49f91b55c9776368e79dfb3100513dc0bfbae Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Thu, 15 Aug 2024 14:03:37 +0200 Subject: [PATCH 51/61] Switch log statement from info to trace Co-authored-by: Michael Kriese --- lib/modules/platform/scm-manager/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 10a66fe940..c2f124a693 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -143,7 +143,7 @@ export async function findPr({ ); if (result) { - logger.info(`Found PR ${JSON.stringify(result)}`); + logger.trace({ result }, `Found PR`); return result; } From 9c5b6331d87618d83614385bcbf65a63c223f75e Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Thu, 15 Aug 2024 14:04:16 +0200 Subject: [PATCH 52/61] Switch log statement from info to trace Co-authored-by: Michael Kriese --- lib/modules/platform/scm-manager/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index c2f124a693..00cf60b7c0 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -161,7 +161,7 @@ export async function getPr(number: number): Promise { const cachedPr = inProgressPrs.find((pr) => pr.number === number); if (cachedPr) { - logger.info('Returning from cached PRs'); + logger.trace('Returning from cached PRs'); return cachedPr; } From 2f0873c030c0a11d57a66244d262c62387f22b40 Mon Sep 17 00:00:00 2001 From: zT-1337 Date: Thu, 15 Aug 2024 14:04:54 +0200 Subject: [PATCH 53/61] Switch log statement from info to trace Co-authored-by: Michael Kriese --- lib/modules/platform/scm-manager/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 00cf60b7c0..7fb8186026 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -167,7 +167,7 @@ export async function getPr(number: number): Promise { try { const result = await scmManagerHttp.getRepoPr(config.repository, number); - logger.info('Returning PR from API'); + logger.trace('Returning PR from API'); return mapPrFromScmToRenovate(result); } catch (error) { logger.error(`Can not find a PR with id ${number}`); From 88af97fa2a4d69f4fda3e4a484ba9af978ab1af1 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 15 Aug 2024 14:17:10 +0200 Subject: [PATCH 54/61] Fix linting errors --- lib/constants/platforms.ts | 2 +- lib/modules/platform/scm-manager/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/constants/platforms.ts b/lib/constants/platforms.ts index 177864c421..819bc0e3fd 100644 --- a/lib/constants/platforms.ts +++ b/lib/constants/platforms.ts @@ -8,7 +8,7 @@ export const PLATFORM_HOST_TYPES = [ 'github', 'gitlab', 'local', - 'scm-manager' + 'scm-manager', ] as const; export type PlatformId = (typeof PLATFORM_HOST_TYPES)[number]; diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index 7fb8186026..de086a3300 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -170,7 +170,7 @@ export async function getPr(number: number): Promise { logger.trace('Returning PR from API'); return mapPrFromScmToRenovate(result); } catch (error) { - logger.error(`Can not find a PR with id ${number}`); + logger.error({ error }, `Can not find a PR with id ${number}`); return null; } } From 743594f5b750bdbadd3bf47284cd60d03bab2b41 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Thu, 15 Aug 2024 16:12:23 +0200 Subject: [PATCH 55/61] Remove credentials check in initRepo --- .../platform/scm-manager/index.spec.ts | 15 ----- lib/modules/platform/scm-manager/index.ts | 16 +---- .../platform/scm-manager/utils.spec.ts | 67 ++++++++++++++----- lib/modules/platform/scm-manager/utils.ts | 14 ++-- 4 files changed, 62 insertions(+), 50 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 555789e4ad..28f55c2eba 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -159,21 +159,6 @@ describe('modules/platform/scm-manager/index', () => { defaultBranch: expectedDefaultBranch, }); }); - - it('should throw error, because username is not provided in host rules', async () => { - hostRules.clear(); - await expect( - initRepo({ repository: `${repo.namespace}/${repo.name}` }), - ).rejects.toThrow('Username is not provided'); - }); - - it('should throw error, because token is not provided in host rules', async () => { - hostRules.clear(); - hostRules.add({ username: user.name }); - await expect( - initRepo({ repository: `${repo.namespace}/${repo.name}` }), - ).rejects.toThrow('Token is not provided'); - }); }); describe(getRepos, () => { diff --git a/lib/modules/platform/scm-manager/index.ts b/lib/modules/platform/scm-manager/index.ts index de086a3300..1a890b6fda 100644 --- a/lib/modules/platform/scm-manager/index.ts +++ b/lib/modules/platform/scm-manager/index.ts @@ -1,7 +1,6 @@ import { logger } from '../../../logger'; import type { BranchStatus } from '../../../types'; import * as git from '../../../util/git'; -import * as hostRules from '../../../util/host-rules'; import ScmManagerHttp from '../../../util/http/scm-manager'; import { sanitize } from '../../../util/sanitize'; import type { @@ -72,22 +71,9 @@ export async function initRepo({ repository, gitUrl, }: RepoParams): Promise { - const hostOptions = hostRules.find({ - hostType: id, - url: scmManagerHttp.getEndpoint(), - }); - - if (!hostOptions.username) { - throw new Error('Username is not provided'); - } - - if (!hostOptions.token) { - throw new Error('Token is not provided'); - } - const repo = await scmManagerHttp.getRepo(repository); const defaultBranch = await scmManagerHttp.getDefaultBranch(repo); - const url = getRepoUrl(repo, gitUrl, hostOptions.username, hostOptions.token); + const url = getRepoUrl(repo, gitUrl, scmManagerHttp.getEndpoint()); config = {} as any; config.repository = repository; diff --git a/lib/modules/platform/scm-manager/utils.spec.ts b/lib/modules/platform/scm-manager/utils.spec.ts index 244c16f70d..443d057f1b 100644 --- a/lib/modules/platform/scm-manager/utils.spec.ts +++ b/lib/modules/platform/scm-manager/utils.spec.ts @@ -1,7 +1,9 @@ import type { MergeStrategy } from '../../../config/types'; +import * as hostRules from '../../../util/host-rules'; import type { GitUrlOption, Pr } from '../types'; import type { PrFilterByState, Repo } from './types'; import { getMergeMethod, getRepoUrl, matchPrState, smartLinks } from './utils'; +import { invalidatePrCache } from './index'; describe('modules/platform/scm-manager/utils', () => { describe(getMergeMethod, () => { @@ -79,16 +81,20 @@ describe('modules/platform/scm-manager/utils', () => { _links: {}, }; - const username = 'tzerr'; - const password = 'password'; + const endpoint = 'http://localhost:8081/scm/api/v2'; const gitHttpEndpoint = 'http://localhost:8081/scm/repo/default/repo'; const gitSshEndpoint = 'ssh://localhost:2222/scm/repo/default/repo'; + beforeEach(() => { + hostRules.add({ token: 'token', username: 'tzerr' }); + invalidatePrCache(); + }); + it.each([['ssh'], ['default'], ['endpoint'], [undefined]])( 'should throw error for option %p, because protocol links are missing', (gitUrl: string | undefined) => { expect(() => - getRepoUrl(repo, gitUrl as GitUrlOption, username, password), + getRepoUrl(repo, gitUrl as GitUrlOption, endpoint), ).toThrow('Missing protocol links.'); }, ); @@ -101,8 +107,7 @@ describe('modules/platform/scm-manager/utils', () => { _links: { protocol: [{ name: 'http', href: gitHttpEndpoint }] }, }, 'ssh', - username, - password, + endpoint, ), ).toThrow('MISSING_SSH_LINK'); }); @@ -115,8 +120,7 @@ describe('modules/platform/scm-manager/utils', () => { _links: { protocol: { name: 'http', href: gitHttpEndpoint } }, }, 'ssh', - username, - password, + endpoint, ), ).toThrow('Expected protocol links to be an array of links.'); }); @@ -129,8 +133,7 @@ describe('modules/platform/scm-manager/utils', () => { _links: { protocol: [{ name: 'ssh', href: gitSshEndpoint }] }, }, 'ssh', - username, - password, + endpoint, ), ).toEqual(gitSshEndpoint); }); @@ -145,8 +148,7 @@ describe('modules/platform/scm-manager/utils', () => { _links: { protocol: [{ name: 'ssh', href: gitSshEndpoint }] }, }, gitUrl as GitUrlOption | undefined, - username, - password, + endpoint, ), ).toThrow('MISSING_HTTP_LINK'); }, @@ -162,13 +164,47 @@ describe('modules/platform/scm-manager/utils', () => { _links: { protocol: [{ name: 'http', href: 'invalid url' }] }, }, gitUrl as GitUrlOption | undefined, - username, - password, + endpoint, ), ).toThrow('MALFORMED_HTTP_LINK'); }, ); + it.each([['endpoint'], ['default'], [undefined]])( + 'should use empty string, because username was not provided. With option %p', + (gitUrl: string | undefined) => { + hostRules.clear(); + expect( + getRepoUrl( + { + ...repo, + _links: { protocol: [{ name: 'http', href: gitHttpEndpoint }] }, + }, + gitUrl as GitUrlOption | undefined, + endpoint, + ), + ).toBe('http://localhost:8081/scm/repo/default/repo'); + }, + ); + + it.each([['endpoint'], ['default'], [undefined]])( + 'should use empty string, because token was not provided. With option %p', + (gitUrl: string | undefined) => { + hostRules.clear(); + hostRules.add({ username: 'tzerr' }); + expect( + getRepoUrl( + { + ...repo, + _links: { protocol: [{ name: 'http', href: gitHttpEndpoint }] }, + }, + gitUrl as GitUrlOption | undefined, + endpoint, + ), + ).toBe('http://tzerr@localhost:8081/scm/repo/default/repo'); + }, + ); + it.each([['endpoint'], ['default'], [undefined]])( 'should provide the http link with username, for option %p', (gitUrl: string | undefined) => { @@ -179,10 +215,9 @@ describe('modules/platform/scm-manager/utils', () => { _links: { protocol: [{ name: 'http', href: gitHttpEndpoint }] }, }, gitUrl as GitUrlOption | undefined, - username, - password, + endpoint, ), - ).toBe('http://tzerr:password@localhost:8081/scm/repo/default/repo'); + ).toBe('http://tzerr:token@localhost:8081/scm/repo/default/repo'); }, ); }); diff --git a/lib/modules/platform/scm-manager/utils.ts b/lib/modules/platform/scm-manager/utils.ts index 0b03c32175..a6d0de576d 100644 --- a/lib/modules/platform/scm-manager/utils.ts +++ b/lib/modules/platform/scm-manager/utils.ts @@ -1,5 +1,6 @@ import type { MergeStrategy } from '../../../config/types'; import { logger } from '../../../logger'; +import * as hostRules from '../../../util/host-rules'; import { regEx } from '../../../util/regex'; import { parseUrl } from '../../../util/url'; import type { GitUrlOption, Pr } from '../types'; @@ -48,8 +49,7 @@ export function smartLinks(body: string): string { export function getRepoUrl( repo: Repo, gitUrl: GitUrlOption | undefined, - username: string, - password: string, + endpoint: string, ): string { const protocolLinks = repo._links.protocol; @@ -83,8 +83,14 @@ export function getRepoUrl( throw new Error('MALFORMED_HTTP_LINK'); } - repoUrl.username = username; - repoUrl.password = password; + const hostOptions = hostRules.find({ + hostType: 'scm-manager', + url: endpoint, + }); + + repoUrl.username = hostOptions.username ?? ''; + repoUrl.password = hostOptions.token ?? ''; + return repoUrl.toString(); } From 8b981a2798553dabb745f554d6a7a7e6b5b760df Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 7 Oct 2024 14:33:34 +0200 Subject: [PATCH 56/61] Fix imports --- lib/modules/platform/scm-manager/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/platform/scm-manager/types.ts b/lib/modules/platform/scm-manager/types.ts index 63e5980818..2cb721d0d2 100644 --- a/lib/modules/platform/scm-manager/types.ts +++ b/lib/modules/platform/scm-manager/types.ts @@ -1,5 +1,5 @@ -import { z } from 'zod'; -import { +import type { z } from 'zod'; +import type { LinkSchema, LinksSchema, PrMergeMethodSchema, From fdc2ccab39e3686cdde59ab1271760982d8a90e9 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 4 Nov 2024 14:58:36 +0100 Subject: [PATCH 57/61] Fix zod schema for repositories --- lib/modules/platform/scm-manager/schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/platform/scm-manager/schema.ts b/lib/modules/platform/scm-manager/schema.ts index 8a036e82e4..631a34252f 100644 --- a/lib/modules/platform/scm-manager/schema.ts +++ b/lib/modules/platform/scm-manager/schema.ts @@ -83,9 +83,9 @@ export const PullRequestSchema = z.object({ const RepoTypeSchema = z.enum(['git', 'svn', 'hg']); export const RepoSchema = z.object({ - contact: z.string(), + contact: z.string().optional().nullable(), creationDate: z.string(), - description: z.string(), + description: z.string().optional().nullable(), lastModified: z.string().optional().nullable(), namespace: z.string(), name: z.string(), From 0d662b1b7818d65aa63dca6103f739ad5e0e0d40 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 4 Nov 2024 15:05:13 +0100 Subject: [PATCH 58/61] Fix zod schema for pull requests --- lib/modules/platform/scm-manager/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/schema.ts b/lib/modules/platform/scm-manager/schema.ts index 631a34252f..0827e447de 100644 --- a/lib/modules/platform/scm-manager/schema.ts +++ b/lib/modules/platform/scm-manager/schema.ts @@ -51,7 +51,7 @@ export const PullRequestSchema = z.object({ source: z.string(), target: z.string(), title: z.string(), - description: z.string(), + description: z.string().optional().nullable(), creationDate: z.string(), lastModified: z.string().optional().nullable(), status: PrStateSchema, From b7b7e475448b41d97c4b29a5d51469cfbe08ef61 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Tue, 5 Nov 2024 12:02:41 +0100 Subject: [PATCH 59/61] Fix 500 error after attempting to update an pr --- lib/modules/platform/scm-manager/index.spec.ts | 7 +++++++ lib/modules/platform/scm-manager/types.ts | 1 - lib/util/http/scm-manager.spec.ts | 15 +++++++++++---- lib/util/http/scm-manager.ts | 18 +++++++++++++++++- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 28f55c2eba..0928e4f4da 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -492,6 +492,13 @@ describe('modules/platform/scm-manager/index', () => { actualPrBody: string | undefined, expectedPrBody: string | undefined, ) => { + httpMock + .scope(endpoint) + .get( + `/pull-requests/${repo.namespace}/${repo.name}/${pullRequest.id}`, + ) + .reply(200, pullRequest); + httpMock .scope(endpoint) .put(`/pull-requests/${repo.namespace}/${repo.name}/1`) diff --git a/lib/modules/platform/scm-manager/types.ts b/lib/modules/platform/scm-manager/types.ts index 2cb721d0d2..4891ac2ea5 100644 --- a/lib/modules/platform/scm-manager/types.ts +++ b/lib/modules/platform/scm-manager/types.ts @@ -22,7 +22,6 @@ export interface PullRequestCreateParams extends PullRequestUpdateParams { export interface PullRequestUpdateParams { title: string; description?: string; - assignees?: string[]; status?: PrState; target?: string; } diff --git a/lib/util/http/scm-manager.spec.ts b/lib/util/http/scm-manager.spec.ts index c5c20f0ab3..007a1b3824 100644 --- a/lib/util/http/scm-manager.spec.ts +++ b/lib/util/http/scm-manager.spec.ts @@ -235,7 +235,6 @@ describe('util/http/scm-manager', () => { target: 'develop', title: 'Test Title', description: 'PR description', - assignees: ['Test assignee'], status: 'OPEN', }; @@ -275,7 +274,6 @@ describe('util/http/scm-manager', () => { target: 'develop', title: 'Test Title', description: 'PR description', - assignees: ['Test assignee'], status: 'OPEN', }), ).rejects.toThrow(); @@ -288,12 +286,17 @@ describe('util/http/scm-manager', () => { const expectedUpdateParams: PullRequestUpdateParams = { title: 'Test Title', description: 'PR description', - assignees: ['Test assignee'], status: 'OPEN', + target: 'new/target', }; const expectedPrId = 1337; + httpMock + .scope(endpoint) + .get(`/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`) + .reply(200, pullRequest); + httpMock .scope(endpoint) .put(`/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`) @@ -313,6 +316,11 @@ describe('util/http/scm-manager', () => { async (response: number) => { const expectedPrId = 1337; + httpMock + .scope(endpoint) + .get(`/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`) + .reply(200, pullRequest); + httpMock .scope(endpoint) .put(`/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`) @@ -325,7 +333,6 @@ describe('util/http/scm-manager', () => { { title: 'Test Title', description: 'PR description', - assignees: ['Test assignee'], status: 'OPEN', }, ), diff --git a/lib/util/http/scm-manager.ts b/lib/util/http/scm-manager.ts index 58b7de82f0..302ee0ee00 100644 --- a/lib/util/http/scm-manager.ts +++ b/lib/util/http/scm-manager.ts @@ -167,11 +167,27 @@ export default class ScmManagerHttp extends Http { id: number, params: PullRequestUpdateParams, ): Promise { + const currentPr = await this.getRepoPr(repoPath, id); await this.putJson(URLS.PULLREQUEST_BY_ID(repoPath, id), { - body: params, + body: this.mergePullRequestWithUpdate(currentPr, params), headers: { 'Content-Type': CONTENT_TYPES.PULLREQUEST, }, }); } + + private mergePullRequestWithUpdate( + pr: PullRequest, + updateParams: PullRequestUpdateParams, + ): PullRequest { + return { + ...pr, + title: updateParams.title, + description: updateParams.description + ? updateParams.description + : pr.description, + status: updateParams.status ? updateParams.status : pr.status, + target: updateParams.target ? updateParams.target : pr.target, + }; + } } From 1d45e628ed91698a3f950167112cb96f5a3f96d7 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 6 Nov 2024 15:23:53 +0100 Subject: [PATCH 60/61] Fix wrong typing --- lib/modules/platform/scm-manager/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/scm-manager/schema.ts b/lib/modules/platform/scm-manager/schema.ts index 0827e447de..83d1e90c76 100644 --- a/lib/modules/platform/scm-manager/schema.ts +++ b/lib/modules/platform/scm-manager/schema.ts @@ -84,7 +84,7 @@ const RepoTypeSchema = z.enum(['git', 'svn', 'hg']); export const RepoSchema = z.object({ contact: z.string().optional().nullable(), - creationDate: z.string(), + creationDate: z.string().optional().nullable(), description: z.string().optional().nullable(), lastModified: z.string().optional().nullable(), namespace: z.string(), From 2c07b6715527af1639de73e04c0e002726e98d26 Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Wed, 6 Nov 2024 16:19:34 +0100 Subject: [PATCH 61/61] Refactor parameterized tests for better readability --- .../platform/scm-manager/index.spec.ts | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/lib/modules/platform/scm-manager/index.spec.ts b/lib/modules/platform/scm-manager/index.spec.ts index 0928e4f4da..14f0dc3e5e 100644 --- a/lib/modules/platform/scm-manager/index.spec.ts +++ b/lib/modules/platform/scm-manager/index.spec.ts @@ -271,31 +271,32 @@ describe('modules/platform/scm-manager/index', () => { ).toEqual(renovatePr); }); - it.each([ - [[], pullRequest.source, pullRequest.title, 'all', null], - [[pullRequest], 'invalid branchName', pullRequest.title, 'all', null], - [[pullRequest], pullRequest.source, 'invalid title', 'all', null], - [[pullRequest], pullRequest.source, null, 'all', renovatePr], - [[pullRequest], pullRequest.source, undefined, 'all', renovatePr], - [[pullRequest], pullRequest.source, pullRequest.title, 'all', renovatePr], - [ - [pullRequest], - pullRequest.source, - pullRequest.title, - 'open', - renovatePr, - ], - [[pullRequest], pullRequest.source, pullRequest.title, '!open', null], - [[pullRequest], pullRequest.source, pullRequest.title, 'closed', null], - ])( - 'search within %p for %p, %p, %p with result %p', - async ( - availablePullRequest: PullRequest[], - branchName: string, - prTitle: string | null | undefined, - state: string, - result: Pr | null, - ) => { + it.each` + availablePullRequest | branchName | prTitle | state | result + ${[]} | ${pullRequest.source} | ${pullRequest.title} | ${'all'} | ${null} + ${[pullRequest]} | ${'invalid branchName'} | ${pullRequest.title} | ${'all'} | ${null} + ${[pullRequest]} | ${pullRequest.source} | ${'invalid title'} | ${'all'} | ${null} + ${[pullRequest]} | ${pullRequest.source} | ${null} | ${'all'} | ${renovatePr} + ${[pullRequest]} | ${pullRequest.source} | ${undefined} | ${'all'} | ${renovatePr} + ${[pullRequest]} | ${pullRequest.source} | ${pullRequest.title} | ${'all'} | ${renovatePr} + ${[pullRequest]} | ${pullRequest.source} | ${pullRequest.title} | ${'open'} | ${renovatePr} + ${[pullRequest]} | ${pullRequest.source} | ${pullRequest.title} | ${'!open'} | ${null} + ${[pullRequest]} | ${pullRequest.source} | ${pullRequest.title} | ${'closed'} | ${null} + `( + 'search within available pull requests for branch name "$branchName", pr title "$prTitle" and state "$state" with result $result ', + async ({ + availablePullRequest, + branchName, + prTitle, + state, + result, + }: { + availablePullRequest: PullRequest[]; + branchName: string; + prTitle: string | undefined | null; + state: string; + result: Pr | null; + }) => { httpMock .scope(endpoint) .get( @@ -321,17 +322,22 @@ describe('modules/platform/scm-manager/index', () => { }); describe(getBranchPr, () => { - it.each([ - [[], pullRequest.source, null], - [[pullRequest], 'invalid branchName', null], - [[pullRequest], pullRequest.source, renovatePr], - ])( - 'search within %p for %p with result %p', - async ( - availablePullRequest: PullRequest[], - branchName: string, - result: Pr | null, - ) => { + it.each` + availablePullRequest | branchName | result + ${[]} | ${pullRequest.source} | ${null} + ${[pullRequest]} | ${'invalid branchName'} | ${null} + ${[pullRequest]} | ${pullRequest.source} | ${renovatePr} + `( + 'search within available pull requests for branch name "$branchName" with result $result', + async ({ + availablePullRequest, + branchName, + result, + }: { + availablePullRequest: PullRequest[]; + branchName: string; + result: Pr | null; + }) => { httpMock .scope(endpoint) .get(