mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-11 22:46:27 +00:00
Merge 2c07b67155
into f97189c600
This commit is contained in:
commit
acd2645447
20 changed files with 2126 additions and 9 deletions
|
@ -12,7 +12,7 @@ If you see anything wrong on this page, please let us know by creating a [Discus
|
||||||
| Dependency Dashboard | Yes | No |
|
| 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 |
|
| 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 |
|
| 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) |
|
| 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 |
|
| Show changelogs | Yes | Yes |
|
||||||
| Compatibility score badges | Four badges showing: Age, Adoption, Passing, Confidence | One badge with overall compatibility score |
|
| Compatibility score badges | Four badges showing: Age, Adoption, Passing, Confidence | One badge with overall compatibility score |
|
||||||
|
|
|
@ -31,10 +31,10 @@ If you're self hosting Renovate, use the latest release if possible.
|
||||||
|
|
||||||
## Renovate core features not supported on all platforms
|
## Renovate core features not supported on all platforms
|
||||||
|
|
||||||
| Feature | Platforms which lack feature | See Renovate issue(s) |
|
| Feature | Platforms which lack feature | See Renovate issue(s) |
|
||||||
| --------------------- | ----------------------------------------------- | ------------------------------------------------------------ |
|
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||||
| Dependency Dashboard | Azure, Bitbucket, Bitbucket Server, Gerrit | [#9592](https://github.com/renovatebot/renovate/issues/9592) |
|
| 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 | |
|
| The Mend Renovate App | Azure, Bitbucket Server, Forgejo, Gitea, GitLab, SCM-Manager | |
|
||||||
|
|
||||||
## Major platform features not supported by Renovate
|
## Major platform features not supported by Renovate
|
||||||
|
|
||||||
|
|
|
@ -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)
|
- [Gitea and Forgejo](../modules/platform/gitea/index.md)
|
||||||
- [github.com and GitHub Enterprise Server](../modules/platform/github/index.md)
|
- [github.com and GitHub Enterprise Server](../modules/platform/github/index.md)
|
||||||
- [GitLab](../modules/platform/gitlab/index.md)
|
- [GitLab](../modules/platform/gitlab/index.md)
|
||||||
|
- [SCM-Manager](../modules/platform/scm-manager/index.md)
|
||||||
|
|
||||||
### GitHub.com token for changelogs
|
### GitHub.com token for changelogs
|
||||||
|
|
||||||
|
|
|
@ -408,7 +408,7 @@ const options: RenovateOptions[] = [
|
||||||
'If set to `true` then Renovate creates draft PRs, instead of normal status PRs.',
|
'If set to `true` then Renovate creates draft PRs, instead of normal status PRs.',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
supportedPlatforms: ['azure', 'gitea', 'github', 'gitlab'],
|
supportedPlatforms: ['azure', 'gitea', 'github', 'gitlab', 'scm-manager'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dryRun',
|
name: 'dryRun',
|
||||||
|
@ -918,7 +918,12 @@ const options: RenovateOptions[] = [
|
||||||
description: 'Username for authentication.',
|
description: 'Username for authentication.',
|
||||||
stage: 'repository',
|
stage: 'repository',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
supportedPlatforms: ['azure', 'bitbucket', 'bitbucket-server'],
|
supportedPlatforms: [
|
||||||
|
'azure',
|
||||||
|
'bitbucket',
|
||||||
|
'bitbucket-server',
|
||||||
|
'scm-manager',
|
||||||
|
],
|
||||||
globalOnly: true,
|
globalOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2896,7 +2901,7 @@ const options: RenovateOptions[] = [
|
||||||
description:
|
description:
|
||||||
'Overrides the default resolution for Git remote, e.g. to switch GitLab from HTTPS to SSH-based.',
|
'Overrides the default resolution for Git remote, e.g. to switch GitLab from HTTPS to SSH-based.',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
supportedPlatforms: ['gitlab', 'bitbucket-server'],
|
supportedPlatforms: ['gitlab', 'bitbucket-server', 'scm-manager'],
|
||||||
allowedValues: ['default', 'ssh', 'endpoint'],
|
allowedValues: ['default', 'ssh', 'endpoint'],
|
||||||
default: 'default',
|
default: 'default',
|
||||||
stage: 'repository',
|
stage: 'repository',
|
||||||
|
|
|
@ -26,6 +26,7 @@ const resolvers = {
|
||||||
github,
|
github,
|
||||||
gitlab,
|
gitlab,
|
||||||
local: null,
|
local: null,
|
||||||
|
'scm-manager': null,
|
||||||
} satisfies Record<PlatformId, Resolver | null>;
|
} satisfies Record<PlatformId, Resolver | null>;
|
||||||
|
|
||||||
export function getPreset({
|
export function getPreset({
|
||||||
|
|
|
@ -8,6 +8,7 @@ export const PLATFORM_HOST_TYPES = [
|
||||||
'github',
|
'github',
|
||||||
'gitlab',
|
'gitlab',
|
||||||
'local',
|
'local',
|
||||||
|
'scm-manager',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type PlatformId = (typeof PLATFORM_HOST_TYPES)[number];
|
export type PlatformId = (typeof PLATFORM_HOST_TYPES)[number];
|
||||||
|
|
|
@ -8,6 +8,7 @@ import * as gitea from './gitea';
|
||||||
import * as github from './github';
|
import * as github from './github';
|
||||||
import * as gitlab from './gitlab';
|
import * as gitlab from './gitlab';
|
||||||
import * as local from './local';
|
import * as local from './local';
|
||||||
|
import * as scmm from './scm-manager';
|
||||||
import type { Platform } from './types';
|
import type { Platform } from './types';
|
||||||
|
|
||||||
const api = new Map<PlatformId, Platform>();
|
const api = new Map<PlatformId, Platform>();
|
||||||
|
@ -22,3 +23,4 @@ api.set(gitea.id, gitea);
|
||||||
api.set(github.id, github);
|
api.set(github.id, github);
|
||||||
api.set(gitlab.id, gitlab);
|
api.set(gitlab.id, gitlab);
|
||||||
api.set(local.id, local);
|
api.set(local.id, local);
|
||||||
|
api.set(scmm.id, scmm);
|
||||||
|
|
650
lib/modules/platform/scm-manager/index.spec.ts
Normal file
650
lib/modules/platform/scm-manager/index.spec.ts
Normal file
|
@ -0,0 +1,650 @@
|
||||||
|
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';
|
||||||
|
import * as _util from '../util';
|
||||||
|
import { mapPrFromScmToRenovate } from './mapper';
|
||||||
|
import type { PrFilterByState, PullRequest, Repo, 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,
|
||||||
|
maxBodyLength,
|
||||||
|
mergePr,
|
||||||
|
setBranchStatus,
|
||||||
|
updatePr,
|
||||||
|
} from './index';
|
||||||
|
|
||||||
|
jest.mock('../../../util/git');
|
||||||
|
jest.mock('../util');
|
||||||
|
const util: jest.Mocked<typeof _util> = mocked(_util);
|
||||||
|
|
||||||
|
const endpoint = 'https://localhost:8080/scm/api/v2';
|
||||||
|
const token = 'TEST_TOKEN';
|
||||||
|
|
||||||
|
const user: User = {
|
||||||
|
mail: 'test@user.de',
|
||||||
|
displayName: 'Test User',
|
||||||
|
name: '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: 'https://localhost:8080/scm/default/repo' },
|
||||||
|
],
|
||||||
|
defaultBranch: {
|
||||||
|
href: 'https://localhost:8080/scm/api/v2/config/git/default/repo/default-branch',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const pullRequest: PullRequest = {
|
||||||
|
id: '1',
|
||||||
|
author: { displayName: 'Thomas Zerr', id: '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/scm-manager/index', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
hostRules.add({ token, username: user.name });
|
||||||
|
invalidatePrCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(initPlatform, () => {
|
||||||
|
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, when token is not configured', async () => {
|
||||||
|
await expect(initPlatform({ endpoint })).rejects.toThrow(
|
||||||
|
'SCM-Manager API token not configured',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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({
|
||||||
|
endpoint,
|
||||||
|
gitAuthor: 'Test User <test@user.de>',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(initRepo, () => {
|
||||||
|
it('should init repo', async () => {
|
||||||
|
const repository = `${repo.namespace}/${repo.name}`;
|
||||||
|
const expectedFingerprint = 'expectedFingerprint';
|
||||||
|
const expectedDefaultBranch = '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);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await initRepo({ repository: `${repo.namespace}/${repo.name}` }),
|
||||||
|
).toEqual({
|
||||||
|
defaultBranch: expectedDefaultBranch,
|
||||||
|
isFork: false,
|
||||||
|
repoFingerprint: expectedFingerprint,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(git.initRepo).toHaveBeenCalledWith({
|
||||||
|
url: `https://${user.name}:${token}@localhost:8080/scm/default/repo`,
|
||||||
|
repository,
|
||||||
|
defaultBranch: expectedDefaultBranch,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(getRepos, () => {
|
||||||
|
it('should return all available repos', async () => {
|
||||||
|
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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(getPrList, () => {
|
||||||
|
it('should return empty array, because no PR could be found', async () => {
|
||||||
|
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([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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[] = [
|
||||||
|
{
|
||||||
|
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: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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);
|
||||||
|
//Fetching from cache
|
||||||
|
expect(await getPrList()).toIncludeAllMembers(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(findPr, () => {
|
||||||
|
it('search in Pull Request without explicitly setting the state as argument', 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 findPr({
|
||||||
|
branchName: pullRequest.source,
|
||||||
|
prTitle: pullRequest.title,
|
||||||
|
}),
|
||||||
|
).toEqual(renovatePr);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(
|
||||||
|
`/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`,
|
||||||
|
)
|
||||||
|
.reply(200, {
|
||||||
|
page: 0,
|
||||||
|
pageTotal: 1,
|
||||||
|
_embedded: {
|
||||||
|
pullRequests: availablePullRequest,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findPr({
|
||||||
|
branchName,
|
||||||
|
prTitle,
|
||||||
|
state: state as PrFilterByState,
|
||||||
|
}),
|
||||||
|
).toEqual(result);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(getBranchPr, () => {
|
||||||
|
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(
|
||||||
|
`/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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(getPr, () => {
|
||||||
|
it('should return null, because PR was not found', 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(404);
|
||||||
|
|
||||||
|
expect(await getPr(1)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await getPr(parseInt(pullRequest.id))).toEqual(renovatePr);
|
||||||
|
});
|
||||||
|
|
||||||
|
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, () => {
|
||||||
|
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,
|
||||||
|
) => {
|
||||||
|
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({
|
||||||
|
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,
|
||||||
|
) => {
|
||||||
|
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`)
|
||||||
|
.reply(204);
|
||||||
|
|
||||||
|
await updatePr({
|
||||||
|
number: 1,
|
||||||
|
prTitle: 'PR Title',
|
||||||
|
prBody: actualPrBody,
|
||||||
|
state: actualState as 'open' | 'closed' | undefined,
|
||||||
|
targetBranch: 'Target/Branch',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(httpMock.allUsed()).toBeTrue();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(mergePr, () => {
|
||||||
|
it('should Not implemented and return false', async () => {
|
||||||
|
const result = await mergePr({ id: 1 });
|
||||||
|
expect(result).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(getBranchStatus, () => {
|
||||||
|
it('should Not implemented and return red', async () => {
|
||||||
|
const result = await getBranchStatus('test/branch', false);
|
||||||
|
expect(result).toBe('red');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(setBranchStatus, () => {
|
||||||
|
it('should Not implemented', async () => {
|
||||||
|
await expect(
|
||||||
|
setBranchStatus({
|
||||||
|
branchName: 'test/branch',
|
||||||
|
context: 'context',
|
||||||
|
description: 'description',
|
||||||
|
state: 'red',
|
||||||
|
}),
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(getBranchStatusCheck, () => {
|
||||||
|
it('should Not implemented and return null', async () => {
|
||||||
|
const result = await getBranchStatusCheck('test/branch', null);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(addReviewers, () => {
|
||||||
|
it('should Not implemented', async () => {
|
||||||
|
await expect(addReviewers(1, ['reviewer'])).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(addAssignees, () => {
|
||||||
|
it('should Not implemented', async () => {
|
||||||
|
await expect(addAssignees(1, ['assignee'])).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(deleteLabel, () => {
|
||||||
|
it('should Not implemented', async () => {
|
||||||
|
await expect(deleteLabel(1, 'label')).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(getIssueList, () => {
|
||||||
|
it('should Not implemented and return empty list', async () => {
|
||||||
|
const result = await getIssueList();
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(findIssue, () => {
|
||||||
|
it('should Not implemented and return null', async () => {
|
||||||
|
const result = await findIssue('issue');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(ensureIssue, () => {
|
||||||
|
it('should Not implemented and return null', async () => {
|
||||||
|
const result = await ensureIssue({ title: 'issue', body: 'body' });
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(ensureIssueClosing, () => {
|
||||||
|
it('should Not implemented', async () => {
|
||||||
|
await expect(ensureIssueClosing('issue')).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(ensureCommentRemoval, () => {
|
||||||
|
it('should Not implemented', 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 Not implemented and return false', async () => {
|
||||||
|
const result = await getRepoForceRebase();
|
||||||
|
expect(result).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(getRawFile, () => {
|
||||||
|
it('should Not implemented and return null', async () => {
|
||||||
|
const result = await getRawFile('file');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(getJsonFile, () => {
|
||||||
|
it('should Not implemented and return undefined', async () => {
|
||||||
|
const result = await getJsonFile('package.json');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(maxBodyLength, () => {
|
||||||
|
it('should return the max body length allowed for an SCM-Manager request body', () => {
|
||||||
|
expect(maxBodyLength()).toBe(200000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
336
lib/modules/platform/scm-manager/index.ts
Normal file
336
lib/modules/platform/scm-manager/index.ts
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
import { logger } from '../../../logger';
|
||||||
|
import type { BranchStatus } from '../../../types';
|
||||||
|
import * as git from '../../../util/git';
|
||||||
|
import ScmManagerHttp from '../../../util/http/scm-manager';
|
||||||
|
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 { getRepoUrl, mapPrState, matchPrState, smartLinks } from './utils';
|
||||||
|
|
||||||
|
interface SCMMRepoConfig {
|
||||||
|
repository: string;
|
||||||
|
prList: Pr[] | null;
|
||||||
|
defaultBranch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const id = 'scm-manager';
|
||||||
|
|
||||||
|
let config: SCMMRepoConfig = {} as any;
|
||||||
|
let scmManagerHttp: ScmManagerHttp;
|
||||||
|
|
||||||
|
export async function initPlatform({
|
||||||
|
endpoint,
|
||||||
|
token,
|
||||||
|
}: PlatformParams): Promise<PlatformResult> {
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new Error('SCM-Manager endpoint not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('SCM-Manager API token not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
scmManagerHttp = new ScmManagerHttp(endpoint, token);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const me = await scmManagerHttp.getCurrentUser();
|
||||||
|
const gitAuthor = `${me.displayName} <${me.mail}>`;
|
||||||
|
const result = { endpoint, gitAuthor };
|
||||||
|
|
||||||
|
logger.info(`Plattform initialized ${JSON.stringify(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({
|
||||||
|
repository,
|
||||||
|
gitUrl,
|
||||||
|
}: RepoParams): Promise<RepoResult> {
|
||||||
|
const repo = await scmManagerHttp.getRepo(repository);
|
||||||
|
const defaultBranch = await scmManagerHttp.getDefaultBranch(repo);
|
||||||
|
const url = getRepoUrl(repo, gitUrl, scmManagerHttp.getEndpoint());
|
||||||
|
|
||||||
|
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,
|
||||||
|
scmManagerHttp.getEndpoint(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.trace({ result }, `Repo initialized`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRepos(): Promise<string[]> {
|
||||||
|
const repos = (await scmManagerHttp.getAllRepos()).filter(
|
||||||
|
(repo) => repo.type === 'git',
|
||||||
|
);
|
||||||
|
const result = repos.map((repo) => `${repo.namespace}/${repo.name}`);
|
||||||
|
logger.debug(`Discovered ${repos.length} repos`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBranchPr(branchName: string): Promise<Pr | null> {
|
||||||
|
return await findPr({ branchName, state: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findPr({
|
||||||
|
branchName,
|
||||||
|
prTitle,
|
||||||
|
state = 'all',
|
||||||
|
}: FindPRConfig): Promise<Pr | null> {
|
||||||
|
const inProgressPrs = await getPrList();
|
||||||
|
const result = inProgressPrs.find(
|
||||||
|
(pr) =>
|
||||||
|
branchName === pr.sourceBranch &&
|
||||||
|
(!prTitle || prTitle === pr.title) &&
|
||||||
|
matchPrState(pr, state),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
logger.trace({ result }, `Found PR`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace(
|
||||||
|
`Could not find PR with source branch ${branchName} and title ${
|
||||||
|
prTitle ?? ''
|
||||||
|
} and state ${state}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPr(number: number): Promise<Pr | null> {
|
||||||
|
const inProgressPrs = await getPrList();
|
||||||
|
const cachedPr = inProgressPrs.find((pr) => pr.number === number);
|
||||||
|
|
||||||
|
if (cachedPr) {
|
||||||
|
logger.trace('Returning from cached PRs');
|
||||||
|
return cachedPr;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await scmManagerHttp.getRepoPr(config.repository, number);
|
||||||
|
logger.trace('Returning PR from API');
|
||||||
|
return mapPrFromScmToRenovate(result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, `Can not find a PR with id ${number}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPrList(): Promise<Pr[]> {
|
||||||
|
if (config.prList === null) {
|
||||||
|
try {
|
||||||
|
config.prList = (
|
||||||
|
await scmManagerHttp.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<Pr> {
|
||||||
|
const createdPr = await scmManagerHttp.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}'`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return mapPrFromScmToRenovate(createdPr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePr({
|
||||||
|
number,
|
||||||
|
prTitle,
|
||||||
|
prBody,
|
||||||
|
state,
|
||||||
|
targetBranch,
|
||||||
|
}: UpdatePrConfig): Promise<void> {
|
||||||
|
await scmManagerHttp.updatePr(config.repository, number, {
|
||||||
|
title: prTitle,
|
||||||
|
description: sanitize(prBody) ?? undefined,
|
||||||
|
target: targetBranch,
|
||||||
|
status: mapPrState(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Updated PR #${number} with title ${prTitle}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergePr(config: MergePRConfig): Promise<boolean> {
|
||||||
|
logger.debug('Not implemented mergePr');
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBranchStatus(
|
||||||
|
branchName: string,
|
||||||
|
internalChecksAsSuccess: boolean,
|
||||||
|
): Promise<BranchStatus> {
|
||||||
|
logger.debug('Not implemented getBranchStatus');
|
||||||
|
return Promise.resolve('red');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBranchStatus(
|
||||||
|
branchStatusConfig: BranchStatusConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.debug('Not implemented setBranchStatus');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBranchStatusCheck(
|
||||||
|
branchName: string,
|
||||||
|
context: string | null | undefined,
|
||||||
|
): Promise<BranchStatus | null> {
|
||||||
|
logger.debug('Not implemented setBranchStatus');
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addReviewers(
|
||||||
|
number: number,
|
||||||
|
reviewers: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
logger.debug('Not implemented addReviewers');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addAssignees(
|
||||||
|
number: number,
|
||||||
|
assignees: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
logger.debug('Not implemented addAssignees');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteLabel(number: number, label: string): Promise<void> {
|
||||||
|
logger.debug('Not implemented deleteLabel');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIssueList(): Promise<Issue[]> {
|
||||||
|
logger.debug('Not implemented getIssueList');
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findIssue(title: string): Promise<Issue | null> {
|
||||||
|
logger.debug('Not implemented findIssue');
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureIssue(
|
||||||
|
config: EnsureIssueConfig,
|
||||||
|
): Promise<'updated' | 'created' | null> {
|
||||||
|
logger.debug('Not implemented ensureIssue');
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureIssueClosing(title: string): Promise<void> {
|
||||||
|
logger.debug('Not implemented ensureIssueClosing');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export function ensureComment(config: EnsureCommentConfig): Promise<boolean> {
|
||||||
|
logger.debug('Not implemented ensureComment');
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureCommentRemoval(
|
||||||
|
ensureCommentRemoval:
|
||||||
|
| EnsureCommentRemovalConfigByTopic
|
||||||
|
| EnsureCommentRemovalConfigByContent,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.debug('Not implemented ensureCommentRemoval');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function massageMarkdown(prBody: string): string {
|
||||||
|
return smartTruncate(smartLinks(prBody), maxBodyLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRepoForceRebase(): Promise<boolean> {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRawFile(
|
||||||
|
fileName: string,
|
||||||
|
repoName?: string,
|
||||||
|
branchOrTag?: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
logger.debug('Not implemented getRawFile');
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getJsonFile(
|
||||||
|
fileName: string,
|
||||||
|
repoName?: string,
|
||||||
|
branchOrTag?: string,
|
||||||
|
): Promise<any> {
|
||||||
|
logger.debug('Not implemented getJsonFile');
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function maxBodyLength(): number {
|
||||||
|
return 200000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export function invalidatePrCache(): void {
|
||||||
|
config.prList = null;
|
||||||
|
}
|
44
lib/modules/platform/scm-manager/mapper.spec.ts
Normal file
44
lib/modules/platform/scm-manager/mapper.spec.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { mapPrFromScmToRenovate } from './mapper';
|
||||||
|
import type { PullRequest as SCMPullRequest } from './types';
|
||||||
|
|
||||||
|
describe('modules/platform/scm-manager/mapper', () => {
|
||||||
|
it('should correctly map the scm-manager 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
23
lib/modules/platform/scm-manager/mapper.ts
Normal file
23
lib/modules/platform/scm-manager/mapper.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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 ? pr.closeDate : undefined,
|
||||||
|
hasAssignees:
|
||||||
|
pr.reviewer !== undefined &&
|
||||||
|
pr.reviewer !== null &&
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
}
|
30
lib/modules/platform/scm-manager/readme.md
Normal file
30
lib/modules/platform/scm-manager/readme.md
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# SCM-Manager
|
||||||
|
|
||||||
|
Renovate supports the [SCM-Manager](https://scm-manager.org) platform.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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`.
|
114
lib/modules/platform/scm-manager/schema.ts
Normal file
114
lib/modules/platform/scm-manager/schema.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const UserSchema = z.object({
|
||||||
|
mail: z.string().optional().nullable(),
|
||||||
|
displayName: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DefaultBranchSchema = z.object({
|
||||||
|
defaultBranch: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LinkSchema = z.object({
|
||||||
|
href: z.string(),
|
||||||
|
name: z.string().optional().nullable(),
|
||||||
|
templated: z.boolean().optional().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LinksSchema = z.record(
|
||||||
|
z.string(),
|
||||||
|
z.union([LinkSchema, z.array(LinkSchema)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PrStateSchema = z.enum(['DRAFT', 'OPEN', 'REJECTED', 'MERGED']);
|
||||||
|
|
||||||
|
export const PrMergeMethodSchema = z.enum([
|
||||||
|
'MERGE_COMMIT',
|
||||||
|
'REBASE',
|
||||||
|
'FAST_FORWARD_IF_POSSIBLE',
|
||||||
|
'SQUASH',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const PullRequestSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
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().optional().nullable(),
|
||||||
|
creationDate: z.string(),
|
||||||
|
lastModified: z.string().optional().nullable(),
|
||||||
|
status: PrStateSchema,
|
||||||
|
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: z.object({
|
||||||
|
todo: z.number(),
|
||||||
|
done: z.number(),
|
||||||
|
}),
|
||||||
|
_links: LinksSchema,
|
||||||
|
_embedded: z.object({
|
||||||
|
defaultConfig: z.object({
|
||||||
|
mergeStrategy: PrMergeMethodSchema,
|
||||||
|
deleteBranchOnMerge: z.boolean(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const RepoTypeSchema = z.enum(['git', 'svn', 'hg']);
|
||||||
|
|
||||||
|
export const RepoSchema = z.object({
|
||||||
|
contact: z.string().optional().nullable(),
|
||||||
|
creationDate: z.string().optional().nullable(),
|
||||||
|
description: z.string().optional().nullable(),
|
||||||
|
lastModified: z.string().optional().nullable(),
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
});
|
37
lib/modules/platform/scm-manager/types.ts
Normal file
37
lib/modules/platform/scm-manager/types.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import type { z } from 'zod';
|
||||||
|
import type {
|
||||||
|
LinkSchema,
|
||||||
|
LinksSchema,
|
||||||
|
PrMergeMethodSchema,
|
||||||
|
PrStateSchema,
|
||||||
|
PullRequestSchema,
|
||||||
|
RepoSchema,
|
||||||
|
UserSchema,
|
||||||
|
} from './schema';
|
||||||
|
|
||||||
|
export type Link = z.infer<typeof LinkSchema>;
|
||||||
|
export type Links = z.infer<typeof LinksSchema>;
|
||||||
|
|
||||||
|
export type User = z.infer<typeof UserSchema>;
|
||||||
|
|
||||||
|
export interface PullRequestCreateParams extends PullRequestUpdateParams {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PullRequestUpdateParams {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
status?: PrState;
|
||||||
|
target?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PullRequest = z.infer<typeof PullRequestSchema>;
|
||||||
|
|
||||||
|
type PrState = z.infer<typeof PrStateSchema>;
|
||||||
|
|
||||||
|
export type PrMergeMethod = z.infer<typeof PrMergeMethodSchema>;
|
||||||
|
|
||||||
|
export type Repo = z.infer<typeof RepoSchema>;
|
||||||
|
|
||||||
|
export type PrFilterByState = 'open' | 'closed' | '!open' | 'all';
|
224
lib/modules/platform/scm-manager/utils.spec.ts
Normal file
224
lib/modules/platform/scm-manager/utils.spec.ts
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
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, () => {
|
||||||
|
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 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, endpoint),
|
||||||
|
).toThrow('Missing protocol links.');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should throw error because of missing SSH link', () => {
|
||||||
|
expect(() =>
|
||||||
|
getRepoUrl(
|
||||||
|
{
|
||||||
|
...repo,
|
||||||
|
_links: { protocol: [{ name: 'http', href: gitHttpEndpoint }] },
|
||||||
|
},
|
||||||
|
'ssh',
|
||||||
|
endpoint,
|
||||||
|
),
|
||||||
|
).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',
|
||||||
|
endpoint,
|
||||||
|
),
|
||||||
|
).toThrow('Expected protocol links to be an array of links.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the provided ssh link', () => {
|
||||||
|
expect(
|
||||||
|
getRepoUrl(
|
||||||
|
{
|
||||||
|
...repo,
|
||||||
|
_links: { protocol: [{ name: 'ssh', href: gitSshEndpoint }] },
|
||||||
|
},
|
||||||
|
'ssh',
|
||||||
|
endpoint,
|
||||||
|
),
|
||||||
|
).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,
|
||||||
|
endpoint,
|
||||||
|
),
|
||||||
|
).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,
|
||||||
|
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) => {
|
||||||
|
expect(
|
||||||
|
getRepoUrl(
|
||||||
|
{
|
||||||
|
...repo,
|
||||||
|
_links: { protocol: [{ name: 'http', href: gitHttpEndpoint }] },
|
||||||
|
},
|
||||||
|
gitUrl as GitUrlOption | undefined,
|
||||||
|
endpoint,
|
||||||
|
),
|
||||||
|
).toBe('http://tzerr:token@localhost:8081/scm/repo/default/repo');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
112
lib/modules/platform/scm-manager/utils.ts
Normal file
112
lib/modules/platform/scm-manager/utils.ts
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
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';
|
||||||
|
import type { PrFilterByState, PrMergeMethod, 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,
|
||||||
|
endpoint: 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) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostOptions = hostRules.find({
|
||||||
|
hostType: 'scm-manager',
|
||||||
|
url: endpoint,
|
||||||
|
});
|
||||||
|
|
||||||
|
repoUrl.username = hostOptions.username ?? '';
|
||||||
|
repoUrl.password = hostOptions.token ?? '';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ platformScmImpls.set('gitea', DefaultGitScm);
|
||||||
platformScmImpls.set('github', GithubScm);
|
platformScmImpls.set('github', GithubScm);
|
||||||
platformScmImpls.set('gitlab', DefaultGitScm);
|
platformScmImpls.set('gitlab', DefaultGitScm);
|
||||||
platformScmImpls.set('local', LocalFs);
|
platformScmImpls.set('local', LocalFs);
|
||||||
|
platformScmImpls.set('scm-manager', DefaultGitScm);
|
||||||
|
|
||||||
let _scm: PlatformScm | undefined;
|
let _scm: PlatformScm | undefined;
|
||||||
|
|
||||||
|
|
343
lib/util/http/scm-manager.spec.ts
Normal file
343
lib/util/http/scm-manager.spec.ts
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
import * as httpMock from '../../../test/http-mock';
|
||||||
|
import type {
|
||||||
|
PullRequest,
|
||||||
|
PullRequestCreateParams,
|
||||||
|
PullRequestUpdateParams,
|
||||||
|
Repo,
|
||||||
|
User,
|
||||||
|
} from '../../modules/platform/scm-manager/types';
|
||||||
|
import ScmManagerHttp from './scm-manager';
|
||||||
|
|
||||||
|
describe('util/http/scm-manager', () => {
|
||||||
|
const endpoint = 'http://localhost:8080/scm/api/v2';
|
||||||
|
const token = 'validApiToken';
|
||||||
|
|
||||||
|
const scmManagerHttp = new ScmManagerHttp(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', id: '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(scmManagerHttp.getEndpoint, () => {
|
||||||
|
it('should return the endpoint', () => {
|
||||||
|
expect(scmManagerHttp.getEndpoint()).toEqual(endpoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(scmManagerHttp.getCurrentUser, () => {
|
||||||
|
it('should return the current user', async () => {
|
||||||
|
const expectedUser: User = {
|
||||||
|
mail: 'test@test.de',
|
||||||
|
displayName: 'Test User',
|
||||||
|
name: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
httpMock.scope(endpoint).get('/me').reply(200, 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(scmManagerHttp.getCurrentUser()).rejects.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(scmManagerHttp.getRepo, () => {
|
||||||
|
it('should return the repo', async () => {
|
||||||
|
httpMock
|
||||||
|
.scope(endpoint)
|
||||||
|
.get(`/repositories/${repo.namespace}/${repo.name}`)
|
||||||
|
.reply(200, repo);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await scmManagerHttp.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(
|
||||||
|
scmManagerHttp.getRepo(`${repo.namespace}/${repo.name}`),
|
||||||
|
).rejects.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(scmManagerHttp.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 scmManagerHttp.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(scmManagerHttp.getAllRepos()).rejects.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(scmManagerHttp.getDefaultBranch, () => {
|
||||||
|
it('should return the default branch', async () => {
|
||||||
|
httpMock
|
||||||
|
.scope(endpoint)
|
||||||
|
.get('/config/git/default/repo/default-branch')
|
||||||
|
.reply(200, {
|
||||||
|
defaultBranch: 'develop',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await scmManagerHttp.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(scmManagerHttp.getDefaultBranch(repo)).rejects.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(scmManagerHttp.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 scmManagerHttp.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(
|
||||||
|
scmManagerHttp.getAllRepoPrs(`${repo.namespace}/${repo.name}`),
|
||||||
|
).rejects.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(scmManagerHttp.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 scmManagerHttp.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(
|
||||||
|
scmManagerHttp.getRepoPr(`${repo.namespace}/${repo.name}`, 1337),
|
||||||
|
).rejects.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(scmManagerHttp.createPr, () => {
|
||||||
|
it('should create PR for a repo', async () => {
|
||||||
|
const expectedCreateParams: PullRequestCreateParams = {
|
||||||
|
source: 'feature/test',
|
||||||
|
target: 'develop',
|
||||||
|
title: 'Test Title',
|
||||||
|
description: 'PR description',
|
||||||
|
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 scmManagerHttp.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(
|
||||||
|
scmManagerHttp.createPr(`${repo.namespace}/${repo.name}`, {
|
||||||
|
source: 'feature/test',
|
||||||
|
target: 'develop',
|
||||||
|
title: 'Test Title',
|
||||||
|
description: 'PR description',
|
||||||
|
status: 'OPEN',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(scmManagerHttp.updatePr, () => {
|
||||||
|
it('should update PR for a repo', async () => {
|
||||||
|
const expectedUpdateParams: PullRequestUpdateParams = {
|
||||||
|
title: 'Test Title',
|
||||||
|
description: 'PR description',
|
||||||
|
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}`)
|
||||||
|
.reply(204);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
scmManagerHttp.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)
|
||||||
|
.get(`/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`)
|
||||||
|
.reply(200, pullRequest);
|
||||||
|
|
||||||
|
httpMock
|
||||||
|
.scope(endpoint)
|
||||||
|
.put(`/pull-requests/${repo.namespace}/${repo.name}/${expectedPrId}`)
|
||||||
|
.reply(response);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
scmManagerHttp.updatePr(
|
||||||
|
`${repo.namespace}/${repo.name}`,
|
||||||
|
expectedPrId,
|
||||||
|
{
|
||||||
|
title: 'Test Title',
|
||||||
|
description: 'PR description',
|
||||||
|
status: 'OPEN',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).rejects.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
193
lib/util/http/scm-manager.ts
Normal file
193
lib/util/http/scm-manager.ts
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
import {
|
||||||
|
DefaultBranchSchema,
|
||||||
|
PagedPullRequestSchema,
|
||||||
|
PagedRepoSchema,
|
||||||
|
PullRequestSchema,
|
||||||
|
RepoSchema,
|
||||||
|
UserSchema,
|
||||||
|
} from '../../modules/platform/scm-manager/schema';
|
||||||
|
import type {
|
||||||
|
Link,
|
||||||
|
PullRequest,
|
||||||
|
PullRequestCreateParams,
|
||||||
|
PullRequestUpdateParams,
|
||||||
|
Repo,
|
||||||
|
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',
|
||||||
|
ALL_REPOS: 'repositories?pageSize=1000000',
|
||||||
|
REPO: (repoPath: string) => `repositories/${repoPath}`,
|
||||||
|
PULLREQUESTS: (repoPath: string) => `pull-requests/${repoPath}`,
|
||||||
|
PULLREQUESTS_WITH_PAGINATION: (repoPath: string) =>
|
||||||
|
`pull-requests/${repoPath}?status=ALL&pageSize=1000000`,
|
||||||
|
PULLREQUEST_BY_ID: (repoPath: string, id: number) =>
|
||||||
|
`pull-requests/${repoPath}/${id}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONTENT_TYPES = {
|
||||||
|
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 interface ScmManagerHttpOptions extends HttpOptions {
|
||||||
|
scmmContentType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ScmManagerHttp extends Http<ScmManagerHttpOptions> {
|
||||||
|
private readonly endpoint: string;
|
||||||
|
|
||||||
|
constructor(endpoint: string, token: string) {
|
||||||
|
super('scm-manager', { throwHttpErrors: true, token });
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async request<T>(
|
||||||
|
requestUrl: string | URL,
|
||||||
|
options?: InternalHttpOptions & ScmManagerHttpOptions,
|
||||||
|
): Promise<HttpResponse<T>> {
|
||||||
|
const opts = {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options?.headers,
|
||||||
|
accept: options?.scmmContentType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return await super.request(resolveBaseUrl(this.endpoint, requestUrl), opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEndpoint(): string {
|
||||||
|
return this.endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getCurrentUser(): Promise<User> {
|
||||||
|
const response = await this.getJson(
|
||||||
|
URLS.ME,
|
||||||
|
{
|
||||||
|
scmmContentType: CONTENT_TYPES.ME,
|
||||||
|
},
|
||||||
|
UserSchema,
|
||||||
|
);
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRepo(repoPath: string): Promise<Repo> {
|
||||||
|
const response = await this.getJson(
|
||||||
|
URLS.REPO(repoPath),
|
||||||
|
{
|
||||||
|
scmmContentType: CONTENT_TYPES.REPOSITORY,
|
||||||
|
},
|
||||||
|
RepoSchema,
|
||||||
|
);
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAllRepos(): Promise<Repo[]> {
|
||||||
|
const response = await this.getJson(
|
||||||
|
URLS.ALL_REPOS,
|
||||||
|
{
|
||||||
|
scmmContentType: CONTENT_TYPES.REPOSITORIES,
|
||||||
|
},
|
||||||
|
PagedRepoSchema,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.body._embedded.repositories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDefaultBranch(repo: Repo): Promise<string> {
|
||||||
|
const defaultBranchUrl = repo._links['defaultBranch'] as Link;
|
||||||
|
const response = await this.getJson(
|
||||||
|
defaultBranchUrl.href,
|
||||||
|
{
|
||||||
|
scmmContentType: CONTENT_TYPES.GIT_CONFIG,
|
||||||
|
},
|
||||||
|
DefaultBranchSchema,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.body.defaultBranch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAllRepoPrs(repoPath: string): Promise<PullRequest[]> {
|
||||||
|
const response = await this.getJson(
|
||||||
|
URLS.PULLREQUESTS_WITH_PAGINATION(repoPath),
|
||||||
|
{
|
||||||
|
scmmContentType: CONTENT_TYPES.PULLREQUESTS,
|
||||||
|
},
|
||||||
|
PagedPullRequestSchema,
|
||||||
|
);
|
||||||
|
return response.body._embedded.pullRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRepoPr(repoPath: string, id: number): Promise<PullRequest> {
|
||||||
|
const response = await this.getJson(
|
||||||
|
URLS.PULLREQUEST_BY_ID(repoPath, id),
|
||||||
|
{
|
||||||
|
scmmContentType: CONTENT_TYPES.PULLREQUEST,
|
||||||
|
},
|
||||||
|
PullRequestSchema,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createPr(
|
||||||
|
repoPath: string,
|
||||||
|
params: PullRequestCreateParams,
|
||||||
|
): Promise<PullRequest> {
|
||||||
|
const createPrResponse = await this.postJson(URLS.PULLREQUESTS(repoPath), {
|
||||||
|
scmmContentType: CONTENT_TYPES.PULLREQUEST,
|
||||||
|
body: params,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': CONTENT_TYPES.PULLREQUEST,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updatePr(
|
||||||
|
repoPath: string,
|
||||||
|
id: number,
|
||||||
|
params: PullRequestUpdateParams,
|
||||||
|
): Promise<void> {
|
||||||
|
const currentPr = await this.getRepoPr(repoPath, id);
|
||||||
|
await this.putJson(URLS.PULLREQUEST_BY_ID(repoPath, id), {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ Supports over [90 different package managers](https://docs.renovatebot.com/modul
|
||||||
|
|
||||||
### Platforms
|
### Platforms
|
||||||
|
|
||||||
Renovate updates code repositories on the following platforms: GitHub, GitLab, Bitbucket, Azure DevOps, AWS Code Commit, Gitea, Forgejo, Gerrit (experimental)
|
Renovate updates code repositories on the following platforms: GitHub, GitLab, Bitbucket, Azure DevOps, AWS Code Commit, Gitea, Forgejo, Gerrit (experimental), SCM-Manager
|
||||||
|
|
||||||
## Ways to run Renovate
|
## Ways to run Renovate
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue