-
Notifications
You must be signed in to change notification settings - Fork 343
Add ToolMetadataExporter #992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
conniey
wants to merge
60
commits into
microsoft:main
Choose a base branch
from
conniey:exporter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
55f284f
Add PackageVersion for Kusto.Data and Kusto.Ingest
conniey d1fc4a3
Add Id to shared McpModels.
conniey 83ab661
Initial commit
conniey 8c32772
Remove AoT
conniey 16c13f7
Add initial mock up
conniey bd98516
Split to use Query and Ingestion endpoint.
conniey ecaeb8b
Add Query to create table.
conniey 972b380
Add LaunchSettings.
conniey dc365e0
Set Default queries folder.
conniey e17d0b7
Adding Appsettings.json. Copy to output
conniey ae49d29
Update Json serialization to output Enum names.
conniey e95572e
Start host
conniey f0861ff
Move JsonOutput parsing. Fix command line arguments to pass to exe
conniey 0d6c505
Pass in command rather than just name in changes.
conniey 0699f0e
Update Logging
conniey d8fbc33
Change interface to use List. Update Ingestion settings for singlejson
conniey 4c6acbf
Fix column mappings
conniey 804d00d
Change to use Direct Ingestion client
conniey e5cc783
Fix formatting issues
conniey badc01f
Fix formatting
conniey 3a335de
Add Friend assembly
conniey 44d3999
Add unit test project
conniey 01b56df
Make method virtual
conniey 2af8847
Fix formatting issue.
conniey 983fde8
Add header
conniey 8d7bc67
Add dev settings
conniey 5bcb034
Add header
conniey ff07ba3
Add ServerName support.
conniey d83833a
Remove test template.
conniey 71d9ba6
Add ServerName
conniey 597db74
Propagate cancellation
conniey fb79c79
Moving classes into src/tests
conniey 8bb3106
Move parsing from command line to use IConfiguration.
conniey b2f78aa
Add ServerName column to Kusto mapping at the end.
conniey 93fa81b
Add documentation to CommandLineOptions
conniey 600b4c6
Add ServerInfoResult for `server info`
conniey 3d1f865
Add IsDryRun, AzMcp to app configuration
conniey fc03c95
Update Program to configure AppConfiguration and CommandLineOptions
conniey 214f08d
Add FileName parameter to utility methods.
conniey c357172
Update ToolAnalyzer to use file name parameter from Utility.
conniey 13e10dc
Create functions for AzMcp program.
conniey 176b01e
Change utility classes into a class
conniey 5bf0592
- Add utility into DI container
conniey 890df96
Use lazy initialization.
conniey 5931cfa
Add tests
conniey 9464f57
Make methods virtual for testability
conniey bac16c8
Add tests for AzaureMcpKustoDatastore
conniey 7216f5f
Add ToolAnalyzerTests
conniey b47db2c
Fix formatting error in AzureMcpKustoDatastore
conniey b2a3274
Fix formatting error in ToolAnalyzerTests
conniey ff9d8e7
Fix whitespace AzmcpProgram
conniey c1af889
Add Ticks to FileName.
conniey e5676b2
Update eng/tools/ToolMetadataExporter/src/Utility.cs
conniey b312304
Remove extra line.
conniey d7280ae
Dispose process.
conniey d9f23fc
Remove unused dependency
conniey 76c869c
Replace with constant.
conniey 4f05d64
Changes to Debug.
conniey 1584cff
Add Tool analysis date.
conniey f60ac82
Add README.
conniey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace ToolMetadataExporter; | ||
|
|
||
| public class AppConfiguration | ||
| { | ||
| public string? IngestionEndpoint { get; set; } | ||
|
|
||
| public string? QueryEndpoint { get; set; } | ||
|
|
||
| public string? DatabaseName { get; set; } | ||
|
|
||
| public string? McpToolEventsTableName { get; set; } | ||
|
|
||
| public string? QueriesFolder { get; set; } = "Resources/queries"; | ||
|
|
||
| public string? WorkDirectory { get; set; } | ||
|
|
||
| public bool IsDryRun { get; set; } | ||
|
|
||
| public string? AzmcpExe { get; set; } | ||
|
|
||
| public bool IsAzmcpExeSpecified { get; set; } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Runtime.CompilerServices; | ||
|
|
||
| [assembly: InternalsVisibleTo("ToolMetadataExporter.UnitTests")] | ||
| [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| global using System; | ||
| global using System.Text.Json; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace ToolMetadataExporter.Models; | ||
|
|
||
| public record AzureMcpTool( | ||
| string ToolId, | ||
| string ToolName, | ||
| string ToolArea); | ||
26 changes: 26 additions & 0 deletions
26
eng/tools/ToolMetadataExporter/src/Models/CommandLineOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace ToolMetadataExporter.Models; | ||
|
|
||
| /// <summary> | ||
| /// Options specified via command line arguments. Supported options are: | ||
| /// <list type="bullet"> | ||
| /// <item><c>--dry-run</c>: If specified, the tool will run in dry-run mode, meaning no changes will be made to the target datastore.</item> | ||
| /// <item><c>--azmcp-exe <path></c>: The path to the azmcp executable to use for interacting with the MCP server.</item> | ||
| /// </list> | ||
| /// </summary> | ||
| internal class CommandLineOptions | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the tool analysis should be performed as a dry run. | ||
| /// </summary> | ||
| /// <remarks>When set to <see langword="true"/>, the operation is performed, output to the console, but not persisted to the datastore. When set to | ||
| /// <see langword="false"/>, the operation is executed normally.</remarks> | ||
| public bool? IsDryRun { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the full path to the AzMcp executable file. | ||
| /// </summary> | ||
| public string? AzmcpExe { get; set; } | ||
| } |
62 changes: 62 additions & 0 deletions
62
eng/tools/ToolMetadataExporter/src/Models/Kusto/McpToolEvent.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Text.Json.Serialization; | ||
| using Kusto.Data.Common; | ||
|
|
||
| namespace ToolMetadataExporter.Models.Kusto; | ||
|
|
||
| public class McpToolEvent | ||
| { | ||
| private const string EventTimeColumn = "EventTime"; | ||
| private const string EventTypeColumn = "EventType"; | ||
| private const string ServerNameColumn = "ServerName"; | ||
| private const string ServerVersionColumn = "ServerVersion"; | ||
| private const string ToolIdColumn = "ToolId"; | ||
| private const string ToolNameColumn = "ToolName"; | ||
| private const string ToolAreaColumn = "ToolArea"; | ||
| private const string ReplacedByToolNameColumn = "ReplacedByToolName"; | ||
| private const string ReplacedByToolAreaColumn = "ReplacedByToolArea"; | ||
|
|
||
| [JsonPropertyName(EventTimeColumn)] | ||
| public DateTimeOffset? EventTime { get; set; } | ||
|
|
||
| [JsonPropertyName(EventTypeColumn)] | ||
| public McpToolEventType? EventType { get; set; } | ||
|
|
||
| [JsonPropertyName(ServerNameColumn)] | ||
| public string? ServerName { get; set; } | ||
|
|
||
| [JsonPropertyName(ServerVersionColumn)] | ||
| public string? ServerVersion { get; set; } | ||
|
|
||
| [JsonPropertyName(ToolIdColumn)] | ||
| public string? ToolId { get; set; } | ||
|
|
||
| [JsonPropertyName(ToolNameColumn)] | ||
| public string? ToolName { get; set; } | ||
|
|
||
| [JsonPropertyName(ToolAreaColumn)] | ||
| public string? ToolArea { get; set; } | ||
|
|
||
| [JsonPropertyName(ReplacedByToolNameColumn)] | ||
| public string? ReplacedByToolName { get; set; } | ||
|
|
||
| [JsonPropertyName(ReplacedByToolAreaColumn)] | ||
| public string? ReplacedByToolArea { get; set; } | ||
|
|
||
| public static ColumnMapping[] GetColumnMappings() | ||
| { | ||
| return [ | ||
| new ColumnMapping { ColumnName = EventTimeColumn, ColumnType = "datetime" }, | ||
| new ColumnMapping { ColumnName = EventTypeColumn, ColumnType = "string"}, | ||
| new ColumnMapping { ColumnName = ReplacedByToolAreaColumn, ColumnType = "string"}, | ||
| new ColumnMapping { ColumnName = ReplacedByToolNameColumn, ColumnType = "string"}, | ||
| new ColumnMapping { ColumnName = ServerVersionColumn, ColumnType = "string" }, | ||
| new ColumnMapping { ColumnName = ToolAreaColumn , ColumnType = "string" }, | ||
| new ColumnMapping { ColumnName = ToolIdColumn, ColumnType = "string"}, | ||
| new ColumnMapping { ColumnName = ToolNameColumn, ColumnType = "string" }, | ||
| new ColumnMapping { ColumnName = ServerNameColumn, ColumnType = "string" }, | ||
| ]; | ||
| } | ||
| } |
16 changes: 16 additions & 0 deletions
16
eng/tools/ToolMetadataExporter/src/Models/Kusto/McpToolEventType.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Text.Json.Serialization; | ||
|
|
||
| namespace ToolMetadataExporter.Models.Kusto; | ||
|
|
||
| public enum McpToolEventType | ||
| { | ||
| [JsonStringEnumMemberName("Created")] | ||
| Created, | ||
| [JsonStringEnumMemberName("Updated")] | ||
| Updated, | ||
| [JsonStringEnumMemberName("Deleted")] | ||
| Deleted | ||
| } |
17 changes: 17 additions & 0 deletions
17
eng/tools/ToolMetadataExporter/src/Models/ModelsSerializationContext.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Text.Json.Serialization; | ||
| using ToolMetadataExporter.Models.Kusto; | ||
|
|
||
| namespace ToolMetadataExporter.Models; | ||
|
|
||
| [JsonSerializable(typeof(ServerInfo))] | ||
| [JsonSerializable(typeof(ServerInfoResult))] | ||
| [JsonSerializable(typeof(McpToolEvent))] | ||
| [JsonSerializable(typeof(McpToolEventType))] | ||
| [JsonSerializable(typeof(List<McpToolEvent>))] | ||
| [JsonSourceGenerationOptions(Converters = [typeof(JsonStringEnumConverter<McpToolEventType>)])] | ||
| public partial class ModelsSerializationContext : JsonSerializerContext | ||
| { | ||
| } |
30 changes: 30 additions & 0 deletions
30
eng/tools/ToolMetadataExporter/src/Models/ServerInfoResult.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Text.Json.Serialization; | ||
|
|
||
| namespace ToolMetadataExporter.Models; | ||
|
|
||
| /// <summary> | ||
| /// The result of a `server info` request from the MCP server. | ||
| /// </summary> | ||
| public class ServerInfoResult | ||
| { | ||
| [JsonPropertyName("status")] | ||
| public int Status { get; set; } | ||
|
|
||
| [JsonPropertyName("message")] | ||
| public string? Message { get; set; } | ||
|
|
||
| [JsonPropertyName("results")] | ||
| public ServerInfo? Results { get; set; } | ||
| } | ||
|
|
||
| public class ServerInfo | ||
| { | ||
| [JsonPropertyName("name")] | ||
| public string Name { get; set; } = string.Empty; | ||
|
|
||
| [JsonPropertyName("version")] | ||
| public string Version { get; set; } = string.Empty; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Azure.Core; | ||
| using Azure.Identity; | ||
| using Kusto.Data; | ||
| using Kusto.Data.Common; | ||
| using Kusto.Data.Net.Client; | ||
| using Kusto.Ingest; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Hosting; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Options; | ||
| using ToolMetadataExporter.Models; | ||
| using ToolMetadataExporter.Services; | ||
|
|
||
| namespace ToolMetadataExporter; | ||
|
|
||
| public class Program | ||
| { | ||
| public static async Task Main(string[] args) | ||
| { | ||
| var builder = Host.CreateApplicationBuilder(args); | ||
|
|
||
| ConfigureServices(builder.Services, builder.Configuration); | ||
| ConfigureAzureServices(builder.Services); | ||
|
|
||
| var host = builder.Build(); | ||
| var analyzer = host.Services.GetRequiredService<ToolAnalyzer>(); | ||
|
|
||
| await analyzer.RunAsync(DateTimeOffset.UtcNow); | ||
| } | ||
|
|
||
| private static void ConfigureServices(IServiceCollection services, IConfiguration configuration) | ||
| { | ||
| services.AddLogging(builder => | ||
| { | ||
| builder.AddConsole(); | ||
| }); | ||
|
|
||
| services.AddSingleton<IAzureMcpDatastore, AzureMcpKustoDatastore>() | ||
| .AddSingleton<Utility>() | ||
| .AddSingleton<AzmcpProgram>() | ||
| .AddSingleton<ToolAnalyzer>(); | ||
|
|
||
| services.AddOptions<CommandLineOptions>() | ||
| .Bind(configuration); | ||
|
|
||
| services.AddOptions<AppConfiguration>() | ||
| .Bind<AppConfiguration>(configuration.GetSection("AppConfig")) | ||
| .Configure<IOptions<CommandLineOptions>>((existing, commandLineOptions) => | ||
| { | ||
| // Command-line IsDryRun overrides appsettings.json file value. | ||
| if (commandLineOptions.Value.IsDryRun.HasValue) | ||
| { | ||
| existing.IsDryRun = commandLineOptions.Value.IsDryRun.Value; | ||
| } | ||
|
|
||
| var exeDir = AppContext.BaseDirectory; | ||
|
|
||
| // If a path to azmcp.exe is not provided. Assume that this is running within the context of | ||
| // the repository and try to find it. | ||
| existing.IsAzmcpExeSpecified = !string.IsNullOrEmpty(commandLineOptions.Value.AzmcpExe); | ||
| if (existing.IsAzmcpExeSpecified) | ||
| { | ||
| existing.AzmcpExe = commandLineOptions.Value.AzmcpExe!; | ||
|
|
||
| if (existing.WorkDirectory == null) | ||
| { | ||
| existing.WorkDirectory = exeDir; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| var repoRoot = Utility.FindRepoRoot(exeDir); | ||
| if (existing.WorkDirectory == null) | ||
| { | ||
| existing.WorkDirectory = Path.Combine(repoRoot, ".work"); | ||
| } | ||
|
|
||
| existing.AzmcpExe = Path.Combine(repoRoot, "eng", "tools", "Azmcp", "azmcp.exe"); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| private static void ConfigureAzureServices(IServiceCollection services) | ||
| { | ||
| services.AddScoped<TokenCredential>(sp => | ||
| { | ||
| var credential = new ChainedTokenCredential( | ||
| new ManagedIdentityCredential(), | ||
| new DefaultAzureCredential() | ||
| ); | ||
|
|
||
| return credential; | ||
| }); | ||
| services.AddSingleton<ICslQueryProvider>(sp => | ||
| { | ||
| var config = sp.GetRequiredService<IOptions<AppConfiguration>>(); | ||
|
|
||
| var connectionStringBuilder = new KustoConnectionStringBuilder(config.Value.QueryEndpoint) | ||
| .WithAadUserPromptAuthentication() | ||
| .WithAadAzCliAuthentication(interactive: true); | ||
|
|
||
| return KustoClientFactory.CreateCslQueryProvider(connectionStringBuilder); | ||
| }); | ||
| services.AddSingleton<IKustoIngestClient>(sp => | ||
| { | ||
| var config = sp.GetRequiredService<IOptions<AppConfiguration>>(); | ||
|
|
||
| var connectionStringBuilder = new KustoConnectionStringBuilder(config.Value.IngestionEndpoint) | ||
| .WithAadUserPromptAuthentication() | ||
| .WithAadAzCliAuthentication(interactive: true); | ||
|
|
||
| return KustoIngestFactory.CreateDirectIngestClient(connectionStringBuilder); | ||
| }); | ||
| } | ||
| } |
10 changes: 10 additions & 0 deletions
10
eng/tools/ToolMetadataExporter/src/Properties/launchSettings.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "profiles": { | ||
| "ToolMetadataExporter": { | ||
| "commandName": "Project", | ||
| "environmentVariables": { | ||
| "DOTNET_ENVIRONMENT": "Development" | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Tool Metadata Exporter | ||
|
|
||
|
|
||
| ## Development Setup | ||
|
|
||
| ### 1. Create Datastore | ||
|
|
||
| 1. Create an Azure Data Explorer Cluster (e.g. "mcp-test-instance") | ||
| 1. Create a database (e.g. "McpToolMetadata") within the cluster | ||
| 1. In https://dataexplorer.azure.com/, connect to cluster | ||
| 1. Click on "Query" tab | ||
| 1. Execute [`CreateTable.kql`](./Resources/queries/CreateTable.kql) against the cluster and database | ||
|
|
||
| ### 2. Configure Application | ||
|
|
||
| 1. Open [`appsettings.Development.json`](./appsettings.Development.json) | ||
| 1. Update "IngestionEndpoint", "QueryEndpoint", "DatabaseName" with the appropriate cluster and database names. Using the example from previous step, it would look like this: | ||
| ```json | ||
| "AppConfig": { | ||
| "IngestionEndpoint": "https://ingest-mcp-test-instance.westus2.kusto.windows.net", | ||
| "QueryEndpoint": "https://mcp-test-instance.westus2.kusto.windows.net", | ||
| "DatabaseName": "McpToolMetadata" | ||
| } | ||
| ``` | ||
| To find values for "QueryEndpoint" and "IngestionEndpoint", navigate to the Azure Data Explorer Cluster in the Azure portal, and in the "Essentials" panel window, look for. | ||
| 1. "IngestionEndpoint" is "Data Ingestion URI" | ||
| 1. "QueryEndpoint" is "URI" | ||
|
|
||
| ### 3. Run Application | ||
|
|
||
| 1. Open a terminal in the project src directory: [$RepositoryRoot/eng/tools/ToolMetadataExporter/src](./) | ||
| 1. Run the application using the command: | ||
| ```bash | ||
| dotnet run --environment DOTNET_ENVIRONMENT=Development | ||
| ``` | ||
| The environment variable `DOTNET_ENVIRONMENT` is set to `Development` to ensure the application uses [`appsettings.Development.json`](./appsettings.Development.json) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.