diff --git a/extensions/ql-vscode/src/config.ts b/extensions/ql-vscode/src/config.ts index 9790ac974..d54c1a040 100644 --- a/extensions/ql-vscode/src/config.ts +++ b/extensions/ql-vscode/src/config.ts @@ -4,7 +4,7 @@ import type { ConfigurationScope, Event, } from "vscode"; -import { ConfigurationTarget, EventEmitter, workspace } from "vscode"; +import { ConfigurationTarget, EventEmitter, workspace, Uri } from "vscode"; import type { DistributionManager } from "./codeql-cli/distribution"; import { extLogger } from "./common/logging/vscode"; import { ONE_DAY_IN_MS } from "./common/time"; @@ -14,6 +14,7 @@ import { SortKey, } from "./variant-analysis/shared/variant-analysis-filter-sort"; import { substituteConfigVariables } from "./common/config-template"; +import { getErrorMessage } from "./common/helpers-pure"; export const ALL_SETTINGS: Setting[] = []; @@ -69,6 +70,52 @@ export const VSCODE_SAVE_BEFORE_START_SETTING = new Setting( VSCODE_DEBUG_SETTING, ); +const VSCODE_GITHUB_ENTERPRISE_SETTING = new Setting( + "github-enterprise", + undefined, +); +export const VSCODE_GITHUB_ENTERPRISE_URI_SETTING = new Setting( + "uri", + VSCODE_GITHUB_ENTERPRISE_SETTING, +); + +/** + * Get the value of the `github-enterprise.uri` setting, parsed as a URI. + * If the value is not set or cannot be parsed, return `undefined`. + */ +export function getEnterpriseUri(): Uri | undefined { + const config = VSCODE_GITHUB_ENTERPRISE_URI_SETTING.getValue(); + if (config) { + try { + let uri = Uri.parse(config, true); + if (uri.scheme === "http") { + uri = uri.with({ scheme: "https" }); + } + return uri; + } catch (e) { + void extLogger.log( + `Failed to parse the GitHub Enterprise URI: ${getErrorMessage(e)}`, + ); + } + } + return undefined; +} + +/** + * Is the GitHub Enterprise URI set? + */ +export function hasEnterpriseUri(): boolean { + return getEnterpriseUri() !== undefined; +} + +/** + * 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"); +} + const ROOT_SETTING = new Setting("codeQL"); // Telemetry configuration @@ -576,6 +623,11 @@ export function getVariantAnalysisDefaultResultsSort(): SortKey { */ const ACTION_BRANCH = new Setting("actionBranch", VARIANT_ANALYSIS_SETTING); +export const VARIANT_ANALYSIS_ENABLE_GHEC_DR = new Setting( + "enableGhecDr", + VARIANT_ANALYSIS_SETTING, +); + export function getActionBranch(): string { return ACTION_BRANCH.getValue() || "main"; } diff --git a/extensions/ql-vscode/src/variant-analysis/ghec-dr.ts b/extensions/ql-vscode/src/variant-analysis/ghec-dr.ts new file mode 100644 index 000000000..0340b594f --- /dev/null +++ b/extensions/ql-vscode/src/variant-analysis/ghec-dr.ts @@ -0,0 +1,18 @@ +import { + VARIANT_ANALYSIS_ENABLE_GHEC_DR, + hasEnterpriseUri, + hasGhecDrUri, +} from "../config"; + +/** + * Determines whether MRVA should be enabled or not for the current GitHub host. + * This is based on the `github-enterprise.uri` setting. + */ +export function isVariantAnalysisEnabledForGitHubHost(): boolean { + return ( + // MRVA is always enabled on github.com + !hasEnterpriseUri() || + // MRVA can be enabled on GHEC-DR using a feature flag + (hasGhecDrUri() && !!VARIANT_ANALYSIS_ENABLE_GHEC_DR.getValue()) + ); +} diff --git a/extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts b/extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts index 5117c6219..fdf8abd7c 100644 --- a/extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts +++ b/extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts @@ -97,6 +97,8 @@ import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders"; import { findVariantAnalysisQlPackRoot } from "./ql"; import { resolveCodeScanningQueryPack } from "./code-scanning-pack"; import { isSarifResultsQueryKind } from "../common/query-metadata"; +import { isVariantAnalysisEnabledForGitHubHost } from "./ghec-dr"; +import { getEnterpriseUri } from "../config"; const maxRetryCount = 3; @@ -327,6 +329,12 @@ export class VariantAnalysisManager token: CancellationToken, openViewAfterSubmission = true, ): Promise { + if (!isVariantAnalysisEnabledForGitHubHost()) { + throw new Error( + `Multi-repository variant analysis is not enabled for ${getEnterpriseUri()}`, + ); + } + await saveBeforeStart(); progress({ diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts index 0091af8f9..7a7403275 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts @@ -1,6 +1,15 @@ -import { CancellationTokenSource, commands, window, Uri } from "vscode"; +import { + CancellationTokenSource, + commands, + window, + Uri, + ConfigurationTarget, +} from "vscode"; import { extLogger } from "../../../../src/common/logging/vscode"; -import { setRemoteControllerRepo } from "../../../../src/config"; +import { + VSCODE_GITHUB_ENTERPRISE_URI_SETTING, + setRemoteControllerRepo, +} from "../../../../src/config"; import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-client"; import { isAbsolute, join } from "path"; @@ -99,6 +108,32 @@ describe("Variant Analysis Manager", () => { await setRemoteControllerRepo("github/vscode-codeql"); }); + it("fails if MRVA is not supported for this GHE URI", async () => { + await VSCODE_GITHUB_ENTERPRISE_URI_SETTING.updateValue( + "https://github.example.com", + ConfigurationTarget.Global, + ); + + const qlPackDetails: QlPackDetails = { + queryFiles: [getFileOrDir("data-remote-qlpack/in-pack.ql")], + qlPackRootPath: getFileOrDir("data-remote-qlpack"), + qlPackFilePath: getFileOrDir("data-remote-qlpack/qlpack.yml"), + language: QueryLanguage.Javascript, + }; + + await expect( + variantAnalysisManager.runVariantAnalysis( + qlPackDetails, + progress, + cancellationTokenSource.token, + ), + ).rejects.toThrow( + new Error( + "Multi-repository variant analysis is not enabled for https://github.example.com/", + ), + ); + }); + it("should run a variant analysis that is part of a qlpack", async () => { const filePath = getFileOrDir("data-remote-qlpack/in-pack.ql"); const qlPackRootPath = getFileOrDir("data-remote-qlpack"); diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/config.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/config.test.ts index 0ee260b3b..65dfcdc73 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/config.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/config.test.ts @@ -1,10 +1,14 @@ -import { workspace } from "vscode"; +import { ConfigurationTarget, workspace } from "vscode"; import type { ConfigListener } from "../../../src/config"; import { CliConfigListener, QueryHistoryConfigListener, QueryServerConfigListener, + VSCODE_GITHUB_ENTERPRISE_URI_SETTING, + getEnterpriseUri, + hasEnterpriseUri, + hasGhecDrUri, } from "../../../src/config"; import { vscodeGetConfigurationMock } from "../test-config"; @@ -126,3 +130,49 @@ describe("config listeners", () => { return new Promise((resolve) => setTimeout(resolve, ms)); } }); + +describe("enterprise URI", () => { + it("detects no enterprise URI when config value is not set", async () => { + expect(getEnterpriseUri()).toBeUndefined(); + expect(hasEnterpriseUri()).toBe(false); + expect(hasGhecDrUri()).toBe(false); + }); + + it("detects no enterprise URI when config value is set to an invalid value", async () => { + await VSCODE_GITHUB_ENTERPRISE_URI_SETTING.updateValue( + "invalid-uri", + ConfigurationTarget.Global, + ); + expect(getEnterpriseUri()).toBeUndefined(); + expect(hasEnterpriseUri()).toBe(false); + expect(hasGhecDrUri()).toBe(false); + }); + + it("detects an enterprise URI when config value is set to a GHES URI", async () => { + await VSCODE_GITHUB_ENTERPRISE_URI_SETTING.updateValue( + "https://github.example.com", + ConfigurationTarget.Global, + ); + expect(getEnterpriseUri()?.toString()).toBe("https://github.example.com/"); + expect(hasEnterpriseUri()).toBe(true); + expect(hasGhecDrUri()).toBe(false); + }); + + it("detects a GHEC-DR URI when config value is set to a GHEC-DR URI", async () => { + await VSCODE_GITHUB_ENTERPRISE_URI_SETTING.updateValue( + "https://example.ghe.com", + ConfigurationTarget.Global, + ); + expect(getEnterpriseUri()?.toString()).toBe("https://example.ghe.com/"); + expect(hasEnterpriseUri()).toBe(true); + expect(hasGhecDrUri()).toBe(true); + }); + + it("Upgrades HTTP URIs to HTTPS", async () => { + await VSCODE_GITHUB_ENTERPRISE_URI_SETTING.updateValue( + "http://example.ghe.com", + ConfigurationTarget.Global, + ); + expect(getEnterpriseUri()?.toString()).toBe("https://example.ghe.com/"); + }); +}); diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/variant-analysis/ghec-dr.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/variant-analysis/ghec-dr.test.ts new file mode 100644 index 000000000..2bad6bdcc --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/variant-analysis/ghec-dr.test.ts @@ -0,0 +1,52 @@ +import { ConfigurationTarget } from "vscode"; +import { + VARIANT_ANALYSIS_ENABLE_GHEC_DR, + VSCODE_GITHUB_ENTERPRISE_URI_SETTING, +} from "../../../../src/config"; +import { isVariantAnalysisEnabledForGitHubHost } from "../../../../src/variant-analysis/ghec-dr"; + +describe("checkVariantAnalysisEnabled", () => { + it("returns cleanly when no enterprise URI is set", async () => { + expect(isVariantAnalysisEnabledForGitHubHost()).toBe(true); + }); + + it("returns false when GHES enterprise URI is set and variant analysis feature flag is not set", async () => { + await VSCODE_GITHUB_ENTERPRISE_URI_SETTING.updateValue( + "https://github.example.com", + ConfigurationTarget.Global, + ); + expect(isVariantAnalysisEnabledForGitHubHost()).toBe(false); + }); + + it("returns false when GHES enterprise URI is set and variant analysis feature flag is set", async () => { + await VSCODE_GITHUB_ENTERPRISE_URI_SETTING.updateValue( + "https://github.example.com", + ConfigurationTarget.Global, + ); + await VARIANT_ANALYSIS_ENABLE_GHEC_DR.updateValue( + "true", + ConfigurationTarget.Global, + ); + expect(isVariantAnalysisEnabledForGitHubHost()).toBe(false); + }); + + it("returns false when GHEC-DR URI is set and variant analysis feature flag is not set", async () => { + await VSCODE_GITHUB_ENTERPRISE_URI_SETTING.updateValue( + "https://example.ghe.com", + ConfigurationTarget.Global, + ); + expect(isVariantAnalysisEnabledForGitHubHost()).toBe(false); + }); + + it("returns true when GHEC-DR URI is set and variant analysis feature flag is set", async () => { + await VSCODE_GITHUB_ENTERPRISE_URI_SETTING.updateValue( + "https://example.ghe.com", + ConfigurationTarget.Global, + ); + await VARIANT_ANALYSIS_ENABLE_GHEC_DR.updateValue( + "true", + ConfigurationTarget.Global, + ); + expect(isVariantAnalysisEnabledForGitHubHost()).toBe(true); + }); +});