-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2988 from github/koesie10/error-default-branch
Add custom error handler for missing default branch
- Loading branch information
Showing
3 changed files
with
261 additions
and
4 deletions.
There are no files selected for viewing
85 changes: 85 additions & 0 deletions
85
extensions/ql-vscode/src/variant-analysis/custom-errors.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { RequestError } from "@octokit/request-error"; | ||
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging"; | ||
|
||
type ApiError = { | ||
resource: string; | ||
field: string; | ||
code: string; | ||
}; | ||
|
||
type ErrorResponse = { | ||
message: string; | ||
errors?: ApiError[]; | ||
}; | ||
|
||
export function handleRequestError( | ||
e: RequestError, | ||
logger: NotificationLogger, | ||
): boolean { | ||
if (e.status !== 422) { | ||
return false; | ||
} | ||
|
||
if (!e.response?.data) { | ||
return false; | ||
} | ||
|
||
const data = e.response.data; | ||
if (!isErrorResponse(data)) { | ||
return false; | ||
} | ||
|
||
if (!data.errors) { | ||
return false; | ||
} | ||
|
||
// This is the only custom error message we have | ||
const missingDefaultBranchError = data.errors.find( | ||
(error) => | ||
error.resource === "Repository" && | ||
error.field === "default_branch" && | ||
error.code === "missing", | ||
); | ||
|
||
if (!missingDefaultBranchError) { | ||
return false; | ||
} | ||
|
||
if ( | ||
!("repository" in missingDefaultBranchError) || | ||
typeof missingDefaultBranchError.repository !== "string" | ||
) { | ||
return false; | ||
} | ||
|
||
if ( | ||
!("default_branch" in missingDefaultBranchError) || | ||
typeof missingDefaultBranchError.default_branch !== "string" | ||
) { | ||
return false; | ||
} | ||
|
||
const createBranchURL = `https://github.com/${ | ||
missingDefaultBranchError.repository | ||
}/new/${encodeURIComponent(missingDefaultBranchError.default_branch)}`; | ||
|
||
void showAndLogErrorMessage( | ||
logger, | ||
`Variant analysis failed because the controller repository ${missingDefaultBranchError.repository} does not have a branch '${missingDefaultBranchError.default_branch}'. ` + | ||
`Please create a '${missingDefaultBranchError.default_branch}' branch by clicking [here](${createBranchURL}) and re-run the variant analysis query.`, | ||
{ | ||
fullMessage: e.message, | ||
}, | ||
); | ||
|
||
return true; | ||
} | ||
|
||
function isErrorResponse(obj: unknown): obj is ErrorResponse { | ||
return ( | ||
typeof obj === "object" && | ||
obj !== null && | ||
"message" in obj && | ||
typeof obj.message === "string" | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
extensions/ql-vscode/test/unit-tests/variant-analysis/custom-errors.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import { RequestError } from "@octokit/request-error"; | ||
import { createMockLogger } from "../../__mocks__/loggerMock"; | ||
import { handleRequestError } from "../../../src/variant-analysis/custom-errors"; | ||
import { faker } from "@faker-js/faker"; | ||
|
||
describe("handleRequestError", () => { | ||
const logger = createMockLogger(); | ||
|
||
it("returns false when handling a non-422 error", () => { | ||
const e = mockRequestError(404, { | ||
message: "Not Found", | ||
}); | ||
expect(handleRequestError(e, logger)).toBe(false); | ||
expect(logger.showErrorMessage).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("returns false when handling a different error without errors", () => { | ||
const e = mockRequestError(422, { | ||
message: | ||
"Unable to trigger a variant analysis. None of the requested repositories could be found.", | ||
}); | ||
expect(handleRequestError(e, logger)).toBe(false); | ||
expect(logger.showErrorMessage).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("returns false when handling an error without response body", () => { | ||
const e = mockRequestError(422, undefined); | ||
expect(handleRequestError(e, logger)).toBe(false); | ||
expect(logger.showErrorMessage).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("returns false when handling an error without response", () => { | ||
const e = new RequestError("Timeout", 500, { | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
request: { | ||
method: "POST", | ||
url: faker.internet.url(), | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
}, | ||
}); | ||
expect(handleRequestError(e, logger)).toBe(false); | ||
expect(logger.showErrorMessage).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("returns false when handling a different error with errors", () => { | ||
const e = mockRequestError(422, { | ||
message: | ||
"Unable to trigger a variant analysis. None of the requested repositories could be found.", | ||
errors: [ | ||
{ | ||
resource: "VariantAnalysis", | ||
field: "repositories", | ||
code: "not_found", | ||
}, | ||
], | ||
}); | ||
expect(handleRequestError(e, logger)).toBe(false); | ||
expect(logger.showErrorMessage).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("returns false when handling without repository field", () => { | ||
const e = mockRequestError(422, { | ||
message: | ||
"Variant analysis failed because controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch in the repository and re-run the variant analysis.", | ||
errors: [ | ||
{ | ||
resource: "Repository", | ||
field: "default_branch", | ||
code: "missing", | ||
default_branch: "main", | ||
}, | ||
], | ||
}); | ||
expect(handleRequestError(e, logger)).toBe(false); | ||
expect(logger.showErrorMessage).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("returns false when handling without default_branch field", () => { | ||
const e = mockRequestError(422, { | ||
message: | ||
"Variant analysis failed because controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch in the repository and re-run the variant analysis.", | ||
errors: [ | ||
{ | ||
resource: "Repository", | ||
field: "default_branch", | ||
code: "missing", | ||
repository: "github/pickles", | ||
}, | ||
], | ||
}); | ||
expect(handleRequestError(e, logger)).toBe(false); | ||
expect(logger.showErrorMessage).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("shows notification when handling a missing default branch error", () => { | ||
const e = mockRequestError(422, { | ||
message: | ||
"Variant analysis failed because controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch in the repository and re-run the variant analysis.", | ||
errors: [ | ||
{ | ||
resource: "Repository", | ||
field: "default_branch", | ||
code: "missing", | ||
repository: "github/pickles", | ||
default_branch: "main", | ||
}, | ||
], | ||
}); | ||
expect(handleRequestError(e, logger)).toBe(true); | ||
expect(logger.showErrorMessage).toHaveBeenCalledWith( | ||
"Variant analysis failed because the controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch by clicking [here](https://github.com/github/pickles/new/main) and re-run the variant analysis query.", | ||
); | ||
}); | ||
}); | ||
|
||
function mockRequestError(status: number, body: any): RequestError { | ||
return new RequestError( | ||
body ? toErrorMessage(body) : "Unknown error", | ||
status, | ||
{ | ||
request: { | ||
method: "POST", | ||
url: faker.internet.url(), | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
}, | ||
response: { | ||
url: faker.internet.url(), | ||
status, | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
data: body, | ||
}, | ||
}, | ||
); | ||
} | ||
|
||
// Copied from https://github.com/octokit/request.js/blob/c67f902350384846f88d91196e7066daadc08357/src/fetch-wrapper.ts#L166 to have a | ||
// somewhat realistic error message | ||
function toErrorMessage(data: any) { | ||
if (typeof data === "string") return data; | ||
|
||
if ("message" in data) { | ||
if (Array.isArray(data.errors)) { | ||
return `${data.message}: ${data.errors.map(JSON.stringify).join(", ")}`; | ||
} | ||
|
||
return data.message; | ||
} | ||
|
||
// istanbul ignore next - just in case | ||
return `Unknown error: ${JSON.stringify(data)}`; | ||
} |