Skip to content

feat(ci): add log export downloads#500

Merged
121watts merged 1 commit intomainfrom
watts/dep-4263-log-export-cli
May 6, 2026
Merged

feat(ci): add log export downloads#500
121watts merged 1 commit intomainfrom
watts/dep-4263-log-export-cli

Conversation

@121watts
Copy link
Copy Markdown
Contributor

@121watts 121watts commented May 5, 2026

Summary

depot ci logs can now download exported job logs to a file through the public CI API instead of relying only on stdout rendering. The new --output-file path streams RPC export chunks directly into a temp file and renames on success, keeping stdout clean for callers.

The command prints a lightweight stderr indicator while the download is running and reports the final byte count after the file is written. Human status surfaces also advertise the download command where it is actually useful: depot ci status shows it only under finished attempts, and depot ci workflow show shows it next to the latest-attempt log command only when that latest attempt is finished.

What Changed

  • Added generated client support for ExportJobAttemptLogs from depot/api#3631.
  • Added depot ci logs --output-file <path> with timestamped text as the default export format.
  • Mapped --output text --output-file to text and --output json --output-file to JSONL.
  • Rejected --follow --output-file and --output-file - before network work.
  • Preserved existing stdout behavior for historical logs, --follow, --timestamps, and --output json.
  • Added stderr start/done messages for file downloads, including the completed file size.
  • Added download hints to ci status and ci workflow show only for finished attempts.

Design Notes

The API export is a framed Connect/gRPC stream, not a raw HTTP download URL. The CLI is the shell-native path for writing export bytes to disk, and the displayed download command is intentionally limited to finished attempts so users do not mistake a partial snapshot for final logs.

The export RPC does not know the final response size before streaming; the CLI reports bytes after the atomic rename succeeds instead of showing a misleading progress bar.

Validation

  • make generate
  • gofmt on touched Go files
  • go test ./pkg/cmd/ci
  • go test ./pkg/cmd/ci ./pkg/api
  • go test ./...
  • go vet ./pkg/cmd/ci ./pkg/api
  • make bin/depot
  • Live DEPOT_API_URL=http://127.0.0.1:8080 ./bin/depot ci logs ghj7phtgc7 --output-file /tmp/depot-logs-indicator.txt
  • git diff --check

Compound Engineering
GPT-5.5


Note

Medium Risk
Adds a new Connect streaming RPC for exporting CI logs and wires it into the CLI with new file-writing behavior; main risk is correctness/robustness of stream framing and atomic file handling across error cases.

Overview
Adds a new CI API surface for finite log exports: introduces ExportJobAttemptLogs in the proto/Connect client and a Go wrapper CIExportJobAttemptLogs that validates targets, requires metadata-before-chunks, and streams chunk bytes into a caller-provided writer.

Extends depot ci logs with --output-file to download exports to disk (atomic temp file + rename), defaults to text export (or JSONL when --output json), suppresses interactive selection and stdout noise during downloads, and rejects invalid combos like --follow with --output-file or --output-file -.

Updates human-oriented ci status and ci workflow show output to advertise a download command (using logs.txt) only for finished attempts, and adds targeted tests covering stream framing errors, fallback resolution, and temp-file cleanup on export failures.

Reviewed by Cursor Bugbot for commit 29b7ba0. Bugbot is set up for automated code reviews on this repo. Configure here.

@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 5, 2026

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: --output-file doesn't disable interactive TUI picker
    • Modified logTargetResolutionOptionsForOutput to check outputFile parameter and disable interactive mode when --output-file is set, preventing scripts from hanging on ambiguous job selection.

Create PR

Or push these changes by commenting:

@cursor push ecc73be0a7
Preview (ecc73be0a7)
diff --git a/pkg/cmd/ci/logs.go b/pkg/cmd/ci/logs.go
--- a/pkg/cmd/ci/logs.go
+++ b/pkg/cmd/ci/logs.go
@@ -129,7 +129,7 @@
 			reporter := newLogFollowReporter(reporterWriter, reporterInteractive)
 
 			// First, try resolving as a run ID (or job ID — the API accepts both).
-			resolutionOptions := logTargetResolutionOptionsForOutput(outputOptions)
+			resolutionOptions := logTargetResolutionOptionsForOutput(outputOptions, outputFile)
 			resp, runErr := ciGetRunStatus(ctx, tokenVal, orgID, id)
 			if runErr == nil {
 				target, err := resolveLogTargetWithOptions(resp, id, job, workflow, resolutionOptions)
@@ -807,8 +807,8 @@
 	allowInteractive bool
 }
 
