Skip to content

Commit

Permalink
fix(github): Remote branch existence check (#23480)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed Jul 23, 2023
1 parent 64a52b0 commit 9e77d59
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 32 deletions.
54 changes: 54 additions & 0 deletions lib/modules/platform/github/branch.spec.ts
@@ -0,0 +1,54 @@
import * as httpMock from '../../../../test/http-mock';
import { remoteBranchExists } from './branch';

describe('modules/platform/github/branch', () => {
it('should return true if the branch exists', async () => {
httpMock
.scope('https://api.github.com')
.head('/repos/my/repo/git/refs/heads/renovate/foobar/')
.reply(404)
.head('/repos/my/repo/git/refs/heads/renovate/foobar')
.reply(200, { ref: 'renovate/foobar' });

const result = await remoteBranchExists('my/repo', 'renovate/foobar');

expect(result).toBe(true);
});

it('should return false if the branch does not exist', async () => {
httpMock
.scope('https://api.github.com')
.head('/repos/my/repo/git/refs/heads/renovate/foobar/')
.reply(404)
.head('/repos/my/repo/git/refs/heads/renovate/foobar')
.reply(404);

const result = await remoteBranchExists('my/repo', 'renovate/foobar');

expect(result).toBe(false);
});

it('should throw an error for nested branches', async () => {
httpMock
.scope('https://api.github.com')
.head('/repos/my/repo/git/refs/heads/renovate/foobar/')
.reply(200);

await expect(
remoteBranchExists('my/repo', 'renovate/foobar')
).rejects.toThrow(
`Trying to create a branch 'renovate/foobar' while it's the part of nested branch`
);
});

it('should throw an error if the request fails for any other reason', async () => {
httpMock
.scope('https://api.github.com')
.head('/repos/my/repo/git/refs/heads/renovate/foobar/')
.reply(500, { message: 'Something went wrong' });

await expect(
remoteBranchExists('my/repo', 'renovate/foobar')
).rejects.toThrow('external-host-error');
});
});
34 changes: 34 additions & 0 deletions lib/modules/platform/github/branch.ts
@@ -0,0 +1,34 @@
import { HttpError } from '../../../util/http';
import { Result } from '../../../util/result';
import { githubApi } from './common';

function headRef(repo: string, branchName: string): Promise<boolean> {
return Result.wrap(
githubApi.headJson(`/repos/${repo}/git/refs/heads/${branchName}`, {
memCache: false,
})
)
.transform(() => true)
.catch((err) => {
if (err instanceof HttpError && err.response?.statusCode === 404) {
return Result.ok(false);
}

return Result.err(err);
})
.unwrapOrThrow();
}

export async function remoteBranchExists(
repo: string,
branchName: string
): Promise<boolean> {
const refNested = `${branchName}/`;
const isNested = await headRef(repo, refNested);
if (isNested) {
const message = `Trying to create a branch '${branchName}' while it's the part of nested branch`;
throw new Error(message);
}

return headRef(repo, branchName);
}
3 changes: 3 additions & 0 deletions lib/modules/platform/github/common.ts
@@ -1,7 +1,10 @@
import is from '@sindresorhus/is';
import { GithubHttp } from '../../../util/http/github';
import { getPrBodyStruct } from '../pr-body';
import type { GhPr, GhRestPr } from './types';

export const githubApi = new GithubHttp();

/**
* @see https://docs.github.com/en/rest/reference/pulls#list-pull-requests
*/
Expand Down
24 changes: 9 additions & 15 deletions lib/modules/platform/github/index.spec.ts
Expand Up @@ -16,6 +16,7 @@ import { setBaseUrl } from '../../../util/http/github';
import { toBase64 } from '../../../util/string';
import { hashBody } from '../pr-body';
import type { CreatePRConfig, RepoParams, UpdatePrConfig } from '../types';
import * as branch from './branch';
import type { ApiPageCache, GhRestPr } from './types';
import * as github from '.';

Expand Down Expand Up @@ -939,8 +940,6 @@ describe('modules/platform/github/index', () => {
])
.head('/repos/some/repo/git/commits/123')
.reply(200)
.head('/repos/some/repo/git/refs/heads/somebranch')
.reply(404)
.post('/repos/some/repo/git/refs')
.reply(201)
.patch('/repos/some/repo/pulls/91')
Expand All @@ -953,6 +952,7 @@ describe('modules/platform/github/index', () => {
updated_at: '01-09-2022',
});
await github.initRepo({ repository: 'some/repo' });
jest.spyOn(branch, 'remoteBranchExists').mockResolvedValueOnce(false);

const pr = await github.getBranchPr('somebranch');
const pr2 = await github.getBranchPr('somebranch');
Expand Down Expand Up @@ -1036,12 +1036,11 @@ describe('modules/platform/github/index', () => {
])
.head('/repos/some/repo/git/commits/123')
.reply(200)
.head('/repos/some/repo/git/refs/heads/somebranch')
.reply(404)
.post('/repos/some/repo/git/refs')
.reply(201)
.patch('/repos/some/repo/pulls/91')
.reply(422);
jest.spyOn(branch, 'remoteBranchExists').mockResolvedValueOnce(false);

await github.initRepo({ repository: 'some/repo' });
const pr = await github.getBranchPr('somebranch');
Expand Down Expand Up @@ -1069,9 +1068,8 @@ describe('modules/platform/github/index', () => {
},
])
.head('/repos/some/repo/git/commits/123')
.reply(200)
.head('/repos/some/repo/git/refs/heads/somebranch')
.reply(422);
.reply(200);
jest.spyOn(branch, 'remoteBranchExists').mockRejectedValueOnce('oops');

