Remote Queries: Create packs for remote queries

This is still a bit rough, but handles two cases:

1. There is a qlpack.yml or codeql-pack.yml file in the same directory
   as the query to run remotely. In this case, run `codeql pack
   packlist` to determine what files to include (and also always include
   the lock file and the query itself. Copy to a temp folder and run
   `pack install`, then `pack bundle`. Finally upload.
2. There is no qlpack in the current directory. Just copy the single
   file to the temp folder and generate a synthetic qlpack before
   installing, bundling and uploading.

Two cases that are not handled:

1. The query file is part of a workspace. Peer dependencies will not be
   found.
2. The query file and its qlpack file are not in the same directory.

These should be possible to handle later.  Also, need to create some
unit and integration tests for this.
This commit is contained in:
Andrew Eisenberg
2021-10-24 16:37:23 -07:00
parent b2a6263431
commit 42051f1620
15 changed files with 517 additions and 97 deletions

View File

@@ -805,8 +805,30 @@ export class CodeQLCliServer implements Disposable {
return this.runJsonCodeQlCliCommand(['pack', 'install'], [dir], 'Installing pack dependencies');
}
async packBundle(dir: string, outputPath: string): Promise<void> {
return this.runJsonCodeQlCliCommand(['pack', 'bundle'], ['-o', outputPath, dir], 'Bundling pack');
async packBundle(dir: string, workspaceFolders: string[], outputPath: string, precompile = true): Promise<void> {
const args = [
'-o',
outputPath,
dir,
'--additional-packs',
workspaceFolders.join(path.delimiter)
];
if (!precompile && await this.cliConstraints.supportsNoPrecompile()) {
args.push('--no-precompile');
}
return this.runJsonCodeQlCliCommand(['pack', 'bundle'], args, 'Bundling pack');
}
async packPacklist(dir: string, includeQueries: boolean): Promise<string[]> {
const args = includeQueries ? [dir] : ['--no-include-queries', dir];
const results = await this.runJsonCodeQlCliCommand(['pack', 'packlist'], args, 'Generating the pack list');
if (await this.cliConstraints.usesNewPackPacklistLayout()) {
return (results as { paths: string[] }).paths;
} else {
return results as string[];
}
}
async generateDil(qloFile: string, outFile: string): Promise<void> {
@@ -1057,6 +1079,12 @@ export function shouldDebugQueryServer() {
&& process.env.QUERY_SERVER_JAVA_DEBUG?.toLocaleLowerCase() !== 'false';
}
export function shouldDebugCliServer() {
return 'CLI_SERVER_JAVA_DEBUG' in process.env
&& process.env.CLI_SERVER_JAVA_DEBUG !== '0'
&& process.env.CLI_SERVER_JAVA_DEBUG?.toLocaleLowerCase() !== 'false';
}
export class CliVersionConstraint {
/**
@@ -1096,6 +1124,16 @@ export class CliVersionConstraint {
*/
public static CLI_VERSION_WITH_DATABASE_UNBUNDLE = new SemVer('2.6.0');
/**
* CLI version where the `--no-precompile` option for pack creation was introduced.
*/
public static CLI_VERSION_WITH_NO_PRECOMPILE = new SemVer('2.7.1');
/**
* CLI version where `pack packlist` layout changed from array to object
*/
public static CLI_VERSION_PACK_PACKLIST_LAYOUT_CHANGE = new SemVer('2.7.1');
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
@@ -1132,4 +1170,11 @@ export class CliVersionConstraint {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_DATABASE_UNBUNDLE);
}
async supportsNoPrecompile() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_NO_PRECOMPILE);
}
async usesNewPackPacklistLayout() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_PACK_PACKLIST_LAYOUT_CHANGE);
}
}

View File

