File: GitHubActionsApi.cs
Web Access
Project: src\tools\Aspire.TestTools\Aspire.TestTools.csproj (Aspire.TestTools)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Aspire.TestTools;
 
public sealed record GitHubActionsJob(
    long Id,
    string Name,
    string HtmlUrl,
    string? Conclusion,
    string? Status);
 
public sealed record GitHubActionsArtifact(
    long Id,
    string Name);
 
public static class GitHubActionsApi
{
    public static async Task<List<GitHubActionsJob>> ListJobsAsync(string repository, long runId, int? runAttempt, CancellationToken cancellationToken)
    {
        List<GitHubActionsJob> jobs = [];
 
        for (var page = 1; ; page++)
        {
            var endpoint = runAttempt is int attempt
                ? $"repos/{repository}/actions/runs/{runId}/attempts/{attempt}/jobs?per_page=100&page={page}"
                : $"repos/{repository}/actions/runs/{runId}/jobs?per_page=100&page={page}";
 
            using var jobsDocument = await GitHubCli.GetJsonAsync(endpoint, cancellationToken).ConfigureAwait(false);
            var jobsArray = jobsDocument.RootElement.GetProperty("jobs");
            if (jobsArray.GetArrayLength() == 0)
            {
                break;
            }
 
            foreach (var job in jobsArray.EnumerateArray())
            {
                jobs.Add(new GitHubActionsJob(
                    Id: job.GetProperty("id").GetInt64(),
                    Name: job.GetProperty("name").GetString() ?? $"job-{job.GetProperty("id").GetInt64()}",
                    HtmlUrl: job.TryGetProperty("html_url", out var htmlUrlElement) ? htmlUrlElement.GetString() ?? string.Empty : string.Empty,
                    Conclusion: job.TryGetProperty("conclusion", out var conclusionElement) ? conclusionElement.GetString() : null,
                    Status: job.TryGetProperty("status", out var statusElement) ? statusElement.GetString() : null));
            }
 
            if (jobsArray.GetArrayLength() < 100)
            {
                break;
            }
        }
 
        return jobs;
    }
 
    public static async Task<List<GitHubActionsArtifact>> ListArtifactsAsync(string repository, long runId, CancellationToken cancellationToken)
    {
        List<GitHubActionsArtifact> artifacts = [];
 
        for (var page = 1; ; page++)
        {
            using var artifactsDocument = await GitHubCli.GetJsonAsync(
                $"repos/{repository}/actions/runs/{runId}/artifacts?per_page=100&page={page}",
                cancellationToken).ConfigureAwait(false);
 
            var artifactsArray = artifactsDocument.RootElement.GetProperty("artifacts");
            if (artifactsArray.GetArrayLength() == 0)
            {
                break;
            }
 
            foreach (var artifact in artifactsArray.EnumerateArray())
            {
                artifacts.Add(new GitHubActionsArtifact(
                    Id: artifact.GetProperty("id").GetInt64(),
                    Name: artifact.GetProperty("name").GetString() ?? $"artifact-{artifact.GetProperty("id").GetInt64()}"));
            }
 
            if (artifactsArray.GetArrayLength() < 100)
            {
                break;
            }
        }
 
        return artifacts;
    }
 
    // Job logs for CLI end-to-end tests embed the raw terminal recording, so they routinely contain ANSI
    // control characters. `gh api` refuses to write those to a non-TTY stdout and fails the call with
    // "the response contains terminal escape sequences; pass --allow-escape-sequences to output it anyway",
    // which previously made this tool unable to file an issue for any terminal-driven test.
    public static Task<string> DownloadJobLogAsync(string repository, long jobId, CancellationToken cancellationToken)
        => GitHubCli.GetStringAsync($"repos/{repository}/actions/jobs/{jobId}/logs", allowEscapeSequences: true, cancellationToken);
 
    public static Task DownloadArtifactZipAsync(string repository, long artifactId, string outputPath, CancellationToken cancellationToken)
        => GitHubCli.DownloadFileAsync($"repos/{repository}/actions/artifacts/{artifactId}/zip", outputPath, cancellationToken);
}