Allow non-ActionExecutionException DetailedExceptions to populate the detailed exit code on a failed keep-going execution phase, and enrich the message when possible to try to pin down any remaining issues.

Not changing the name of the code/deprecating because we don't yet know what the issue is: perhaps it's random other DetailedExceptions, or exceptions that are not properly bubbling up, etc.

PiperOrigin-RevId: 334455888
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
index 8fb293e..0e32500 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
@@ -21,7 +21,7 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
-import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.skyframe.DetailedException;
 import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.ExitCode;
 import net.starlark.java.syntax.Location;
@@ -31,7 +31,7 @@
  * Typically these are re-raised ExecException throwables.
  */
 @ThreadSafe
-public class ActionExecutionException extends Exception {
+public class ActionExecutionException extends Exception implements DetailedException {
 
   private final ActionAnalysisMetadata action;
   private final NestedSet<Cause> rootCauses;
@@ -150,10 +150,7 @@
     return detailedExitCode.getExitCode();
   }
 
-  /**
-   * Returns the pair of {@link ExitCode} and optional {@link FailureDetail} to return from this
-   * Bazel invocation because of this action execution failure.
-   */
+  @Override
   public DetailedExitCode getDetailedExitCode() {
     return detailedExitCode;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BUILD b/src/main/java/com/google/devtools/build/lib/actions/BUILD
index a645223..4470dea 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/actions/BUILD
@@ -80,6 +80,7 @@
         "//src/main/java/com/google/devtools/build/lib/profiler",
         "//src/main/java/com/google/devtools/build/lib/profiler:google-auto-profiler-utils",
         "//src/main/java/com/google/devtools/build/lib/shell",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:detailed_exceptions",
         "//src/main/java/com/google/devtools/build/lib/skyframe:sane_analysis_exception",
         "//src/main/java/com/google/devtools/build/lib/skyframe:sky_functions",
         "//src/main/java/com/google/devtools/build/lib/skyframe:skyframe_aware_action",
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java b/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java
index effbe9a..d31e005 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java
@@ -21,7 +21,7 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
-import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.skyframe.DetailedException;
 import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.ExitCode;
 
@@ -39,7 +39,7 @@
  * propagated by specifying the exit code to the constructor using a {@link DetailedExitCode}.
  */
 @ThreadSafe
-public class BuildFailedException extends Exception {
+public class BuildFailedException extends Exception implements DetailedException {
   private final boolean catastrophic;
   private final NestedSet<Cause> rootCauses;
   private final boolean errorAlreadyShown;
@@ -79,10 +79,7 @@
     return errorAlreadyShown || getMessage() == null;
   }
 
-  /**
-   * Returns the pair of {@link ExitCode} and optional {@link FailureDetail} to return from this
-   * Bazel invocation.
-   */
+  @Override
   public DetailedExitCode getDetailedExitCode() {
     return detailedExitCode;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java b/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java
index c31c653..247ff7a 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Range;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.GoogleLogger;
 import com.google.devtools.build.lib.actions.ActionCacheChecker;
 import com.google.devtools.build.lib.actions.ActionExecutionException;
 import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
@@ -50,6 +51,7 @@
 import com.google.devtools.build.lib.skyframe.AspectValueKey.AspectKey;
 import com.google.devtools.build.lib.skyframe.Builder;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
+import com.google.devtools.build.lib.skyframe.DetailedException;
 import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
 import com.google.devtools.build.lib.skyframe.TopDownActionCache;
 import com.google.devtools.build.lib.util.AbruptExitException;
@@ -76,6 +78,7 @@
  */
 @VisibleForTesting
 public class SkyframeBuilder implements Builder {
+  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
 
   private final ResourceManager resourceManager;
   private final SkyframeExecutor skyframeExecutor;
@@ -276,22 +279,42 @@
         //   2. First non-infrastructure error with non-null exit code
         //   3. If the build fails but no interpretable error is specified, BUILD_FAILURE.
         DetailedExitCode detailedExitCode = null;
+        Throwable undetailedCause = null;
         for (Map.Entry<SkyKey, ErrorInfo> error : result.errorMap().entrySet()) {
           Throwable cause = error.getValue().getException();
-          if (cause instanceof ActionExecutionException) {
+          if (cause instanceof DetailedException) {
             // Update global exit code when current exit code is not null and global exit code has
             // a lower 'reporting' priority.
             detailedExitCode =
                 DetailedExitCodeComparator.chooseMoreImportantWithFirstIfTie(
-                    detailedExitCode, ((ActionExecutionException) cause).getDetailedExitCode());
+                    detailedExitCode, ((DetailedException) cause).getDetailedExitCode());
+            if (!(cause instanceof ActionExecutionException)
+                && !(cause instanceof MissingInputFileException)) {
+              logger.atWarning().withCause(cause).log(
+                  "Non-action-execution/missing-input exception for %s", error);
+            }
+          } else {
+            undetailedCause = cause;
           }
         }
-
-        return detailedExitCode != null
-            ? detailedExitCode
-            : createDetailedExitCode(
-                "keep_going execution failed without an action failure",
-                Code.NON_ACTION_EXECUTION_FAILURE);
+        if (detailedExitCode != null) {
+          return detailedExitCode;
+        }
+        if (undetailedCause == null) {
+          logger.atWarning().log("No exceptions found despite error in %s", result);
+          return createDetailedExitCode(
+              "keep_going execution failed without an action failure",
+              Code.NON_ACTION_EXECUTION_FAILURE);
+        }
+        logger.atWarning().withCause(undetailedCause).log(
+            "No detailed exception found in %s", result);
+        return createDetailedExitCode(
+            "keep_going execution failed without an action failure: "
+                + undetailedCause.getMessage()
+                + " ("
+                + undetailedCause.getClass().getSimpleName()
+                + ")",
+            Code.NON_ACTION_EXECUTION_FAILURE);
       }
       ErrorInfo errorInfo = Preconditions.checkNotNull(result.getError(), result);
       Exception exception = errorInfo.getException();
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
index 8fb3804..f6b7d73 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
@@ -362,6 +362,7 @@
         "//src/main/java/com/google/devtools/build/lib/bazel:modules",
         "//src/main/java/com/google/devtools/build/lib/causes",
         "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//src/main/protobuf:failure_details_java_proto",
         "//src/test/java/com/google/devtools/build/lib/buildtool/util",
         "//src/test/java/com/google/devtools/build/lib/packages:testutil",
         "//src/test/java/com/google/devtools/build/lib/testutil",
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/MissingInputActionTest.java b/src/test/java/com/google/devtools/build/lib/buildtool/MissingInputActionTest.java
index bc5ea49..e9477f2 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/MissingInputActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/MissingInputActionTest.java
@@ -23,6 +23,8 @@
 import com.google.devtools.build.lib.causes.Cause;
 import com.google.devtools.build.lib.packages.util.MockGenruleSupport;
 import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.testutil.Suite;
 import com.google.devtools.build.lib.testutil.TestSpec;
 import com.google.devtools.build.lib.vfs.Path;
@@ -113,8 +115,33 @@
     }
   }
 
+  @Test
+  public void testMissingTopLevelInput() throws Exception {
+    // Create a rule that exports a missing source file as a top-level artifact so that the missing
+    // file will be detected by the TargetCompletion function, not an ActionExecution function.
+    write(
+        "foo/missing.bzl",
+        "def _missing_impl(ctx):",
+        "    return DefaultInfo(files = depset(ctx.files.srcs))",
+        "",
+        "missing = rule(",
+        "               implementation = _missing_impl,",
+        "               attrs = { 'srcs': attr.label_list(allow_files = True) }",
+        ")");
+    write(
+        "foo/BUILD",
+        "load('missing.bzl', 'missing')",
+        "missing(name = 'foo', srcs = ['missing.sh'])");
+    addOptions("--keep_going");
+    assertMissingInputOnBuild("//foo:foo", 0);
+    events.assertContainsError("foo/BUILD:2:8: //foo:foo: missing input file '//foo:missing.sh'");
+    events.assertContainsEventWithFrequency("missing input file", 1);
+  }
+
   private void assertMissingInputOnBuild(String label, int numMissing) {
     BuildFailedException e = assertThrows(BuildFailedException.class, () -> buildTarget(label));
+    FailureDetail failureDetail = e.getDetailedExitCode().getFailureDetail();
+    assertThat(failureDetail.getExecution().getCode()).isEqualTo(Code.SOURCE_INPUT_MISSING);
     if (numMissing > 0) {
       String expected = numMissing + " input file(s) do not exist";
         assertThat(e).hasMessageThat().contains(expected);