diff --git a/go/extractor/diagnostics/BUILD.bazel b/go/extractor/diagnostics/BUILD.bazel index 436cf15e8549..698c27e02b65 100644 --- a/go/extractor/diagnostics/BUILD.bazel +++ b/go/extractor/diagnostics/BUILD.bazel @@ -1,6 +1,6 @@ # generated running `bazel run //go/gazelle`, do not edit -load("@rules_go//go:def.bzl", "go_library") +load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "diagnostics", @@ -9,3 +9,10 @@ go_library( visibility = ["//visibility:public"], deps = ["//go/extractor/util"], ) + +go_test( + name = "diagnostics_test", + srcs = ["diagnostics_test.go"], + embed = [":diagnostics"], + deps = ["@com_github_stretchr_testify//assert"], +) diff --git a/go/extractor/diagnostics/diagnostics.go b/go/extractor/diagnostics/diagnostics.go index 4a49347f0f76..a91a9efac0d1 100644 --- a/go/extractor/diagnostics/diagnostics.go +++ b/go/extractor/diagnostics/diagnostics.go @@ -3,7 +3,7 @@ package diagnostics import ( "encoding/json" "fmt" - "log" + "log/slog" "os" "strings" "time" @@ -56,18 +56,65 @@ type diagnostic struct { var diagnosticsEmitted, diagnosticsLimit uint = 0, 100 var noDiagnosticDirPrinted bool = false -func emitDiagnostic(sourceid, sourcename, markdownMessage string, severity diagnosticSeverity, visibility *visibilityStruct, location *locationStruct) { - if diagnosticsEmitted < diagnosticsLimit { - diagnosticsEmitted += 1 +type DiagnosticsWriter interface { + WriteDiagnostic(d diagnostic) +} - diagnosticDir := os.Getenv("CODEQL_EXTRACTOR_GO_DIAGNOSTIC_DIR") - if diagnosticDir == "" { - if !noDiagnosticDirPrinted { - log.Println("No diagnostic directory set, so not emitting diagnostic") - noDiagnosticDirPrinted = true - } - return +type FileDiagnosticsWriter struct { + diagnosticDir string +} + +func (writer *FileDiagnosticsWriter) WriteDiagnostic(d diagnostic) { + if writer == nil { + return + } + + content, err := json.Marshal(d) + if err != nil { + slog.Error("Failed to encode diagnostic as JSON", slog.Any("err", err)) + return + } + + targetFile, err := os.CreateTemp(writer.diagnosticDir, "go-extractor.*.json") + if err != nil { + slog.Error("Failed to create diagnostic file", slog.Any("err", err)) + return + } + defer func() { + if err := targetFile.Close(); err != nil { + slog.Error("Failed to close diagnostic file", slog.Any("err", err)) + } + }() + + _, err = targetFile.Write(content) + if err != nil { + slog.Error("Failed to write to diagnostic file", slog.Any("err", err)) + } +} + +var DefaultWriter *FileDiagnosticsWriter = nil + +func NewFileDiagnosticsWriter() *FileDiagnosticsWriter { + diagnosticDir := os.Getenv("CODEQL_EXTRACTOR_GO_DIAGNOSTIC_DIR") + if diagnosticDir == "" { + if !noDiagnosticDirPrinted { + slog.Warn("No diagnostic directory set, so not emitting diagnostics") + noDiagnosticDirPrinted = true } + return nil + } + + return &FileDiagnosticsWriter{diagnosticDir} +} + +func init() { + DefaultWriter = NewFileDiagnosticsWriter() +} + +// Emits a diagnostic using the specified `DiagnosticsWriter`. +func emitDiagnosticTo(writer DiagnosticsWriter, sourceid, sourcename, markdownMessage string, severity diagnosticSeverity, visibility *visibilityStruct, location *locationStruct) { + if diagnosticsEmitted < diagnosticsLimit { + diagnosticsEmitted += 1 timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000") + "Z" @@ -93,33 +140,15 @@ func emitDiagnostic(sourceid, sourcename, markdownMessage string, severity diagn } } - content, err := json.Marshal(d) - if err != nil { - log.Println(err) - return - } - - targetFile, err := os.CreateTemp(diagnosticDir, "go-extractor.*.json") - if err != nil { - log.Println("Failed to create diagnostic file: ") - log.Println(err) - return - } - defer func() { - if err := targetFile.Close(); err != nil { - log.Println("Failed to close diagnostic file:") - log.Println(err) - } - }() - - _, err = targetFile.Write(content) - if err != nil { - log.Println("Failed to write to diagnostic file: ") - log.Println(err) - } + writer.WriteDiagnostic(d) } } +// Emits a diagnostic using the default `DiagnosticsWriter`. +func emitDiagnostic(sourceid, sourcename, markdownMessage string, severity diagnosticSeverity, visibility *visibilityStruct, location *locationStruct) { + emitDiagnosticTo(DefaultWriter, sourceid, sourcename, markdownMessage, severity, visibility, location) +} + func EmitPackageDifferentOSArchitecture(pkgPath string) { emitDiagnostic( "go/autobuilder/package-different-os-architecture", @@ -141,7 +170,7 @@ func plural(n int, singular, plural string) string { const maxNumPkgPaths = 5 -func EmitCannotFindPackages(pkgPaths []string) { +func EmitCannotFindPackages(writer DiagnosticsWriter, pkgPaths []string) { numPkgPaths := len(pkgPaths) numPrinted := numPkgPaths @@ -188,7 +217,8 @@ func EmitCannotFindPackages(pkgPaths []string) { "If any of the packages are already present in the repository, but were not found, then you may need a [custom build command](https://docs.github.com/en/code-security/how-tos/scan-code-for-vulnerabilities/manage-your-configuration/codeql-code-scanning-for-compiled-languages)." } - emitDiagnostic( + emitDiagnosticTo( + writer, "go/autobuilder/package-not-found", "Some packages could not be found", message, diff --git a/go/extractor/diagnostics/diagnostics_test.go b/go/extractor/diagnostics/diagnostics_test.go new file mode 100644 index 000000000000..f2b560004bae --- /dev/null +++ b/go/extractor/diagnostics/diagnostics_test.go @@ -0,0 +1,85 @@ +package diagnostics + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type memoryDiagnosticsWriter struct { + diagnostics []diagnostic +} + +func newMemoryDiagnosticsWriter() *memoryDiagnosticsWriter { + return &memoryDiagnosticsWriter{[]diagnostic{}} +} + +func (writer *memoryDiagnosticsWriter) WriteDiagnostic(d diagnostic) { + writer.diagnostics = append(writer.diagnostics, d) +} + +func Test_EmitCannotFindPackages_Default(t *testing.T) { + writer := newMemoryDiagnosticsWriter() + + // Clear environment variables that affect the diagnostic message. + t.Setenv("GITHUB_EVENT_NAME", "") + t.Setenv("GITHUB_ACTIONS", "") + + EmitCannotFindPackages(writer, []string{"github.com/github/foo"}) + + assert.Len(t, writer.diagnostics, 1, "Expected one diagnostic to be emitted") + + d := writer.diagnostics[0] + assert.Equal(t, d.Source.Id, "go/autobuilder/package-not-found") + assert.Equal(t, d.Severity, string(severityWarning)) + assert.True(t, d.Visibility.CliSummaryTable) + assert.True(t, d.Visibility.StatusPage) + assert.True(t, d.Visibility.Telemetry) + // Non-Actions suggestion for private registries + assert.Contains(t, d.MarkdownMessage, "ensure that the necessary credentials and environment variables are set up") + // Custom build command suggestion + assert.Contains(t, d.MarkdownMessage, "If any of the packages are already present in the repository") +} + +func Test_EmitCannotFindPackages_Dynamic(t *testing.T) { + writer := newMemoryDiagnosticsWriter() + + // Set environment variables that affect the diagnostic message. + t.Setenv("GITHUB_EVENT_NAME", "dynamic") + t.Setenv("GITHUB_ACTIONS", "true") + + EmitCannotFindPackages(writer, []string{"github.com/github/foo"}) + + assert.Len(t, writer.diagnostics, 1, "Expected one diagnostic to be emitted") + + d := writer.diagnostics[0] + assert.Equal(t, d.Source.Id, "go/autobuilder/package-not-found") + assert.Equal(t, d.Severity, string(severityWarning)) + // Dynamic workflow suggestion for private registries + assert.Contains(t, d.MarkdownMessage, "can grant access to private registries for GitHub security products") + // No default suggestions for private registries and custom build command + assert.NotContains(t, d.MarkdownMessage, "ensure that the necessary credentials and environment variables are set up") + assert.NotContains(t, d.MarkdownMessage, "If any of the packages are already present in the repository") +} + +func Test_EmitCannotFindPackages_Actions(t *testing.T) { + writer := newMemoryDiagnosticsWriter() + + // Set environment variables that affect the diagnostic message. + t.Setenv("GITHUB_EVENT_NAME", "push") + t.Setenv("GITHUB_ACTIONS", "true") + + EmitCannotFindPackages(writer, []string{"github.com/github/foo"}) + + assert.Len(t, writer.diagnostics, 1, "Expected one diagnostic to be emitted") + + d := writer.diagnostics[0] + assert.Equal(t, d.Source.Id, "go/autobuilder/package-not-found") + assert.Equal(t, d.Severity, string(severityWarning)) + // Advanced workflow suggestion for private registries + assert.Contains(t, d.MarkdownMessage, "add a step to your workflow which sets up") + // No default suggestion for private registries + assert.NotContains(t, d.MarkdownMessage, "ensure that the necessary credentials and environment variables are set up") + // Custom build command suggestion + assert.Contains(t, d.MarkdownMessage, "If any of the packages are already present in the repository") +} diff --git a/go/extractor/extractor.go b/go/extractor/extractor.go index 314fb8a56c13..bbcd32c10d24 100644 --- a/go/extractor/extractor.go +++ b/go/extractor/extractor.go @@ -223,7 +223,7 @@ func ExtractWithFlags(buildFlags []string, patterns []string, extractTests bool, }) if len(pkgsNotFound) > 0 { - diagnostics.EmitCannotFindPackages(pkgsNotFound) + diagnostics.EmitCannotFindPackages(diagnostics.DefaultWriter, pkgsNotFound) } for _, pkg := range pkgs { diff --git a/go/extractor/go.mod b/go/extractor/go.mod index 62d42b037ef6..c88573bb8c2b 100644 --- a/go/extractor/go.mod +++ b/go/extractor/go.mod @@ -13,4 +13,10 @@ require ( golang.org/x/tools v0.41.0 ) -require golang.org/x/sync v0.19.0 // indirect +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + golang.org/x/sync v0.19.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go/extractor/go.sum b/go/extractor/go.sum index d462d8f36686..838db152fd67 100644 --- a/go/extractor/go.sum +++ b/go/extractor/go.sum @@ -1,8 +1,17 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=