Skip to content

Commit 0b638b6

Browse files
Merge pull request #1538 from github/robertbrignull/submit-variant-analysis
Implement submitting a live-results variant analysis
2 parents ac3b94d + ce7c711 commit 0b638b6

7 files changed

Lines changed: 472 additions & 263 deletions

File tree

extensions/ql-vscode/src/remote-queries/gh-api/gh-api-client.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
VariantAnalysisRepoTask,
77
VariantAnalysisSubmissionRequest
88
} from './variant-analysis';
9+
import { Repository } from './repository';
910

1011
export async function submitVariantAnalysis(
1112
credentials: Credentials,
@@ -73,13 +74,13 @@ export async function getVariantAnalysisRepo(
7374
return response.data;
7475
}
7576

76-
export async function getRepositoryIdFromNwo(
77+
export async function getRepositoryFromNwo(
7778
credentials: Credentials,
7879
owner: string,
7980
repo: string
80-
): Promise<number> {
81+
): Promise<Repository> {
8182
const octokit = await credentials.getOctokit();
8283

8384
const response = await octokit.rest.repos.get({ owner, repo });
84-
return response.data.id;
85+
return response.data as Repository;
8586
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { RemoteQuery } from './remote-query';
2+
import { VariantAnalysis } from './shared/variant-analysis';
23

34
export interface RemoteQuerySubmissionResult {
45
queryDirPath?: string;
56
query?: RemoteQuery;
7+
variantAnalysis?: VariantAnalysis;
68
}

extensions/ql-vscode/src/remote-queries/run-remote-query.ts

Lines changed: 140 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ import {
1717
import { Credentials } from '../authentication';
1818
import * as cli from '../cli';
1919
import { logger } from '../logging';
20-
import { getActionBranch, getRemoteControllerRepo, setRemoteControllerRepo } from '../config';
20+
import { getActionBranch, getRemoteControllerRepo, isVariantAnalysisLiveResultsEnabled, setRemoteControllerRepo } from '../config';
2121
import { ProgressCallback, UserCancellationException } from '../commandRunner';
22-
import { OctokitResponse } from '@octokit/types/dist-types';
22+
import { OctokitResponse, RequestError } from '@octokit/types/dist-types';
2323
import { RemoteQuery } from './remote-query';
2424
import { RemoteQuerySubmissionResult } from './remote-query-submission-result';
2525
import { QueryMetadata } from '../pure/interface-types';
2626
import { getErrorMessage, REPO_REGEX } from '../pure/helpers-pure';
27+
import * as ghApiClient from './gh-api/gh-api-client';
2728
import { getRepositorySelection, isValidSelection, RepositorySelection } from './repository-selection';
29+
import { parseVariantAnalysisQueryLanguage, VariantAnalysis, VariantAnalysisStatus, VariantAnalysisSubmission } from './shared/variant-analysis';
30+
import { Repository } from './shared/repository';
2831

2932
export interface QlPack {
3033
name: string;
@@ -210,31 +213,7 @@ export async function runRemoteQuery(
210213
message: 'Determining controller repo'
211214
});
212215

213-
// Get the controller repo from the config, if it exists.
214-
// If it doesn't exist, prompt the user to enter it, and save that value to the config.
215-
let controllerRepo: string | undefined;
216-
controllerRepo = getRemoteControllerRepo();
217-
if (!controllerRepo || !REPO_REGEX.test(controllerRepo)) {
218-
void logger.log(controllerRepo ? 'Invalid controller repository name.' : 'No controller repository defined.');
219-
controllerRepo = await window.showInputBox({
220-
title: 'Controller repository in which to run the GitHub Actions workflow for this variant analysis',
221-
placeHolder: '<owner>/<repo>',
222-
prompt: 'Enter the name of a GitHub repository in the format <owner>/<repo>',
223-
ignoreFocusOut: true,
224-
});
225-
if (!controllerRepo) {
226-
void showAndLogErrorMessage('No controller repository entered.');
227-
return;
228-
} else if (!REPO_REGEX.test(controllerRepo)) { // Check if user entered invalid input
229-
void showAndLogErrorMessage('Invalid repository format. Must be a valid GitHub repository in the format <owner>/<repo>.');
230-
return;
231-
}
232-
void logger.log(`Setting the controller repository as: ${controllerRepo}`);
233-
await setRemoteControllerRepo(controllerRepo);
234-
}
235-
236-
void logger.log(`Using controller repository: ${controllerRepo}`);
237-
const [owner, repo] = controllerRepo.split('/');
216+
const controllerRepo = await getControllerRepo(credentials);
238217

239218
progress({
240219
maxStep: 4,
@@ -259,31 +238,84 @@ export async function runRemoteQuery(
259238
});
260239

261240
const actionBranch = getActionBranch();
262-
const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, owner, repo, base64Pack, dryRun);
263241
const queryStartTime = Date.now();
264242
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
265243

266-
if (dryRun) {
267-
return { queryDirPath: remoteQueryDir.path };
268-
} else {
269-
if (!apiResponse) {
270-
return;
244+
if (isVariantAnalysisLiveResultsEnabled()) {
245+
const queryName = getQueryName(queryMetadata, queryFile);
246+
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(language);
247+
if (variantAnalysisLanguage === undefined) {
248+
throw new UserCancellationException(`Found unsupported language: ${language}`);
271249
}
272250

273-
const workflowRunId = apiResponse.workflow_run_id;
274-
const repositoryCount = apiResponse.repositories_queried.length;
275-
const remoteQuery = await buildRemoteQueryEntity(
276-
queryFile,
277-
queryMetadata,
278-
owner,
279-
repo,
280-
queryStartTime,
281-
workflowRunId,
282-
language,
283-
repositoryCount);
284-
285-
// don't return the path because it has been deleted
286-
return { query: remoteQuery };
251+
const variantAnalysisSubmission: VariantAnalysisSubmission = {
252+
startTime: queryStartTime,
253+
actionRepoRef: actionBranch,
254+
controllerRepoId: controllerRepo.id,
255+
query: {
256+
name: queryName,
257+
filePath: queryFile,
258+
pack: base64Pack,
259+
language: variantAnalysisLanguage,
260+
},
261+
databases: {
262+
repositories: repoSelection.repositories,
263+
repositoryLists: repoSelection.repositoryLists,
264+
repositoryOwners: repoSelection.owners
265+
}
266+
};
267+
268+
const variantAnalysisResponse = await ghApiClient.submitVariantAnalysis(
269+
credentials,
270+
variantAnalysisSubmission
271+
);
272+
273+
const variantAnalysis: VariantAnalysis = {
274+
id: variantAnalysisResponse.id,
275+
controllerRepoId: variantAnalysisResponse.controller_repo.id,
276+
query: {
277+
name: variantAnalysisSubmission.query.name,
278+
filePath: variantAnalysisSubmission.query.filePath,
279+
language: variantAnalysisSubmission.query.language,
280+
},
281+
databases: {
282+
repositories: variantAnalysisSubmission.databases.repositories,
283+
repositoryLists: variantAnalysisSubmission.databases.repositoryLists,
284+
repositoryOwners: variantAnalysisSubmission.databases.repositoryOwners,
285+
},
286+
status: VariantAnalysisStatus.InProgress,
287+
};
288+
289+
// TODO: Remove once we have a proper notification
290+
void showAndLogInformationMessage('Variant analysis submitted for processing');
291+
void logger.log(`Variant analysis:\n${JSON.stringify(variantAnalysis, null, 2)}`);
292+
293+
return { variantAnalysis };
294+
295+
} else {
296+
const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, controllerRepo, base64Pack, dryRun);
297+
298+
if (dryRun) {
299+
return { queryDirPath: remoteQueryDir.path };
300+
} else {
301+
if (!apiResponse) {
302+
return;
303+
}
304+
305+
const workflowRunId = apiResponse.workflow_run_id;
306+
const repositoryCount = apiResponse.repositories_queried.length;
307+
const remoteQuery = await buildRemoteQueryEntity(
308+
queryFile,
309+
queryMetadata,
310+
controllerRepo,
311+
queryStartTime,
312+
workflowRunId,
313+
language,
314+
repositoryCount);
315+
316+
// don't return the path because it has been deleted
317+
return { query: remoteQuery };
318+
}
287319
}
288320

289321
} finally {
@@ -301,8 +333,7 @@ async function runRemoteQueriesApiRequest(
301333
ref: string,
302334
language: string,
303335
repoSelection: RepositorySelection,
304-
owner: string,
305-
repo: string,
336+
controllerRepo: Repository,
306337
queryPackBase64: string,
307338
dryRun = false
308339
): Promise<void | QueriesResponse> {
@@ -318,8 +349,7 @@ async function runRemoteQueriesApiRequest(
318349
if (dryRun) {
319350
void showAndLogInformationMessage('[DRY RUN] Would have sent request. See extension log for the payload.');
320351
void logger.log(JSON.stringify({
321-
owner,
322-
repo,
352+
controllerRepo,
323353
data: {
324354
...data,
325355
queryPackBase64: queryPackBase64.substring(0, 100) + '... ' + queryPackBase64.length + ' bytes'
@@ -331,14 +361,13 @@ async function runRemoteQueriesApiRequest(
331361
try {
332362
const octokit = await credentials.getOctokit();
333363
const response: OctokitResponse<QueriesResponse, number> = await octokit.request(
334-
'POST /repos/:owner/:repo/code-scanning/codeql/queries',
364+
'POST /repos/:controllerRepo/code-scanning/codeql/queries',
335365
{
336-
owner,
337-
repo,
366+
controllerRepo: controllerRepo.fullName,
338367
data
339368
}
340369
);
341-
const { popupMessage, logMessage } = parseResponse(owner, repo, response.data);
370+
const { popupMessage, logMessage } = parseResponse(controllerRepo, response.data);
342371
void showAndLogInformationMessage(popupMessage, { fullMessage: logMessage });
343372
return response.data;
344373
} catch (error: any) {
@@ -354,14 +383,14 @@ const eol = os.EOL;
354383
const eol2 = os.EOL + os.EOL;
355384

356385
// exported for testing only
357-
export function parseResponse(owner: string, repo: string, response: QueriesResponse) {
386+
export function parseResponse(controllerRepo: Repository, response: QueriesResponse) {
358387
const repositoriesQueried = response.repositories_queried;
359388
const repositoryCount = repositoriesQueried.length;
360389

361-
const popupMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. [Click here to see the progress](https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}).`
390+
const popupMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. [Click here to see the progress](https://github.com/${controllerRepo.fullName}/actions/runs/${response.workflow_run_id}).`
362391
+ (response.errors ? `${eol2}Some repositories could not be scheduled. See extension log for details.` : '');
363392

364-
let logMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. See https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}.`;
393+
let logMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. See https://github.com/${controllerRepo.fullName}/actions/runs/${response.workflow_run_id}.`;
365394
logMessage += `${eol2}Repositories queried:${eol}${repositoriesQueried.join(', ')}`;
366395
if (response.errors) {
367396
const { invalid_repositories, repositories_without_database, private_repositories, cutoff_repositories, cutoff_repositories_count } = response.errors;
@@ -425,29 +454,75 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string
425454
async function buildRemoteQueryEntity(
426455
queryFilePath: string,
427456
queryMetadata: QueryMetadata | undefined,
428-
controllerRepoOwner: string,
429-
controllerRepoName: string,
457+
controllerRepo: Repository,
430458
queryStartTime: number,
431459
workflowRunId: number,
432460
language: string,
433461
repositoryCount: number
434462
): Promise<RemoteQuery> {
435-
// The query name is either the name as specified in the query metadata, or the file name.
436-
const queryName = queryMetadata?.name ?? path.basename(queryFilePath);
437-
463+
const queryName = getQueryName(queryMetadata, queryFilePath);
438464
const queryText = await fs.readFile(queryFilePath, 'utf8');
465+
const [owner, name] = controllerRepo.fullName.split('/');
439466

440467
return {
441468
queryName,
442469
queryFilePath,
443470
queryText,
444471
language,
445472
controllerRepository: {
446-
owner: controllerRepoOwner,
447-
name: controllerRepoName,
473+
owner,
474+
name,
448475
},
449476
executionStartTime: queryStartTime,
450477
actionsWorkflowRunId: workflowRunId,
451478
repositoryCount,
452479
};
453480
}
481+
482+
function getQueryName(queryMetadata: QueryMetadata | undefined, queryFilePath: string): string {
483+
// The query name is either the name as specified in the query metadata, or the file name.
484+
return queryMetadata?.name ?? path.basename(queryFilePath);
485+
}
486+
487+
async function getControllerRepo(credentials: Credentials): Promise<Repository> {
488+
// Get the controller repo from the config, if it exists.
489+
// If it doesn't exist, prompt the user to enter it, and save that value to the config.
490+
let controllerRepoNwo: string | undefined;
491+
controllerRepoNwo = getRemoteControllerRepo();
492+
if (!controllerRepoNwo || !REPO_REGEX.test(controllerRepoNwo)) {
493+
void logger.log(controllerRepoNwo ? 'Invalid controller repository name.' : 'No controller repository defined.');
494+
controllerRepoNwo = await window.showInputBox({
495+
title: 'Controller repository in which to run the GitHub Actions workflow for this variant analysis',
496+
placeHolder: '<owner>/<repo>',
497+
prompt: 'Enter the name of a GitHub repository in the format <owner>/<repo>',
498+
ignoreFocusOut: true,
499+
});
500+
if (!controllerRepoNwo) {
501+
throw new UserCancellationException('No controller repository entered.');
502+
} else if (!REPO_REGEX.test(controllerRepoNwo)) { // Check if user entered invalid input
503+
throw new UserCancellationException('Invalid repository format. Must be a valid GitHub repository in the format <owner>/<repo>.');
504+
}
505+
void logger.log(`Setting the controller repository as: ${controllerRepoNwo}`);
506+
await setRemoteControllerRepo(controllerRepoNwo);
507+
}
508+
509+
void logger.log(`Using controller repository: ${controllerRepoNwo}`);
510+
const [owner, repo] = controllerRepoNwo.split('/');
511+
512+
try {
513+
const controllerRepo = await ghApiClient.getRepositoryFromNwo(credentials, owner, repo);
514+
void logger.log(`Controller repository ID: ${controllerRepo.id}`);
515+
return {
516+
id: controllerRepo.id,
517+
fullName: controllerRepo.full_name,
518+
private: controllerRepo.private,
519+
};
520+
521+
} catch (e: any) {
522+
if ((e as RequestError).status === 404) {
523+
throw new Error(`Controller repository "${owner}/${repo}" not found`);
524+
} else {
525+
throw new Error(`Error getting controller repository "${owner}/${repo}": ${e.message}`);
526+
}
527+
}
528+
}

extensions/ql-vscode/src/remote-queries/shared/variant-analysis.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export enum VariantAnalysisQueryLanguage {
3030
Ruby = 'ruby'
3131
}
3232

33+
export function parseVariantAnalysisQueryLanguage(language: string): VariantAnalysisQueryLanguage | undefined {
34+
return Object.values(VariantAnalysisQueryLanguage).find(x => x === language);
35+
}
36+
3337
export enum VariantAnalysisStatus {
3438
InProgress = 'inProgress',
3539
Succeeded = 'succeeded',

0 commit comments

Comments
 (0)