diff --git a/src/CodeQLToolkit.Features/CodeQL/Lifecycle/CodeQLLifecycleFeature.cs b/src/CodeQLToolkit.Features/CodeQL/Lifecycle/CodeQLLifecycleFeature.cs index 73e388f..fef374f 100644 --- a/src/CodeQLToolkit.Features/CodeQL/Lifecycle/CodeQLLifecycleFeature.cs +++ b/src/CodeQLToolkit.Features/CodeQL/Lifecycle/CodeQLLifecycleFeature.cs @@ -1,4 +1,5 @@ using CodeQLToolkit.Features.CodeQL.Lifecycle.Targets; +using CodeQLToolkit.Shared.CodeQL; using System.CommandLine; namespace CodeQLToolkit.Features.CodeQL.Lifecycle @@ -19,9 +20,9 @@ public void Register(Command parentCommand) var setVersionCommand = new Command("version", "Sets the version of CodeQL used."); - var cliVersionOption = new Option("--cli-version", () => "2.11.6", "The version of the cli to use. Example: `2.11.6`.") { IsRequired = true }; - var standardLibraryVersionOption = new Option("--standard-library-version", () => "codeql-cli/v2.11.6", "The version of the standard library to use. Example: `codeql-cli/v2.11.6`.") { IsRequired = true }; - var bundleVersionOption = new Option("--bundle-version", () => "codeql-bundle-20221211", "The bundle version to use. Example: `codeql-bundle-20221211`.") { IsRequired = true }; + var cliVersionOption = new Option("--cli-version", GitHubReleaseResolver.GetLatestCLIVersion, "The version of the cli to use. Example: `2.25.1`.") { IsRequired = true }; + var standardLibraryVersionOption = new Option("--standard-library-version", GitHubReleaseResolver.GetLatestStandardLibraryVersion, "The version of the standard library to use. Example: `codeql-cli/v2.25.1`.") { IsRequired = true }; + var bundleVersionOption = new Option("--bundle-version", GitHubReleaseResolver.GetLatestBundleVersion, "The bundle version to use. Example: `codeql-bundle-v2.25.1`.") { IsRequired = true }; setVersionCommand.Add(cliVersionOption); setVersionCommand.Add(standardLibraryVersionOption); diff --git a/src/CodeQLToolkit.Shared/CodeQL/GitHubReleaseResolver.cs b/src/CodeQLToolkit.Shared/CodeQL/GitHubReleaseResolver.cs new file mode 100644 index 0000000..dbfdf42 --- /dev/null +++ b/src/CodeQLToolkit.Shared/CodeQL/GitHubReleaseResolver.cs @@ -0,0 +1,96 @@ +using Newtonsoft.Json.Linq; +using System.Net.Http.Headers; + +namespace CodeQLToolkit.Shared.CodeQL +{ + /// + /// Resolves the latest release versions for CodeQL CLI, standard library, and bundle + /// from GitHub's public API. Falls back to hardcoded values if the request fails. + /// + public static class GitHubReleaseResolver + { + public const string FallbackCLIVersion = "2.25.1"; + public const string FallbackStandardLibraryVersion = "codeql-cli/v2.25.1"; + public const string FallbackBundleVersion = "codeql-bundle-v2.25.1"; + + private static readonly HttpClient _client = CreateClient(null); + + private static HttpClient CreateClient(HttpMessageHandler? handler) + { + var client = handler != null ? new HttpClient(handler) : new HttpClient(); + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("qlt", "1.0")); + client.Timeout = TimeSpan.FromSeconds(5); + return client; + } + + /// + /// Fetches the latest CodeQL CLI version string (e.g. "2.25.1"). + /// Falls back to on any error. + /// + public static string GetLatestCLIVersion() => GetLatestCLIVersion(_client); + + /// + /// Fetches the latest standard library version string (e.g. "codeql-cli/v2.25.1"). + /// Falls back to on any error. + /// + public static string GetLatestStandardLibraryVersion() => GetLatestStandardLibraryVersion(_client); + + /// + /// Fetches the latest bundle version string (e.g. "codeql-bundle-v2.25.1"). + /// Falls back to on any error. + /// + public static string GetLatestBundleVersion() => GetLatestBundleVersion(_client); + + // Internal overloads for testing — accept a custom HttpClient. + + internal static string GetLatestCLIVersion(HttpClient client) + { + try + { + var tag = GetLatestTagName(client, "https://api.github.com/repos/github/codeql-cli-binaries/releases/latest"); + return tag.TrimStart('v'); + } + catch + { + return FallbackCLIVersion; + } + } + + internal static string GetLatestStandardLibraryVersion(HttpClient client) + { + try + { + var tag = GetLatestTagName(client, "https://api.github.com/repos/github/codeql-cli-binaries/releases/latest"); + return $"codeql-cli/v{tag.TrimStart('v')}"; + } + catch + { + return FallbackStandardLibraryVersion; + } + } + + internal static string GetLatestBundleVersion(HttpClient client) + { + try + { + // codeql-action release tags are already in the form "codeql-bundle-vX.Y.Z" + return GetLatestTagName(client, "https://api.github.com/repos/github/codeql-action/releases/latest"); + } + catch + { + return FallbackBundleVersion; + } + } + + internal static HttpClient CreateTestClient(HttpMessageHandler handler) => CreateClient(handler); + + private static string GetLatestTagName(HttpClient client, string url) + { + var response = client.GetAsync(url).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + var json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + var obj = JObject.Parse(json); + return obj["tag_name"]!.Value()!; + } + } +} diff --git a/src/CodeQLToolkit.Shared/CodeQLToolkit.Shared.csproj b/src/CodeQLToolkit.Shared/CodeQLToolkit.Shared.csproj index d75dfc5..38a00c9 100644 --- a/src/CodeQLToolkit.Shared/CodeQLToolkit.Shared.csproj +++ b/src/CodeQLToolkit.Shared/CodeQLToolkit.Shared.csproj @@ -6,12 +6,18 @@ enable + + + <_Parameter1>CodeQLToolkit.Shared.Tests + + + - + diff --git a/test/CodeQLToolkit.Shared.Tests/CodeQL/GitHubReleaseResolverTests.cs b/test/CodeQLToolkit.Shared.Tests/CodeQL/GitHubReleaseResolverTests.cs new file mode 100644 index 0000000..c2de8d8 --- /dev/null +++ b/test/CodeQLToolkit.Shared.Tests/CodeQL/GitHubReleaseResolverTests.cs @@ -0,0 +1,155 @@ +using CodeQLToolkit.Shared.CodeQL; +using System.Net; +using System.Text; + +namespace CodeQLToolkit.Shared.Tests.CodeQL +{ + public class GitHubReleaseResolverTests + { + // Fake HttpMessageHandler that returns a fixed response. + private class StubHandler : HttpMessageHandler + { + private readonly HttpStatusCode _statusCode; + private readonly string _body; + + public StubHandler(HttpStatusCode statusCode, string body) + { + _statusCode = statusCode; + _body = body; + } + + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = new HttpResponseMessage(_statusCode) + { + Content = new StringContent(_body, Encoding.UTF8, "application/json") + }; + return Task.FromResult(response); + } + } + + private static HttpClient MakeClient(HttpStatusCode status, string body) + => GitHubReleaseResolver.CreateTestClient(new StubHandler(status, body)); + + private const string CliResponse = "{\"tag_name\":\"v2.99.0\",\"name\":\"v2.99.0\"}"; + private const string BundleResponse = "{\"tag_name\":\"codeql-bundle-v3.28.5\",\"name\":\"codeql-bundle-v3.28.5\"}"; + + // ── GetLatestCLIVersion ────────────────────────────────────────────── + + [Test] + public void GetLatestCLIVersion_ParsesTagAndStripsV() + { + var client = MakeClient(HttpStatusCode.OK, CliResponse); + var result = GitHubReleaseResolver.GetLatestCLIVersion(client); + Assert.That(result, Is.EqualTo("2.99.0")); + } + + [Test] + public void GetLatestCLIVersion_ReturnsFallback_OnNetworkError() + { + var client = MakeClient(HttpStatusCode.ServiceUnavailable, ""); + var result = GitHubReleaseResolver.GetLatestCLIVersion(client); + Assert.That(result, Is.EqualTo(GitHubReleaseResolver.FallbackCLIVersion)); + } + + [Test] + public void GetLatestCLIVersion_ReturnsFallback_OnMalformedJson() + { + var client = MakeClient(HttpStatusCode.OK, "not-json"); + var result = GitHubReleaseResolver.GetLatestCLIVersion(client); + Assert.That(result, Is.EqualTo(GitHubReleaseResolver.FallbackCLIVersion)); + } + + [Test] + public void GetLatestCLIVersion_ReturnsFallback_OnMissingTagName() + { + var client = MakeClient(HttpStatusCode.OK, "{\"name\":\"v2.99.0\"}"); + var result = GitHubReleaseResolver.GetLatestCLIVersion(client); + Assert.That(result, Is.EqualTo(GitHubReleaseResolver.FallbackCLIVersion)); + } + + // ── GetLatestStandardLibraryVersion ───────────────────────────────── + + [Test] + public void GetLatestStandardLibraryVersion_FormatsCorrectly() + { + var client = MakeClient(HttpStatusCode.OK, CliResponse); + var result = GitHubReleaseResolver.GetLatestStandardLibraryVersion(client); + Assert.That(result, Is.EqualTo("codeql-cli/v2.99.0")); + } + + [Test] + public void GetLatestStandardLibraryVersion_ReturnsFallback_OnNetworkError() + { + var client = MakeClient(HttpStatusCode.InternalServerError, ""); + var result = GitHubReleaseResolver.GetLatestStandardLibraryVersion(client); + Assert.That(result, Is.EqualTo(GitHubReleaseResolver.FallbackStandardLibraryVersion)); + } + + [Test] + public void GetLatestStandardLibraryVersion_ReturnsFallback_OnMalformedJson() + { + var client = MakeClient(HttpStatusCode.OK, "not-json"); + var result = GitHubReleaseResolver.GetLatestStandardLibraryVersion(client); + Assert.That(result, Is.EqualTo(GitHubReleaseResolver.FallbackStandardLibraryVersion)); + } + + // ── GetLatestBundleVersion ─────────────────────────────────────────── + + [Test] + public void GetLatestBundleVersion_FormatsCorrectly() + { + var client = MakeClient(HttpStatusCode.OK, BundleResponse); + var result = GitHubReleaseResolver.GetLatestBundleVersion(client); + Assert.That(result, Is.EqualTo("codeql-bundle-v3.28.5")); + } + + [Test] + public void GetLatestBundleVersion_ReturnsFallback_OnNetworkError() + { + var client = MakeClient(HttpStatusCode.ServiceUnavailable, ""); + var result = GitHubReleaseResolver.GetLatestBundleVersion(client); + Assert.That(result, Is.EqualTo(GitHubReleaseResolver.FallbackBundleVersion)); + } + + [Test] + public void GetLatestBundleVersion_ReturnsFallback_OnMalformedJson() + { + var client = MakeClient(HttpStatusCode.OK, "not-json"); + var result = GitHubReleaseResolver.GetLatestBundleVersion(client); + Assert.That(result, Is.EqualTo(GitHubReleaseResolver.FallbackBundleVersion)); + } + + [Test] + public void GetLatestBundleVersion_ReturnsFallback_OnMissingTagName() + { + var client = MakeClient(HttpStatusCode.OK, "{\"name\":\"codeql-bundle-v3.28.5\"}"); + var result = GitHubReleaseResolver.GetLatestBundleVersion(client); + Assert.That(result, Is.EqualTo(GitHubReleaseResolver.FallbackBundleVersion)); + } + + // ── Fallback value format sanity checks ────────────────────────────── + + [Test] + public void FallbackCLIVersion_HasCorrectFormat() + { + // Should be X.Y.Z with no leading 'v' + var parts = GitHubReleaseResolver.FallbackCLIVersion.Split('.'); + Assert.That(parts.Length, Is.EqualTo(3)); + Assert.That(GitHubReleaseResolver.FallbackCLIVersion, Does.Not.StartWith("v")); + } + + [Test] + public void FallbackStandardLibraryVersion_HasCorrectFormat() + { + Assert.That(GitHubReleaseResolver.FallbackStandardLibraryVersion, Does.StartWith("codeql-cli/v")); + } + + [Test] + public void FallbackBundleVersion_HasCorrectFormat() + { + Assert.That(GitHubReleaseResolver.FallbackBundleVersion, Does.StartWith("codeql-bundle-v")); + } + } +}