88 "time"
99
1010 "github.com/buildbuddy-io/buildbuddy/cli/arg"
11+ "github.com/buildbuddy-io/buildbuddy/cli/executions"
1112 "github.com/buildbuddy-io/buildbuddy/cli/log"
1213 "github.com/buildbuddy-io/buildbuddy/cli/login"
1314 "github.com/buildbuddy-io/buildbuddy/server/real_environment"
4546 inputRoot = flags .String ("input_root" , "" , "Input root directory. By default, the action will have no inputs. Incompatible with --input_root_digest." )
4647 inputRootDigest = flags .String ("input_root_digest" , "" , "Digest of the input root directory. This is useful to re-run an existing action. Users can also use `bb download` to fetch the input tree locally. Incompatible with --input_root." )
4748 outputPaths = flag .New (flags , "output_path" , []string {}, "Path to an expected output file or directory. The path should be relative to the workspace root. This flag can be specified more than once." )
49+ output = flags .String ("output" , "stdio" , "Output format: `stdio` to print command stdout/stderr, `id` to print only execution ID, `json` to print execution response and logs as JSON, or `markdown` (or `md`) to print a markdown summary." )
4850 // Note: bazel has remote_default_exec_properties but it has somewhat
4951 // confusing semantics, so we call this "exec_properties" to avoid
5052 // confusion.
@@ -92,6 +94,13 @@ func HandleExecute(args []string) (int, error) {
9294 log .Print (usage )
9395 return 1 , nil
9496 }
97+ * output = strings .ToLower (* output )
98+ if * output == "md" {
99+ * output = "markdown"
100+ }
101+ if * output != "stdio" && * output != "id" && * output != "json" && * output != "markdown" {
102+ return - 1 , fmt .Errorf ("invalid --output %q (allowed values: stdio, id, json, markdown, md)" , * output )
103+ }
95104 if err := execute (cmdArgs ); err != nil {
96105 return - 1 , err
97106 }
@@ -187,14 +196,16 @@ func execute(cmdArgs []string) error {
187196 }
188197 log .Debugf ("Waiting for execution to complete" )
189198 var rsp * rexec.Response
199+ var executionErr error
190200 for {
191201 msg , err := stream .Recv ()
192202 if err != nil {
193203 return err
194204 }
195205 if msg .Err != nil {
196- // We failed to execute.
197- return msg .Err
206+ // Keep track of execution errors so we can still render available
207+ // response logs for structured output modes.
208+ executionErr = msg .Err
198209 }
199210 // Log execution state
200211 progress := & repb.ExecutionProgress {}
@@ -213,18 +224,51 @@ func execute(cmdArgs []string) error {
213224 break
214225 }
215226 }
227+ // Defensive guard: we should only exit the loop on a Done message, which
228+ // always sets rsp. Keep this check to avoid nil dereference if that
229+ // invariant is ever broken by future changes.
230+ if rsp == nil {
231+ return fmt .Errorf ("execute stream ended before completion" )
232+ }
216233 log .Debugf ("Execution completed in %s" , time .Since (stageStart ))
217- stageStart = time .Now ()
218- log .Debugf ("Downloading result" )
219- res , err := rexec .GetResult (ctx , env , * instanceName , df , rsp .ExecuteResponse .GetResult ())
220- if err != nil {
221- return status .WrapError (err , "execution failed" )
234+ if executionErr != nil && * output == "stdio" {
235+ return executionErr
222236 }
223- log .Debugf ("Downloaded results in %s" , time .Since (stageStart ))
224- log .Debugf ("End-to-end execution time: %s" , time .Since (start ))
237+ var logs * rexec.ExecutionLogs
238+ if * output == "json" || * output == "markdown" {
239+ logs , err = rexec .GetExecutionLogs (ctx , env .GetByteStreamClient (), * instanceName , df , rsp .ExecuteResponse )
240+ if err != nil {
241+ log .Warnf ("Could not fetch all execution logs: %s" , err )
242+ }
243+ }
244+ switch * output {
245+ case "id" :
246+ if _ , err := fmt .Fprintln (os .Stdout , rsp .GetName ()); err != nil {
247+ return err
248+ }
249+ case "json" :
250+ if err := executions .WriteJSONOutput (os .Stdout , rsp .GetName (), rsp .ExecuteResponse , logs ); err != nil {
251+ return fmt .Errorf ("write json output: %w" , err )
252+ }
253+ case "markdown" :
254+ if _ , err := fmt .Fprint (os .Stdout , executions .RenderMarkdownWithDetails (rsp .GetName (), rsp .ExecuteResponse , logs )); err != nil {
255+ return err
256+ }
257+ case "stdio" :
258+ stageStart = time .Now ()
259+ log .Debugf ("Downloading result" )
260+ res , err := rexec .GetResult (ctx , env , * instanceName , df , rsp .ExecuteResponse .GetResult ())
261+ if err != nil {
262+ return status .WrapError (err , "execution failed" )
263+ }
264+ log .Debugf ("Downloaded results in %s" , time .Since (stageStart ))
225265
226- os .Stdout .Write (res .Stdout )
227- os .Stderr .Write (res .Stderr )
266+ os .Stdout .Write (res .Stdout )
267+ os .Stderr .Write (res .Stderr )
268+ default :
269+ return fmt .Errorf ("invalid --output %q" , * output )
270+ }
271+ log .Debugf ("End-to-end execution time: %s" , time .Since (start ))
228272
229273 if * responseJSONFile != "" {
230274 b , err := protojson .Marshal (rsp .ExecuteResponse )
@@ -235,6 +279,9 @@ func execute(cmdArgs []string) error {
235279 return fmt .Errorf ("write response JSON file: %w" , err )
236280 }
237281 }
282+ if executionErr != nil {
283+ return executionErr
284+ }
238285
239286 return nil
240287}
0 commit comments