Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions docs/variant-analysis-custom-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Custom variant analysis server (internal)

> [!WARNING]
> **Internal and unsupported.** This is not a public feature. The settings below are not declared in
> `package.json`, do not appear in the Settings UI, and may change or be removed without notice.
> Do not document them in user-facing docs or the changelog.

By default, multi-repository variant analysis (MRVA) uses the GitHub REST API and the user's GitHub sign-in.
An organization can instead send variant analyses to its own server. That server must implement the
GitHub variant analysis REST endpoints and authenticate requests with a bearer token. A separate
extension, owned by the organization, supplies the token.

## Settings

Both settings are only read from **local or remote user** settings. Workspace and folder values are ignored,
so a repository cannot redirect queries or tokens.
`inspect().globalValue` is VS Code's merged user value, including remote user settings;
the remote value takes precedence over the local value. These are not OS-wide policy settings.
Administrators can provision the remote user settings file directly. A companion extension writing
with `ConfigurationTarget.Global` should not assume that the write always targets the remote file:
VS Code's choice depends on the setting's scope and whether a remote value already exists.

| Setting | Value |
| --- | --- |
| `codeQL.variantAnalysis.serverUrl` | Base URL of the server. Must be `https:` and must not contain credentials. A trailing `/` is removed. |
| `codeQL.variantAnalysis.serverTokenProvider` | ID (`publisher.name`) of the installed extension that provides the token. |

If `serverUrl` is set but invalid, variant analysis fails instead of falling back to GitHub.
Query strings and fragments are rejected; base path prefixes are supported.
The hidden server setting is the opt-in switch; there is no additional canary gate.

## Token provider extension

The extension named in `serverTokenProvider` must return the following from `activate()`:

```ts
export interface VariantAnalysisServerTokenProvider {
getVariantAnalysisServerToken(serverUrl: string): Promise<string | undefined>;
openVariantAnalysisLogs?(serverUrl: string, analysisId: number): Promise<void>;
}
```

CodeQL activates the extension if it isn't already running, and calls `getVariantAnalysisServerToken`
before every request. The provider is responsible for caching and refreshing the token. It should return
`undefined` if no token is available, so that CodeQL can show a clear error instead of sending an
unauthenticated request. The token is never logged.
CodeQL waits at most 30 seconds for activation and token acquisition combined. On timeout, no API
request is sent; the provider's own work is not forcibly stopped. Finish sign-in in the provider and retry.
HTTP 401/403 errors identify the provider and ask the user to re-authenticate; there is no automatic retry
or token-refresh argument.

Only the extension that the user or administrator names is called. Extensions cannot register
themselves as providers.

The optional `openVariantAnalysisLogs` hook enables **View logs** in history and result views.
The provider owns log retrieval, authentication, and presentation (for example, opening a browser
or an output channel). CodeQL passes the originating server URL and analysis/session ID, not an
Actions workflow ID. Without the hook the action is hidden. Capability discovery activates the
provider in the background with the same 30-second activation bound. History and result views render
immediately without the action, then refresh when discovery finishes. Discovery is shared and cached
per server URL and provider ID, including failures; changing either setting clears the cache.
If discovery fails, CodeQL logs the error and leaves the action hidden without delaying saved results.
Opening logs does not request a token from CodeQL.
The hook should resolve once its viewer is opened and reject on failure. Runs from another configured
server cannot invoke it. No log endpoint or GitHub Actions compatibility is required.

Providers should publish their exports immediately from `activate()`. Catalog downloads, sign-in
prompts, and other potentially unbounded work must not block activation; perform them lazily in the
operation that needs them.

## Behavior when a server is configured

