Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CodeQLToolkit.Features.CodeQL.Lifecycle.Targets;
using CodeQLToolkit.Shared.CodeQL;
using System.CommandLine;

namespace CodeQLToolkit.Features.CodeQL.Lifecycle
Expand All @@ -19,9 +20,9 @@ public void Register(Command parentCommand)

var setVersionCommand = new Command("version", "Sets the version of CodeQL used.");

var cliVersionOption = new Option<string>("--cli-version", () => "2.11.6", "The version of the cli to use. Example: `2.11.6`.") { IsRequired = true };
var standardLibraryVersionOption = new Option<string>("--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<string>("--bundle-version", () => "codeql-bundle-20221211", "The bundle version to use. Example: `codeql-bundle-20221211`.") { IsRequired = true };
var cliVersionOption = new Option<string>("--cli-version", GitHubReleaseResolver.GetLatestCLIVersion, "The version of the cli to use. Example: `2.25.1`.") { IsRequired = true };
var standardLibraryVersionOption = new Option<string>("--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<string>("--bundle-version", GitHubReleaseResolver.GetLatestBundleVersion, "The bundle version to use. Example: `codeql-bundle-v2.25.1`.") { IsRequired = true };

setVersionCommand.Add(cliVersionOption);
setVersionCommand.Add(standardLibraryVersionOption);
Expand Down
96 changes: 96 additions & 0 deletions src/CodeQLToolkit.Shared/CodeQL/GitHubReleaseResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Newtonsoft.Json.Linq;
using System.Net.Http.Headers;

namespace CodeQLToolkit.Shared.CodeQL
{
/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>
/// Fetches the latest CodeQL CLI version string (e.g. "2.25.1").
/// Falls back to <see cref="FallbackCLIVersion"/> on any error.
/// </summary>
public static string GetLatestCLIVersion() => GetLatestCLIVersion(_client);

/// <summary>
/// Fetches the latest standard library version string (e.g. "codeql-cli/v2.25.1").
/// Falls back to <see cref="FallbackStandardLibraryVersion"/> on any error.
/// </summary>
public static string GetLatestStandardLibraryVersion() => GetLatestStandardLibraryVersion(_client);

/// <summary>
/// Fetches the latest bundle version string (e.g. "codeql-bundle-v2.25.1").
/// Falls back to <see cref="FallbackBundleVersion"/> on any error.
/// </summary>
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<string>()!;
}
}
}
8 changes: 7 additions & 1 deletion src/CodeQLToolkit.Shared/CodeQLToolkit.Shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>CodeQLToolkit.Shared.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<PackageReference Include="LibGit2Sharp" Version="0.29.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.2" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.2" />
<PackageReference Include="Scriban" Version="5.7.0" />
<PackageReference Include="Scriban" Version="6.6.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="YamlDotNet" Version="15.1.2" />
</ItemGroup>
Expand Down
155 changes: 155 additions & 0 deletions test/CodeQLToolkit.Shared.Tests/CodeQL/GitHubReleaseResolverTests.cs
Original file line number Diff line number Diff line change
@@ -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<HttpResponseMessage> 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"));
}
}
}