Skip to content
Snippets Groups Projects
Unverified Commit c9357cc3 authored by Pascal Mathis's avatar Pascal Mathis Committed by GitHub
Browse files

feat: add support for gitea platform (#5509)

parent 54eb3588
Branches
Tags
No related merge requests found
Showing
with 4249 additions and 2 deletions
......@@ -133,6 +133,12 @@ First, [create a personal access token](https://docs.microsoft.com/en-us/azure/d
Configure it either as `token` in your `config.js` file, or in environment variable `RENOVATE_TOKEN`, or via CLI `--token=`.
Don't forget to configure `platform=azure` somewhere in config.
### Gitea
First, [create a access token](https://docs.gitea.io/en-us/api-usage/#authentication-via-the-api) for your bot account.
Configure it as `token` in your `config.js` file, or in environment variable `RENOVATE_TOKEN`, or via CLI `--token=`.
Don't forget to configure `platform=gitea` somewhere in config.
## GitHub.com token for release notes
If you are running on any platform except github.com, it's important to also configure `GITHUB_COM_TOKEN` containing a personal access token for github.com. This account can actually be _any_ account on GitHub, and needs only read-only access. It's used when fetching release notes for repositories in order to increase the hourly API limit.
......
......@@ -9,3 +9,4 @@ Currently supported platforms are:
- Bitbucket Server
- GitHub (github.com, GitHub Enterprise)
- GitLab (gitlab.com, self-hosted)
- Gitea (gitea.com, self-hosted)
export const PLATFORM_TYPE_AZURE = 'azure';
export const PLATFORM_TYPE_BITBUCKET = 'bitbucket';
export const PLATFORM_TYPE_BITBUCKET_SERVER = 'bitbucket-server';
export const PLATFORM_TYPE_GITEA = 'gitea';
export const PLATFORM_TYPE_GITHUB = 'github';
export const PLATFORM_TYPE_GITLAB = 'gitlab';
......@@ -178,7 +178,7 @@ export interface Platform {
deleteBranch(branchName: string, closePr?: boolean): Promise<void>;
ensureComment(ensureComment: EnsureCommentConfig): Promise<boolean>;
branchExists(branchName: string): Promise<boolean>;
setBaseBranch(baseBranch: string): Promise<void>;
setBaseBranch(baseBranch?: string): Promise<void>;
commitFilesToBranch(commitFile: CommitFilesConfig): Promise<void>;
getPr(number: number): Promise<Pr>;
findPr(findPRConfig: FindPRConfig): Promise<Pr>;
......
# Gitea
Gitea support is considered in **beta** release status. Mostly, it just needs more feedback/testing. If you have been using it and think it's reliable, please let us know.
## Unsupported platform features/concepts
- **Adding reviewers to PRs not supported**: While Gitea supports a basic implementation for supporting PR reviews, no API support has been implemented so far.
- **Ignoring Renovate PRs by close**: As Gitea does not expose the branch name of a PR once it has been deleted, all issued pull requests are immortal.
import URL from 'url';
import { GotApi, GotApiOptions, GotResponse } from '../common';
import { PLATFORM_TYPE_GITEA } from '../../constants/platforms';
import got from '../../util/got';
const hostType = PLATFORM_TYPE_GITEA;
let baseUrl: string;
function getPaginationContainer(body: any): any[] {
if (Array.isArray(body) && body.length) {
return body;
}
if (Array.isArray(body?.data) && body.data.length) {
return body.data;
}
return null;
}
async function get(path: string, options?: any): Promise<GotResponse> {
const opts = {
hostType,
baseUrl,
json: true,
...options,
};
const res = await got(path, opts);
const pc = getPaginationContainer(res.body);
if (opts.paginate && pc) {
const url = URL.parse(res.url, true);
const total = parseInt(res.headers['x-total-count'] as string, 10);
let nextPage = parseInt(url.query.page as string, 10) || 1 + 1;
while (total && pc.length < total) {
nextPage += 1;
url.query.page = nextPage.toString();
const nextRes = await got(URL.format(url), opts);
pc.push(...getPaginationContainer(nextRes.body));
}
}
return res;
}
const helpers = ['get', 'post', 'put', 'patch', 'head', 'delete'];
export type GiteaGotOptions = {
paginate?: boolean;
token?: string;
} & GotApiOptions;
export interface GiteaGotApi extends GotApi<GiteaGotOptions> {
setBaseUrl(url: string): void;
}
export const api: GiteaGotApi = {} as any;
for (const x of helpers) {
(api as any)[x] = (path: string, options: any): Promise<GotResponse> =>
get(path, { ...options, method: x.toUpperCase() });
}
// eslint-disable-next-line @typescript-eslint/unbound-method
api.setBaseUrl = (e: string): void => {
baseUrl = e;
};
import { URLSearchParams } from 'url';
import { api, GiteaGotOptions } from './gitea-got-wrapper';
import { GotResponse } from '../common';
export type PRState = 'open' | 'closed' | 'all';
export type IssueState = 'open' | 'closed' | 'all';
export type CommitStatusType =
| 'pending'
| 'success'
| 'error'
| 'failure'
| 'warning'
| 'unknown';
export type PRMergeMethod = 'merge' | 'rebase' | 'rebase-merge' | 'squash';
export interface PR {
number: number;
state: PRState;
title: string;
body: string;
mergeable: boolean;
created_at: string;
closed_at: string;
diff_url: string;
base?: {
ref: string;
};
head?: {
ref: string;
sha: string;
repo?: Repo;
};
}
export interface Issue {
number: number;
state: IssueState;
title: string;
body: string;
assignees: User[];
}
export interface User {
id: number;
email: string;
full_name: string;
username: string;
}
export interface Repo {
allow_merge_commits: boolean;
allow_rebase: boolean;
allow_rebase_explicit: boolean;
allow_squash_merge: boolean;
archived: boolean;
clone_url: string;
default_branch: string;
empty: boolean;
fork: boolean;
full_name: string;
mirror: boolean;
owner: User;
permissions: RepoPermission;
}
export interface RepoPermission {
admin: boolean;
pull: boolean;
push: boolean;
}
export interface RepoSearchResults {
ok: boolean;
data: Repo[];
}
export interface RepoContents {
path: string;
content?: string;
contentString?: string;
}
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: CommitUser;
}
export interface CommitUser {
name: string;
email: string;
username: string;
}
export interface CommitStatus {
id: number;
status: CommitStatusType;
context: string;
description: string;
target_url: string;
}
export interface CombinedCommitStatus {
worstStatus: CommitStatusType;
statuses: CommitStatus[];
}
export type RepoSearchParams = {
uid?: number;
};
export type IssueCreateParams = {} & IssueUpdateParams;
export type IssueUpdateParams = {
title?: string;
body?: string;
state?: IssueState;
assignees?: string[];
};
export type IssueSearchParams = {
state?: IssueState;
};
export type PRCreateParams = {
base?: string;
head?: string;
} & PRUpdateParams;
export type PRUpdateParams = {
title?: string;
body?: string;
assignees?: string[];
labels?: number[];
state?: PRState;
};
export type PRSearchParams = {
state?: PRState;
labels?: number[];
};
export type PRMergeParams = {
Do: PRMergeMethod;
};
export type CommentCreateParams = {} & CommentUpdateParams;
export type CommentUpdateParams = {
body: string;
};
export type CommitStatusCreateParams = {
context?: string;
description?: string;
state?: CommitStatusType;
target_url?: string;
};
const urlEscape = (raw: string): string => encodeURIComponent(raw);
const commitStatusStates: CommitStatusType[] = [
'unknown',
'success',
'pending',
'warning',
'failure',
'error',
];
function queryParams(params: Record<string, any>): URLSearchParams {
const usp = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (Array.isArray(v)) {
for (const item of v) {
usp.append(k, item.toString());
}
} else {
usp.append(k, v.toString());
}
}
return usp;
}
export async function getCurrentUser(options?: GiteaGotOptions): Promise<User> {
const url = 'user';
const res: GotResponse<User> = await api.get(url, options);
return res.body;
}
export async function searchRepos(
params: RepoSearchParams,
options?: GiteaGotOptions
): Promise<Repo[]> {
const query = queryParams(params).toString();
const url = `repos/search?${query}`;
const res: GotResponse<RepoSearchResults> = await api.get(url, {
...options,
paginate: true,
});
if (!res.body.ok) {
throw new Error(
'Unable to search for repositories, ok flag has not been set'
);
}
return res.body.data;
}
export async function getRepo(
repoPath: string,
options?: GiteaGotOptions
): Promise<Repo> {
const url = `repos/${repoPath}`;
const res: GotResponse<Repo> = await api.get(url, options);
return res.body;
}
export async function getRepoContents(
repoPath: string,
filePath: string,
ref?: string,
options?: GiteaGotOptions
): Promise<RepoContents> {
const query = queryParams(ref ? { ref } : {}).toString();
const url = `repos/${repoPath}/contents/${urlEscape(filePath)}?${query}`;
const res: GotResponse<RepoContents> = await api.get(url, options);
if (res.body.content) {
res.body.contentString = Buffer.from(res.body.content, 'base64').toString();
}
return res.body;
}
export async function createPR(
repoPath: string,
params: PRCreateParams,
options?: GiteaGotOptions
): Promise<PR> {
const url = `repos/${repoPath}/pulls`;
const res: GotResponse<PR> = await api.post(url, {
...options,
body: params,
});
return res.body;
}
export async function updatePR(
repoPath: string,
idx: number,
params: PRUpdateParams,
options?: GiteaGotOptions
): Promise<PR> {
const url = `repos/${repoPath}/pulls/${idx}`;
const res: GotResponse<PR> = await api.patch(url, {
...options,
body: params,
});
return res.body;
}
export async function closePR(
repoPath: string,
idx: number,
options?: GiteaGotOptions
): Promise<void> {
await updatePR(repoPath, idx, {
...options,
state: 'closed',
});
}
export async function mergePR(
repoPath: string,
idx: number,
method: PRMergeMethod,
options?: GiteaGotOptions
): Promise<void> {
const params: PRMergeParams = { Do: method };
const url = `repos/${repoPath}/pulls/${idx}/merge`;
await api.post(url, {
...options,
body: params,
});
}
export async function getPR(
repoPath: string,
idx: number,
options?: GiteaGotOptions
): Promise<PR> {
const url = `repos/${repoPath}/pulls/${idx}`;
const res: GotResponse<PR> = await api.get(url, options);
return res.body;
}
export async function searchPRs(
repoPath: string,
params: PRSearchParams,
options?: GiteaGotOptions
): Promise<PR[]> {
const query = queryParams(params).toString();
const url = `repos/${repoPath}/pulls?${query}`;
const res: GotResponse<PR[]> = await api.get(url, {
...options,
paginate: true,
});
return res.body;
}
export async function createIssue(
repoPath: string,
params: IssueCreateParams,
options?: GiteaGotOptions
): Promise<Issue> {
const url = `repos/${repoPath}/issues`;
const res: GotResponse<Issue> = await api.post(url, {
...options,
body: params,
});
return res.body;
}
export async function updateIssue(
repoPath: string,
idx: number,
params: IssueUpdateParams,
options?: GiteaGotOptions
): Promise<Issue> {
const url = `repos/${repoPath}/issues/${idx}`;
const res: GotResponse<Issue> = await api.patch(url, {
...options,
body: params,
});
return res.body;
}
export async function closeIssue(
repoPath: string,
idx: number,
options?: GiteaGotOptions
): Promise<void> {
await updateIssue(repoPath, idx, {
...options,
state: 'closed',
});
}
export async function searchIssues(
repoPath: string,
params: IssueSearchParams,
options?: GiteaGotOptions
): Promise<Issue[]> {
const query = queryParams(params).toString();
const url = `repos/${repoPath}/issues?${query}`;
const res: GotResponse<Issue[]> = await api.get(url, {
...options,
paginate: true,
});
return res.body;
}
export async function getRepoLabels(
repoPath: string,
options?: GiteaGotOptions
): Promise<Label[]> {
const url = `repos/${repoPath}/labels`;
const res: GotResponse<Label[]> = await api.get(url, options);
return res.body;
}
export async function unassignLabel(
repoPath: string,
issue: number,
label: number,
options?: GiteaGotOptions
): Promise<void> {
const url = `repos/${repoPath}/issues/${issue}/labels/${label}`;
await api.delete(url, options);
}
export async function createComment(
repoPath: string,
issue: number,
body: string,
options?: GiteaGotOptions
): Promise<Comment> {
const params: CommentCreateParams = { body };
const url = `repos/${repoPath}/issues/${issue}/comments`;
const res: GotResponse<Comment> = await api.post(url, {
...options,
body: params,
});
return res.body;
}
export async function updateComment(
repoPath: string,
idx: number,
body: string,
options?: GiteaGotOptions
): Promise<Comment> {
const params: CommentUpdateParams = { body };
const url = `repos/${repoPath}/issues/comments/${idx}`;
const res: GotResponse<Comment> = await api.patch(url, {
...options,
body: params,
});
return res.body;
}
export async function deleteComment(
repoPath,
idx: number,
options?: GiteaGotOptions
): Promise<void> {
const url = `repos/${repoPath}/issues/comments/${idx}`;
await api.delete(url, options);
}
export async function getComments(
repoPath,
issue: number,
options?: GiteaGotOptions
): Promise<Comment[]> {
const url = `repos/${repoPath}/issues/${issue}/comments`;
const res: GotResponse<Comment[]> = await api.get(url, options);
return res.body;
}
export async function createCommitStatus(
repoPath: string,
branchCommit: string,
params: CommitStatusCreateParams,
options?: GiteaGotOptions
): Promise<CommitStatus> {
const url = `repos/${repoPath}/statuses/${branchCommit}`;
const res: GotResponse<CommitStatus> = await api.post(url, {
...options,
body: params,
});
return res.body;
}
export async function getCombinedCommitStatus(
repoPath: string,
branchName: string,
options?: GiteaGotOptions
): Promise<CombinedCommitStatus> {
const url = `repos/${repoPath}/commits/${urlEscape(branchName)}/statuses`;
const res: GotResponse<CommitStatus[]> = await api.get(url, {
...options,
paginate: true,
});
let worstState = 0;
for (const cs of res.body) {
worstState = Math.max(worstState, commitStatusStates.indexOf(cs.status));
}
return {
worstStatus: commitStatusStates[worstState],
statuses: res.body,
};
}
export async function getBranch(
repoPath: string,
branchName: string,
options?: GiteaGotOptions
): Promise<Branch> {
const url = `repos/${repoPath}/branches/${urlEscape(branchName)}`;
const res: GotResponse<Branch> = await api.get(url, options);
return res.body;
}
This diff is collapsed.
import { logger } from '../../logger';
import { create } from './util';
import {
PLATFORM_TYPE_GITEA,
PLATFORM_TYPE_GITHUB,
PLATFORM_TYPE_GITLAB,
} from '../../constants/platforms';
......@@ -17,7 +18,10 @@ export default create({
{ hostname: options.hostname },
'Converting token to Bearer auth'
);
if (options.hostType === PLATFORM_TYPE_GITHUB) {
if (
options.hostType === PLATFORM_TYPE_GITHUB ||
options.hostType === PLATFORM_TYPE_GITEA
) {
options.headers.authorization = `token ${options.token}`; // eslint-disable-line no-param-reassign
} else if (options.hostType === PLATFORM_TYPE_GITLAB) {
options.headers['Private-token'] = options.token; // eslint-disable-line no-param-reassign
......
......@@ -45,6 +45,51 @@ Array [
]
`;
exports[`platform has a list of supported methods for gitea 1`] = `
Array [
"addAssignees",
"addReviewers",
"branchExists",
"cleanRepo",
"commitFilesToBranch",
"createPr",
"deleteBranch",
"deleteLabel",
"ensureComment",
"ensureCommentRemoval",
"ensureIssue",
"ensureIssueClosing",
"findIssue",
"findPr",
"getAllRenovateBranches",
"getBranchLastCommitTime",
"getBranchPr",
"getBranchStatus",
"getBranchStatusCheck",
"getCommitMessages",
"getFile",
"getFileList",
"getIssueList",
"getPr",
"getPrBody",
"getPrFiles",
"getPrList",
"getRepoForceRebase",
"getRepoStatus",
"getRepos",
"getVulnerabilityAlerts",
"initPlatform",
"initRepo",
"isBranchStale",
"mergeBranch",
"mergePr",
"setBaseBranch",
"setBranchPrefix",
"setBranchStatus",
"updatePr",
]
`;
exports[`platform has a list of supported methods for github 1`] = `
Array [
"addAssignees",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`platform/gitea createPr should use base branch by default 1`] = `
Object {
"body": "pr-body",
"branchName": "pr-branch",
"canMerge": true,
"closedAt": "2017-12-28T12:17:48Z",
"createdAt": "2014-04-01T05:14:20Z",
"displayNumber": "Pull Request #42",
"isConflicted": false,
"isModified": undefined,
"isStale": undefined,
"number": 42,
"sha": "0d9c7726c3d628b7e28af234595cfd20febdbf8e",
"sourceRepo": "some/repo",
"state": "open",
"targetBranch": "devel",
"title": "pr-title",
}
`;
exports[`platform/gitea createPr should use default branch if requested 1`] = `
Object {
"body": "pr-body",
"branchName": "pr-branch",
"canMerge": true,
"closedAt": "2017-12-28T12:17:48Z",
"createdAt": "2014-04-01T05:14:20Z",
"displayNumber": "Pull Request #42",
"isConflicted": false,
"isModified": undefined,
"isStale": undefined,
"number": 42,
"sha": "0d9c7726c3d628b7e28af234595cfd20febdbf8e",
"sourceRepo": "some/repo",
"state": "open",
"targetBranch": "master",
"title": "pr-title",
}
`;
exports[`platform/gitea getPr should fallback to direct fetching if cache fails 1`] = `
Object {
"body": "some random pull request",
"branchName": "some-head-ref",
"canMerge": true,
"closedAt": null,
"createdAt": "2015-03-22T20:36:16Z",
"displayNumber": "Pull Request #1",
"isConflicted": false,
"isModified": true,
"isStale": false,
"number": 1,
"sha": "some-head-sha",
"sourceRepo": "some/repo",
"state": "open",
"targetBranch": "some-base-ref",
"title": "Some PR",
}
`;
exports[`platform/gitea getPr should return enriched pull request which exists 1`] = `
Object {
"body": "other random pull request",
"branchName": "other-head-ref",
"canMerge": true,
"closedAt": "2016-01-09T10:03:21Z",
"createdAt": "2011-08-18T22:30:38Z",
"displayNumber": "Pull Request #2",
"isConflicted": false,
"isModified": false,
"isStale": false,
"number": 2,
"sha": "other-head-sha",
"sourceRepo": "some/repo",
"state": "closed",
"targetBranch": "other-base-ref",
"title": "Other PR",
}
`;
exports[`platform/gitea getPrList should return list of pull requests 1`] = `
Array [
Object {
"body": "some random pull request",
"branchName": "some-head-ref",
"canMerge": true,
"closedAt": null,
"createdAt": "2015-03-22T20:36:16Z",
"displayNumber": "Pull Request #1",
"isConflicted": false,
"isModified": undefined,
"isStale": undefined,
"number": 1,
"sha": "some-head-sha",
"sourceRepo": "some/repo",
"state": "open",
"targetBranch": "some-base-ref",
"title": "Some PR",
},
Object {
"body": "other random pull request",
"branchName": "other-head-ref",
"canMerge": true,
"closedAt": "2016-01-09T10:03:21Z",
"createdAt": "2011-08-18T22:30:38Z",
"displayNumber": "Pull Request #2",
"isConflicted": false,
"isModified": undefined,
"isStale": undefined,
"number": 2,
"sha": "other-head-sha",
"sourceRepo": "some/repo",
"state": "closed",
"targetBranch": "other-base-ref",
"title": "Other PR",
},
]
`;
exports[`platform/gitea getRepos should return an array of repos 1`] = `
Array [
"a/b",
"c/d",
]
`;
exports[`platform/gitea initPlatform() should support custom endpoint 1`] = `
Object {
"endpoint": "https://gitea.renovatebot.com/",
"gitAuthor": "Renovate Bot <renovate@example.com>",
}
`;
exports[`platform/gitea initPlatform() should support default endpoint 1`] = `
Object {
"endpoint": "https://gitea.com/api/v1/",
"gitAuthor": "Renovate Bot <renovate@example.com>",
}
`;
exports[`platform/gitea initPlatform() should use username as author name if full name is missing 1`] = `
Object {
"endpoint": "https://gitea.com/api/v1/",
"gitAuthor": "renovate <renovate@example.com>",
}
`;
exports[`platform/gitea initRepo should fall back to merge method "merge" 1`] = `
Object {
"baseBranch": "master",
"isFork": false,
}
`;
exports[`platform/gitea initRepo should fall back to merge method "rebase-merge" 1`] = `
Object {
"baseBranch": "master",
"isFork": false,
}
`;
exports[`platform/gitea initRepo should fall back to merge method "squash" 1`] = `
Object {
"baseBranch": "master",
"isFork": false,
}
`;
import { GotResponse } from '../../../lib/platform';
import { partial } from '../../util';
import { GotFn } from '../../../lib/util/got';
import { GiteaGotApi } from '../../../lib/platform/gitea/gitea-got-wrapper';
describe('platform/gitea/gitea-got-wrapper', () => {
let api: GiteaGotApi;
let got: jest.Mocked<GotFn> & jest.Mock;
const baseURL = 'https://gitea.renovatebot.com/api/v1';
beforeEach(async () => {
jest.resetAllMocks();
jest.mock('../../../lib/util/got');
api = (await import('../../../lib/platform/gitea/gitea-got-wrapper'))
.api as any;
got = (await import('../../../lib/util/got')).api as any;
api.setBaseUrl(baseURL);
});
it('supports responses without pagination when enabled', async () => {
got.mockResolvedValueOnce(
partial<GotResponse>({
body: { hello: 'world' },
})
);
const res = await api.get('pagination-example-1', { paginate: true });
expect(res.body).toEqual({ hello: 'world' });
});
it('supports root-level pagination', async () => {
got.mockResolvedValueOnce(
partial<GotResponse>({
body: ['abc', 'def', 'ghi'],
headers: { 'x-total-count': '5' },
url: `${baseURL}/pagination-example-1`,
})
);
got.mockResolvedValueOnce(
partial<GotResponse>({
body: ['jkl'],
})
);
got.mockResolvedValueOnce(
partial<GotResponse>({
body: ['mno', 'pqr'],
})
);
const res = await api.get('pagination-example-1', { paginate: true });
expect(res.body).toHaveLength(6);
expect(res.body).toEqual(['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr']);
expect(got).toHaveBeenCalledTimes(3);
});
it('supports pagination on data property', async () => {
got.mockResolvedValueOnce(
partial<GotResponse>({
body: { data: ['abc', 'def', 'ghi'] },
headers: { 'x-total-count': '5' },
url: `${baseURL}/pagination-example-2`,
})
);
got.mockResolvedValueOnce(
partial<GotResponse>({
body: { data: ['jkl'] },
})
);
got.mockResolvedValueOnce(
partial<GotResponse>({
body: { data: ['mno', 'pqr'] },
})
);
const res = await api.get('pagination-example-2', { paginate: true });
expect(res.body.data).toHaveLength(6);
expect(res.body.data).toEqual(['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr']);
expect(got).toHaveBeenCalledTimes(3);
});
});
This diff is collapsed.
This diff is collapsed.
import * as github from '../../lib/platform/github';
import * as gitlab from '../../lib/platform/gitlab';
import * as gitea from '../../lib/platform/gitea';
import * as azure from '../../lib/platform/azure';
import * as bitbucket from '../../lib/platform/bitbucket';
import * as bitbucketServer from '../../lib/platform/bitbucket-server';
......@@ -50,6 +51,11 @@ describe('platform', () => {
expect(gitlabMethods).toMatchSnapshot();
});
it('has a list of supported methods for gitea', () => {
const giteaMethods = Object.keys(gitea).sort();
expect(giteaMethods).toMatchSnapshot();
});
it('has a list of supported methods for azure', () => {
const azureMethods = Object.keys(azure).sort();
expect(azureMethods).toMatchSnapshot();
......@@ -61,6 +67,12 @@ describe('platform', () => {
expect(githubMethods).toMatchObject(gitlabMethods);
});
it('has same API for github and gitea', () => {
const githubMethods = Object.keys(github).sort();
const giteaMethods = Object.keys(gitea).sort();
expect(githubMethods).toMatchObject(giteaMethods);
});
it('has same API for github and azure', () => {
const githubMethods = Object.keys(github).sort();
const azureMethods = Object.keys(azure).sort();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment