File: OverflowIssueRenderer.cs
Web Access
Project: src\tools\AnalyzeCiFailure\AnalyzeCiFailure.csproj (AnalyzeCiFailure)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text;
using System.Text.RegularExpressions;
 
namespace AnalyzeCiFailure;
 
/// <summary>Pages complete aggregate reports and replaces only their explicitly managed content.</summary>
internal static class OverflowIssueRenderer
{
    private const string Start = "<!-- ci-failure-overflow-content:start -->";
    private const string End = "<!-- ci-failure-overflow-content:end -->";
 
    internal static bool HasMarker(string? body, string marker)
        => body is not null && (body.StartsWith(marker + "\n", StringComparison.Ordinal) ||
            body.StartsWith(marker + "\r\n", StringComparison.Ordinal));
 
    internal static IReadOnlyList<string> Pages(IEnumerable<string> rows, string runUrl)
    {
        var pages = new List<string>();
        var header = $"[automated] CI failure report\n\n[Source CI run]({runUrl})\n\n";
        var page = new StringBuilder(header);
        foreach (var row in rows)
        {
            if (Encoding.UTF8.GetByteCount(row) > 40000)
            {
                throw new ValidationException("One aggregate report entry exceeds the publication budget.");
            }
            if (Encoding.UTF8.GetByteCount(page.ToString()) + Encoding.UTF8.GetByteCount(row) > 50000)
            {
                pages.Add(page.ToString());
                page.Clear().Append(header);
            }
            page.Append(row);
        }
        pages.Add(page.ToString());
        return pages;
    }
 
    internal static string Merge(string? existing, string marker, string content)
    {
        var section = Start + "\n" + content + "\n" + End;
        if (existing is null)
        {
            return marker + "\n" + section + "\n";
        }
        // A diagnostic can contain "`<!-- ... -->`" inline. Only complete marker
        // lines delimit publisher-owned sections; inline text remains evidence.
        var starts = Regex.Matches(existing, "^" + Regex.Escape(Start) + @"\r?$", RegexOptions.Multiline | RegexOptions.CultureInvariant);
        var ends = Regex.Matches(existing, "^" + Regex.Escape(End) + @"\r?$", RegexOptions.Multiline | RegexOptions.CultureInvariant);
        if (!HasMarker(existing, marker) || starts.Count != 1 || ends.Count != 1 ||
            ends[0].Index < starts[0].Index)
        {
            throw new ValidationException("Aggregate report has missing or ambiguous managed sections.");
        }
        var result = existing[..starts[0].Index] + section + existing[(ends[0].Index + End.Length)..];
        ValidationRules.RequireCommentBudget(Encoding.UTF8.GetByteCount(result));
        return result;
    }
}