Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(github): Remote branch existence check #23480

Merged
merged 14 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 57 additions & 0 deletions lib/modules/platform/github/branch.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as httpMock from '../../../../test/http-mock';
import { GithubHttp } from '../../../util/http/github';
import { remoteBranchExists } from './branch';

describe('modules/platform/github/branch', () => {
const http = new GithubHttp();

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(http, '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(http, '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(http, '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(http, 'my/repo', 'renovate/foobar')
).rejects.toThrow('external-host-error');
});
});
38 changes: 38 additions & 0 deletions lib/modules/platform/github/branch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Http, HttpError } from '../../../util/http';
import { Result } from '../../../util/result';

function headRef(
http: Http,
zharinov marked this conversation as resolved.
Show resolved Hide resolved
repo: string,
branchName: string
): Promise<boolean> {
return Result.wrap(
http.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(
http: Http,
repo: string,
branchName: string
): Promise<boolean> {
const refNested = `${branchName}/`;
const isNested = await headRef(http, 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(http, repo, branchName);
}
24 changes: 9 additions & 15 deletions lib/modules/platform/github/index.spec.ts
Original file line number Diff line number Diff line change
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: 11 additions & 14 deletions lib/modules/platform/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import type {
} from '../types';
import { repoFingerprint } from '../util';
import { smartTruncate } from '../utils/pr-body';
import { remoteBranchExists } from './branch';
import { coerceRestPr } from './common';
import {
enableAutoMergeMutation,
Expand Down Expand Up @@ -760,25 +761,21 @@ 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(
githubApi,
repository,
branchName
);

if (branchExists) {
try {
Expand All @@ -787,17 +784,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