await github.initRepo({ repository: 'some/repo' });
const pr = await github.getBranchPr('somebranch');
Expand Down Expand Up @@ -3387,10 +3385,9 @@ describe('modules/platform/github/index', () => {
.reply(200, { sha: '222' })
.head('/repos/some/repo/git/commits/222')
.reply(200)
.head('/repos/some/repo/git/refs/heads/foo/bar')
.reply(404)
.post('/repos/some/repo/git/refs')
.reply(200);
jest.spyOn(branch, 'remoteBranchExists').mockResolvedValueOnce(false);

const res = await github.commitFiles({
branchName: 'foo/bar',
Expand All @@ -3417,10 +3414,9 @@ describe('modules/platform/github/index', () => {
.reply(200, { sha: '222' })
.head('/repos/some/repo/git/commits/222')
.reply(200)
.head('/repos/some/repo/git/refs/heads/foo/bar')
.reply(200)
.patch('/repos/some/repo/git/refs/heads/foo/bar')
.reply(200);
jest.spyOn(branch, 'remoteBranchExists').mockResolvedValueOnce(true);

const res = await github.commitFiles({
branchName: 'foo/bar',
Expand All @@ -3447,12 +3443,11 @@ describe('modules/platform/github/index', () => {
.reply(200, { sha: '222' })
.head('/repos/some/repo/git/commits/222')
.reply(200)
.head('/repos/some/repo/git/refs/heads/foo/bar')
.reply(200)
.patch('/repos/some/repo/git/refs/heads/foo/bar')
.reply(422)
.post('/repos/some/repo/git/refs')
.reply(200);
jest.spyOn(branch, 'remoteBranchExists').mockResolvedValueOnce(true);

const res = await github.commitFiles({
branchName: 'foo/bar',
Expand All @@ -3479,10 +3474,9 @@ describe('modules/platform/github/index', () => {
.reply(200, { sha: '222' })
.head('/repos/some/repo/git/commits/222')
.reply(200)
.head('/repos/some/repo/git/refs/heads/foo/bar')
.reply(200)
.patch('/repos/some/repo/git/refs/heads/foo/bar')
.reply(404);
jest.spyOn(branch, 'remoteBranchExists').mockResolvedValueOnce(true);

const res = await github.commitFiles({
branchName: 'foo/bar',
Expand Down
25 changes: 8 additions & 17 deletions lib/modules/platform/github/index.ts
Expand Up @@ -61,7 +61,8 @@ import type {
} from '../types';
import { repoFingerprint } from '../util';
import { smartTruncate } from '../utils/pr-body';
import { coerceRestPr } from './common';
import { remoteBranchExists } from './branch';
import { coerceRestPr, githubApi } from './common';
import {
enableAutoMergeMutation,
getIssuesQuery,
Expand All @@ -87,8 +88,6 @@ import { getUserDetails, getUserEmail } from './user';

export const id = 'github';

const githubApi = new githubHttp.GithubHttp();

let config: LocalRepoConfig;
let platformConfig: PlatformConfig;

Expand Down Expand Up @@ -760,25 +759,17 @@ export async function findPr({
const REOPEN_THRESHOLD_MILLIS = 1000 * 60 * 60 * 24 * 7;

async function ensureBranchSha(branchName: string, sha: string): Promise<void> {
const repository = config.repository!;
try {
const commitUrl = `/repos/${config.repository}/git/commits/${sha}`;
const commitUrl = `/repos/${repository}/git/commits/${sha}`;
await githubApi.head(commitUrl, { memCache: false });
} catch (err) {
logger.error({ err, sha, branchName }, 'Commit not found');
throw err;
}

const refUrl = `/repos/${config.repository}/git/refs/heads/${branchName}`;
let branchExists = false;
let branchResult: undefined | HttpResponse<string>;
try {
branchResult = await githubApi.head(refUrl, { memCache: false });
branchExists = true;
} catch (err) {
if (err.statusCode !== 404) {
throw err;
}
}
const branchExists = await remoteBranchExists(repository, branchName);

if (branchExists) {
try {
Expand All @@ -787,17 +778,17 @@ async function ensureBranchSha(branchName: string, sha: string): Promise<void> {
} catch (err) {
if (err.err?.response?.statusCode === 422) {
logger.debug(
{ branchResult, err },
{ err },
'Branch update failed due to reference not existing - will try to create'
);
} else {
logger.warn({ refUrl, err, branchResult }, 'Error updating branch');
logger.warn({ refUrl, err }, 'Error updating branch');
throw err;
}
}
}

await githubApi.postJson(`/repos/${config.repository}/git/refs`, {
await githubApi.postJson(`/repos/${repository}/git/refs`, {
body: { sha, ref: `refs/heads/${branchName}` },
});
}
Expand Down

0 comments on commit 9e77d59

Please sign in to comment.