- MRVA is enabled regardless of `github-enterprise.uri`. The GitHub sign-in, GitHub database
downloads and other extensions that read `github-enterprise.uri` are not affected.
- No controller repository is needed. Requests use controller repository ID `0`.
- Runs retain their originating server URL. Polling, task retrieval and cancellation fail if
the current server setting differs. Restore the original setting to resume them. Legacy runs
without a server URL are treated as GitHub runs.
- The GitHub token is never sent to the server, and results are never uploaded to GitHub:
"Export to gist" is unavailable; "View logs" uses the companion's optional hook, not GitHub Actions.
- Removing a running server analysis from history offers to cancel it through the server.
Dismissing that offer removes only the local history entry; the server run continues.
- Result artifacts are downloaded from `artifact_url` without authentication, as on GitHub.
- If a repository task has no `database_commit_sha`, file links point to `HEAD`.
These links may not match the analyzed revision.
- Custom-server file links use `repository.html_url` from the task or analysis response.
If it is absent or not an HTTP(S) URL, results still display, but file locations are plain text.
GitHub run links retain their existing host fallback.
- GitHub's built-in repository lists and the repository tree's "Open on GitHub" action are hidden
while a custom server is configured.
- The canary-only autofix action still downloads repository source through GitHub sign-in.
It requires the analyzed repository and revision to be available on the configured GitHub host.
Custom source retrieval is not supported.

## Server contract

All paths are relative to `serverUrl`. Requests send `Authorization: Bearer <token>` and JSON bodies.
Redirects are rejected rather than forwarding query packs to another endpoint.
Request and response shapes match `src/variant-analysis/gh-api/variant-analysis.ts`.

| Operation | Request |
| --- | --- |
| Submit | `POST /repositories/0/code-scanning/codeql/variant-analyses` |
| Get status | `GET /repositories/0/code-scanning/codeql/variant-analyses/{id}` |
| Get repository task | `GET /repositories/0/code-scanning/codeql/variant-analyses/{id}/repositories/{repoId}` |
| Cancel | `POST /repositories/0/code-scanning/codeql/variant-analyses/{id}/cancel` |

Variant analysis responses must include `controller_repo`; subsequent requests use its `id` as the
repository path parameter (normally `0`). The analysis `id` is the server's run/session identifier.
Custom-server cancellation does not require `actions_workflow_run_id`; GitHub cancellation still uses
the GitHub Actions route and requires that field. Errors should be returned as non-2xx responses with a JSON body
`{ "message": "..." }`. CodeQL shows the status and that message to the user.

## Running CodeQL in a remote workspace

CodeQL declares a Node `main` entry point, so VS Code normally classifies it as a workspace extension
without an explicit `extensionKind`. Install both CodeQL and its token provider in the same remote
extension host. No override should normally be necessary.

If a deployment has overridden the placement, inspect `remote.extensionKind` and the editor's
product overrides. The user setting `{ "github.vscode-codeql": ["workspace"] }` under
`remote.extensionKind` can override placement, but should not be installed as a blanket workaround.

