@@ -17,14 +17,17 @@ import {
1717import { Credentials } from '../authentication' ;
1818import * as cli from '../cli' ;
1919import { logger } from '../logging' ;
20- import { getActionBranch , getRemoteControllerRepo , setRemoteControllerRepo } from '../config' ;
20+ import { getActionBranch , getRemoteControllerRepo , isVariantAnalysisLiveResultsEnabled , setRemoteControllerRepo } from '../config' ;
2121import { ProgressCallback , UserCancellationException } from '../commandRunner' ;
22- import { OctokitResponse } from '@octokit/types/dist-types' ;
22+ import { OctokitResponse , RequestError } from '@octokit/types/dist-types' ;
2323import { RemoteQuery } from './remote-query' ;
2424import { RemoteQuerySubmissionResult } from './remote-query-submission-result' ;
2525import { QueryMetadata } from '../pure/interface-types' ;
2626import { getErrorMessage , REPO_REGEX } from '../pure/helpers-pure' ;
27+ import * as ghApiClient from './gh-api/gh-api-client' ;
2728import { getRepositorySelection , isValidSelection , RepositorySelection } from './repository-selection' ;
29+ import { parseVariantAnalysisQueryLanguage , VariantAnalysis , VariantAnalysisStatus , VariantAnalysisSubmission } from './shared/variant-analysis' ;
30+ import { Repository } from './shared/repository' ;
2831
2932export 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;
354383const 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
425454async 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+ }
0 commit comments