@@ -716,13 +716,25 @@ async function activateWithInstalledDistribution(
);
// The "runRemoteQuery" command is internal-only.
ctx.subscriptions.push(
commandRunner('codeQL.runRemoteQuery', async (
commandRunnerWithProgress('codeQL.runRemoteQuery', async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
) => {
if (isCanary()) {
progress({
maxStep: 5,
step: 0,
message: 'Getting credentials'
});
const credentials = await Credentials.initialize(ctx);
await runRemoteQuery(cliServer, credentials, uri || window.activeTextEditor?.document.uri);
await runRemoteQuery(cliServer, credentials, uri || window.activeTextEditor?.document.uri, false, progress, token);
} else {
throw new Error('Remote queries require the CodeQL Canary version to run.');
}
}, {
title: 'Run Remote Query',
cancellable: true
})
);
ctx.subscriptions.push(

View File

@@ -10,6 +10,7 @@ import {
env
} from 'vscode';
import { CodeQLCliServer, QlpacksInfo } from './cli';
import { UserCancellationException } from './commandRunner';
import { logger } from './logging';
/**
@@ -494,14 +495,25 @@ export async function findLanguage(
void logger.log('Could not autodetect query language. Select language manually.');
}
}
const availableLanguages = Object.keys(await cliServer.resolveLanguages());
// will be undefined if user cancels the quick pick.
return await askForLanguage(cliServer, false);
}
export async function askForLanguage(cliServer: CodeQLCliServer, throwOnEmpty = true): Promise<string | undefined> {
const availableLanguages = Object.keys(await cliServer.resolveLanguages()).sort();
const language = await Window.showQuickPick(
availableLanguages,
{ placeHolder: 'Select target language for your query', ignoreFocusOut: true }
);
if (!language) {
// This only happens if the user cancels the quick pick.
void showAndLogErrorMessage('Language not found. Language must be specified manually.');
if (throwOnEmpty) {
throw new UserCancellationException('Cancelled.');
} else {
void showAndLogErrorMessage('Language not found. Language must be specified manually.');
}
}
return language;
}

View File

@@ -1,13 +1,15 @@
import { QuickPickItem, Uri, window } from 'vscode';
import { CancellationToken, QuickPickItem, Uri, window } from 'vscode';
import * as path from 'path';
import * as yaml from 'js-yaml';
import * as fs from 'fs-extra';
import * as tmp from 'tmp-promise';
import { findLanguage, showAndLogErrorMessage, showAndLogInformationMessage, showInformationMessageWithAction } from './helpers';
import { askForLanguage, findLanguage, getOnDiskWorkspaceFolders, showAndLogErrorMessage, showAndLogInformationMessage, showInformationMessageWithAction } from './helpers';
import { Credentials } from './authentication';
import * as cli from './cli';
import { logger } from './logging';
import { getRemoteControllerRepo, getRemoteRepositoryLists, setRemoteControllerRepo } from './config';
import { tmpDir } from './run-queries';
import { ProgressCallback, UserCancellationException } from './commandRunner';
interface Config {
repositories: string[];
ref?: string;
@@ -70,93 +72,244 @@ export async function getRepositories(): Promise<string[] | undefined> {
}
}
async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: string): Promise<string> {
/**
* Two possibilities:
* 1. There is no qlpack.yml in this directory. Assume this is a lone query and generate a synthetic qlpack for it.
* 2. There is a qlpack.yml in this directory. Assume this is a query pack and use the yml to pack the query before uploading it.
*
* @returns the entire qlpack as a base64 string.
*/
async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: string, queryPackDir: string, fallbackLanguage?: string): Promise<{
base64Pack: string,
language: string
}> {
const originalPackRoot = path.dirname(queryFile);
// TODO this assumes that the qlpack.yml is in the same directory as the query file, but in reality,
// the file could be in a parent directory.
const targetQueryFileName = path.join(queryPackDir, path.basename(queryFile));
const packRoot = path.dirname(queryFile);
// the server is expecting the query file to be named `query.ql`. Rename it here.
const renamedQueryFile = path.join(queryPackDir, 'query.ql');
const bundlePath = await tmp.tmpName({
postfix: '.tgz',
prefix: 'qlpack-',
});
let language: string | undefined;
if (await fs.pathExists(path.join(originalPackRoot, 'qlpack.yml'))) {
// don't include ql files. We only want the queryFile to be copied.
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
await cliServer.packBundle(packRoot, bundlePath);
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
[path.join(originalPackRoot, 'qlpack.lock.yml'), path.join(originalPackRoot, 'codeql-pack.lock.yml'), queryFile]
.forEach(aboslutePath => {
if (aboslutePath) {
toCopy.push(aboslutePath);
}
});
void logger.log(`Copying ${toCopy.length} files to ${queryPackDir}`);
await fs.copy(originalPackRoot, queryPackDir, {
filter: (file: string) =>
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist
!!toCopy.find(f => f === file || f.startsWith(file + path.sep)
)
});
language = await findLanguage(cliServer, Uri.file(targetQueryFileName));
} else {
// open popup to ask for language if not already hardcoded
language = fallbackLanguage || await askForLanguage(cliServer);
// copy only the query file to the query pack directory
// and generate a synthetic query pack
// TODO this has a limitation that query packs inside of a workspace will not resolve it's peer dependencies.
// Something to work on later. For now, we will only support query packs that are not in a workspace.
void logger.log(`Copying ${queryFile} to ${queryPackDir}`);
await fs.copy(queryFile, targetQueryFileName);
void logger.log('Generating synthetic query pack');
const syntheticQueryPack = {
name: 'codeql-remote/query',
version: '1.0.0',
dependencies: {
[`codeql/${language}-all`]: '*',
}
};
await fs.writeFile(path.join(queryPackDir, 'qlpack.yml'), yaml.safeDump(syntheticQueryPack));
}
if (!language) {
throw new UserCancellationException('Could not determine language.');
}
await fs.rename(targetQueryFileName, renamedQueryFile);
const bundlePath = await getPackedBundlePath(queryPackDir);
void logger.log(`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`);
await cliServer.packInstall(queryPackDir);
const workspaceFolders = getOnDiskWorkspaceFolders();
await cliServer.packBundle(queryPackDir, workspaceFolders, bundlePath, false);
const base64Pack = (await fs.readFile(bundlePath)).toString('base64');
await fs.unlink(bundlePath);
return base64Pack;
return {
base64Pack,
language
};
}
export async function runRemoteQuery(cliServer: cli.CodeQLCliServer, credentials: Credentials, uri?: Uri) {
if (!uri?.fsPath.endsWith('.ql')) {
async function createRemoteQueriesTempDirectory() {
const remoteQueryDir = await tmp.dir({ dir: tmpDir.name, unsafeCleanup: true });
const queryPackDir = path.join(remoteQueryDir.path, 'query-pack');
await fs.mkdirp(queryPackDir);
return { remoteQueryDir, queryPackDir };
}
async function getPackedBundlePath(queryPackDir: string) {
return tmp.tmpName({
dir: path.dirname(queryPackDir),
postfix: 'generated.tgz',
prefix: 'qlpack',
});
}
export async function runRemoteQuery(
cliServer: cli.CodeQLCliServer,
credentials: Credentials,
uri: Uri | undefined,
dryRun: boolean,
progress: ProgressCallback,
token: CancellationToken
): Promise<void | string> {
const { remoteQueryDir, queryPackDir } = await createRemoteQueriesTempDirectory();
try {
if (!uri?.fsPath.endsWith('.ql')) {
throw new UserCancellationException('Not a CodeQL query file.');
}
progress({
maxStep: 5,
step: 1,
message: 'Determining project list'
});
const queryFile = uri.fsPath;
const repositoriesFile = queryFile.substring(0, queryFile.length - '.ql'.length) + '.repositories';
let ref: string | undefined;
// For the case of single file remote queries, use the language from the config in order to avoid the user having to select it.
let fallbackLanguage: string | undefined;
let repositories: string[] | undefined;
progress({
maxStep: 5,
step: 2,
message: 'Determining query target language'
});
// If the user has an explicit `.repositories` file, use that.
// Otherwise, prompt user to select repositories from the `codeQL.remoteQueries.repositoryLists` setting.
if (await fs.pathExists(repositoriesFile)) {
void logger.log(`Found '${repositoriesFile}'. Using information from that file to run ${queryFile}.`);
const config = yaml.safeLoad(await fs.readFile(repositoriesFile, 'utf8')) as Config;
ref = config.ref || 'main';
fallbackLanguage = config.language;
repositories = config.repositories;
} else {
ref = 'main';
repositories = await getRepositories();
}
if (!repositories || repositories.length === 0) {
// No error message needed, since `getRepositories` already displays one.
throw new UserCancellationException('No repositories to query.');
}
progress({
maxStep: 5,
step: 3,
message: 'Determining controller repo'
});
// Get the controller repo from the config, if it exists.
// If it doesn't exist, prompt the user to enter it, and save that value to the config.
let controllerRepo: string | undefined;
controllerRepo = getRemoteControllerRepo();
if (!controllerRepo || !REPO_REGEX.test(controllerRepo)) {
void logger.log(controllerRepo ? 'Invalid controller repository name.' : 'No controller repository defined.');
controllerRepo = await window.showInputBox({
title: 'Controller repository in which to display progress and results of remote queries',
placeHolder: '<owner>/<repo>',
prompt: 'Enter the name of a GitHub repository in the format <owner>/<repo>',
ignoreFocusOut: true,
});
if (!controllerRepo) {
void showAndLogErrorMessage('No controller repository entered.');
return;
} else if (!REPO_REGEX.test(controllerRepo)) { // Check if user entered invalid input
void showAndLogErrorMessage('Invalid repository format. Must be a valid GitHub repository in the format <owner>/<repo>.');
return;
}
void logger.log(`Setting the controller repository as: ${controllerRepo}`);
await setRemoteControllerRepo(controllerRepo);
}
void logger.log(`Using controller repository: ${controllerRepo}`);
const [owner, repo] = controllerRepo.split('/');
progress({
maxStep: 5,
step: 4,
message: 'Bundling the query pack'
});
if (token.isCancellationRequested) {
throw new UserCancellationException('Cancelled');
}
const { base64Pack, language } = await generateQueryPack(cliServer, queryFile, queryPackDir, fallbackLanguage);
if (token.isCancellationRequested) {
throw new UserCancellationException('Cancelled');
}
progress({
maxStep: 5,
step: 5,
message: 'Sending request'
});
await runRemoteQueriesApiRequest(credentials, ref, language, repositories, owner, repo, base64Pack, dryRun);
if (dryRun) {
return remoteQueryDir.path;
} else {
// don't return the path because it has been deleted
return;
}
} finally {
if (dryRun) {
// If we are in a dry run keep the data around for debugging purposes.
void logger.log(`[DRY RUN] Not deleting ${queryPackDir}.`);
} else {
await remoteQueryDir.cleanup();
}
}
}
async function runRemoteQueriesApiRequest(
credentials: Credentials,
ref: string,
language: string,
repositories: string[],
owner: string,
repo: string,
queryPackBase64: string,
dryRun = false
): Promise<void> {
if (dryRun) {
void showAndLogInformationMessage('[DRY RUN] Would have sent request. See extension log for the payload.');
void logger.log(JSON.stringify({ ref, language, repositories, owner, repo, queryPackBase64: queryPackBase64.substring(0, 100) + '... ' + queryPackBase64.length + ' bytes' }));
return;
}
const queryFile = uri.fsPath;
const query = await fs.readFile(queryFile, 'utf8');
const repositoriesFile = queryFile.substring(0, queryFile.length - '.ql'.length) + '.repositories';
let ref: string | undefined;
let language: string | undefined;
let repositories: string[] | undefined;
await cliServer.packInstall(path.dirname(queryFile));
// If the user has an explicit `.repositories` file, use that.
// Otherwise, prompt user to select repositories from the `codeQL.remoteQueries.repositoryLists` setting.
if (await fs.pathExists(repositoriesFile)) {
void logger.log(`Found '${repositoriesFile}'. Using information from that file to run ${queryFile}.`);
const config = yaml.safeLoad(await fs.readFile(repositoriesFile, 'utf8')) as Config;
ref = config.ref || 'main';
language = config.language || await findLanguage(cliServer, uri);
repositories = config.repositories;
} else {
ref = 'main';
[language, repositories] = await Promise.all([findLanguage(cliServer, uri), getRepositories()]);
}
if (!language) {
return; // No error message needed, since `findLanguage` already displays one.
}
if (!repositories || repositories.length === 0) {
return; // No error message needed, since `getRepositories` already displays one.
}
// Get the controller repo from the config, if it exists.
// If it doesn't exist, prompt the user to enter it, and save that value to the config.
let controllerRepo: string | undefined;
controllerRepo = getRemoteControllerRepo();
if (!controllerRepo || !REPO_REGEX.test(controllerRepo)) {
void logger.log(controllerRepo ? 'Invalid controller repository name.' : 'No controller repository defined.');
controllerRepo = await window.showInputBox({
title: 'Controller repository in which to display progress and results of remote queries',
placeHolder: '<owner>/<repo>',
prompt: 'Enter the name of a GitHub repository in the format <owner>/<repo>',
ignoreFocusOut: true,
});
if (!controllerRepo) {
void showAndLogErrorMessage('No controller repository entered.');
return;
} else if (!REPO_REGEX.test(controllerRepo)) { // Check if user entered invalid input
void showAndLogErrorMessage('Invalid repository format. Must be a valid GitHub repository in the format <owner>/<repo>.');
return;
}
void logger.log(`Setting the controller repository as: ${controllerRepo}`);
await setRemoteControllerRepo(controllerRepo);
}
void logger.log(`Using controller repository: ${controllerRepo}`);
const [owner, repo] = controllerRepo.split('/');
const queryPackBase64 = await generateQueryPack(cliServer, queryFile);
await runRemoteQueriesApiRequest(credentials, ref, language, repositories, query, owner, repo, queryPackBase64);
}
async function runRemoteQueriesApiRequest(credentials: Credentials, ref: string, language: string, repositories: string[], query: string, owner: string, repo: string, queryPackBase64: string): Promise<void> {
const octokit = await credentials.getOctokit();
try {
const octokit = await credentials.getOctokit();
await octokit.request(
'POST /repos/:owner/:repo/code-scanning/codeql/queries',
{
@@ -166,20 +319,29 @@ async function runRemoteQueriesApiRequest(credentials: Credentials, ref: string,
ref,
language,
repositories,
query,
queryPack: queryPackBase64,
query_pack: queryPackBase64,
}
}
);
void showAndLogInformationMessage(`Successfully scheduled runs. [Click here to see the progress](https://github.com/${owner}/${repo}/actions).`);
} catch (error) {
await attemptRerun(error, credentials, ref, language, repositories, query, owner, repo, queryPackBase64);
await attemptRerun(error, credentials, ref, language, repositories, owner, repo, queryPackBase64, dryRun);
}
}
/** Attempts to rerun the query on only the valid repositories */
export async function attemptRerun(error: any, credentials: Credentials, ref: string, language: string, repositories: string[], query: string, owner: string, repo: string, queryPackBase64: string) {
export async function attemptRerun(
error: any,
credentials: Credentials,
ref: string,
language: string,
repositories: string[],
owner: string,
repo: string,
queryPackBase64: string,
dryRun = false
) {
if (typeof error.message === 'string' && error.message.includes('Some repositories were invalid')) {
const invalidRepos = error?.response?.data?.invalid_repos || [];
const reposWithoutDbUploads = error?.response?.data?.repos_without_db_uploads || [];
@@ -202,11 +364,9 @@ export async function attemptRerun(error: any, credentials: Credentials, ref: st
if (rerunQuery) {
const validRepositories = repositories.filter(r => !invalidRepos.includes(r) && !reposWithoutDbUploads.includes(r));
void logger.log(`Rerunning query on set of valid repositories: ${JSON.stringify(validRepositories)}`);
await runRemoteQueriesApiRequest(credentials, ref, language, validRepositories, query, owner, repo, queryPackBase64);
await runRemoteQueriesApiRequest(credentials, ref, language, validRepositories, owner, repo, queryPackBase64, dryRun);
}
} else {
void showAndLogErrorMessage(error);
}
}

View File

@@ -0,0 +1,2 @@
// This file should not be included the remote query pack.
select 1

View File

@@ -0,0 +1,3 @@
int number() {
result = 1
}

View File

@@ -0,0 +1,4 @@
import javascript
import lib
select number()

View File

@@ -0,0 +1,4 @@
import javascript
import lib
select number()

View File

@@ -0,0 +1,3 @@
int number() {
result = 1
}

View File

@@ -0,0 +1,2 @@
// This file should not be included the remote query pack.
select 1

View File

@@ -0,0 +1,5 @@
name: github/remote-query-pack
version: 0.0.0
extractor: javascript
dependencies:
codeql/javascript-all: '*'

View File

@@ -1,6 +1,10 @@
import 'mocha';
import 'sinon-chai';
import { runTestsInDirectory } from '../index-template';
import 'mocha';
import * as sinonChai from 'sinon-chai';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
chai.use(sinonChai);
// The simple database used throughout the tests
export function run(): Promise<void> {

View File

@@ -0,0 +1,164 @@
import { assert, expect } from 'chai';
import * as path from 'path';
import * as sinon from 'sinon';
import { CancellationToken, extensions, QuickPickItem, Uri, window } from 'vscode';
import 'mocha';
import * as fs from 'fs-extra';
import * as yaml from 'js-yaml';
import { runRemoteQuery } from '../../run-remote-query';
import { Credentials } from '../../authentication';
import { CodeQLCliServer } from '../../cli';
import { CodeQLExtensionInterface } from '../../extension';
import { setRemoteControllerRepo } from '../../config';
import { UserCancellationException } from '../../commandRunner';
describe('Remote queries', function() {
const baseDir = path.join(__dirname, '../../../src/vscode-tests/cli-integration');
let sandbox: sinon.SinonSandbox;
// up to 3 minutes per test
this.timeout(3 * 60 * 1000);
let cli: CodeQLCliServer;
let credentials: Credentials = {} as unknown as Credentials;
let token: CancellationToken;
let progress: sinon.SinonSpy;
let showQuickPickSpy: sinon.SinonStub;
beforeEach(async () => {
sandbox = sinon.createSandbox();
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
if ('cliServer' in extension) {
cli = extension.cliServer;
} else {
throw new Error('Extension not initialized. Make sure cli is downloaded and installed properly.');
}
credentials = {} as unknown as Credentials;
token = {
isCancellationRequested: false
} as unknown as CancellationToken;
progress = sandbox.spy();
// Should not have asked for a language
showQuickPickSpy = sandbox.stub(window, 'showQuickPick')
.onFirstCall().resolves({ repoList: ['github/vscode-codeql'] } as unknown as QuickPickItem)
.onSecondCall().resolves('javascript' as unknown as QuickPickItem);
// always run in the vscode-codeql repo
void setRemoteControllerRepo('github/vscode-codeql');
});
afterEach(() => {
sandbox.restore();
});
it('should run a remote query that is part of a qlpack', async () => {
const fileUri = getFile('data-remote-qlpack/in-pack.ql');
const queryPackRootDir = (await runRemoteQuery(cli, credentials, fileUri, true, progress, token))!;
// to retrieve the list of repositories
expect(showQuickPickSpy).to.have.been.calledOnce;
// check a few files that we know should exist and others that we know should not
// the tarball to deliver to the server
expect(fs.readdirSync(queryPackRootDir).find(f => f.startsWith('qlpack-') && f.endsWith('-generated.tgz'))).not.to.be.undefined;
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
// in-pack.ql renamed to query.ql
expect(fs.existsSync(path.join(queryPackDir, 'query.ql'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
expect(fs.existsSync(
// depending on the cli version, we should have one of these files
path.join(queryPackDir, 'qlpack.lock.yml') || path.join(queryPackDir, 'codeql-pack.lock.yml')
)).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
// the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/github/remote-query-pack/0.0.0/');
expect(fs.existsSync(path.join(compiledPackDir, 'query.ql'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
expect(fs.existsSync(
// depending on the cli version, we should have one of these files
path.join(compiledPackDir, 'qlpack.lock.yml') || path.join(queryPackDir, 'codeql-pack.lock.yml')
)).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
// dependencies
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
const packNames = fs.readdirSync(libraryDir).sort();
expect(packNames).to.deep.equal(['javascript-all', 'javascript-upgrades']);
});
it('should run a remote query that is not part of a qlpack', async () => {
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const queryPackRootDir = (await runRemoteQuery(cli, credentials, fileUri, true, progress, token))!;
// to retrieve the list of repositories
// and a second time to ask for the language
expect(showQuickPickSpy).to.have.been.calledTwice;
// check a few files that we know should exist and others that we know should not
// the tarball to deliver to the server
expect(fs.readdirSync(queryPackRootDir).find(f => f.startsWith('qlpack-') && f.endsWith('-generated.tgz'))).not.to.be.undefined;
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
// in-pack.ql renamed to query.ql
expect(fs.existsSync(path.join(queryPackDir, 'query.ql'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
expect(fs.existsSync(
// depending on the cli version, we should have one of these files
path.join(queryPackDir, 'qlpack.lock.yml') || path.join(queryPackDir, 'codeql-pack.lock.yml')
)).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'lib.qll'))).to.be.false;
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
// the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/1.0.0/');
expect(fs.existsSync(path.join(compiledPackDir, 'query.ql'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
expect(fs.existsSync(
// depending on the cli version, we should have one of these files
path.join(compiledPackDir, 'qlpack.lock.yml') || path.join(queryPackDir, 'codeql-pack.lock.yml')
)).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.false;
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
// should have generated a correct qlpack file
const qlpackContents: any = yaml.safeLoad(fs.readFileSync(path.join(compiledPackDir, 'qlpack.yml'), 'utf8'));
expect(qlpackContents.name).to.equal('codeql-remote/query');
expect(qlpackContents.version).to.equal('1.0.0');
expect(qlpackContents.dependencies?.['codeql/javascript-all']).to.equal('*');
// dependencies
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
const packNames = fs.readdirSync(libraryDir).sort();
expect(packNames).to.deep.equal(['javascript-all', 'javascript-upgrades']);
});
it('should cancel a run before uploading', async () => {
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, token);
token.isCancellationRequested = true;
try {
await promise;
assert.fail('should have thrown');
} catch (e) {
expect(e).to.be.instanceof(UserCancellationException);
}
});
function getFile(file: string): Uri {
return Uri.file(path.join(baseDir, file));
}
});

View File

@@ -44,7 +44,7 @@ const _10MB = _1MB * 10;
// CLI version to test. Hard code the latest as default. And be sure
// to update the env if it is not otherwise set.
const CLI_VERSION = process.env.CLI_VERSION || 'v2.6.3';
const CLI_VERSION = process.env.CLI_VERSION || 'v2.7.0';
process.env.CLI_VERSION = CLI_VERSION;
// Base dir where CLIs will be downloaded into