From 21d6bf371e6ec8f218b1c6d6fda7ef7b7a9be142 Mon Sep 17 00:00:00 2001
From: Matt Toohey <mtoohey@block.xyz>
Date: Fri, 1 Nov 2024 15:36:32 +1100
Subject: [PATCH] fix: language plugins should always include an error with
 level=ERROR for BuildFailure (#3296)

Otherwise `buildengine` tries to read a nil schema value leading to a
crash
---
 go-runtime/goplugin/service.go                     |  4 ++--
 internal/buildengine/languageplugin/plugin.go      | 11 +++++++++++
 internal/buildengine/languageplugin/plugin_test.go |  3 ++-
 3 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/go-runtime/goplugin/service.go b/go-runtime/goplugin/service.go
index 894dc9083a..208072fde3 100644
--- a/go-runtime/goplugin/service.go
+++ b/go-runtime/goplugin/service.go
@@ -419,8 +419,8 @@ func build(ctx context.Context, projectRoot, stubsRoot string, buildCtx buildCon
 	m, buildErrs, err := compile.Build(ctx, projectRoot, stubsRoot, buildCtx.Config, buildCtx.Schema, transaction, false)
 	if err != nil {
 		return buildFailure(buildCtx, isAutomaticRebuild, builderrors.Error{
-			Type:  builderrors.FTL,
-			Level: builderrors.ErrorLevel(builderrors.COMPILER),
+			Type:  builderrors.COMPILER,
+			Level: builderrors.ERROR,
 			Msg:   "compile: " + err.Error(),
 		}), nil
 	}
diff --git a/internal/buildengine/languageplugin/plugin.go b/internal/buildengine/languageplugin/plugin.go
index a681361a3f..6e57a2972a 100644
--- a/internal/buildengine/languageplugin/plugin.go
+++ b/internal/buildengine/languageplugin/plugin.go
@@ -552,6 +552,17 @@ func buildResultFromProto(result either.Either[*langpb.BuildEvent_BuildSuccess,
 
 		errs := langpb.ErrorsFromProto(buildFailure.Errors)
 		builderrors.SortErrorsByPosition(errs)
+
+		if !builderrors.ContainsTerminalError(errs) {
+			// This happens if the language plugin returns BuildFailure but does not include any errors with level ERROR.
+			// Language plugins should always include at least one error with level ERROR in the case of a build failure.
+			errs = append(errs, builderrors.Error{
+				Msg:   "unexpected build failure without error level ERROR",
+				Level: builderrors.ERROR,
+				Type:  builderrors.FTL,
+			})
+		}
+
 		return BuildResult{
 			Errors:                 errs,
 			InvalidateDependencies: buildFailure.InvalidateDependencies,
diff --git a/internal/buildengine/languageplugin/plugin_test.go b/internal/buildengine/languageplugin/plugin_test.go
index 5e1c94a479..335f639d4c 100644
--- a/internal/buildengine/languageplugin/plugin_test.go
+++ b/internal/buildengine/languageplugin/plugin_test.go
@@ -412,7 +412,8 @@ func buildEventWithBuildError(contextID string, isAutomaticRebuild bool, msg str
 				IsAutomaticRebuild: isAutomaticRebuild,
 				Errors: langpb.ErrorsToProto([]builderrors.Error{
 					{
-						Msg: msg,
+						Msg:   msg,
+						Level: builderrors.ERROR,
 					},
 				}),
 			},