| File: CauseHistoryStore.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.RegularExpressions; namespace AnalyzeCiFailure; /// <summary>Owns one cause-history checkout, its layout and ordered persistence receipts.</summary> internal sealed class CauseHistoryStore( AnalyzerCommandExecutor command, string repository, string directory, AnalysisDiagnostics diagnostics) { internal const string BranchName = "memory/ci-failure-analysis"; private async Task<bool> OpenAsync(bool create) { var git = new GitRepository(command); if (Directory.Exists(directory) || File.Exists(directory)) { throw new ValidationException("History checkout destination already exists. Use a new directory to read current history."); } var remote = await git.GetOriginUrlAsync(); var url = remote.ExitCode == 0 ? remote.Output.Trim() : $"https://github.com/{repository}.git"; var reference = await git.FindRemoteBranchAsync(url, BranchName); // ls-remote exit 2 means a successful request with no matching reference. // Authentication, transport, and malformed responses must not erase history. if (!(reference.ExitCode is 0 or 2)) { throw new ValidationException($"Unable to load CI failure history: {CiFailureJson.Display(DiagnosticTextSanitizer.Sanitize(reference.Error, limit: 4096))}"); } if (reference.ExitCode == 0) { if (!reference.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries) .Any(line => Regex.IsMatch(line, @"^[0-9a-fA-F]{40,64}\trefs/heads/" + Regex.Escape(BranchName) + @"\z"))) { throw new ValidationException("Unable to load CI failure history: invalid memory branch reference"); } await git.CloneBranchAsync(url, BranchName, directory); } else if (create) { await git.InitializeAsync(directory); await git.CreateOrphanBranchAsync(directory, BranchName); await git.AddOriginAsync(directory, url); } else { return false; } if (create) { await ConfigurePublicationAsync(git, directory); } return true; } private static async Task ConfigurePublicationAsync(GitRepository git, string directory) { await git.ConfigureCommitIdentityAsync(directory, "github-actions[bot]", "github-actions[bot]@users.noreply.github.com"); await git.ConfigureGitHubCredentialsAsync(directory); } internal Task<bool> OpenForReadAsync() => OpenAsync(create: false); internal async Task OpenForPublicationAsync() => await OpenAsync(create: true); internal Task<string> GetHeadCommitAsync() => new GitRepository(command).GetHeadCommitAsync(directory); internal string CausePath(string fileName) => Path.Combine(directory, "causes", fileName); internal string RunPath(string runId) => Path.Combine(directory, "runs", runId + ".json"); internal IEnumerable<string> EnumerateCauseFiles() => Directory.Exists(Path.Combine(directory, "causes")) ? Directory.EnumerateFiles(Path.Combine(directory, "causes"), "*.json") : []; internal void CreateRecordDirectories() { Directory.CreateDirectory(Path.Combine(directory, "runs")); Directory.CreateDirectory(Path.Combine(directory, "causes")); } internal async Task CommitAndPushAsync(string runId, IEnumerable<string> paths) { var files = paths.Select(p => Path.GetRelativePath(directory, p)).ToArray(); if (files.Length == 0) { return; } var git = new GitRepository(command); await git.StageFilesAsync(directory, files); var changed = await git.GetStagedFileNamesAsync(directory); if (changed.Length == 0) { diagnostics.Receipt("history-push", "skipped", new { reason = "no-changes" }); return; } await git.CommitAsync(directory, $"Update CI failure analysis for run {runId}"); var commit = await GetHeadCommitAsync(); try { await git.PushBranchAsync(directory, BranchName); diagnostics.Receipt("history-push", "confirmed", new { commit, branch = BranchName }); } catch (Exception error) { diagnostics.Receipt("history-push", "unknown", new { commit, reason = error.Message }); throw; } } }