File: tools\GenerateCITimeline\GitHubApi.cs
Web Access
Project: src\tests\Infrastructure.Tests\Infrastructure.Tests.csproj (Infrastructure.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Text.Json;
 
internal static class GitHubApi
{
    public static async Task<JsonElement> CallAsync(string endpoint)
    {
        var psi = new ProcessStartInfo("gh", ["api", endpoint, "--paginate"])
        {
            RedirectStandardOutput = true,
            RedirectStandardError = true,
        };
 
        using var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start gh process.");
        // Read both streams concurrently to avoid deadlock when a pipe buffer fills.
        var stdoutTask = process.StandardOutput.ReadToEndAsync();
        var stderrTask = process.StandardError.ReadToEndAsync();
        await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false);
        var stdout = stdoutTask.Result;
        var stderr = stderrTask.Result;
        await process.WaitForExitAsync().ConfigureAwait(false);
 
        if (process.ExitCode != 0)
        {
            throw new InvalidOperationException($"gh api failed: {stderr.Trim()}");
        }
 
        var text = stdout.Trim();
        if (string.IsNullOrEmpty(text))
        {
            using var emptyDoc = JsonDocument.Parse("{}");
            return emptyDoc.RootElement.Clone();
        }
 
        // gh --paginate can produce multiple JSON objects concatenated together.
        // Parse them all and merge if needed.
        var objects = new List<JsonElement>();
        var reader = new Utf8JsonReader(
            System.Text.Encoding.UTF8.GetBytes(text),
#if NET9_0_OR_GREATER
            new JsonReaderOptions { AllowMultipleValues = true });
#else
            default);
#endif
        while (reader.Read())
        {
            if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
            {
                var element = JsonElement.ParseValue(ref reader);
                objects.Add(element);
            }
        }
 
        if (objects.Count == 1)
        {
            return objects[0];
        }
 
        // Merge arrays or objects with array values
        if (objects[0].ValueKind == JsonValueKind.Array)
        {
            var merged = new List<JsonElement>();
            foreach (var obj in objects)
            {
                if (obj.ValueKind == JsonValueKind.Array)
                {
                    foreach (var item in obj.EnumerateArray())
                    {
                        merged.Add(item);
                    }
                }
                else
                {
                    merged.Add(obj);
                }
            }
 
            using var doc = JsonDocument.Parse($"[{string.Join(",", merged.Select(e => e.GetRawText()))}]");
            return doc.RootElement.Clone();
        }
 
        if (objects[0].ValueKind == JsonValueKind.Object)
        {
            var merged = new Dictionary<string, List<string>>();
            foreach (var obj in objects)
            {
                foreach (var prop in obj.EnumerateObject())
                {
                    if (!merged.TryGetValue(prop.Name, out var list))
                    {
                        list = [];
                        merged[prop.Name] = list;
                    }
 
                    if (prop.Value.ValueKind == JsonValueKind.Array)
                    {
                        foreach (var item in prop.Value.EnumerateArray())
                        {
                            list.Add(item.GetRawText());
                        }
                    }
                    else if (list.Count == 0)
                    {
                        list.Add(prop.Value.GetRawText());
                    }
                    else
                    {
                        // Overwrite scalar value
                        list.Clear();
                        list.Add(prop.Value.GetRawText());
                    }
                }
            }
 
            var parts = merged.Select(kv =>
            {
                var val = kv.Value.Count == 1 ? kv.Value[0] : $"[{string.Join(",", kv.Value)}]";
                return $"\"{kv.Key}\":{val}";
            });
            using var doc = JsonDocument.Parse($"{{{string.Join(",", parts)}}}");
            return doc.RootElement.Clone();
        }
 
        return objects[0];
    }
 
    public static async Task<(JsonElement RunInfo, List<JsonElement> Jobs)> FetchRunDataAsync(string repo, string runId)
    {
        var runInfo = await CallAsync($"/repos/{repo}/actions/runs/{runId}").ConfigureAwait(false);
        var jobsData = await CallAsync($"/repos/{repo}/actions/runs/{runId}/jobs").ConfigureAwait(false);
 
        var jobs = new List<JsonElement>();
        if (jobsData.ValueKind == JsonValueKind.Object && jobsData.TryGetProperty("jobs", out var jobsArray))
        {
            foreach (var j in jobsArray.EnumerateArray())
            {
                jobs.Add(j);
            }
        }
        else if (jobsData.ValueKind == JsonValueKind.Array)
        {
            foreach (var j in jobsData.EnumerateArray())
            {
                jobs.Add(j);
            }
        }
 
        return (runInfo, jobs);
    }
 
    public static (JsonElement RunInfo, List<JsonElement> Jobs) LoadJsonData(string path)
    {
        var text = File.ReadAllText(path);
        using var doc = JsonDocument.Parse(text);
        var root = doc.RootElement;
 
        JsonElement runInfo;
        if (root.TryGetProperty("run_info", out var ri))
        {
            runInfo = ri.Clone();
        }
        else
        {
            using var emptyDoc = JsonDocument.Parse("{}");
            runInfo = emptyDoc.RootElement.Clone();
        }
 
        var jobs = new List<JsonElement>();
        if (root.TryGetProperty("jobs", out var jobsArray))
        {
            foreach (var j in jobsArray.EnumerateArray())
            {
                jobs.Add(j.Clone());
            }
        }
 
        return (runInfo, jobs);
    }
}