diff --git a/docs/content/Running during CI.md b/docs/content/Running during CI.md index 8ce60b8..410d061 100644 --- a/docs/content/Running during CI.md +++ b/docs/content/Running during CI.md @@ -42,6 +42,7 @@ Example when using MSBuild: ## GitHub Actions +### GitHub Advanced Security If you are using [GitHub Actions](https://docs.github.com/en/code-security/codeql-cli/using-the-advanced-functionality-of-the-codeql-cli/sarif-output) you can easily send the *sarif file* to [CodeQL](https://codeql.github.com/). ```yml @@ -68,4 +69,14 @@ Sample: See [fsproject/fantomas#2962](https://github.com/fsprojects/fantomas/pull/2962) for more information. +### Github Workflow Commands +If you cannot use GitHub Advanced Security (e.g. if your repository is private), you can get similar annotations by running the analyzers with `--output-format github`. +This will make the analyzers print their results as [GitHub Workflow Commands](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions). +If you for instance have a GitHub Action to run analyzers on every pull request, these annotations will show up in the "Files changed" on the pull request. +If the annotations don't show correctly, you might need to set the `code-root` to the root of the repository. + +Note that GitHub has a hard limit of 10 annotations of each type (notice, warning, error) per CI step. +This means that only the first 10 errors, the first 10 warnings and the first 10 hints/info results from analyzers will generate annotations. +The workflow log will contain all analyzer results even if a job hits the annotation limits. + [Previous]({{fsdocs-previous-page-link}}) diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index 8b79100..f4f27a3 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -35,6 +35,7 @@ type Arguments = | [] FSC_Args of string | [] Code_Root of string | [] Verbosity of string + | [] Output_Format of string interface IArgParserTemplate with member s.Usage = @@ -67,6 +68,8 @@ type Arguments = | FSC_Args _ -> "Pass in the raw fsc compiler arguments. Cannot be combined with the `--project` flag." | Code_Root _ -> "Root of the current code repository, used in the sarif report to construct the relative file path. The current working directory is used by default." + | Output_Format _ -> + "Format in which to write analyzer results to stdout. The available options are: default, github." type SeverityMappings = { @@ -103,6 +106,16 @@ let mapMessageToSeverity (mappings: SeverityMappings) (msg: FSharp.Analyzers.SDK } } +[] +type OutputFormat = +| Default +| GitHub + +let parseOutputFormat = function +| "github" -> Ok OutputFormat.GitHub +| "default" -> Ok OutputFormat.Default +| other -> Error $"Unknown output format: %s{other}." + let mutable logLevel = LogLevel.Warning let fcs = Utils.createFCS None @@ -258,7 +271,7 @@ let runFscArgs runProject client projectOptions excludeIncludeFiles mappings -let printMessages (msgs: AnalyzerMessage list) = +let printMessagesInDefaultFormat (msgs: AnalyzerMessage list) = let severityToLogLevel = Map.ofArray @@ -300,13 +313,70 @@ let printMessages (msgs: AnalyzerMessage list) = () -let writeReport (results: AnalyzerMessage list) (codeRoot: string option) (report: string) = - try - let codeRoot = - match codeRoot with - | None -> Directory.GetCurrentDirectory() |> Uri - | Some root -> Path.GetFullPath root |> Uri +let printMessagesInGitHubFormat (codeRoot : Uri) (msgs: AnalyzerMessage list) = + let severityToLogLevel = + Map.ofArray + [| + Severity.Error, LogLevel.Error + Severity.Warning, LogLevel.Warning + Severity.Info, LogLevel.Information + Severity.Hint, LogLevel.Trace + |] + + let severityToGitHubAnnotationType = + Map.ofArray + [| + Severity.Error, "error" + Severity.Warning, "warning" + Severity.Info, "notice" + Severity.Hint, "notice" + |] + + if List.isEmpty msgs then + logger.LogInformation("No messages found from the analyzer(s)") + + use factory = + LoggerFactory.Create(fun builder -> + builder + .AddCustomFormatter(fun options -> options.UseAnalyzersMsgStyle <- true) + .SetMinimumLevel(LogLevel.Trace) + |> ignore + ) + + // No category name because GitHub needs the annotation type to be the first + // element on each line. + let msgLogger = factory.CreateLogger("") + + msgs + |> List.iter (fun analyzerMessage -> + let m = analyzerMessage.Message + + // We want file names to be relative to the repository so GitHub will recognize them. + // GitHub also only understands Unix-style directory separators. + let relativeFileName = + codeRoot.MakeRelativeUri(Uri(m.Range.FileName)) + |> _.OriginalString + + msgLogger.Log( + severityToLogLevel[m.Severity], + "::{0} file={1},line={2},endLine={3},col={4},endColumn={5},title={6} ({7})::{8}: {9}", + severityToGitHubAnnotationType[m.Severity], + relativeFileName, + m.Range.StartLine, + m.Range.EndLine, + m.Range.StartColumn, + m.Range.EndColumn, + analyzerMessage.Name, + m.Code, + m.Severity.ToString(), + m.Message + ) + ) + + () +let writeReport (results: AnalyzerMessage list) (codeRoot: Uri) (report: string) = + try // Construct full path to ensure path separators are normalized. let report = Path.GetFullPath report // Ensure the parent directory exists @@ -551,6 +621,14 @@ let main argv = properties |> List.iter (fun (k, v) -> logger.LogInformation("Property {0}={1}", k, v)) + let outputFormat = + results.TryGetResult <@ Output_Format @> + |> Option.map parseOutputFormat + |> Option.defaultValue (Ok OutputFormat.Default) + |> Result.defaultWith (fun errMsg -> + logger.LogError("{0} Using default output format.", errMsg) + OutputFormat.Default) + let analyzersPaths = results.GetResults(<@ Analyzers_Path @>) |> List.concat @@ -659,7 +737,14 @@ let main argv = let results = results |> List.concat - printMessages results + let codeRoot = + match codeRoot with + | None -> Directory.GetCurrentDirectory() |> Uri + | Some root -> Path.GetFullPath root |> Uri + + match outputFormat with + | OutputFormat.Default -> printMessagesInDefaultFormat results + | OutputFormat.GitHub -> printMessagesInGitHubFormat codeRoot results report |> Option.iter (writeReport results codeRoot)