Files
sbox-public/engine/Tools/SboxBuild/GitHub.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

381 lines
10 KiB
C#

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace Facepunch;
internal static class GitHub
{
private static readonly HttpClient httpClient = new();
private static readonly Dictionary<string, long> checkRunIds = [];
public record struct CheckRunParams(
string Name,
string Status,
string Conclusion = null,
string Title = null,
string Summary = null,
string Details = null
);
/// <summary>
/// Post a check run status to GitHub
/// </summary>
public static bool PostCheckRun( CheckRunParams parameters )
{
if ( !Utility.IsCi() )
return true;
try
{
return PostCheckRunAsync( parameters ).GetAwaiter().GetResult();
}
catch ( Exception ex )
{
Console.WriteLine( $"Error posting check run: {ex.Message}" );
return false;
}
}
/// <summary>
/// Gets the check run ID for a step name if available
/// </summary>
public static bool TryGetCheckRunId( string stepName, out long checkRunId )
{
return checkRunIds.TryGetValue( stepName, out checkRunId );
}
private static async Task<bool> PostCheckRunAsync( CheckRunParams parameters )
{
// Validate required environment variables
if ( !TryGetTokenAndRepositoryFromEnv( out var token, out var repository ) )
return false;
// Get the SHA
string sha = GetCommitSha();
if ( string.IsNullOrEmpty( sha ) )
return false;
// Get timestamps in ISO 8601 format
string currentTime = DateTime.UtcNow.ToString( "yyyy-MM-ddTHH:mm:ssZ" );
// Set headers for all requests
SetupHttpClient( token );
// If we have an ID for this check run, update it instead of creating a new one
if ( checkRunIds.TryGetValue( parameters.Name, out long checkRunId ) )
{
if ( await UpdateCheckRun( repository, checkRunId, parameters, currentTime ) )
{
return true;
}
// If update failed, we'll fall through to create a new check run
}
// Create a new check run
return await CreateCheckRun( repository, sha, parameters, currentTime );
}
private static async Task<bool> UpdateCheckRun(
string repository, long checkRunId, CheckRunParams parameters, string currentTime )
{
// Build update payload - slightly different from create payload (no head_sha)
object updatePayload;
var outputObject = new
{
title = parameters.Title ?? parameters.Name,
summary = parameters.Summary ?? string.Empty,
text = parameters.Details ?? string.Empty
};
if ( parameters.Status == "completed" )
{
updatePayload = new
{
status = parameters.Status,
conclusion = parameters.Conclusion ?? "neutral",
completed_at = currentTime,
output = outputObject
};
}
else
{
updatePayload = new
{
status = parameters.Status,
output = outputObject
};
}
var jsonContent = JsonSerializer.Serialize( updatePayload );
var content = new StringContent( jsonContent, Encoding.UTF8, "application/json" );
var updateUrl = $"https://api.github.com/repos/{repository}/check-runs/{checkRunId}";
// Use PATCH to update the existing check run
var response = await httpClient.PatchAsync( updateUrl, content );
if ( !response.IsSuccessStatusCode )
{
var responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine( $"GitHub API error updating check run: {response.StatusCode} - {responseContent}" );
// If the check run no longer exists (404), remove it from our dictionary
// and create a new one instead
if ( response.StatusCode == System.Net.HttpStatusCode.NotFound )
{
checkRunIds.Remove( parameters.Name );
}
return false;
}
return true;
}
private static async Task<bool> CreateCheckRun(
string repository, string sha, CheckRunParams parameters, string currentTime )
{
object updatePayload;
var outputObject = new
{
title = parameters.Title ?? parameters.Name,
summary = parameters.Summary ?? string.Empty,
text = parameters.Details ?? string.Empty
};
if ( parameters.Status == "completed" )
{
updatePayload = new
{
name = parameters.Name,
head_sha = sha,
status = parameters.Status,
conclusion = parameters.Conclusion ?? "neutral",
started_at = currentTime,
completed_at = currentTime,
output = outputObject
};
}
else
{
updatePayload = new
{
name = parameters.Name,
head_sha = sha,
status = parameters.Status,
started_at = currentTime,
output = outputObject
};
}
var jsonContent = JsonSerializer.Serialize( updatePayload );
var content = new StringContent( jsonContent, Encoding.UTF8, "application/json" );
var url = $"https://api.github.com/repos/{repository}/check-runs";
// Send request to create a new check run
var response = await httpClient.PostAsync( url, content );
if ( !response.IsSuccessStatusCode )
{
var errorResponseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine( $"GitHub API error creating check run: {response.StatusCode} - {errorResponseContent}" );
return false;
}
var responseContent = await response.Content.ReadAsStringAsync();
// Simple parsing of the response to get the ID
using var jsonDoc = JsonDocument.Parse( responseContent );
if ( jsonDoc.RootElement.TryGetProperty( "id", out var idElement ) )
{
long id = idElement.GetInt64();
checkRunIds[parameters.Name] = id;
}
return true;
}
/// <summary>
/// Gets a URL to the current workflow's logs
/// </summary>
public static string GetWorkflowLogsUrl()
{
return GetWorkflowLogsUrlAsync().GetAwaiter().GetResult();
}
private static async Task<string> GetWorkflowLogsUrlAsync()
{
// Get required environment variables
if ( !TryGetTokenAndRepositoryFromEnv( out var token, out var repository ) )
return string.Empty;
SetupHttpClient( token );
var runId = Environment.GetEnvironmentVariable( "GITHUB_RUN_ID" );
if ( string.IsNullOrEmpty( runId ) )
{
Console.WriteLine( "GITHUB_RUN_ID not set" );
return string.Empty;
}
long jobId = await FindJobId( repository, runId );
// Construct the logs URL
if ( jobId != -1 )
{
return $"https://github.com/{repository}/actions/runs/{runId}/job/{jobId}";
}
return $"https://github.com/{repository}/actions/runs/{runId}";
}
private static async Task<long> FindJobId( string repository, string runId )
{
long jobId = -1;
try
{
// Query the API to get jobs for this run
var jobsUrl = $"https://api.github.com/repos/{repository}/actions/runs/{runId}/jobs";
var response = await httpClient.GetAsync( jobsUrl );
if ( !response.IsSuccessStatusCode )
{
var errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine( $"GitHub API error: {response.StatusCode} - {errorContent}" );
return jobId;
}
var content = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse( content );
// Get the current job name
var jobName = Environment.GetEnvironmentVariable( "GITHUB_JOB" );
// Make sure the jobs property exists
if ( !doc.RootElement.TryGetProperty( "jobs", out var jobs ) )
{
Console.WriteLine( "Jobs property not found in API response" );
return jobId;
}
// First pass: try to find exact job name match
foreach ( var job in jobs.EnumerateArray() )
{
if ( job.TryGetProperty( "name", out var nameElement ) &&
string.Equals( nameElement.GetString(), jobName, StringComparison.Ordinal ) )
{
jobId = job.GetProperty( "id" ).GetInt64();
break;
}
}
// Second pass: try case-insensitive match if exact match failed
if ( jobId == -1 )
{
foreach ( var job in jobs.EnumerateArray() )
{
if ( job.TryGetProperty( "name", out var nameElement ) &&
string.Equals( nameElement.GetString(), jobName, StringComparison.OrdinalIgnoreCase ) )
{
jobId = job.GetProperty( "id" ).GetInt64();
break;
}
}
}
// If we still couldn't find by name, use the first job
if ( jobId == -1 && jobs.GetArrayLength() > 0 )
{
var firstJob = jobs[0];
jobId = firstJob.GetProperty( "id" ).GetInt64();
}
}
catch ( Exception ex )
{
Console.WriteLine( $"Error getting job ID: {ex.Message}" );
}
return jobId;
}
private static string GetCommitSha()
{
string sha;
var eventName = Environment.GetEnvironmentVariable( "GITHUB_EVENT_NAME" );
if ( eventName == "pull_request" )
{
// For pull requests, we need to get the head SHA from the event payload
sha = Environment.GetEnvironmentVariable( "GITHUB_EVENT_PULL_REQUEST_HEAD_SHA" );
if ( string.IsNullOrEmpty( sha ) )
{
// Try to get it from the event file
try
{
var eventPath = Environment.GetEnvironmentVariable( "GITHUB_EVENT_PATH" );
if ( !string.IsNullOrEmpty( eventPath ) )
{
var eventJson = System.IO.File.ReadAllText( eventPath );
using ( var doc = JsonDocument.Parse( eventJson ) )
{
sha = doc.RootElement
.GetProperty( "pull_request" )
.GetProperty( "head" )
.GetProperty( "sha" )
.GetString();
}
}
}
catch
{
// If we can't get the SHA from the event file, fall back to GITHUB_SHA
sha = Environment.GetEnvironmentVariable( "GITHUB_SHA" );
}
}
}
else
{
// For other events, use the commit SHA
sha = Environment.GetEnvironmentVariable( "GITHUB_SHA" );
}
if ( string.IsNullOrEmpty( sha ) )
{
Console.WriteLine( "Commit SHA not available" );
}
return sha ?? "";
}
private static bool TryGetTokenAndRepositoryFromEnv( out string token, out string repository )
{
token = Environment.GetEnvironmentVariable( "GITHUB_TOKEN" ) ?? string.Empty;
if ( string.IsNullOrEmpty( token ) )
{
Console.WriteLine( "GITHUB_TOKEN not set" );
repository = string.Empty;
return false;
}
repository = Environment.GetEnvironmentVariable( "GITHUB_REPOSITORY" ) ?? string.Empty;
if ( string.IsNullOrEmpty( repository ) )
{
Console.WriteLine( "GITHUB_REPOSITORY not set" );
return false;
}
return true;
}
private static void SetupHttpClient( string token )
{
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue( "application/vnd.github+json" ) );
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", token );
httpClient.DefaultRequestHeaders.Add( "User-Agent", "SboxBuild" );
httpClient.DefaultRequestHeaders.Add( "X-GitHub-Api-Version", "2022-11-28" );
}
}