diff --git a/extensions/ql-vscode/src/codeql-cli/distribution.ts b/extensions/ql-vscode/src/codeql-cli/distribution.ts index 3edc5f982..804528331 100644 --- a/extensions/ql-vscode/src/codeql-cli/distribution.ts +++ b/extensions/ql-vscode/src/codeql-cli/distribution.ts @@ -51,6 +51,16 @@ const DEFAULT_DISTRIBUTION_OWNER_NAME = "github"; */ const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries"; +/** + * Owner name of the nightly version of the extension-managed distribution on GitHub. + */ +const NIGHTLY_DISTRIBUTION_OWNER_NAME = "dsp-testing"; + +/** + * Repository name of the nightly version of the extension-managed distribution on GitHub. + */ +const NIGHTLY_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-nightlies"; + /** * Range of versions of the CLI that are compatible with the extension. * @@ -453,9 +463,18 @@ class ExtensionSpecificDistributionManager { void extLogger.log( `Searching for latest release including ${requiredAssetName}.`, ); + + const versionRange = this.usingNightlyReleases + ? undefined + : this.versionRange; + const orderBySemver = !this.usingNightlyReleases; + const includePrerelease = + this.usingNightlyReleases || this.config.includePrerelease; + return this.createReleasesApiConsumer().getLatestRelease( - this.versionRange, - this.config.includePrerelease, + versionRange, + orderBySemver, + includePrerelease, (release) => { // v2.12.3 was released with a bug that causes the extension to fail // so we force the extension to ignore it. @@ -485,19 +504,40 @@ class ExtensionSpecificDistributionManager { } private createReleasesApiConsumer(): ReleasesApiConsumer { - const ownerName = this.config.ownerName - ? this.config.ownerName - : DEFAULT_DISTRIBUTION_OWNER_NAME; - const repositoryName = this.config.repositoryName - ? this.config.repositoryName - : DEFAULT_DISTRIBUTION_REPOSITORY_NAME; return new ReleasesApiConsumer( - ownerName, - repositoryName, + this.distributionOwnerName, + this.distributionRepositoryName, this.config.personalAccessToken, ); } + private get distributionOwnerName(): string { + if (this.config.ownerName) { + return this.config.ownerName; + } else if (this.config.channel === "nightly") { + return NIGHTLY_DISTRIBUTION_OWNER_NAME; + } else { + return DEFAULT_DISTRIBUTION_OWNER_NAME; + } + } + + private get distributionRepositoryName(): string { + if (this.config.repositoryName) { + return this.config.repositoryName; + } else if (this.config.channel === "nightly") { + return NIGHTLY_DISTRIBUTION_REPOSITORY_NAME; + } else { + return DEFAULT_DISTRIBUTION_REPOSITORY_NAME; + } + } + + private get usingNightlyReleases(): boolean { + return ( + this.distributionOwnerName === NIGHTLY_DISTRIBUTION_OWNER_NAME && + this.distributionRepositoryName === NIGHTLY_DISTRIBUTION_REPOSITORY_NAME + ); + } + private async bumpDistributionFolderIndex(): Promise { const index = this.extensionContext.globalState.get( ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, @@ -570,7 +610,8 @@ export class ReleasesApiConsumer { } public async getLatestRelease( - versionRange: semver.Range, + versionRange: semver.Range | undefined, + orderBySemver = true, includePrerelease = false, additionalCompatibilityCheck?: (release: GithubRelease) => boolean, ): Promise { @@ -583,12 +624,14 @@ export class ReleasesApiConsumer { return false; } - const version = semver.parse(release.tag_name); - if ( - version === null || - !semver.satisfies(version, versionRange, { includePrerelease }) - ) { - return false; + if (versionRange !== undefined) { + const version = semver.parse(release.tag_name); + if ( + version === null || + !semver.satisfies(version, versionRange, { includePrerelease }) + ) { + return false; + } } return ( @@ -597,10 +640,9 @@ export class ReleasesApiConsumer { }); // Tag names must all be parsable to semvers due to the previous filtering step. const latestRelease = compatibleReleases.sort((a, b) => { - const versionComparison = semver.compare( - semver.parse(b.tag_name)!, - semver.parse(a.tag_name)!, - ); + const versionComparison = orderBySemver + ? semver.compare(semver.parse(b.tag_name)!, semver.parse(a.tag_name)!) + : b.id - a.id; if (versionComparison !== 0) { return versionComparison; } diff --git a/extensions/ql-vscode/src/config.ts b/extensions/ql-vscode/src/config.ts index 5f206572d..cf018a186 100644 --- a/extensions/ql-vscode/src/config.ts +++ b/extensions/ql-vscode/src/config.ts @@ -95,6 +95,7 @@ const PERSONAL_ACCESS_TOKEN_SETTING = new Setting( "personalAccessToken", DISTRIBUTION_SETTING, ); +const CLI_CHANNEL_SETTING = new Setting("channel", DISTRIBUTION_SETTING); // Query History configuration const QUERY_HISTORY_SETTING = new Setting("queryHistory", ROOT_SETTING); @@ -111,6 +112,8 @@ const DISTRIBUTION_CHANGE_SETTINGS = [ PERSONAL_ACCESS_TOKEN_SETTING, ]; +export type CLIChannel = "stable" | "nightly"; + export interface DistributionConfig { readonly customCodeQlPath?: string; updateCustomCodeQlPath: (newPath: string | undefined) => Promise; @@ -118,6 +121,7 @@ export interface DistributionConfig { personalAccessToken?: string; ownerName?: string; repositoryName?: string; + channel: CLIChannel; onDidChangeConfiguration?: Event; } @@ -278,6 +282,10 @@ export class DistributionConfigListener ); } + public get channel(): CLIChannel { + return CLI_CHANNEL_SETTING.getValue() === "nightly" ? "nightly" : "stable"; + } + protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void { this.handleDidChangeConfigurationForRelevantSettings( DISTRIBUTION_CHANGE_SETTINGS, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/codeql-cli/distribution.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/codeql-cli/distribution.test.ts index 6c577aec9..e6ddd8aec 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/codeql-cli/distribution.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/codeql-cli/distribution.test.ts @@ -85,6 +85,15 @@ describe("Releases API consumer", () => { prerelease: true, tag_name: "v3.1.2-pre-2.0", }, + // Has a tag_name that is not valid semver + { + assets: [], + created_at: "2019-08-010T00:00:00Z", + id: 6, + name: "", + prerelease: true, + tag_name: "codeql-bundle-20231220", + }, ]; class MockReleasesApiConsumer extends ReleasesApiConsumer { @@ -98,15 +107,26 @@ describe("Releases API consumer", () => { } } - it("picked release has version with the highest precedence", async () => { + it("picked release is non-prerelease with the highest semver", async () => { const consumer = new MockReleasesApiConsumer(owner, repo); const latestRelease = await consumer.getLatestRelease( unconstrainedVersionRange, + true, ); expect(latestRelease.id).toBe(2); }); + it("picked release is non-prerelease with highest id", async () => { + const consumer = new MockReleasesApiConsumer(owner, repo); + + const latestRelease = await consumer.getLatestRelease( + unconstrainedVersionRange, + false, + ); + expect(latestRelease.id).toBe(3); + }); + it("version of picked release is within the version range", async () => { const consumer = new MockReleasesApiConsumer(owner, repo); @@ -128,6 +148,7 @@ describe("Releases API consumer", () => { const latestRelease = await consumer.getLatestRelease( new Range("2.*.*"), true, + true, (release) => release.assets.some((asset) => asset.name === "exampleAsset.txt"), ); @@ -138,7 +159,7 @@ describe("Releases API consumer", () => { const consumer = new MockReleasesApiConsumer(owner, repo); await expect( - consumer.getLatestRelease(new Range("2.*.*"), true, (release) => + consumer.getLatestRelease(new Range("2.*.*"), true, true, (release) => release.assets.some( (asset) => asset.name === "otherExampleAsset.txt", ), @@ -152,9 +173,21 @@ describe("Releases API consumer", () => { const latestRelease = await consumer.getLatestRelease( unconstrainedVersionRange, true, + true, ); expect(latestRelease.id).toBe(5); }); + + it("ignores invalid semver and picks (pre-)release with highest id", async () => { + const consumer = new MockReleasesApiConsumer(owner, repo); + + const latestRelease = await consumer.getLatestRelease( + undefined, + false, + true, + ); + expect(latestRelease.id).toBe(6); + }); }); it("gets correct assets for a release", async () => {