From b0f4828c9fa31563be70dd00c28bba2cfb3f77a8 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 4 Jun 2024 14:47:44 -0400 Subject: [PATCH 1/7] First pass at code. Prototype of generating checks This has a prototype to create the proper JSON format for the status check. Remaining task: Consume the output and POST the results. --- tools/StandardAnchorTags/DiagnosticIDs.cs | 6 + tools/StandardAnchorTags/Program.cs | 293 ++++++++++-------- .../ReferenceUpdateProcessor.cs | 18 +- .../StandardAnchorTags.csproj | 4 + .../TocSectionNumberBuilder.cs | 23 +- .../Utilities/GitHubCheck/CheckAnnotation.cs | 24 ++ tools/Utilities/GitHubCheck/CheckOutput.cs | 15 + tools/Utilities/GitHubCheck/CheckResult.cs | 53 ++++ tools/Utilities/StatusCheckLogger.cs | 145 +++++++++ 9 files changed, 429 insertions(+), 152 deletions(-) create mode 100644 tools/StandardAnchorTags/DiagnosticIDs.cs create mode 100644 tools/Utilities/GitHubCheck/CheckAnnotation.cs create mode 100644 tools/Utilities/GitHubCheck/CheckOutput.cs create mode 100644 tools/Utilities/GitHubCheck/CheckResult.cs create mode 100644 tools/Utilities/StatusCheckLogger.cs diff --git a/tools/StandardAnchorTags/DiagnosticIDs.cs b/tools/StandardAnchorTags/DiagnosticIDs.cs new file mode 100644 index 000000000..e89613386 --- /dev/null +++ b/tools/StandardAnchorTags/DiagnosticIDs.cs @@ -0,0 +1,6 @@ +namespace StandardAnchorTags; +internal class DiagnosticIDs +{ + public const string TOC001 = nameof(TOC001); + public const string TOC002 = nameof(TOC002); +} diff --git a/tools/StandardAnchorTags/Program.cs b/tools/StandardAnchorTags/Program.cs index 635889c0c..9d23dd67f 100644 --- a/tools/StandardAnchorTags/Program.cs +++ b/tools/StandardAnchorTags/Program.cs @@ -3,6 +3,9 @@ namespace StandardAnchorTags; +/// +/// The entry point +/// public class Program { const string TOCHeader = ""; @@ -11,10 +14,19 @@ public class Program private const string FilesPath = "../standard/clauses.json"; private const string GrammarFile = "grammar.md"; - private static Clauses? standardClauses; - - static async Task Main(string[] args) + private static Clauses? standardClauses = null!; + + /// + /// Update section numbers, anchor tags, and the TOC + /// + /// The GitHub owner org (for example, "dotnet") + /// The GitHub repo name (for example, "csharpstandard") + /// The commit sha, when run as a GitHub action + /// True for a dry run, false to update the text in all files + /// 0 on success, non-zero on failure + static async Task Main(string owner, string repo, string? headSha = null, bool dryrun =false) { + var logger = new StatusCheckLogger("..", "TOC and Anchor updater"); using FileStream openStream = File.OpenRead(FilesPath); standardClauses = await JsonSerializer.DeserializeAsync(openStream); if (standardClauses is null) @@ -23,153 +35,172 @@ static async Task Main(string[] args) return 1; } - bool dryRun = ((args.Length > 0) && (args[0].Contains("dryrun"))); - if (dryRun) + if (dryrun) { Console.WriteLine("Doing a dry run"); } try { - Console.WriteLine("=========================== Front Matter ==================================="); - var sectionMap = new TocSectionNumberBuilder(PathToStandard, dryRun); - foreach (var file in standardClauses.FrontMatter) + TocSectionNumberBuilder sectionMap = await BuildSectionMap(dryrun, logger); + + if (!dryrun) { - Console.WriteLine($" -- {file}"); - await sectionMap.AddFrontMatterTocEntries(file); + using StreamWriter readme = await WriteUpdatedTOC(sectionMap); } - Console.WriteLine("================= GENERATE UPDATED SECTION NUMBERS ========================="); - Console.WriteLine("============================ Scope and Conformance ======================================"); - foreach (var file in standardClauses.ScopeAndConformance) - { - Console.WriteLine($" -- {file}"); - await sectionMap.AddContentsToTOC(file); + await UpdateAllAnchors(dryrun, logger, sectionMap); - } - Console.WriteLine("============================ Lexical Structure ======================================"); - foreach (var file in standardClauses.LexicalStructure) - { - Console.WriteLine($" -- {file}"); - await sectionMap.AddContentsToTOC(file); - } - Console.WriteLine("============================ Main text======================================"); - foreach (var file in standardClauses.MainBody) - { - Console.WriteLine($" -- {file}"); - await sectionMap.AddContentsToTOC(file); - } - Console.WriteLine("============================ Unsafe clauses======================================"); - foreach (var file in standardClauses.UnsafeClauses) - { - Console.WriteLine($" -- {file}"); - await sectionMap.AddContentsToTOC(file); - } - Console.WriteLine("============================= Annexes ======================================"); - sectionMap.FinishMainSection(); - foreach (var file in standardClauses.Annexes) + if (!dryrun) { - Console.WriteLine($" -- {file}"); - await sectionMap.AddContentsToTOC(file); - } - if (!dryRun) - { - Console.WriteLine("Update TOC"); - var existingReadMe = await ReadExistingReadMe(); - using var readme = new StreamWriter(ReadMePath, false); - await readme.WriteAsync(existingReadMe); - await readme.WriteLineAsync(TOCHeader); - await readme.WriteLineAsync(); - await readme.WriteAsync(sectionMap.Toc); + using GenerateGrammar grammarGenerator = await GenerateNewGrammar(); } + } + finally + { + var JsonPacket = logger.BuildCheckRunResult(owner, repo, headSha ?? "fakeSha").ToJson(); + Console.WriteLine(JsonPacket); + } + return logger.Success ? 0 : 1; + } - Console.WriteLine("======================= UPDATE ALL REFERENCES =============================="); - var fixup = new ReferenceUpdateProcessor(PathToStandard, sectionMap.LinkMap, dryRun); + private static async Task BuildSectionMap(bool dryrun, StatusCheckLogger logger) + { + Console.WriteLine("=========================== Front Matter ==================================="); + var sectionMap = new TocSectionNumberBuilder(PathToStandard, logger, dryrun); + foreach (var file in standardClauses!.FrontMatter) + { + Console.WriteLine($" -- {file}"); + await sectionMap.AddFrontMatterTocEntries(file); + } - Console.WriteLine("=========================== Front Matter ==================================="); - foreach (var file in standardClauses.FrontMatter) - { - Console.WriteLine($" -- {file}"); - await fixup.ReplaceReferences(file); - } - Console.WriteLine("============================ Scope and Conformance ======================================"); - foreach (var file in standardClauses.ScopeAndConformance) - { - Console.WriteLine($" -- {file}"); - await fixup.ReplaceReferences(file); + Console.WriteLine("================= GENERATE UPDATED SECTION NUMBERS ========================="); + Console.WriteLine("============================ Scope and Conformance ======================================"); + foreach (var file in standardClauses.ScopeAndConformance) + { + Console.WriteLine($" -- {file}"); + await sectionMap.AddContentsToTOC(file); - } - Console.WriteLine("============================ Lexical Structure ======================================"); - foreach (var file in standardClauses.LexicalStructure) - { - Console.WriteLine($" -- {file}"); - await fixup.ReplaceReferences(file); - } - Console.WriteLine("============================ Main text======================================"); - foreach (var file in standardClauses.MainBody) - { - Console.WriteLine($" -- {file}"); - await fixup.ReplaceReferences(file); + } + Console.WriteLine("============================ Lexical Structure ======================================"); + foreach (var file in standardClauses.LexicalStructure) + { + Console.WriteLine($" -- {file}"); + await sectionMap.AddContentsToTOC(file); + } + Console.WriteLine("============================ Main text======================================"); + foreach (var file in standardClauses.MainBody) + { + Console.WriteLine($" -- {file}"); + await sectionMap.AddContentsToTOC(file); + } + Console.WriteLine("============================ Unsafe clauses======================================"); + foreach (var file in standardClauses.UnsafeClauses) + { + Console.WriteLine($" -- {file}"); + await sectionMap.AddContentsToTOC(file); + } + Console.WriteLine("============================= Annexes ======================================"); + sectionMap.FinishMainSection(); + foreach (var file in standardClauses.Annexes) + { + Console.WriteLine($" -- {file}"); + await sectionMap.AddContentsToTOC(file); + } + return sectionMap; + } - } - Console.WriteLine("============================ Unsafe clauses======================================"); - foreach (var file in standardClauses.UnsafeClauses) - { - Console.WriteLine($" -- {file}"); - await fixup.ReplaceReferences(file); + private static async Task WriteUpdatedTOC(TocSectionNumberBuilder sectionMap) + { + Console.WriteLine("Update TOC"); + var existingReadMe = await ReadExistingReadMe(); + var readme = new StreamWriter(ReadMePath, false); + await readme.WriteAsync(existingReadMe); + await readme.WriteLineAsync(TOCHeader); + await readme.WriteLineAsync(); + await readme.WriteAsync(sectionMap.Toc); + return readme; + } - } - Console.WriteLine("============================= Annexes ======================================"); - foreach (var file in standardClauses.Annexes) - { - Console.WriteLine($" -- {file}"); - await fixup.ReplaceReferences(file); - } - if (!dryRun) - { - Console.WriteLine("======================= READ EXISTING GRAMMAR HEADERS ======================="); - var headers = await GenerateGrammar.ReadExistingHeaders(PathToStandard, GrammarFile); - - Console.WriteLine("======================= GENERATE GRAMMAR ANNEX =============================="); - using var grammarGenerator = new GenerateGrammar(GrammarFile, PathToStandard, headers); - - Console.WriteLine("============================ Lexical Structure ======================================"); - - await grammarGenerator.WriteHeader(); - foreach (var file in standardClauses.LexicalStructure) - { - Console.WriteLine($" -- {file}"); - await grammarGenerator.ExtractGrammarFrom(file); - } - Console.WriteLine("============================ Main text======================================"); - - await grammarGenerator.WriteSyntaxHeader(); - foreach (var file in standardClauses.MainBody) - { - Console.WriteLine($" -- {file}"); - await grammarGenerator.ExtractGrammarFrom(file); - } - Console.WriteLine("============================ Unsafe clauses======================================"); - await grammarGenerator.WriteUnsafeExtensionHeader(); - foreach (var file in standardClauses.UnsafeClauses) - { - Console.WriteLine($" -- {file}"); - await grammarGenerator.ExtractGrammarFrom(file); - } - await grammarGenerator.WriteGrammarFooter(); - } - return fixup.ErrorCount; + private static async Task UpdateAllAnchors(bool dryrun, StatusCheckLogger logger, TocSectionNumberBuilder sectionMap) + { + Console.WriteLine("======================= UPDATE ALL REFERENCES =============================="); + var fixup = new ReferenceUpdateProcessor(PathToStandard, logger, sectionMap.LinkMap, dryrun); + + Console.WriteLine("=========================== Front Matter ==================================="); + foreach (var file in standardClauses!.FrontMatter) + { + Console.WriteLine($" -- {file}"); + await fixup.ReplaceReferences(file); } - catch (InvalidOperationException e) + Console.WriteLine("============================ Scope and Conformance ======================================"); + foreach (var file in standardClauses.ScopeAndConformance) { - Console.WriteLine("\tError encountered:"); - Console.WriteLine(e.Message.ToString()); - Console.WriteLine("To recover, do the following:"); - Console.WriteLine("1. Discard all changes from the section numbering tool"); - Console.WriteLine("2. Fix the error noted above."); - Console.WriteLine("3. Run the tool again."); - return 1; + Console.WriteLine($" -- {file}"); + await fixup.ReplaceReferences(file); + + } + Console.WriteLine("============================ Lexical Structure ======================================"); + foreach (var file in standardClauses.LexicalStructure) + { + Console.WriteLine($" -- {file}"); + await fixup.ReplaceReferences(file); + } + Console.WriteLine("============================ Main text======================================"); + foreach (var file in standardClauses.MainBody) + { + Console.WriteLine($" -- {file}"); + await fixup.ReplaceReferences(file); + + } + Console.WriteLine("============================ Unsafe clauses======================================"); + foreach (var file in standardClauses.UnsafeClauses) + { + Console.WriteLine($" -- {file}"); + await fixup.ReplaceReferences(file); + + } + Console.WriteLine("============================= Annexes ======================================"); + foreach (var file in standardClauses.Annexes) + { + Console.WriteLine($" -- {file}"); + await fixup.ReplaceReferences(file); + } + } + + private static async Task GenerateNewGrammar() + { + Console.WriteLine("======================= READ EXISTING GRAMMAR HEADERS ======================="); + var headers = await GenerateGrammar.ReadExistingHeaders(PathToStandard, GrammarFile); + + Console.WriteLine("======================= GENERATE GRAMMAR ANNEX =============================="); + var grammarGenerator = new GenerateGrammar(GrammarFile, PathToStandard, headers); + + Console.WriteLine("============================ Lexical Structure ======================================"); + + await grammarGenerator.WriteHeader(); + foreach (var file in standardClauses.LexicalStructure) + { + Console.WriteLine($" -- {file}"); + await grammarGenerator.ExtractGrammarFrom(file); + } + Console.WriteLine("============================ Main text======================================"); + + await grammarGenerator.WriteSyntaxHeader(); + foreach (var file in standardClauses.MainBody) + { + Console.WriteLine($" -- {file}"); + await grammarGenerator.ExtractGrammarFrom(file); + } + Console.WriteLine("============================ Unsafe clauses======================================"); + await grammarGenerator.WriteUnsafeExtensionHeader(); + foreach (var file in standardClauses.UnsafeClauses) + { + Console.WriteLine($" -- {file}"); + await grammarGenerator.ExtractGrammarFrom(file); } + await grammarGenerator.WriteGrammarFooter(); + return grammarGenerator; } private static async Task ReadExistingReadMe() @@ -182,4 +213,4 @@ private static async Task ReadExistingReadMe() return contents[..index]; } -} \ No newline at end of file +} diff --git a/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs b/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs index 586373f2e..0022f0859 100644 --- a/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs +++ b/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs @@ -1,4 +1,5 @@ using System.Text; +using Utilities; namespace StandardAnchorTags; @@ -9,11 +10,12 @@ internal class ReferenceUpdateProcessor private readonly IReadOnlyDictionary linkMap; private readonly bool dryRun; private readonly string PathToFiles; - public int ErrorCount { get; private set; } + private readonly StatusCheckLogger logger; - public ReferenceUpdateProcessor(string pathToFiles, IReadOnlyDictionary linkMap, bool dryRun) + public ReferenceUpdateProcessor(string pathToFiles, StatusCheckLogger logger, IReadOnlyDictionary linkMap, bool dryRun) { PathToFiles = pathToFiles; + this.logger = logger; this.linkMap = linkMap; this.dryRun = dryRun; } @@ -60,14 +62,8 @@ private string ProcessSectionLinks(string line, int lineNumber, string file) if ((referenceText.Length > 1) && (!linkMap.ContainsKey(referenceText))) { - var msg = $"Section reference [{referenceText}] not found at line {lineNumber} in {file}"; - if (dryRun) - { - ErrorCount++; - Console.WriteLine(msg); - } - else - throw new InvalidOperationException(msg); + var diagnostic = new Diagnostic(file, lineNumber, lineNumber, $"`{referenceText}` not found", DiagnosticIDs.TOC002); + logger.LogFailure(diagnostic); } else { linkText = linkMap[referenceText].FormattedMarkdownLink; @@ -135,4 +131,4 @@ private static Range ExpandToIncludeExistingLink(string line, Range range) return new Range(previous, endIndex + 1); } -} \ No newline at end of file +} diff --git a/tools/StandardAnchorTags/StandardAnchorTags.csproj b/tools/StandardAnchorTags/StandardAnchorTags.csproj index 240e54f0c..8157f79fc 100644 --- a/tools/StandardAnchorTags/StandardAnchorTags.csproj +++ b/tools/StandardAnchorTags/StandardAnchorTags.csproj @@ -11,6 +11,10 @@ + + + + diff --git a/tools/StandardAnchorTags/TocSectionNumberBuilder.cs b/tools/StandardAnchorTags/TocSectionNumberBuilder.cs index 8e28db9d0..48f42a983 100644 --- a/tools/StandardAnchorTags/TocSectionNumberBuilder.cs +++ b/tools/StandardAnchorTags/TocSectionNumberBuilder.cs @@ -1,5 +1,6 @@ using System.Text; using System.Text.RegularExpressions; +using Utilities; namespace StandardAnchorTags; @@ -25,6 +26,7 @@ private struct SectionHeader private const string AnnexPattern = @"^[A-Z](\.\d+)*$"; private readonly string PathToStandardFiles; + private readonly StatusCheckLogger logger; private readonly bool dryRun; // String builder to store the full TOC for the standard. @@ -39,9 +41,10 @@ private struct SectionHeader /// /// Construct the map Builder. /// - public TocSectionNumberBuilder(string pathFromToolToStandard, bool dryRun) + public TocSectionNumberBuilder(string pathFromToolToStandard, StatusCheckLogger logger, bool dryRun) { PathToStandardFiles = pathFromToolToStandard; + this.logger = logger; this.dryRun = dryRun; } @@ -55,19 +58,19 @@ public TocSectionNumberBuilder(string pathFromToolToStandard, bool dryRun) /// public async Task AddFrontMatterTocEntries(string fileName) { - using var stream = File.OpenText(Path.Combine(PathToStandardFiles,fileName)); + string path = Path.Combine(PathToStandardFiles, fileName); + using var stream = File.OpenText(path); string? line = await stream.ReadLineAsync(); + if (line?.StartsWith("# ") == true) { - if (line?.StartsWith("# ") == true) - { - var linkText = line[2..]; - tocContent.AppendLine($"- [{linkText}]({fileName})"); - // Done: return. - return; - } + var linkText = line[2..]; + tocContent.AppendLine($"- [{linkText}]({fileName})"); + // Done: return. + return; } // Getting here means this file doesn't have an H1. That's an error: - throw new InvalidOperationException($"File {fileName} doesn't have an H1 tag as its first line."); + var diagnostic = new Diagnostic(path, 1, 1, "File doesn't have an H1 tag as its first line.", DiagnosticIDs.TOC001); + logger.LogFailure(diagnostic); } public async Task AddContentsToTOC(string filename) diff --git a/tools/Utilities/GitHubCheck/CheckAnnotation.cs b/tools/Utilities/GitHubCheck/CheckAnnotation.cs new file mode 100644 index 000000000..d8fe27c21 --- /dev/null +++ b/tools/Utilities/GitHubCheck/CheckAnnotation.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace Utilities.GitHubCheck; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AnnotationLevel +{ + Notice, + Warning, + Failure +} + +public readonly record struct CheckAnnotation +{ + public string Path { get; init; } + + public string StartLine { get; init; } + + public string EndLine { get; init; } + + public AnnotationLevel Level { get; init; } + + public string Message { get; init; } +} diff --git a/tools/Utilities/GitHubCheck/CheckOutput.cs b/tools/Utilities/GitHubCheck/CheckOutput.cs new file mode 100644 index 000000000..5d1d0ae66 --- /dev/null +++ b/tools/Utilities/GitHubCheck/CheckOutput.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Utilities.GitHubCheck; + +public class CheckOutput +{ + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("summary")] + public required string Summary { get; set; } + + [JsonPropertyName("annotations")] + public required List Annotations { get; set; } +} diff --git a/tools/Utilities/GitHubCheck/CheckResult.cs b/tools/Utilities/GitHubCheck/CheckResult.cs new file mode 100644 index 000000000..d4b0520c3 --- /dev/null +++ b/tools/Utilities/GitHubCheck/CheckResult.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Utilities.GitHubCheck; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum CheckStatus +{ + Queued, + InProgress, + Requested, + Waiting, + Pending, + Completed +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum CheckConclusion +{ + ActionRequired, + Cancelled, + Failure, + Neutral, + Skipped, + Stale, + StartupFailure, + Success +} + +public record class CheckResult +{ + public required string Owner { get; init; } + + public required string Repo { get; init; } + + public required string Name { get; init; } + + public required string HeadSha { get; init; } + + public required CheckStatus Status { get; init; } + + public required CheckConclusion Conclusion { get; init; } + + public required CheckOutput Output { get; init; } + + public string ToJson() => JsonSerializer.Serialize(this, + new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, + WriteIndented = true + }); +} diff --git a/tools/Utilities/StatusCheckLogger.cs b/tools/Utilities/StatusCheckLogger.cs new file mode 100644 index 000000000..3e26d1436 --- /dev/null +++ b/tools/Utilities/StatusCheckLogger.cs @@ -0,0 +1,145 @@ +using Utilities.GitHubCheck; + +namespace Utilities; + +/// +/// Record for a single diagnostic +/// +/// The source file in the PR +/// The message for the output daignostic +/// The error message ID +/// The start line (index from 1) +/// The end line (index from 1) +public record Diagnostic(string file, int StartLine, int EndLine, string Message, string Id); + +/// +/// This class writes the status of the check to the console in the format GitHub supports +/// +/// +/// For all of our tools, if all error and warning messages are formatted correctly, GitHub +/// will show those errors and warnings inline in the files tab for the PR. Let's format +/// them correctly. +/// +/// The path to the root of the repository +/// The name of the tool that is running the check +public class StatusCheckLogger(string pathToRoot, string toolName) +{ + private List annotations = []; + public bool Success { get; private set; } = true; + + // Utility method to format the path to unix style, from the root of the repository. + private string FormatPath(string path) => Path.GetRelativePath(pathToRoot, path).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + private void WriteMessageToConsole(string prefix, Diagnostic d) => Console.WriteLine($"{prefix}{toolName}-{d.Id}::file={FormatPath(d.file)},line={d.StartLine}::{d.Message}"); + + /// + /// Log a notice from the status check + /// + /// The diagnostic + /// + /// Add the diagnostic to the annotation list and + /// log the diagnostic information to console. + /// + public void LogNotice(Diagnostic d) + { + WriteMessageToConsole("", d); + annotations.Add(new () + { + Path = FormatPath(d.file), + StartLine = d.StartLine.ToString(), + EndLine = d.EndLine.ToString(), + Level = AnnotationLevel.Notice, + Message = d.Message + }); + } + + /// + /// Log a warning from the status check + /// + /// The diagnostic + /// + /// Add the diagnostic to the annotation list and + /// log the warning notice to the console. + /// + public void LogWarning(Diagnostic d) + { + WriteMessageToConsole("⚠️", d); + annotations.Add(new () + { + Path = FormatPath(d.file), + StartLine = d.StartLine.ToString(), + EndLine = d.EndLine.ToString(), + Level = AnnotationLevel.Warning, + Message = d.Message + }); + Success = false; + } + + /// + /// Log a failure from the status check + /// + /// The diagnostic + /// + /// Add the diagnostic to the annotation list and + /// log the failure notice to the console. + /// This method is distinct from in + /// that this method does not throw an exception. Its purpose is to log + /// the failure but allow the tool to continue running further checks. + /// + public void LogFailure(Diagnostic d) + { + WriteMessageToConsole("❌", d); + annotations.Add(new() + { + Path = FormatPath(d.file), + StartLine = d.StartLine.ToString(), + EndLine = d.EndLine.ToString(), + Level = AnnotationLevel.Failure, + Message = d.Message + }); + Success = false; + } + + /// + /// Log a failure from the status check and throw for exit + /// + /// The diagnostic + /// + /// Add the diagnostic to the annotation list and + /// log the failure notice to the console. + /// This method is distinct from in + /// that this method throws an exception. Its purpose is to log + /// the failure and immediately exit, foregoing any further checks. + /// + public void ExitOnFailure(Diagnostic d) + { + LogFailure(d); + throw new InvalidOperationException(d.Message); + } + + /// + /// Return the object required for a full status check + /// + /// The GitHub owner (or organization) + /// The GitHub repo name + /// The head sha when running as a GitHub action + /// The full check run result object + public CheckResult BuildCheckRunResult(string owner, string repo, string sha) + { + return new () + { + Owner = owner, + Repo = repo, + HeadSha = sha, + Name = toolName, + Status = CheckStatus.Completed, + Conclusion = Success ? CheckConclusion.Success : CheckConclusion.Failure, + Output = new() + { + Title = $"{toolName} Check Run results", + Summary = $"{toolName} result is {(Success ? "success" : "failure")} with {annotations.Count} diagnostics.", + Annotations = annotations + } + }; + } +} From 8715aa86c9f88f59bfca24f11bed4190a3e1fe6d Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 5 Jun 2024 16:58:13 -0400 Subject: [PATCH 2/7] Add POST capability. POST the check results back to GitHub --- tools/StandardAnchorTags/Program.cs | 9 ++- .../ReferenceUpdateProcessor.cs | 6 +- .../Utilities/GitHubCheck/CheckAnnotation.cs | 24 ------- tools/Utilities/GitHubCheck/CheckOutput.cs | 15 ---- tools/Utilities/GitHubCheck/CheckResult.cs | 53 -------------- tools/Utilities/StatusCheckLogger.cs | 71 ++++++++++--------- tools/Utilities/Utilities.csproj | 1 + 7 files changed, 46 insertions(+), 133 deletions(-) delete mode 100644 tools/Utilities/GitHubCheck/CheckAnnotation.cs delete mode 100644 tools/Utilities/GitHubCheck/CheckOutput.cs delete mode 100644 tools/Utilities/GitHubCheck/CheckResult.cs diff --git a/tools/StandardAnchorTags/Program.cs b/tools/StandardAnchorTags/Program.cs index 9d23dd67f..157a6fd4e 100644 --- a/tools/StandardAnchorTags/Program.cs +++ b/tools/StandardAnchorTags/Program.cs @@ -21,10 +21,11 @@ public class Program /// /// The GitHub owner org (for example, "dotnet") /// The GitHub repo name (for example, "csharpstandard") + /// The GitHub token, when run as an action /// The commit sha, when run as a GitHub action /// True for a dry run, false to update the text in all files /// 0 on success, non-zero on failure - static async Task Main(string owner, string repo, string? headSha = null, bool dryrun =false) + static async Task Main(string owner, string repo, string? token = null, string? headSha = null, bool dryrun =false) { var logger = new StatusCheckLogger("..", "TOC and Anchor updater"); using FileStream openStream = File.OpenRead(FilesPath); @@ -58,8 +59,10 @@ static async Task Main(string owner, string repo, string? headSha = null, b } finally { - var JsonPacket = logger.BuildCheckRunResult(owner, repo, headSha ?? "fakeSha").ToJson(); - Console.WriteLine(JsonPacket); + if ((token is not null) && (headSha is not null)) + { + await logger.BuildCheckRunResult(token, owner, repo, headSha); + } } return logger.Success ? 0 : 1; } diff --git a/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs b/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs index 0022f0859..99b84539e 100644 --- a/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs +++ b/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs @@ -32,7 +32,7 @@ public async Task ReplaceReferences(string file) { lineNumber++; var updatedLine = line.Contains(sectionReference) - ? ProcessSectionLinks(line, lineNumber, file) + ? ProcessSectionLinks(line, lineNumber, inputPath) : line; await writeStream.WriteLineAsync(updatedLine); } @@ -49,7 +49,7 @@ public async Task ReplaceReferences(string file) } } - private string ProcessSectionLinks(string line, int lineNumber, string file) + private string ProcessSectionLinks(string line, int lineNumber, string path) { var returnedLine = new StringBuilder(); int index = 0; @@ -62,7 +62,7 @@ private string ProcessSectionLinks(string line, int lineNumber, string file) if ((referenceText.Length > 1) && (!linkMap.ContainsKey(referenceText))) { - var diagnostic = new Diagnostic(file, lineNumber, lineNumber, $"`{referenceText}` not found", DiagnosticIDs.TOC002); + var diagnostic = new Diagnostic(path, lineNumber, lineNumber, $"`{referenceText}` not found", DiagnosticIDs.TOC002); logger.LogFailure(diagnostic); } else { diff --git a/tools/Utilities/GitHubCheck/CheckAnnotation.cs b/tools/Utilities/GitHubCheck/CheckAnnotation.cs deleted file mode 100644 index d8fe27c21..000000000 --- a/tools/Utilities/GitHubCheck/CheckAnnotation.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Utilities.GitHubCheck; - -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum AnnotationLevel -{ - Notice, - Warning, - Failure -} - -public readonly record struct CheckAnnotation -{ - public string Path { get; init; } - - public string StartLine { get; init; } - - public string EndLine { get; init; } - - public AnnotationLevel Level { get; init; } - - public string Message { get; init; } -} diff --git a/tools/Utilities/GitHubCheck/CheckOutput.cs b/tools/Utilities/GitHubCheck/CheckOutput.cs deleted file mode 100644 index 5d1d0ae66..000000000 --- a/tools/Utilities/GitHubCheck/CheckOutput.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Utilities.GitHubCheck; - -public class CheckOutput -{ - [JsonPropertyName("title")] - public required string Title { get; set; } - - [JsonPropertyName("summary")] - public required string Summary { get; set; } - - [JsonPropertyName("annotations")] - public required List Annotations { get; set; } -} diff --git a/tools/Utilities/GitHubCheck/CheckResult.cs b/tools/Utilities/GitHubCheck/CheckResult.cs deleted file mode 100644 index d4b0520c3..000000000 --- a/tools/Utilities/GitHubCheck/CheckResult.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Utilities.GitHubCheck; - -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum CheckStatus -{ - Queued, - InProgress, - Requested, - Waiting, - Pending, - Completed -} - -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum CheckConclusion -{ - ActionRequired, - Cancelled, - Failure, - Neutral, - Skipped, - Stale, - StartupFailure, - Success -} - -public record class CheckResult -{ - public required string Owner { get; init; } - - public required string Repo { get; init; } - - public required string Name { get; init; } - - public required string HeadSha { get; init; } - - public required CheckStatus Status { get; init; } - - public required CheckConclusion Conclusion { get; init; } - - public required CheckOutput Output { get; init; } - - public string ToJson() => JsonSerializer.Serialize(this, - new JsonSerializerOptions() - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, - WriteIndented = true - }); -} diff --git a/tools/Utilities/StatusCheckLogger.cs b/tools/Utilities/StatusCheckLogger.cs index 3e26d1436..0edea2a97 100644 --- a/tools/Utilities/StatusCheckLogger.cs +++ b/tools/Utilities/StatusCheckLogger.cs @@ -1,4 +1,4 @@ -using Utilities.GitHubCheck; +using Octokit; namespace Utilities; @@ -24,7 +24,7 @@ public record Diagnostic(string file, int StartLine, int EndLine, string Message /// The name of the tool that is running the check public class StatusCheckLogger(string pathToRoot, string toolName) { - private List annotations = []; + private List annotations = []; public bool Success { get; private set; } = true; // Utility method to format the path to unix style, from the root of the repository. @@ -43,14 +43,11 @@ public class StatusCheckLogger(string pathToRoot, string toolName) public void LogNotice(Diagnostic d) { WriteMessageToConsole("", d); - annotations.Add(new () - { - Path = FormatPath(d.file), - StartLine = d.StartLine.ToString(), - EndLine = d.EndLine.ToString(), - Level = AnnotationLevel.Notice, - Message = d.Message - }); + annotations.Add( + new(FormatPath(d.file), + d.StartLine, d.EndLine, + CheckAnnotationLevel.Notice, $"{d.Id}::{d.Message}") + ); } /// @@ -64,14 +61,11 @@ public void LogNotice(Diagnostic d) public void LogWarning(Diagnostic d) { WriteMessageToConsole("⚠️", d); - annotations.Add(new () - { - Path = FormatPath(d.file), - StartLine = d.StartLine.ToString(), - EndLine = d.EndLine.ToString(), - Level = AnnotationLevel.Warning, - Message = d.Message - }); + annotations.Add( + new(FormatPath(d.file), + d.StartLine, d.EndLine, + CheckAnnotationLevel.Warning, $"{d.Id}::{d.Message}") + ); Success = false; } @@ -89,14 +83,11 @@ public void LogWarning(Diagnostic d) public void LogFailure(Diagnostic d) { WriteMessageToConsole("❌", d); - annotations.Add(new() - { - Path = FormatPath(d.file), - StartLine = d.StartLine.ToString(), - EndLine = d.EndLine.ToString(), - Level = AnnotationLevel.Failure, - Message = d.Message - }); + annotations.Add( + new(FormatPath(d.file), + d.StartLine, d.EndLine, + CheckAnnotationLevel.Failure, $"{d.Id}::{d.Message}") + ); Success = false; } @@ -124,22 +115,32 @@ public void ExitOnFailure(Diagnostic d) /// The GitHub repo name /// The head sha when running as a GitHub action /// The full check run result object - public CheckResult BuildCheckRunResult(string owner, string repo, string sha) + public async Task BuildCheckRunResult(string token, string owner, string repo, string sha) { - return new () + NewCheckRun result = new(toolName, sha) { - Owner = owner, - Repo = repo, - HeadSha = sha, - Name = toolName, Status = CheckStatus.Completed, Conclusion = Success ? CheckConclusion.Success : CheckConclusion.Failure, - Output = new() + Output = new($"{toolName} Check Run results", $"{toolName} result is {(Success ? "success" : "failure")} with {annotations.Count} diagnostics.") { - Title = $"{toolName} Check Run results", - Summary = $"{toolName} result is {(Success ? "success" : "failure")} with {annotations.Count} diagnostics.", Annotations = annotations } }; + + var prodInformation = new ProductHeaderValue("TC49-TG2", "1.0.0"); + var tokenAuth = new Credentials(token); + var client = new GitHubClient(prodInformation); + client.Credentials = tokenAuth; + + try + { + await client.Check.Run.Create(owner, repo, result); + } + // If the token does not have the correct permissions, we will get a 403 + // Once running on a branch on the dotnet org, this should work correctly. + catch (Octokit.ForbiddenException) + { + Console.WriteLine("===== WARNING: Could not create a check run.====="); + } } } diff --git a/tools/Utilities/Utilities.csproj b/tools/Utilities/Utilities.csproj index 167796798..d962fe85e 100644 --- a/tools/Utilities/Utilities.csproj +++ b/tools/Utilities/Utilities.csproj @@ -7,6 +7,7 @@ + From 4045e23034e8682fff39c9d08198ed35d43ad8bb Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 6 Jun 2024 10:13:55 -0400 Subject: [PATCH 3/7] Update YML and shell script --- .github/workflows/renumber-sections.yaml | 5 ++++- tools/StandardAnchorTags/Program.cs | 4 ++-- tools/run-section-renumber.sh | 7 +------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/renumber-sections.yaml b/.github/workflows/renumber-sections.yaml index 1cc0828c3..a4e46b74f 100644 --- a/.github/workflows/renumber-sections.yaml +++ b/.github/workflows/renumber-sections.yaml @@ -14,8 +14,11 @@ on: jobs: renumber-sections: runs-on: ubuntu-latest + permissions: + checks: write env: DOTNET_NOLOGO: true + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Check out our repo @@ -29,4 +32,4 @@ jobs: - name: Run section renumbering dry run run: | cd tools - ./run-section-renumber.sh --dryrun + ./run-section-renumber.sh ${{ github.event.pull_request.head.sha }} diff --git a/tools/StandardAnchorTags/Program.cs b/tools/StandardAnchorTags/Program.cs index 157a6fd4e..411172b77 100644 --- a/tools/StandardAnchorTags/Program.cs +++ b/tools/StandardAnchorTags/Program.cs @@ -21,13 +21,13 @@ public class Program /// /// The GitHub owner org (for example, "dotnet") /// The GitHub repo name (for example, "csharpstandard") - /// The GitHub token, when run as an action /// The commit sha, when run as a GitHub action /// True for a dry run, false to update the text in all files /// 0 on success, non-zero on failure - static async Task Main(string owner, string repo, string? token = null, string? headSha = null, bool dryrun =false) + static async Task Main(string owner, string repo, string? headSha = null, bool dryrun =false) { var logger = new StatusCheckLogger("..", "TOC and Anchor updater"); + var token = Environment.GetEnvironmentVariable("GH_TOKEN"); using FileStream openStream = File.OpenRead(FilesPath); standardClauses = await JsonSerializer.DeserializeAsync(openStream); if (standardClauses is null) diff --git a/tools/run-section-renumber.sh b/tools/run-section-renumber.sh index a192bbc82..81aab1903 100755 --- a/tools/run-section-renumber.sh +++ b/tools/run-section-renumber.sh @@ -4,12 +4,7 @@ set -e declare -r PROJECT=StandardAnchorTags -if [ "$1" == "--dryrun" ] -then - echo "Performing a dry run" -fi - -dotnet run --project $PROJECT -- $1 +dotnet run --project $PROJECT -- --owner dotnet --repo csharpstandard --dryrun true --head-sha $1 if [ -n "$GITHUB_OUTPUT" ] then From b9ed8e32effd715bb8d1f47cc6943a886f6ec1be Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 6 Jun 2024 13:42:19 -0400 Subject: [PATCH 4/7] Introduce error for check run --- standard/basic-concepts.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/standard/basic-concepts.md b/standard/basic-concepts.md index a09db7b87..3f7cda217 100644 --- a/standard/basic-concepts.md +++ b/standard/basic-concepts.md @@ -2,6 +2,8 @@ ## 7.1 Application startup +Let's add a link to §Something-that-doesnt-exist so I can test the workflow integration. + A program may be compiled either as a ***class library*** to be used as part of other applications, or as an ***application*** that may be started directly. The mechanism for determining this mode of compilation is implementation-specific and external to this specification. A program compiled as an application shall contain at least one method qualifying as an entry point by satisfying the following requirements: From 52592d8efd41611185a9236fef841583eeabcaa0 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 6 Jun 2024 14:43:59 -0400 Subject: [PATCH 5/7] Remove error introduced for testing --- standard/basic-concepts.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/standard/basic-concepts.md b/standard/basic-concepts.md index 3f7cda217..a09db7b87 100644 --- a/standard/basic-concepts.md +++ b/standard/basic-concepts.md @@ -2,8 +2,6 @@ ## 7.1 Application startup -Let's add a link to §Something-that-doesnt-exist so I can test the workflow integration. - A program may be compiled either as a ***class library*** to be used as part of other applications, or as an ***application*** that may be started directly. The mechanism for determining this mode of compilation is implementation-specific and external to this specification. A program compiled as an application shall contain at least one method qualifying as an entry point by satisfying the following requirements: From 7d713488e8691dcead0c6492b01c017c3c1b3199 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 6 Jun 2024 15:08:16 -0400 Subject: [PATCH 6/7] remove warnings. This way, the only annotations that are displayed actually matter for the tool being run --- tools/StandardAnchorTags/GenerateGrammar.cs | 53 +++++++++++++++++++ tools/StandardAnchorTags/Program.cs | 2 +- tools/StandardAnchorTags/SectionLink.cs | 13 +++++ .../TocSectionNumberBuilder.cs | 5 ++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/tools/StandardAnchorTags/GenerateGrammar.cs b/tools/StandardAnchorTags/GenerateGrammar.cs index 45022d986..dc178ed49 100644 --- a/tools/StandardAnchorTags/GenerateGrammar.cs +++ b/tools/StandardAnchorTags/GenerateGrammar.cs @@ -2,13 +2,34 @@ namespace StandardAnchorTags; +/// +/// The data storage for the headers of the grammar file +/// +/// The text for the lexical section header +/// The text for the syntactic section header +/// The text for the unsafe extensions header +/// The footer public record GrammarHeaders(string LexicalHeader, string SyntacticHeader, string UnsafeExtensionsHeader, string GrammarFooter); +/// +/// This class generates a grammar file from the ANTLR blocks in the standard +/// +/// +/// The full grammar is in small pieces in each clause of the standard. Then, +/// its duplicated in an Annex. Rather than copy and paste by hand, generate that +/// Annex from the other smaller parts. +/// public class GenerateGrammar : IDisposable { + /// + /// Read the existing headers from a grammar file + /// + /// Path to the standard files (likely ../standard) + /// The filename for the grammar annex + /// The task that will return the headers when complete. public static async Task ReadExistingHeaders(string pathToStandard, string grammarFile) { GrammarHeaders headers = new GrammarHeaders("", "", "", ""); @@ -50,6 +71,12 @@ public static async Task ReadExistingHeaders(string pathToStanda private readonly string pathToStandardFiles; private readonly StreamWriter grammarStream; + /// + /// Construct a new grammar generator + /// + /// The path to the file + /// The path to the files in the standard + /// The header text public GenerateGrammar(string grammarPath, string pathToStandardFiles, GrammarHeaders headers) { grammarStream = new StreamWriter(Path.Combine(pathToStandardFiles, grammarPath), false); @@ -57,10 +84,28 @@ public GenerateGrammar(string grammarPath, string pathToStandardFiles, GrammarHe informativeTextBlocks = headers; } + /// + /// Write the header text that appears before the grammar output. + /// + /// The task public async Task WriteHeader() => await grammarStream.WriteAsync(informativeTextBlocks.LexicalHeader); + + /// + /// Write the header text for the syntactic section + /// + /// The task public async Task WriteSyntaxHeader() => await grammarStream.WriteAsync(informativeTextBlocks.SyntacticHeader); + + /// + /// Write the header text for the unsafe extensions section + /// + /// The task public async Task WriteUnsafeExtensionHeader() => await grammarStream.WriteAsync(informativeTextBlocks.UnsafeExtensionsHeader); + /// + /// Write the footer text that appears after the grammar output. + /// + /// The task public async Task WriteGrammarFooter() { await grammarStream.WriteAsync(informativeTextBlocks.GrammarFooter); @@ -68,6 +113,11 @@ public async Task WriteGrammarFooter() grammarStream.Close(); } + /// + /// Extract the grammar from one file in the standard + /// + /// The input file from the standard + /// The task public async Task ExtractGrammarFrom(string inputFileName) { string inputFilePath = $"{pathToStandardFiles}/{inputFileName}"; @@ -106,5 +156,8 @@ public async Task ExtractGrammarFrom(string inputFileName) } } + /// + /// Dispose of the stream + /// public void Dispose() => grammarStream.Dispose(); } diff --git a/tools/StandardAnchorTags/Program.cs b/tools/StandardAnchorTags/Program.cs index 411172b77..f1c9814ae 100644 --- a/tools/StandardAnchorTags/Program.cs +++ b/tools/StandardAnchorTags/Program.cs @@ -182,7 +182,7 @@ private static async Task GenerateNewGrammar() Console.WriteLine("============================ Lexical Structure ======================================"); await grammarGenerator.WriteHeader(); - foreach (var file in standardClauses.LexicalStructure) + foreach (var file in standardClauses!.LexicalStructure) { Console.WriteLine($" -- {file}"); await grammarGenerator.ExtractGrammarFrom(file); diff --git a/tools/StandardAnchorTags/SectionLink.cs b/tools/StandardAnchorTags/SectionLink.cs index 1d632b00e..573db5cb8 100644 --- a/tools/StandardAnchorTags/SectionLink.cs +++ b/tools/StandardAnchorTags/SectionLink.cs @@ -6,6 +6,13 @@ public readonly struct SectionLink { private const char sectionReference = '§'; + + /// + /// Constructor for a section link. + /// + /// The old link + /// The new link + /// The anchor text public SectionLink(string oldLink, string newLink, string anchor) { ExistingLinkText = oldLink; @@ -29,8 +36,14 @@ public SectionLink(string oldLink, string newLink, string anchor) /// public string AnchorText { get; } + /// + /// The markdown link for the section. + /// public string FormattedMarkdownLink => $"[{sectionReference}{NewLinkText}]({AnchorText})"; + /// + /// The markdown link and text for the TOC + /// public string TOCMarkdownLink() => $"[{sectionReference}{NewLinkText}]({AnchorText})"; } diff --git a/tools/StandardAnchorTags/TocSectionNumberBuilder.cs b/tools/StandardAnchorTags/TocSectionNumberBuilder.cs index 48f42a983..64d0e7791 100644 --- a/tools/StandardAnchorTags/TocSectionNumberBuilder.cs +++ b/tools/StandardAnchorTags/TocSectionNumberBuilder.cs @@ -73,6 +73,11 @@ public async Task AddFrontMatterTocEntries(string fileName) logger.LogFailure(diagnostic); } + /// + /// Process this file, updating TOC and anchors + /// + /// The input file + /// The task public async Task AddContentsToTOC(string filename) { string pathToFile = $"{PathToStandardFiles}/{filename}"; From 9758231f6df3729cd77fa787fff4a6495cad66dd Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Mon, 24 Jun 2024 10:47:03 -0400 Subject: [PATCH 7/7] Respond to feedback Warnings shouldn't be treated as errors. --- tools/Utilities/StatusCheckLogger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Utilities/StatusCheckLogger.cs b/tools/Utilities/StatusCheckLogger.cs index 0edea2a97..b9240a2b9 100644 --- a/tools/Utilities/StatusCheckLogger.cs +++ b/tools/Utilities/StatusCheckLogger.cs @@ -57,6 +57,7 @@ public void LogNotice(Diagnostic d) /// /// Add the diagnostic to the annotation list and /// log the warning notice to the console. + /// Warnings are logged, but the process reports "success" to GitHub. /// public void LogWarning(Diagnostic d) { @@ -66,7 +67,6 @@ public void LogWarning(Diagnostic d) d.StartLine, d.EndLine, CheckAnnotationLevel.Warning, $"{d.Id}::{d.Message}") ); - Success = false; } ///