Skip to content

Commit bc778c3

Browse files
authored
Merge pull request #230 from fkj/add-github-logging-output-format
Add GitHub logging output format
2 parents 31f668f + 1e0d381 commit bc778c3

File tree

2 files changed

+104
-8
lines changed

2 files changed

+104
-8
lines changed

docs/content/Running during CI.md

+11
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Example when using MSBuild:
4242

4343
## GitHub Actions
4444

45+
### GitHub Advanced Security
4546
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/).
4647

4748
```yml
@@ -68,4 +69,14 @@ Sample:
6869

6970
See [fsproject/fantomas#2962](https://github.com/fsprojects/fantomas/pull/2962) for more information.
7071

72+
### Github Workflow Commands
73+
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`.
74+
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).
75+
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.
76+
If the annotations don't show correctly, you might need to set the `code-root` to the root of the repository.
77+
78+
Note that GitHub has a hard limit of 10 annotations of each type (notice, warning, error) per CI step.
79+
This means that only the first 10 errors, the first 10 warnings and the first 10 hints/info results from analyzers will generate annotations.
80+
The workflow log will contain all analyzer results even if a job hits the annotation limits.
81+
7182
[Previous]({{fsdocs-previous-page-link}})

src/FSharp.Analyzers.Cli/Program.fs

+93-8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Arguments =
3535
| [<Unique>] FSC_Args of string
3636
| [<Unique>] Code_Root of string
3737
| [<Unique; AltCommandLine("-v")>] Verbosity of string
38+
| [<Unique>] Output_Format of string
3839

3940
interface IArgParserTemplate with
4041
member s.Usage =
@@ -67,6 +68,8 @@ type Arguments =
6768
| FSC_Args _ -> "Pass in the raw fsc compiler arguments. Cannot be combined with the `--project` flag."
6869
| Code_Root _ ->
6970
"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."
71+
| Output_Format _ ->
72+
"Format in which to write analyzer results to stdout. The available options are: default, github."
7073

7174
type SeverityMappings =
7275
{
@@ -103,6 +106,16 @@ let mapMessageToSeverity (mappings: SeverityMappings) (msg: FSharp.Analyzers.SDK
103106
}
104107
}
105108

109+
[<RequireQualifiedAccess>]
110+
type OutputFormat =
111+
| Default
112+
| GitHub
113+
114+
let parseOutputFormat = function
115+
| "github" -> Ok OutputFormat.GitHub
116+
| "default" -> Ok OutputFormat.Default
117+
| other -> Error $"Unknown output format: %s{other}."
118+
106119
let mutable logLevel = LogLevel.Warning
107120

108121
let fcs = Utils.createFCS None
@@ -258,7 +271,7 @@ let runFscArgs
258271

259272
runProject client projectOptions excludeIncludeFiles mappings
260273

261-
let printMessages (msgs: AnalyzerMessage list) =
274+
let printMessagesInDefaultFormat (msgs: AnalyzerMessage list) =
262275

263276
let severityToLogLevel =
264277
Map.ofArray
@@ -300,13 +313,70 @@ let printMessages (msgs: AnalyzerMessage list) =
300313

301314
()
302315

303-
let writeReport (results: AnalyzerMessage list) (codeRoot: string option) (report: string) =
304-
try
305-
let codeRoot =
306-
match codeRoot with
307-
| None -> Directory.GetCurrentDirectory() |> Uri
308-
| Some root -> Path.GetFullPath root |> Uri
316+
let printMessagesInGitHubFormat (codeRoot : Uri) (msgs: AnalyzerMessage list) =
317+
let severityToLogLevel =
318+
Map.ofArray
319+
[|
320+
Severity.Error, LogLevel.Error
321+
Severity.Warning, LogLevel.Warning
322+
Severity.Info, LogLevel.Information
323+
Severity.Hint, LogLevel.Trace
324+
|]
325+
326+
let severityToGitHubAnnotationType =
327+
Map.ofArray
328+
[|
329+
Severity.Error, "error"
330+
Severity.Warning, "warning"
331+
Severity.Info, "notice"
332+
Severity.Hint, "notice"
333+
|]
334+
335+
if List.isEmpty msgs then
336+
logger.LogInformation("No messages found from the analyzer(s)")
337+
338+
use factory =
339+
LoggerFactory.Create(fun builder ->
340+
builder
341+
.AddCustomFormatter(fun options -> options.UseAnalyzersMsgStyle <- true)
342+
.SetMinimumLevel(LogLevel.Trace)
343+
|> ignore
344+
)
345+
346+
// No category name because GitHub needs the annotation type to be the first
347+
// element on each line.
348+
let msgLogger = factory.CreateLogger("")
349+
350+
msgs
351+
|> List.iter (fun analyzerMessage ->
352+
let m = analyzerMessage.Message
353+
354+
// We want file names to be relative to the repository so GitHub will recognize them.
355+
// GitHub also only understands Unix-style directory separators.
356+
let relativeFileName =
357+
codeRoot.MakeRelativeUri(Uri(m.Range.FileName))
358+
|> _.OriginalString
359+
360+
msgLogger.Log(
361+
severityToLogLevel[m.Severity],
362+
"::{0} file={1},line={2},endLine={3},col={4},endColumn={5},title={6} ({7})::{8}: {9}",
363+
severityToGitHubAnnotationType[m.Severity],
364+
relativeFileName,
365+
m.Range.StartLine,
366+
m.Range.EndLine,
367+
m.Range.StartColumn,
368+
m.Range.EndColumn,
369+
analyzerMessage.Name,
370+
m.Code,
371+
m.Severity.ToString(),
372+
m.Message
373+
)
374+
)
375+
376+
()
309377

378+
let writeReport (results: AnalyzerMessage list) (codeRoot: Uri) (report: string) =
379+
try
310380
// Construct full path to ensure path separators are normalized.
311381
let report = Path.GetFullPath report
312382
// Ensure the parent directory exists
@@ -551,6 +621,14 @@ let main argv =
551621
properties
552622
|> List.iter (fun (k, v) -> logger.LogInformation("Property {0}={1}", k, v))
553623

624+
let outputFormat =
625+
results.TryGetResult <@ Output_Format @>
626+
|> Option.map parseOutputFormat
627+
|> Option.defaultValue (Ok OutputFormat.Default)
628+
|> Result.defaultWith (fun errMsg ->
629+
logger.LogError("{0} Using default output format.", errMsg)
630+
OutputFormat.Default)
631+
554632
let analyzersPaths =
555633
results.GetResults(<@ Analyzers_Path @>)
556634
|> List.concat
@@ -659,7 +737,14 @@ let main argv =
659737

660738
let results = results |> List.concat
661739

662-
printMessages results
740+
let codeRoot =
741+
match codeRoot with
742+
| None -> Directory.GetCurrentDirectory() |> Uri
743+
| Some root -> Path.GetFullPath root |> Uri
744+
745+
match outputFormat with
746+
| OutputFormat.Default -> printMessagesInDefaultFormat results
747+
| OutputFormat.GitHub -> printMessagesInGitHubFormat codeRoot results
663748

664749
report |> Option.iter (writeReport results codeRoot)
665750

0 commit comments

Comments
 (0)