diff --git a/extensions/ql-vscode/src/distribution.ts b/extensions/ql-vscode/src/distribution.ts index 3de02376f..b2a441347 100644 --- a/extensions/ql-vscode/src/distribution.ts +++ b/extensions/ql-vscode/src/distribution.ts @@ -322,16 +322,22 @@ class ExtensionSpecificDistributionManager { } private async getLatestRelease(): Promise { - const release = await this.createReleasesApiConsumer().getLatestRelease(this._versionRange, this._config.includePrerelease); - // FIXME: Look for platform-specific codeql distribution if available - release.assets = release.assets.filter(asset => asset.name === 'codeql.zip'); - if (release.assets.length === 0) { - throw new Error("Release had no asset named codeql.zip"); - } - else if (release.assets.length > 1) { - throw new Error("Release had more than one asset named codeql.zip"); - } - return release; + return await this.createReleasesApiConsumer().getLatestRelease( + this._versionRange, + this._config.includePrerelease, + release => { + // FIXME: Look for platform-specific codeql distribution if available + // https://github.com/github/vscode-codeql/issues/417 + const matchingAssets = release.assets.filter(asset => asset.name === 'codeql.zip'); + if (matchingAssets.length !== 1) { + if (matchingAssets.length > 1) { + logger.log("WARNING: Ignoring a release with more than one asset named codeql.zip"); + } + return false; + } + return true; + } + ); } private createReleasesApiConsumer(): ReleasesApiConsumer { @@ -391,7 +397,7 @@ export class ReleasesApiConsumer { this._repoName = repoName; } - public async getLatestRelease(versionRange: semver.Range, includePrerelease = false): Promise { + public async getLatestRelease(versionRange: semver.Range, includePrerelease = false, additionalCompatibilityCheck?: (release: GithubRelease) => boolean): Promise { const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`; const allReleases: GithubRelease[] = await (await this.makeApiCall(apiPath)).json(); const compatibleReleases = allReleases.filter(release => { @@ -400,7 +406,11 @@ export class ReleasesApiConsumer { } const version = semver.parse(release.tag_name); - return version !== null && semver.satisfies(version, versionRange); + if (version === null || !semver.satisfies(version, versionRange)) { + return false; + } + + return !additionalCompatibilityCheck || additionalCompatibilityCheck(release); }); // Tag names must all be parsable to semvers due to the previous filtering step. const latestRelease = compatibleReleases.sort((a, b) => { diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/distribution.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/distribution.test.ts index d7de15d14..7377193e8 100644 --- a/extensions/ql-vscode/src/vscode-tests/no-workspace/distribution.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/distribution.test.ts @@ -35,7 +35,11 @@ describe("Releases API consumer", () => { "tag_name": "v3.1.1" }, { - "assets": [], + "assets": [{ + id: 1, + name: "exampleAsset.txt", + size: 1 + }], "created_at": "2019-09-05T00:00:00Z", "id": 3, "name": "", @@ -85,6 +89,26 @@ describe("Releases API consumer", () => { expect(latestRelease.id).to.equal(1); }); + it("picking latest release: release passes additional compatibility test if additional compatibility test specified", async () => { + class MockReleasesApiConsumer extends ReleasesApiConsumer { + protected async makeApiCall(apiPath: string): Promise { + if (apiPath === `/repos/${owner}/${repo}/releases`) { + return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse))); + } + return Promise.reject(new Error(`Unknown API path: ${apiPath}`)); + } + } + + const consumer = new MockReleasesApiConsumer(owner, repo); + + const latestRelease = await consumer.getLatestRelease( + new semver.Range("2.*.*"), + true, + release => release.assets.some(asset => asset.name === "exampleAsset.txt") + ); + expect(latestRelease.id).to.equal(3); + }); + it("picking latest release: includes prereleases when option set", async () => { class MockReleasesApiConsumer extends ReleasesApiConsumer { protected async makeApiCall(apiPath: string): Promise {