-func logTargetResolutionOptionsForOutput(outputOptions logOutputOptions) logTargetResolutionOptions {
-	return logTargetResolutionOptions{allowInteractive: !outputOptions.json()}
+func logTargetResolutionOptionsForOutput(outputOptions logOutputOptions, outputFile string) logTargetResolutionOptions {
+	return logTargetResolutionOptions{allowInteractive: !outputOptions.json() && outputFile == ""}
 }
 
 // jobKeyShort returns the short form of a job key (after the first colon),

diff --git a/pkg/cmd/ci/logs_test.go b/pkg/cmd/ci/logs_test.go
--- a/pkg/cmd/ci/logs_test.go
+++ b/pkg/cmd/ci/logs_test.go
@@ -75,7 +75,7 @@
 		},
 	}
 
-	options := logTargetResolutionOptionsForOutput(logOutputOptions{output: logOutputJSON})
+	options := logTargetResolutionOptionsForOutput(logOutputOptions{output: logOutputJSON}, "")
 	if options.allowInteractive {
 		t.Fatal("json output should disable interactive job resolution")
 	}
@@ -89,6 +89,34 @@
 	}
 }
 
+func TestResolveLogTargetOutputFileOptionsReturnNonInteractiveAmbiguity(t *testing.T) {
+	resp := &civ1.GetRunStatusResponse{
+		RunId: "run-1",
+		Workflows: []*civ1.WorkflowStatus{
+			{
+				WorkflowPath: ".depot/workflows/ci.yml",
+				Jobs: []*civ1.JobStatus{
+					{JobId: "job-1", JobKey: "build", Status: "finished"},
+					{JobId: "job-2", JobKey: "test", Status: "running"},
+				},
+			},
+		},
+	}
+
+	options := logTargetResolutionOptionsForOutput(logOutputOptions{output: logOutputText}, "/tmp/logs.txt")
+	if options.allowInteractive {
+		t.Fatal("output-file should disable interactive job resolution")
+	}
+
+	_, err := resolveLogTargetWithOptions(resp, "run-1", "", "", options)
+	if err == nil {
+		t.Fatal("expected ambiguity error")
+	}
+	if !strings.Contains(err.Error(), "run has multiple jobs, specify one with --job") {
+		t.Fatalf("expected multiple-jobs ambiguity error, got: %v", err)
+	}
+}
+
 func TestFindLogsJob_MatchByJobKey(t *testing.T) {
 	resp := &civ1.GetRunStatusResponse{
 		RunId: "run-1",

You can send follow-ups to the cloud agent here.

Comment thread pkg/cmd/ci/logs.go
@121watts 121watts force-pushed the watts/dep-4263-log-export-cli branch 2 times, most recently from b0763ec to 8220e30 Compare May 5, 2026 19:18
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Silent success without file when no logs available
    • Modified code to write noLogsMessage to stderr via cmd.ErrOrStderr() when outputFile is set, ensuring user receives feedback instead of silent success.

Create PR

Or push these changes by commenting:

@cursor push 937bcbab52
Preview (937bcbab52)
diff --git a/pkg/cmd/ci/logs.go b/pkg/cmd/ci/logs.go
--- a/pkg/cmd/ci/logs.go
+++ b/pkg/cmd/ci/logs.go
@@ -136,15 +136,19 @@
 				if follow && err != nil {
 					target, err = resolveLogTargetWithFollowRetry(ctx, tokenVal, orgID, id, job, workflow, err, reporter, resolutionOptions)
 				}
-				if err != nil {
-					return err
-				}
-				if target.noLogsMessage != "" {
+			if err != nil {
+				return err
+			}
+			if target.noLogsMessage != "" {
+				if outputFile != "" {
+					fmt.Fprintln(cmd.ErrOrStderr(), target.noLogsMessage)
+				} else {
 					reporter.Message(target.noLogsMessage)
-					return nil
 				}
+				return nil
+			}
 
-				if outputFile != "" {
+			if outputFile != "" {
 					return downloadLogsToFile(ctx, tokenVal, orgID, api.CILogStreamTarget{AttemptID: target.attemptID}, outputOptions, outputFile, cmd.ErrOrStderr())
 				} else if follow {
 					if err := streamLogsWithFollowUX(ctx, tokenVal, orgID, target, cmd.OutOrStdout(), reporter, outputOptions); err != nil {

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 8220e30. Configure here.

Comment thread pkg/cmd/ci/logs.go
@121watts 121watts force-pushed the watts/dep-4263-log-export-cli branch from 8220e30 to 29b7ba0 Compare May 5, 2026 20:42
Comment thread proto/depot/ci/v1/ci.proto
@121watts 121watts merged commit 23b0fe2 into main May 6, 2026
18 checks passed
@121watts 121watts deleted the watts/dep-4263-log-export-cli branch May 6, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants