Merge pull request #3607 from github/koesie10/ghec-dr-variant-analysis
Add GHEC-DR support
This commit is contained in:
@@ -31,4 +31,9 @@ export interface Credentials {
|
||||
* @returns An OAuth access token, or undefined.
|
||||
*/
|
||||
getExistingAccessToken(): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Returns the ID of the authentication provider to use.
|
||||
*/
|
||||
authProviderId: string;
|
||||
}
|
||||
|
||||
@@ -29,37 +29,45 @@ function validGitHubNwoOrOwner(
|
||||
|
||||
/**
|
||||
* Extracts an NWO from a GitHub URL.
|
||||
* @param githubUrl The GitHub repository URL
|
||||
* @param repositoryUrl The GitHub repository URL
|
||||
* @param githubUrl The URL of the GitHub instance
|
||||
* @return The corresponding NWO, or undefined if the URL is not valid
|
||||
*/
|
||||
export function getNwoFromGitHubUrl(githubUrl: string): string | undefined {
|
||||
return getNwoOrOwnerFromGitHubUrl(githubUrl, "nwo");
|
||||
export function getNwoFromGitHubUrl(
|
||||
repositoryUrl: string,
|
||||
githubUrl: URL,
|
||||
): string | undefined {
|
||||
return getNwoOrOwnerFromGitHubUrl(repositoryUrl, githubUrl, "nwo");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts an owner from a GitHub URL.
|
||||
* @param githubUrl The GitHub repository URL
|
||||
* @param repositoryUrl The GitHub repository URL
|
||||
* @param githubUrl The URL of the GitHub instance
|
||||
* @return The corresponding Owner, or undefined if the URL is not valid
|
||||
*/
|
||||
export function getOwnerFromGitHubUrl(githubUrl: string): string | undefined {
|
||||
return getNwoOrOwnerFromGitHubUrl(githubUrl, "owner");
|
||||
export function getOwnerFromGitHubUrl(
|
||||
repositoryUrl: string,
|
||||
githubUrl: URL,
|
||||
): string | undefined {
|
||||
return getNwoOrOwnerFromGitHubUrl(repositoryUrl, githubUrl, "owner");
|
||||
}
|
||||
|
||||
function getNwoOrOwnerFromGitHubUrl(
|
||||
githubUrl: string,
|
||||
repositoryUrl: string,
|
||||
githubUrl: URL,
|
||||
kind: "owner" | "nwo",
|
||||
): string | undefined {
|
||||
const validHostnames = [githubUrl.hostname, `www.${githubUrl.hostname}`];
|
||||
|
||||
try {
|
||||
let paths: string[];
|
||||
const urlElements = githubUrl.split("/");
|
||||
if (
|
||||
urlElements[0] === "github.com" ||
|
||||
urlElements[0] === "www.github.com"
|
||||
) {
|
||||
paths = githubUrl.split("/").slice(1);
|
||||
const urlElements = repositoryUrl.split("/");
|
||||
if (validHostnames.includes(urlElements[0])) {
|
||||
paths = repositoryUrl.split("/").slice(1);
|
||||
} else {
|
||||
const uri = new URL(githubUrl);
|
||||
if (uri.hostname !== "github.com" && uri.hostname !== "www.github.com") {
|
||||
const uri = new URL(repositoryUrl);
|
||||
if (!validHostnames.includes(uri.hostname)) {
|
||||
return;
|
||||
}
|
||||
paths = uri.pathname.split("/").filter((segment: string) => segment);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { authentication } from "vscode";
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import type { Credentials } from "../authentication";
|
||||
import { AppOctokit } from "../octokit";
|
||||
|
||||
export const GITHUB_AUTH_PROVIDER_ID = "github";
|
||||
import { hasGhecDrUri } from "../../config";
|
||||
import { getOctokitBaseUrl } from "./octokit";
|
||||
|
||||
// We need 'repo' scope for triggering workflows, 'gist' scope for exporting results to Gist,
|
||||
// and 'read:packages' for reading private CodeQL packages.
|
||||
@@ -16,30 +16,24 @@ const SCOPES = ["repo", "gist", "read:packages"];
|
||||
*/
|
||||
export class VSCodeCredentials implements Credentials {
|
||||
/**
|
||||
* A specific octokit to return, otherwise a new authenticated octokit will be created when needed.
|
||||
*/
|
||||
private octokit: Octokit | undefined;
|
||||
|
||||
/**
|
||||
* Creates or returns an instance of Octokit.
|
||||
* Creates or returns an instance of Octokit. The returned instance should
|
||||
* not be stored and reused, as it may become out-of-date with the current
|
||||
* authentication session.
|
||||
*
|
||||
* @returns An instance of Octokit.
|
||||
*/
|
||||
async getOctokit(): Promise<Octokit> {
|
||||
if (this.octokit) {
|
||||
return this.octokit;
|
||||
}
|
||||
|
||||
const accessToken = await this.getAccessToken();
|
||||
|
||||
return new AppOctokit({
|
||||
auth: accessToken,
|
||||
baseUrl: getOctokitBaseUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
async getAccessToken(): Promise<string> {
|
||||
const session = await authentication.getSession(
|
||||
GITHUB_AUTH_PROVIDER_ID,
|
||||
this.authProviderId,
|
||||
SCOPES,
|
||||
{ createIfNone: true },
|
||||
);
|
||||
@@ -49,11 +43,18 @@ export class VSCodeCredentials implements Credentials {
|
||||
|
||||
async getExistingAccessToken(): Promise<string | undefined> {
|
||||
const session = await authentication.getSession(
|
||||
GITHUB_AUTH_PROVIDER_ID,
|
||||
this.authProviderId,
|
||||
SCOPES,
|
||||
{ createIfNone: false },
|
||||
);
|
||||
|
||||
return session?.accessToken;
|
||||
}
|
||||
|
||||
public get authProviderId(): string {
|
||||
if (hasGhecDrUri()) {
|
||||
return "github-enterprise";
|
||||
}
|
||||
return "github";
|
||||
}
|
||||
}
|
||||
|
||||
15
extensions/ql-vscode/src/common/vscode/octokit.ts
Normal file
15
extensions/ql-vscode/src/common/vscode/octokit.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getGitHubInstanceApiUrl } from "../../config";
|
||||
|
||||
/**
|
||||
* Returns the Octokit base URL to use based on the GitHub instance URL.
|
||||
*
|
||||
* This is necessary because the Octokit base URL should not have a trailing
|
||||
* slash, but this is included by default in a URL.
|
||||
*/
|
||||
export function getOctokitBaseUrl(): string {
|
||||
let apiUrl = getGitHubInstanceApiUrl().toString();
|
||||
if (apiUrl.endsWith("/")) {
|
||||
apiUrl = apiUrl.slice(0, -1);
|
||||
}
|
||||
return apiUrl;
|
||||
}
|
||||
@@ -108,12 +108,55 @@ export function hasEnterpriseUri(): boolean {
|
||||
return getEnterpriseUri() !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the uri look like GHEC-DR?
|
||||
*/
|
||||
function isGhecDrUri(uri: Uri | undefined): boolean {
|
||||
return uri !== undefined && uri.authority.toLowerCase().endsWith(".ghe.com");
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the GitHub Enterprise URI set to something that looks like GHEC-DR?
|
||||
*/
|
||||
export function hasGhecDrUri(): boolean {
|
||||
const uri = getEnterpriseUri();
|
||||
return uri !== undefined && uri.authority.toLowerCase().endsWith(".ghe.com");
|
||||
return isGhecDrUri(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* The URI for GitHub.com.
|
||||
*/
|
||||
export const GITHUB_URL = new URL("https://github.com");
|
||||
export const GITHUB_API_URL = new URL("https://api.github.com");
|
||||
|
||||
/**
|
||||
* If the GitHub Enterprise URI is set to something that looks like GHEC-DR, return it.
|
||||
*/
|
||||
export function getGhecDrUri(): Uri | undefined {
|
||||
const uri = getEnterpriseUri();
|
||||
if (isGhecDrUri(uri)) {
|
||||
return uri;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getGitHubInstanceUrl(): URL {
|
||||
const ghecDrUri = getGhecDrUri();
|
||||
if (ghecDrUri) {
|
||||
return new URL(ghecDrUri.toString());
|
||||
}
|
||||
return GITHUB_URL;
|
||||
}
|
||||
|
||||
export function getGitHubInstanceApiUrl(): URL {
|
||||
const ghecDrUri = getGhecDrUri();
|
||||
if (ghecDrUri) {
|
||||
const url = new URL(ghecDrUri.toString());
|
||||
url.hostname = `api.${url.hostname}`;
|
||||
return url;
|
||||
}
|
||||
return GITHUB_API_URL;
|
||||
}
|
||||
|
||||
const ROOT_SETTING = new Setting("codeQL");
|
||||
@@ -570,6 +613,11 @@ export async function setRemoteControllerRepo(repo: string | undefined) {
|
||||
export interface VariantAnalysisConfig {
|
||||
controllerRepo: string | undefined;
|
||||
showSystemDefinedRepositoryLists: boolean;
|
||||
/**
|
||||
* This uses a URL instead of a URI because the URL class is available in
|
||||
* unit tests and is fully browser-compatible.
|
||||
*/
|
||||
githubUrl: URL;
|
||||
onDidChangeConfiguration?: Event<void>;
|
||||
}
|
||||
|
||||
@@ -591,6 +639,10 @@ export class VariantAnalysisConfigListener
|
||||
public get showSystemDefinedRepositoryLists(): boolean {
|
||||
return !hasEnterpriseUri();
|
||||
}
|
||||
|
||||
public get githubUrl(): URL {
|
||||
return getGitHubInstanceUrl();
|
||||
}
|
||||
}
|
||||
|
||||
const VARIANT_ANALYSIS_FILTER_RESULTS = new Setting(
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AppOctokit } from "../common/octokit";
|
||||
import type { ProgressCallback } from "../common/vscode/progress";
|
||||
import { UserCancellationException } from "../common/vscode/progress";
|
||||
import type { EndpointDefaults } from "@octokit/types";
|
||||
import { getOctokitBaseUrl } from "../common/vscode/octokit";
|
||||
|
||||
export async function getCodeSearchRepositories(
|
||||
query: string,
|
||||
@@ -54,6 +55,7 @@ async function provideOctokitWithThrottling(
|
||||
|
||||
const octokit = new MyOctokit({
|
||||
auth,
|
||||
baseUrl: getOctokitBaseUrl(),
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: EndpointDefaults): boolean => {
|
||||
void logger.log(
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
addDatabaseSourceToWorkspace,
|
||||
allowHttp,
|
||||
downloadTimeout,
|
||||
getGitHubInstanceUrl,
|
||||
hasGhecDrUri,
|
||||
isCanary,
|
||||
} from "../config";
|
||||
import { showAndLogInformationMessage } from "../common/logging";
|
||||
@@ -150,10 +152,11 @@ export class DatabaseFetcher {
|
||||
maxStep: 2,
|
||||
});
|
||||
|
||||
const instanceUrl = getGitHubInstanceUrl();
|
||||
|
||||
const options: InputBoxOptions = {
|
||||
title:
|
||||
'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
|
||||
placeHolder: "https://github.com/<owner>/<repo> or <owner>/<repo>",
|
||||
title: `Enter a GitHub repository URL or "name with owner" (e.g. ${new URL("/github/codeql", instanceUrl).toString()} or github/codeql)`,
|
||||
placeHolder: `${new URL("/", instanceUrl).toString()}<owner>/<repo> or <owner>/<repo>`,
|
||||
ignoreFocusOut: true,
|
||||
};
|
||||
|
||||
@@ -180,12 +183,14 @@ export class DatabaseFetcher {
|
||||
makeSelected = true,
|
||||
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
const nwo = getNwoFromGitHubUrl(githubRepo) || githubRepo;
|
||||
const nwo =
|
||||
getNwoFromGitHubUrl(githubRepo, getGitHubInstanceUrl()) || githubRepo;
|
||||
if (!isValidGitHubNwo(nwo)) {
|
||||
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
|
||||
}
|
||||
|
||||
const credentials = isCanary() ? this.app.credentials : undefined;
|
||||
const credentials =
|
||||
isCanary() || hasGhecDrUri() ? this.app.credentials : undefined;
|
||||
|
||||
const octokit = credentials
|
||||
? await credentials.getOctokit()
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Octokit } from "@octokit/rest";
|
||||
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import { showNeverAskAgainDialog } from "../../common/vscode/dialog";
|
||||
import type { GitHubDatabaseConfig } from "../../config";
|
||||
import { hasGhecDrUri } from "../../config";
|
||||
import type { Credentials } from "../../common/authentication";
|
||||
import { AppOctokit } from "../../common/octokit";
|
||||
import type { ProgressCallback } from "../../common/vscode/progress";
|
||||
@@ -67,7 +68,10 @@ export async function listDatabases(
|
||||
credentials: Credentials,
|
||||
config: GitHubDatabaseConfig,
|
||||
): Promise<ListDatabasesResult | undefined> {
|
||||
const hasAccessToken = !!(await credentials.getExistingAccessToken());
|
||||
// On GHEC-DR, unauthenticated requests will never work, so we should always ask
|
||||
// for authentication.
|
||||
const hasAccessToken =
|
||||
!!(await credentials.getExistingAccessToken()) || hasGhecDrUri();
|
||||
|
||||
let octokit = hasAccessToken
|
||||
? await credentials.getOctokit()
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { App } from "../../common/app";
|
||||
import { QueryLanguage } from "../../common/query-language";
|
||||
import { getCodeSearchRepositories } from "../code-search-api";
|
||||
import { showAndLogErrorMessage } from "../../common/logging";
|
||||
import { getGitHubInstanceUrl } from "../../config";
|
||||
|
||||
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
|
||||
remoteDatabaseKind: string;
|
||||
@@ -146,16 +147,19 @@ export class DbPanel extends DisposableObject {
|
||||
}
|
||||
|
||||
private async addNewRemoteRepo(parentList?: string): Promise<void> {
|
||||
const instanceUrl = getGitHubInstanceUrl();
|
||||
|
||||
const repoName = await window.showInputBox({
|
||||
title: "Add a repository",
|
||||
prompt: "Insert a GitHub repository URL or name with owner",
|
||||
placeHolder: "<owner>/<repo> or https://github.com/<owner>/<repo>",
|
||||
placeHolder: `<owner>/<repo> or ${new URL("/", instanceUrl).toString()}<owner>/<repo>`,
|
||||
});
|
||||
if (!repoName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nwo = getNwoFromGitHubUrl(repoName) || repoName;
|
||||
const nwo =
|
||||
getNwoFromGitHubUrl(repoName, getGitHubInstanceUrl()) || repoName;
|
||||
if (!isValidGitHubNwo(nwo)) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
@@ -176,17 +180,20 @@ export class DbPanel extends DisposableObject {
|
||||
}
|
||||
|
||||
private async addNewRemoteOwner(): Promise<void> {
|
||||
const instanceUrl = getGitHubInstanceUrl();
|
||||
|
||||
const ownerName = await window.showInputBox({
|
||||
title: "Add all repositories of a GitHub org or owner",
|
||||
prompt: "Insert a GitHub organization or owner name",
|
||||
placeHolder: "<owner> or https://github.com/<owner>",
|
||||
placeHolder: `<owner> or ${new URL("/", instanceUrl).toString()}<owner>`,
|
||||
});
|
||||
|
||||
if (!ownerName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = getOwnerFromGitHubUrl(ownerName) || ownerName;
|
||||
const owner =
|
||||
getOwnerFromGitHubUrl(ownerName, getGitHubInstanceUrl()) || ownerName;
|
||||
if (!isValidGitHubOwner(owner)) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
@@ -411,7 +418,7 @@ export class DbPanel extends DisposableObject {
|
||||
if (treeViewItem.dbItem === undefined) {
|
||||
throw new Error("Unable to open on GitHub. Please select a valid item.");
|
||||
}
|
||||
const githubUrl = getGitHubUrl(treeViewItem.dbItem);
|
||||
const githubUrl = getGitHubUrl(treeViewItem.dbItem, getGitHubInstanceUrl());
|
||||
if (!githubUrl) {
|
||||
throw new Error(
|
||||
"Unable to open on GitHub. Please select a variant analysis repository or owner.",
|
||||
|
||||
@@ -62,12 +62,15 @@ function canImportCodeSearch(dbItem: DbItem): boolean {
|
||||
return DbItemKind.RemoteUserDefinedList === dbItem.kind;
|
||||
}
|
||||
|
||||
export function getGitHubUrl(dbItem: DbItem): string | undefined {
|
||||
export function getGitHubUrl(
|
||||
dbItem: DbItem,
|
||||
githubUrl: URL,
|
||||
): string | undefined {
|
||||
switch (dbItem.kind) {
|
||||
case DbItemKind.RemoteOwner:
|
||||
return `https://github.com/${dbItem.ownerName}`;
|
||||
return new URL(`/${dbItem.ownerName}`, githubUrl).toString();
|
||||
case DbItemKind.RemoteRepo:
|
||||
return `https://github.com/${dbItem.repoFullName}`;
|
||||
return new URL(`/${dbItem.repoFullName}`, githubUrl).toString();
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
joinOrderWarningThreshold,
|
||||
QueryHistoryConfigListener,
|
||||
QueryServerConfigListener,
|
||||
VariantAnalysisConfigListener,
|
||||
} from "./config";
|
||||
import {
|
||||
AstViewer,
|
||||
@@ -865,8 +866,10 @@ async function activateWithInstalledDistribution(
|
||||
"variant-analyses",
|
||||
);
|
||||
await ensureDir(variantAnalysisStorageDir);
|
||||
const variantAnalysisConfig = new VariantAnalysisConfigListener();
|
||||
const variantAnalysisResultsManager = new VariantAnalysisResultsManager(
|
||||
cliServer,
|
||||
variantAnalysisConfig,
|
||||
extLogger,
|
||||
);
|
||||
|
||||
@@ -876,6 +879,7 @@ async function activateWithInstalledDistribution(
|
||||
variantAnalysisStorageDir,
|
||||
variantAnalysisResultsManager,
|
||||
dbModule.dbManager,
|
||||
variantAnalysisConfig,
|
||||
);
|
||||
ctx.subscriptions.push(variantAnalysisManager);
|
||||
ctx.subscriptions.push(variantAnalysisResultsManager);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getActionsWorkflowRunUrl as getVariantAnalysisActionsWorkflowRunUrl,
|
||||
} from "../variant-analysis/shared/variant-analysis";
|
||||
import type { QueryLanguage } from "../common/query-language";
|
||||
import { getGitHubInstanceUrl } from "../config";
|
||||
|
||||
export type QueryHistoryInfo = LocalQueryInfo | VariantAnalysisHistoryItem;
|
||||
|
||||
@@ -79,5 +80,8 @@ export function buildRepoLabel(item: VariantAnalysisHistoryItem): string {
|
||||
export function getActionsWorkflowRunUrl(
|
||||
item: VariantAnalysisHistoryItem,
|
||||
): string {
|
||||
return getVariantAnalysisActionsWorkflowRunUrl(item.variantAnalysis);
|
||||
return getVariantAnalysisActionsWorkflowRunUrl(
|
||||
item.variantAnalysis,
|
||||
getGitHubInstanceUrl(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type ErrorResponse = {
|
||||
|
||||
export function handleRequestError(
|
||||
e: RequestError,
|
||||
githubUrl: URL,
|
||||
logger: NotificationLogger,
|
||||
): boolean {
|
||||
if (e.status !== 422) {
|
||||
@@ -60,9 +61,12 @@ export function handleRequestError(
|
||||
return false;
|
||||
}
|
||||
|
||||
const createBranchURL = `https://github.com/${
|
||||
missingDefaultBranchError.repository
|
||||
}/new/${encodeURIComponent(missingDefaultBranchError.default_branch)}`;
|
||||
const createBranchURL = new URL(
|
||||
`/${
|
||||
missingDefaultBranchError.repository
|
||||
}/new/${encodeURIComponent(missingDefaultBranchError.default_branch)}`,
|
||||
githubUrl,
|
||||
).toString();
|
||||
|
||||
void showAndLogErrorMessage(
|
||||
logger,
|
||||
|
||||
@@ -295,10 +295,14 @@ export function getSkippedRepoCount(
|
||||
|
||||
export function getActionsWorkflowRunUrl(
|
||||
variantAnalysis: VariantAnalysis,
|
||||
githubUrl: URL,
|
||||
): string {
|
||||
const {
|
||||
actionsWorkflowRunId,
|
||||
controllerRepo: { fullName },
|
||||
} = variantAnalysis;
|
||||
return `https://github.com/${fullName}/actions/runs/${actionsWorkflowRunId}`;
|
||||
return new URL(
|
||||
`/${fullName}/actions/runs/${actionsWorkflowRunId}`,
|
||||
githubUrl,
|
||||
).toString();
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ import {
|
||||
REPO_STATES_FILENAME,
|
||||
writeRepoStates,
|
||||
} from "./repo-states-store";
|
||||
import { GITHUB_AUTH_PROVIDER_ID } from "../common/vscode/authentication";
|
||||
import { FetchError } from "node-fetch";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
@@ -98,6 +97,7 @@ import { findVariantAnalysisQlPackRoot } from "./ql";
|
||||
import { resolveCodeScanningQueryPack } from "./code-scanning-pack";
|
||||
import { isSarifResultsQueryKind } from "../common/query-metadata";
|
||||
import { isVariantAnalysisEnabledForGitHubHost } from "./ghec-dr";
|
||||
import type { VariantAnalysisConfig } from "../config";
|
||||
import { getEnterpriseUri } from "../config";
|
||||
|
||||
const maxRetryCount = 3;
|
||||
@@ -158,6 +158,7 @@ export class VariantAnalysisManager
|
||||
private readonly storagePath: string,
|
||||
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager,
|
||||
private readonly dbManager: DbManager,
|
||||
private readonly config: VariantAnalysisConfig,
|
||||
) {
|
||||
super();
|
||||
this.variantAnalysisMonitor = this.push(
|
||||
@@ -426,7 +427,10 @@ export class VariantAnalysisManager
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
// If the error is handled by the handleRequestError function, we don't need to throw
|
||||
if (e instanceof RequestError && handleRequestError(e, this.app.logger)) {
|
||||
if (
|
||||
e instanceof RequestError &&
|
||||
handleRequestError(e, this.config.githubUrl, this.app.logger)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -745,7 +749,7 @@ export class VariantAnalysisManager
|
||||
private async onDidChangeSessions(
|
||||
event: AuthenticationSessionsChangeEvent,
|
||||
): Promise<void> {
|
||||
if (event.provider.id !== GITHUB_AUTH_PROVIDER_ID) {
|
||||
if (event.provider.id !== this.app.credentials.authProviderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -951,7 +955,10 @@ export class VariantAnalysisManager
|
||||
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
||||
}
|
||||
|
||||
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(variantAnalysis);
|
||||
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(
|
||||
variantAnalysis,
|
||||
this.config.githubUrl,
|
||||
);
|
||||
|
||||
await this.app.commands.execute(
|
||||
"vscode.open",
|
||||
|
||||
@@ -23,6 +23,7 @@ import { DisposableObject } from "../common/disposable-object";
|
||||
import { EventEmitter } from "vscode";
|
||||
import { unzipToDirectoryConcurrently } from "../common/unzip-concurrently";
|
||||
import { readRepoTask, writeRepoTask } from "./repo-tasks-store";
|
||||
import type { VariantAnalysisConfig } from "../config";
|
||||
|
||||
type CacheKey = `${number}/${string}`;
|
||||
|
||||
@@ -62,6 +63,7 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
|
||||
constructor(
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly config: VariantAnalysisConfig,
|
||||
private readonly logger: Logger,
|
||||
) {
|
||||
super();
|
||||
@@ -192,7 +194,7 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
throw new Error("Missing database commit SHA");
|
||||
}
|
||||
|
||||
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(
|
||||
const fileLinkPrefix = this.createGitHubFileLinkPrefix(
|
||||
repoTask.repository.fullName,
|
||||
repoTask.databaseCommitSha,
|
||||
);
|
||||
@@ -283,11 +285,11 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
return join(variantAnalysisStoragePath, fullName);
|
||||
}
|
||||
|
||||
private createGitHubDotcomFileLinkPrefix(
|
||||
fullName: string,
|
||||
sha: string,
|
||||
): string {
|
||||
return `https://github.com/${fullName}/blob/${sha}`;
|
||||
private createGitHubFileLinkPrefix(fullName: string, sha: string): string {
|
||||
return new URL(
|
||||
`/${fullName}/blob/${sha}`,
|
||||
this.config.githubUrl,
|
||||
).toString();
|
||||
}
|
||||
|
||||
public removeAnalysisResults(variantAnalysis: VariantAnalysis) {
|
||||
|
||||
@@ -15,6 +15,7 @@ function makeTestOctokit(octokit: Octokit): Credentials {
|
||||
"getExistingAccessToken not supported by test credentials",
|
||||
);
|
||||
},
|
||||
authProviderId: "github",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ export function createMockVariantAnalysisConfig(): VariantAnalysisConfig {
|
||||
return {
|
||||
controllerRepo: "foo/bar",
|
||||
showSystemDefinedRepositoryLists: true,
|
||||
githubUrl: new URL("https://github.com"),
|
||||
onDidChangeConfiguration: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
} from "../../../src/common/github-url-identifier-helper";
|
||||
|
||||
describe("github url identifier helper", () => {
|
||||
const githubUrl = new URL("https://github.com");
|
||||
|
||||
describe("valid GitHub Nwo Or Owner method", () => {
|
||||
it("should return true for valid owner", () => {
|
||||
expect(isValidGitHubOwner("github")).toBe(true);
|
||||
@@ -23,51 +25,96 @@ describe("github url identifier helper", () => {
|
||||
|
||||
describe("getNwoFromGitHubUrl method", () => {
|
||||
it("should handle invalid urls", () => {
|
||||
expect(getNwoFromGitHubUrl("")).toBe(undefined);
|
||||
expect(getNwoFromGitHubUrl("https://ww.github.com/foo/bar")).toBe(
|
||||
expect(getNwoFromGitHubUrl("", githubUrl)).toBe(undefined);
|
||||
expect(
|
||||
getNwoFromGitHubUrl("https://ww.github.com/foo/bar", githubUrl),
|
||||
).toBe(undefined);
|
||||
expect(
|
||||
getNwoFromGitHubUrl("https://tenant.ghe.com/foo/bar", githubUrl),
|
||||
).toBe(undefined);
|
||||
expect(getNwoFromGitHubUrl("https://www.github.com/foo", githubUrl)).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(getNwoFromGitHubUrl("https://www.github.com/foo")).toBe(undefined);
|
||||
expect(getNwoFromGitHubUrl("foo")).toBe(undefined);
|
||||
expect(getNwoFromGitHubUrl("foo/bar")).toBe(undefined);
|
||||
expect(getNwoFromGitHubUrl("foo", githubUrl)).toBe(undefined);
|
||||
expect(getNwoFromGitHubUrl("foo/bar", githubUrl)).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should handle valid urls", () => {
|
||||
expect(getNwoFromGitHubUrl("github.com/foo/bar")).toBe("foo/bar");
|
||||
expect(getNwoFromGitHubUrl("www.github.com/foo/bar")).toBe("foo/bar");
|
||||
expect(getNwoFromGitHubUrl("https://github.com/foo/bar")).toBe("foo/bar");
|
||||
expect(getNwoFromGitHubUrl("http://github.com/foo/bar")).toBe("foo/bar");
|
||||
expect(getNwoFromGitHubUrl("https://www.github.com/foo/bar")).toBe(
|
||||
expect(getNwoFromGitHubUrl("github.com/foo/bar", githubUrl)).toBe(
|
||||
"foo/bar",
|
||||
);
|
||||
expect(getNwoFromGitHubUrl("https://github.com/foo/bar/sub/pages")).toBe(
|
||||
expect(getNwoFromGitHubUrl("www.github.com/foo/bar", githubUrl)).toBe(
|
||||
"foo/bar",
|
||||
);
|
||||
expect(getNwoFromGitHubUrl("https://github.com/foo/bar", githubUrl)).toBe(
|
||||
"foo/bar",
|
||||
);
|
||||
expect(getNwoFromGitHubUrl("http://github.com/foo/bar", githubUrl)).toBe(
|
||||
"foo/bar",
|
||||
);
|
||||
expect(
|
||||
getNwoFromGitHubUrl("https://www.github.com/foo/bar", githubUrl),
|
||||
).toBe("foo/bar");
|
||||
expect(
|
||||
getNwoFromGitHubUrl("https://github.com/foo/bar/sub/pages", githubUrl),
|
||||
).toBe("foo/bar");
|
||||
expect(
|
||||
getNwoFromGitHubUrl(
|
||||
"https://tenant.ghe.com/foo/bar",
|
||||
new URL("https://tenant.ghe.com"),
|
||||
),
|
||||
).toBe("foo/bar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOwnerFromGitHubUrl method", () => {
|
||||
it("should handle invalid urls", () => {
|
||||
expect(getOwnerFromGitHubUrl("")).toBe(undefined);
|
||||
expect(getOwnerFromGitHubUrl("https://ww.github.com/foo/bar")).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(getOwnerFromGitHubUrl("foo")).toBe(undefined);
|
||||
expect(getOwnerFromGitHubUrl("foo/bar")).toBe(undefined);
|
||||
expect(getOwnerFromGitHubUrl("", githubUrl)).toBe(undefined);
|
||||
expect(
|
||||
getOwnerFromGitHubUrl("https://ww.github.com/foo/bar", githubUrl),
|
||||
).toBe(undefined);
|
||||
expect(
|
||||
getOwnerFromGitHubUrl("https://tenant.ghe.com/foo/bar", githubUrl),
|
||||
).toBe(undefined);
|
||||
expect(getOwnerFromGitHubUrl("foo", githubUrl)).toBe(undefined);
|
||||
expect(getOwnerFromGitHubUrl("foo/bar", githubUrl)).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should handle valid urls", () => {
|
||||
expect(getOwnerFromGitHubUrl("http://github.com/foo/bar")).toBe("foo");
|
||||
expect(getOwnerFromGitHubUrl("https://github.com/foo/bar")).toBe("foo");
|
||||
expect(getOwnerFromGitHubUrl("https://www.github.com/foo/bar")).toBe(
|
||||
expect(
|
||||
getOwnerFromGitHubUrl("http://github.com/foo/bar", githubUrl),
|
||||
).toBe("foo");
|
||||
expect(
|
||||
getOwnerFromGitHubUrl("https://github.com/foo/bar", githubUrl),
|
||||
).toBe("foo");
|
||||
expect(
|
||||
getOwnerFromGitHubUrl("https://www.github.com/foo/bar", githubUrl),
|
||||
).toBe("foo");
|
||||
expect(
|
||||
getOwnerFromGitHubUrl(
|
||||
"https://github.com/foo/bar/sub/pages",
|
||||
githubUrl,
|
||||
),
|
||||
).toBe("foo");
|
||||
expect(
|
||||
getOwnerFromGitHubUrl("https://www.github.com/foo", githubUrl),
|
||||
).toBe("foo");
|
||||
expect(getOwnerFromGitHubUrl("github.com/foo", githubUrl)).toBe("foo");
|
||||
expect(getOwnerFromGitHubUrl("www.github.com/foo", githubUrl)).toBe(
|
||||
"foo",
|
||||
);
|
||||
expect(
|
||||
getOwnerFromGitHubUrl("https://github.com/foo/bar/sub/pages"),
|
||||
getOwnerFromGitHubUrl(
|
||||
"https://tenant.ghe.com/foo/bar",
|
||||
new URL("https://tenant.ghe.com"),
|
||||
),
|
||||
).toBe("foo");
|
||||
expect(
|
||||
getOwnerFromGitHubUrl(
|
||||
"https://tenant.ghe.com/foo",
|
||||
new URL("https://tenant.ghe.com"),
|
||||
),
|
||||
).toBe("foo");
|
||||
expect(getOwnerFromGitHubUrl("https://www.github.com/foo")).toBe("foo");
|
||||
expect(getOwnerFromGitHubUrl("github.com/foo")).toBe("foo");
|
||||
expect(getOwnerFromGitHubUrl("www.github.com/foo")).toBe("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,32 +92,52 @@ describe("getDbItemActions", () => {
|
||||
});
|
||||
|
||||
describe("getGitHubUrl", () => {
|
||||
it("should return the correct url for a remote owner", () => {
|
||||
const githubUrl = new URL("https://github.com");
|
||||
|
||||
it("should return the correct url for a remote owner with github.com", () => {
|
||||
const dbItem = createRemoteOwnerDbItem();
|
||||
|
||||
const actualUrl = getGitHubUrl(dbItem);
|
||||
const actualUrl = getGitHubUrl(dbItem, githubUrl);
|
||||
const expectedUrl = `https://github.com/${dbItem.ownerName}`;
|
||||
|
||||
expect(actualUrl).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it("should return the correct url for a remote repo", () => {
|
||||
it("should return the correct url for a remote owner with GHEC-DR", () => {
|
||||
const dbItem = createRemoteOwnerDbItem();
|
||||
|
||||
const actualUrl = getGitHubUrl(dbItem, new URL("https://tenant.ghe.com"));
|
||||
const expectedUrl = `https://tenant.ghe.com/${dbItem.ownerName}`;
|
||||
|
||||
expect(actualUrl).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it("should return the correct url for a remote repo with github.com", () => {
|
||||
const dbItem = createRemoteRepoDbItem();
|
||||
|
||||
const actualUrl = getGitHubUrl(dbItem);
|
||||
const actualUrl = getGitHubUrl(dbItem, githubUrl);
|
||||
const expectedUrl = `https://github.com/${dbItem.repoFullName}`;
|
||||
|
||||
expect(actualUrl).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it("should return the correct url for a remote repo with GHEC-DR", () => {
|
||||
const dbItem = createRemoteRepoDbItem();
|
||||
|
||||
const actualUrl = getGitHubUrl(dbItem, new URL("https://tenant.ghe.com"));
|
||||
const expectedUrl = `https://tenant.ghe.com/${dbItem.repoFullName}`;
|
||||
|
||||
expect(actualUrl).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it("should return undefined for other remote db items", () => {
|
||||
const dbItem0 = createRootRemoteDbItem();
|
||||
const dbItem1 = createRemoteSystemDefinedListDbItem();
|
||||
const dbItem2 = createRemoteUserDefinedListDbItem();
|
||||
|
||||
const actualUrl0 = getGitHubUrl(dbItem0);
|
||||
const actualUrl1 = getGitHubUrl(dbItem1);
|
||||
const actualUrl2 = getGitHubUrl(dbItem2);
|
||||
const actualUrl0 = getGitHubUrl(dbItem0, githubUrl);
|
||||
const actualUrl1 = getGitHubUrl(dbItem1, githubUrl);
|
||||
const actualUrl2 = getGitHubUrl(dbItem2, githubUrl);
|
||||
|
||||
expect(actualUrl0).toBeUndefined();
|
||||
expect(actualUrl1).toBeUndefined();
|
||||
|
||||
@@ -151,13 +151,29 @@ describe("isVariantAnalysisComplete", () => {
|
||||
});
|
||||
|
||||
describe("getActionsWorkflowRunUrl", () => {
|
||||
it("should get the run url", () => {
|
||||
it("should get the run url on github.com", () => {
|
||||
const variantAnalysis = createMockVariantAnalysis({});
|
||||
|
||||
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(variantAnalysis);
|
||||
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(
|
||||
variantAnalysis,
|
||||
new URL("https://github.com"),
|
||||
);
|
||||
|
||||
expect(actionsWorkflowRunUrl).toBe(
|
||||
`https://github.com/${variantAnalysis.controllerRepo.fullName}/actions/runs/${variantAnalysis.actionsWorkflowRunId}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should get the run url on GHEC-DR", () => {
|
||||
const variantAnalysis = createMockVariantAnalysis({});
|
||||
|
||||
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(
|
||||
variantAnalysis,
|
||||
new URL("https://tenant.ghe.com"),
|
||||
);
|
||||
|
||||
expect(actionsWorkflowRunUrl).toBe(
|
||||
`https://tenant.ghe.com/${variantAnalysis.controllerRepo.fullName}/actions/runs/${variantAnalysis.actionsWorkflowRunId}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,14 @@ import { handleRequestError } from "../../../src/variant-analysis/custom-errors"
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
describe("handleRequestError", () => {
|
||||
const githubUrl = new URL("https://github.com");
|
||||
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(handleRequestError(e, githubUrl, logger)).toBe(false);
|
||||
expect(logger.showErrorMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -19,13 +20,13 @@ describe("handleRequestError", () => {
|
||||
message:
|
||||
"Unable to trigger a variant analysis. None of the requested repositories could be found.",
|
||||
});
|
||||
expect(handleRequestError(e, logger)).toBe(false);
|
||||
expect(handleRequestError(e, githubUrl, 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(handleRequestError(e, githubUrl, logger)).toBe(false);
|
||||
expect(logger.showErrorMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -42,7 +43,7 @@ describe("handleRequestError", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(handleRequestError(e, logger)).toBe(false);
|
||||
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
|
||||
expect(logger.showErrorMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -58,7 +59,7 @@ describe("handleRequestError", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(handleRequestError(e, logger)).toBe(false);
|
||||
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
|
||||
expect(logger.showErrorMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -75,7 +76,7 @@ describe("handleRequestError", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(handleRequestError(e, logger)).toBe(false);
|
||||
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
|
||||
expect(logger.showErrorMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -92,11 +93,11 @@ describe("handleRequestError", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(handleRequestError(e, logger)).toBe(false);
|
||||
expect(handleRequestError(e, githubUrl, logger)).toBe(false);
|
||||
expect(logger.showErrorMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows notification when handling a missing default branch error", () => {
|
||||
it("shows notification when handling a missing default branch error with github.com URL", () => {
|
||||
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.",
|
||||
@@ -110,11 +111,33 @@ describe("handleRequestError", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(handleRequestError(e, logger)).toBe(true);
|
||||
expect(handleRequestError(e, githubUrl, 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.",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows notification when handling a missing default branch error with GHEC-DR URL", () => {
|
||||
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, new URL("https://tenant.ghe.com"), 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://tenant.ghe.com/github/pickles/new/main) and re-run the variant analysis query.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function mockRequestError(status: number, body: any): RequestError {
|
||||
|
||||
@@ -78,8 +78,10 @@ describe("Variant Analysis Manager", () => {
|
||||
new DbConfigStore(app),
|
||||
createMockVariantAnalysisConfig(),
|
||||
);
|
||||
const variantAnalysisConfig = createMockVariantAnalysisConfig();
|
||||
variantAnalysisResultsManager = new VariantAnalysisResultsManager(
|
||||
cli,
|
||||
variantAnalysisConfig,
|
||||
extLogger,
|
||||
);
|
||||
variantAnalysisManager = new VariantAnalysisManager(
|
||||
@@ -88,6 +90,7 @@ describe("Variant Analysis Manager", () => {
|
||||
storagePath,
|
||||
variantAnalysisResultsManager,
|
||||
dbManager,
|
||||
variantAnalysisConfig,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
} from "../../../../src/variant-analysis/shared/variant-analysis";
|
||||
import { mockedObject } from "../../utils/mocking.helpers";
|
||||
import { createMockVariantAnalysisConfig } from "../../../factories/config";
|
||||
|
||||
jest.setTimeout(10_000);
|
||||
|
||||
@@ -27,8 +28,10 @@ describe(VariantAnalysisResultsManager.name, () => {
|
||||
variantAnalysisId = faker.number.int();
|
||||
|
||||
const cli = mockedObject<CodeQLCliServer>({});
|
||||
const variantAnalysisConfig = createMockVariantAnalysisConfig();
|
||||
variantAnalysisResultsManager = new VariantAnalysisResultsManager(
|
||||
cli,
|
||||
variantAnalysisConfig,
|
||||
extLogger,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -56,8 +56,10 @@ describe("Variant Analysis Manager", () => {
|
||||
new DbConfigStore(app),
|
||||
createMockVariantAnalysisConfig(),
|
||||
);
|
||||
const variantAnalysisConfig = createMockVariantAnalysisConfig();
|
||||
const variantAnalysisResultsManager = new VariantAnalysisResultsManager(
|
||||
cli,
|
||||
variantAnalysisConfig,
|
||||
extLogger,
|
||||
);
|
||||
variantAnalysisManager = new VariantAnalysisManager(
|
||||
@@ -66,6 +68,7 @@ describe("Variant Analysis Manager", () => {
|
||||
storagePath,
|
||||
variantAnalysisResultsManager,
|
||||
dbManager,
|
||||
variantAnalysisConfig,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user