VS Code implementation references:
[configuration inspection](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/api/common/extHostConfiguration.ts),
[local/remote user configuration merging](https://github.com/microsoft/vscode/blob/main/src/vs/platform/configuration/common/configurationModels.ts),
and [extension kind inference](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts).
16 changes: 8 additions & 8 deletions extensions/ql-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@
},
{
"command": "codeQLVariantAnalysisRepositories.openOnGitHubContextMenu",
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canBeOpenedOnGitHub/",
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canBeOpenedOnGitHub/ && !config.codeQL.variantAnalysis.serverUrl",
"group": "2_qlContextMenu@1"
},
{
Expand Down Expand Up @@ -1245,12 +1245,12 @@
{
"command": "codeQLQueryHistory.removeHistoryItemContextMenu",
"group": "7_queryHistory@0",
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledRemoteResultsItemWithoutLogs || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == remoteResultsItemWithoutLogs || viewItem == cancelledRemoteResultsItemWithoutLogs || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
},
{
"command": "codeQLQueryHistory.removeHistoryItemContextInline",
"group": "inline",
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledRemoteResultsItemWithoutLogs || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == remoteResultsItemWithoutLogs || viewItem == cancelledRemoteResultsItemWithoutLogs || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
},
{
"command": "codeQLQueryHistory.renameItem",
Expand Down Expand Up @@ -1300,7 +1300,7 @@
{
"command": "codeQLQueryHistory.exportResults",
"group": "1_queryHistory@0",
"when": "view == codeQLQueryHistory && viewItem == remoteResultsItem"
"when": "view == codeQLQueryHistory && (viewItem == remoteResultsItem || viewItem == remoteResultsItemWithoutLogs)"
},
{
"command": "codeQLQueryHistory.viewCsvResults",
Expand All @@ -1325,7 +1325,7 @@
{
"command": "codeQLQueryHistory.cancel",
"group": "9_qlCommands",
"when": "viewItem == inProgressResultsItem || viewItem == inProgressRemoteResultsItem"
"when": "viewItem == inProgressResultsItem || viewItem == inProgressRemoteResultsItem || viewItem == inProgressRemoteResultsItemWithoutLogs"
},
{
"command": "codeQLQueryHistory.openOnGithub",
Expand All @@ -1335,12 +1335,12 @@
{
"command": "codeQLQueryHistory.copyRepoList",
"group": "1_queryHistory@1",
"when": "viewItem == remoteResultsItem"
"when": "viewItem == remoteResultsItem || viewItem == remoteResultsItemWithoutLogs"
},
{
"command": "codeQLQueryHistory.viewAutofixes",
"group": "1_queryHistory@2",
"when": "viewItem == remoteResultsItem && config.codeQL.canary"
"when": "(viewItem == remoteResultsItem || viewItem == remoteResultsItemWithoutLogs) && config.codeQL.canary"
},
{
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
Expand Down Expand Up @@ -2034,7 +2034,7 @@
{
"view": "codeQLVariantAnalysisRepositories",
"contents": "Set up a controller repository to start using variant analysis. [Learn more](https://codeql.github.com/docs/codeql-for-visual-studio-code/running-codeql-queries-at-scale-with-mrva#controller-repository) about controller repositories. \n[Set up controller repository](command:codeQLVariantAnalysisRepositories.setupControllerRepository)",
"when": "!config.codeQL.variantAnalysis.controllerRepo"
"when": "!config.codeQL.variantAnalysis.controllerRepo && !config.codeQL.variantAnalysis.serverUrl"
}
]
},
Expand Down
4 changes: 2 additions & 2 deletions extensions/ql-vscode/src/common/bqrs-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export function isEmptyPath(uriStr: string) {

export function tryGetRemoteLocation(
loc: UrlValue | undefined,
fileLinkPrefix: string,
fileLinkPrefix: string | undefined,
sourceLocationPrefix: string | undefined,
): string | undefined {
if (!loc || !isUrlValueResolvable(loc)) {
if (!fileLinkPrefix || !loc || !isUrlValueResolvable(loc)) {
return undefined;
}

Expand Down
5 changes: 4 additions & 1 deletion extensions/ql-vscode/src/common/location-link-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ export function createRemoteFileRef(
endLine?: number,
startColumn?: number,
endColumn?: number,
): string {
): string | undefined {
if (!fileLink.fileLinkPrefix) {
return undefined;
}
if (
startColumn &&
endColumn &&
Expand Down
89 changes: 88 additions & 1 deletion extensions/ql-vscode/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ export class Setting {
.get<T>(this.name)!;
}

getGlobalValue<T>(): T | undefined {
if (this.parent === undefined) {
throw new Error("Cannot get the value of a root setting.");
}
return workspace
.getConfiguration(this.parent.qualifiedName)
.inspect<T>(this.name)?.globalValue;
}

updateValue<T>(value: T, target: ConfigurationTarget): Thenable<void> {
if (this.parent === undefined) {
throw new Error("Cannot update the value of a root setting.");
Expand Down Expand Up @@ -605,6 +614,84 @@ export async function setRemoteControllerRepo(repo: string | undefined) {
await REMOTE_CONTROLLER_REPO.updateValue(repo, ConfigurationTarget.Global);
}

/**
* INTERNAL AND UNSUPPORTED: these settings are intentionally not declared in
* package.json and are not documented publicly. See
* docs/variant-analysis-custom-server.md.
*
* `serverUrl` points variant analysis at a self-hosted server that implements
* the GitHub variant analysis REST endpoints, instead of GitHub.
* `serverTokenProvider` is the ID of an installed extension that supplies the
* bearer token for that server.
*
* Both are only read from user or machine (global) settings, so a workspace
* cannot redirect queries or tokens.
*/
const VARIANT_ANALYSIS_SERVER_URL = new Setting(
"serverUrl",
VARIANT_ANALYSIS_SETTING,
);
const VARIANT_ANALYSIS_SERVER_TOKEN_PROVIDER = new Setting(
"serverTokenProvider",
VARIANT_ANALYSIS_SETTING,
);

export interface VariantAnalysisServerConfig {
/** The server's base URL, always `https:` and without a trailing slash. */
url: string;
/** ID of the extension that provides the bearer token. */
tokenProviderExtensionId: string | undefined;
}

function getGlobalStringSetting(setting: Setting): string | undefined {
const value = setting.getGlobalValue<string>();
return typeof value === "string" && value.trim() ? value.trim() : undefined;
Comment on lines +647 to +648
}

/**
* Returns the custom variant analysis server configuration, or `undefined`
* if variant analysis should use GitHub. Throws if the server URL is invalid,
* rather than silently falling back to GitHub.
*/
export function getVariantAnalysisServerConfig():
VariantAnalysisServerConfig | undefined {
const rawUrl = getGlobalStringSetting(VARIANT_ANALYSIS_SERVER_URL);
if (!rawUrl) {
return undefined;
}

let url: URL;
try {
url = new URL(rawUrl);
} catch {
throw new Error(
`Invalid ${VARIANT_ANALYSIS_SERVER_URL.qualifiedName} setting: ${rawUrl}`,
);
}
if (
url.protocol !== "https:" ||
url.username ||
url.password ||
url.search ||
url.hash
Comment on lines +675 to +676
) {
throw new Error(
`${VARIANT_ANALYSIS_SERVER_URL.qualifiedName} must be an https URL without credentials, a query string, or a fragment.`,
);
}

return {
url: url.toString().replace(/\/+$/, ""),
tokenProviderExtensionId: getGlobalStringSetting(
VARIANT_ANALYSIS_SERVER_TOKEN_PROVIDER,
),
};
}

export function isVariantAnalysisServerConfigured(): boolean {
return getGlobalStringSetting(VARIANT_ANALYSIS_SERVER_URL) !== undefined;
}

export interface VariantAnalysisConfig {
controllerRepo: string | undefined;
showSystemDefinedRepositoryLists: boolean;
Expand Down Expand Up @@ -632,7 +719,7 @@ export class VariantAnalysisConfigListener
}

public get showSystemDefinedRepositoryLists(): boolean {
return !hasEnterpriseUri();
return !hasEnterpriseUri() && !isVariantAnalysisServerConfigured();
}

public get githubUrl(): URL {
Expand Down
10 changes: 9 additions & 1 deletion extensions/ql-vscode/src/databases/ui/db-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ 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";
import {
getGitHubInstanceUrl,
isVariantAnalysisServerConfigured,
} from "../../config";

export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
remoteDatabaseKind: string;
Expand Down Expand Up @@ -415,6 +418,11 @@ export class DbPanel extends DisposableObject {
}

private async openOnGitHub(treeViewItem: DbTreeViewItem): Promise<void> {
if (isVariantAnalysisServerConfigured()) {
throw new Error(
"Opening repositories on GitHub is not supported with a custom variant analysis server.",
);
}
if (treeViewItem.dbItem === undefined) {
throw new Error("Unable to open on GitHub. Please select a valid item.");
}
Expand Down
2 changes: 2 additions & 0 deletions extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import { LogScannerService } from "./log-insights/log-scanner-service";
import { VariantAnalysisView } from "./variant-analysis/variant-analysis-view";
import { VariantAnalysisViewSerializer } from "./variant-analysis/variant-analysis-view-serializer";
import { VariantAnalysisManager } from "./variant-analysis/variant-analysis-manager";
import { watchVariantAnalysisLogCapabilities } from "./variant-analysis/variant-analysis-api";
import { createVariantAnalysisContentProvider } from "./variant-analysis/variant-analysis-content-provider";
import { VSCodeMockGitHubApiServer } from "./common/mock-gh-api/vscode/vscode-mock-gh-api-server";
import { VariantAnalysisResultsManager } from "./variant-analysis/variant-analysis-results-manager";
Expand Down Expand Up @@ -920,6 +921,7 @@ async function activateWithInstalledDistribution(
);
await ensureDir(variantAnalysisStorageDir);
const variantAnalysisConfig = new VariantAnalysisConfigListener();
ctx.subscriptions.push(watchVariantAnalysisLogCapabilities());
const variantAnalysisResultsManager = new VariantAnalysisResultsManager(
cliServer,
variantAnalysisConfig,
Expand Down
Loading
Loading