From 61758e74ba7d9259be62e0564a8b5e8a69f17b0a Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Tue, 19 May 2026 12:29:35 +0200 Subject: [PATCH] batches: upload large exec artifacts Batch executor jobs can produce stdout, stderr, and diffs large enough to bloat the final JSONL result stream. Upload oversized step artifacts from the existing `src batch exec` execution path instead of adding a separate command or user-facing auth flags. `src batch exec` now discovers the executor job context from the environment, uses the existing executor job bearer token plus executor name auth model to stream large artifacts to the Batch Changes artifacts endpoint, and records returned references on `AfterStepResult`. Small outputs stay inline for compatibility, and per-step progress output is suppressed when artifact uploads are enabled so large logs are not duplicated into task-step JSONL events. companian branch in sg: wb/proto-exec-upload Test Plan: - go test ./cmd/src ./internal/batches/executor ./internal/batches/ui github.com/sourcegraph/sourcegraph/lib/batches/execution --- cmd/src/batch_exec.go | 9 +- cmd/src/batch_exec_artifacts.go | 108 +++++++++++++++++++ cmd/src/batch_exec_artifacts_test.go | 69 ++++++++++++ internal/batches/executor/artifacts.go | 143 +++++++++++++++++++++++++ internal/batches/executor/run_steps.go | 67 ++++++++---- internal/batches/ui/json_lines.go | 21 ++-- lib/batches/execution/results.go | 70 ++++++++---- 7 files changed, 439 insertions(+), 48 deletions(-) create mode 100644 cmd/src/batch_exec_artifacts.go create mode 100644 cmd/src/batch_exec_artifacts_test.go create mode 100644 internal/batches/executor/artifacts.go diff --git a/cmd/src/batch_exec.go b/cmd/src/batch_exec.go index 11aadf39e3..addfcd3faa 100644 --- a/cmd/src/batch_exec.go +++ b/cmd/src/batch_exec.go @@ -123,7 +123,11 @@ Examples: } func executeBatchSpecInWorkspaces(ctx context.Context, flags *executorModeFlags) (err error) { - ui := &ui.JSONLines{BinaryDiffs: flags.binaryDiffs} + artifactUploader, err := newBatchArtifactUploaderFromEnv(cfg.endpointURL) + if err != nil { + return err + } + ui := &ui.JSONLines{BinaryDiffs: flags.binaryDiffs, SuppressStepOutput: artifactUploader != nil} // Ensure the temp dir exists. tempDir := flags.tempDir @@ -214,6 +218,9 @@ func executeBatchSpecInWorkspaces(ctx context.Context, flags *executorModeFlags) UI: taskExecUI.StepsExecutionUI(task), ForceRoot: !flags.runAsImageUser, BinaryDiffs: flags.binaryDiffs, + + ArtifactUploader: artifactUploader, + ArtifactUploadChunkSize: defaultArtifactUploadChunkSize, } results, err := executor.RunSteps(ctx, opts) diff --git a/cmd/src/batch_exec_artifacts.go b/cmd/src/batch_exec_artifacts.go new file mode 100644 index 0000000000..f26ff373c6 --- /dev/null +++ b/cmd/src/batch_exec_artifacts.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "os" + "strconv" + "strings" + + "github.com/sourcegraph/src-cli/internal/batches/executor" + + "github.com/sourcegraph/sourcegraph/lib/batches/execution" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +const defaultArtifactUploadChunkSize = 1 << 20 + +type batchArtifactUploader struct { + endpointURL *url.URL + jobID int + jobToken string + executorName string + client *http.Client +} + +var _ executor.ArtifactUploader = (*batchArtifactUploader)(nil) + +func newBatchArtifactUploaderFromEnv(endpointURL *url.URL) (*batchArtifactUploader, error) { + jobID, _ := strconv.Atoi(os.Getenv("SRC_EXECUTOR_JOB_ID")) + jobToken := os.Getenv("SRC_EXECUTOR_JOB_TOKEN") + executorName := os.Getenv("SRC_EXECUTOR_NAME") + + configured := jobID != 0 || jobToken != "" || executorName != "" + if !configured { + return nil, nil + } + if jobID == 0 || jobToken == "" || executorName == "" { + return nil, errors.New("artifact upload requires job ID, job token, and executor name") + } + + return &batchArtifactUploader{ + endpointURL: endpointURL, + jobID: jobID, + jobToken: jobToken, + executorName: executorName, + client: http.DefaultClient, + }, nil +} + +func (u *batchArtifactUploader) Upload(ctx context.Context, artifactKey string, r io.Reader) (execution.ArtifactReference, error) { + if strings.Contains(artifactKey, "/") || strings.Contains(artifactKey, "\\") || strings.Contains(artifactKey, "..") { + return execution.ArtifactReference{}, errors.Newf("invalid artifact key %q", artifactKey) + } + sizeReader := &artifactSizeReader{r: r} + + url := u.endpointURL.JoinPath( + ".executors", + "queue", + "batches", + "jobs", + strconv.Itoa(u.jobID), + "artifacts", + artifactKey, + ) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), sizeReader) + if err != nil { + return execution.ArtifactReference{}, err + } + req.Header.Set("Authorization", "Bearer "+u.jobToken) + req.Header.Set("X-Sourcegraph-Executor-Name", u.executorName) + + resp, err := u.client.Do(req) + if err != nil { + return execution.ArtifactReference{}, err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return execution.ArtifactReference{}, errors.Newf("artifact upload failed with status %s: %s", resp.Status, strings.TrimSpace(string(body))) + } + + var ref execution.ArtifactReference + if err := json.NewDecoder(resp.Body).Decode(&ref); err != nil { + return execution.ArtifactReference{}, errors.Wrap(err, "decoding artifact upload response") + } + if ref.URL == "" && ref.ObjectStorageKey == "" { + return execution.ArtifactReference{}, errors.New("artifact upload response did not include a URL or object storage key") + } + ref.Size = sizeReader.size + return ref, nil +} + +type artifactSizeReader struct { + r io.Reader + size int64 +} + +func (r *artifactSizeReader) Read(p []byte) (int, error) { + n, err := r.r.Read(p) + if n > 0 { + r.size += int64(n) + } + return n, err +} diff --git a/cmd/src/batch_exec_artifacts_test.go b/cmd/src/batch_exec_artifacts_test.go new file mode 100644 index 0000000000..923cf280fa --- /dev/null +++ b/cmd/src/batch_exec_artifacts_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/sourcegraph/sourcegraph/lib/batches/execution" +) + +func TestBatchArtifactUploaderUploadAddsMetadata(t *testing.T) { + const artifactContents = "artifact contents" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Fatalf("unexpected method %q", r.Method) + } + if r.URL.Path != "/.executors/queue/batches/jobs/42/artifacts/stdout" { + t.Fatalf("unexpected path %q", r.URL.Path) + } + if got := r.Header.Get("Authorization"); got != "Bearer token" { + t.Fatalf("unexpected authorization header %q", got) + } + if got := r.Header.Get("X-Sourcegraph-Executor-Name"); got != "executor" { + t.Fatalf("unexpected executor header %q", got) + } + + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + if string(body) != artifactContents { + t.Fatalf("unexpected body %q", string(body)) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(execution.ArtifactReference{ObjectStorageKey: "key"}) + })) + t.Cleanup(server.Close) + + endpointURL, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + uploader := &batchArtifactUploader{ + endpointURL: endpointURL, + jobID: 42, + jobToken: "token", + executorName: "executor", + client: server.Client(), + } + + ref, err := uploader.Upload(context.Background(), "stdout", strings.NewReader(artifactContents)) + if err != nil { + t.Fatal(err) + } + + if ref.ObjectStorageKey != "key" { + t.Fatalf("unexpected object storage key %q", ref.ObjectStorageKey) + } + if ref.Size != int64(len(artifactContents)) { + t.Fatalf("unexpected size %d", ref.Size) + } +} diff --git a/internal/batches/executor/artifacts.go b/internal/batches/executor/artifacts.go new file mode 100644 index 0000000000..5f669da927 --- /dev/null +++ b/internal/batches/executor/artifacts.go @@ -0,0 +1,143 @@ +package executor + +import ( + "bytes" + "context" + "fmt" + "io" + "math" + "os" + + "github.com/sourcegraph/sourcegraph/lib/batches/execution" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +const maxInlineArtifactSize = math.MaxInt64 + +type ArtifactUploader interface { + Upload(ctx context.Context, artifactKey string, r io.Reader) (execution.ArtifactReference, error) +} + +type stepOutput struct { + stdout *artifactOutput + stderr *artifactOutput +} + +func newStepOutput(dir string, threshold int64) (*stepOutput, error) { + stdout, err := newArtifactOutput(dir, "stdout-*", threshold) + if err != nil { + return nil, err + } + stderr, err := newArtifactOutput(dir, "stderr-*", threshold) + if err != nil { + stdout.cleanup() + return nil, err + } + return &stepOutput{stdout: stdout, stderr: stderr}, nil +} + +func (o *stepOutput) cleanup() { + o.stdout.cleanup() + o.stderr.cleanup() +} + +type artifactOutput struct { + file *os.File + buf bytes.Buffer + size int64 + threshold int64 +} + +func newArtifactOutput(dir, pattern string, threshold int64) (*artifactOutput, error) { + file, err := os.CreateTemp(dir, pattern) + if err != nil { + return nil, errors.Wrap(err, "creating artifact output file") + } + return &artifactOutput{file: file, threshold: threshold}, nil +} + +func (o *artifactOutput) writer() io.Writer { return o } + +func (o *artifactOutput) Write(p []byte) (int, error) { + n, err := o.file.Write(p) + o.size += int64(n) + if o.size <= o.threshold { + _, _ = o.buf.Write(p[:n]) + } + return n, err +} + +func (o *artifactOutput) inline() string { + if o.size > o.threshold { + return "" + } + return o.buf.String() +} + +func (o *artifactOutput) shouldUpload(threshold int64) bool { + return o.size > threshold +} + +func (o *artifactOutput) reader() (io.Reader, error) { + if _, err := o.file.Seek(0, io.SeekStart); err != nil { + return nil, errors.Wrap(err, "rewinding artifact output file") + } + return o.file, nil +} + +func (o *artifactOutput) cleanup() { + if o.file == nil { + return + } + name := o.file.Name() + _ = o.file.Close() + _ = os.Remove(name) + o.file = nil +} + +func uploadStepArtifacts(ctx context.Context, uploader ArtifactUploader, threshold int64, stepIndex int, result *execution.AfterStepResult, output *stepOutput) error { + defer output.cleanup() + + if output.stdout.shouldUpload(threshold) { + ref, err := uploadArtifactOutput(ctx, uploader, artifactKey(stepIndex, "stdout"), output.stdout) + if err != nil { + return err + } + result.StdoutArtifact = &ref + } + + if output.stderr.shouldUpload(threshold) { + ref, err := uploadArtifactOutput(ctx, uploader, artifactKey(stepIndex, "stderr"), output.stderr) + if err != nil { + return err + } + result.StderrArtifact = &ref + } + + if int64(len(result.Diff)) > threshold { + ref, err := uploader.Upload(ctx, artifactKey(stepIndex, "diff"), bytes.NewReader(result.Diff)) + if err != nil { + return errors.Wrap(err, "uploading diff artifact") + } + result.DiffArtifact = &ref + result.Diff = nil + } + + return nil +} + +func uploadArtifactOutput(ctx context.Context, uploader ArtifactUploader, key string, output *artifactOutput) (execution.ArtifactReference, error) { + reader, err := output.reader() + if err != nil { + return execution.ArtifactReference{}, err + } + ref, err := uploader.Upload(ctx, key, reader) + if err != nil { + return execution.ArtifactReference{}, errors.Wrapf(err, "uploading %s artifact", key) + } + return ref, nil +} + +func artifactKey(stepIndex int, name string) string { + return fmt.Sprintf("step-%d-%s", stepIndex, name) +} diff --git a/internal/batches/executor/run_steps.go b/internal/batches/executor/run_steps.go index 47b5715887..6ee36773b4 100644 --- a/internal/batches/executor/run_steps.go +++ b/internal/batches/executor/run_steps.go @@ -57,6 +57,13 @@ type RunStepsOpts struct { ForceRoot bool BinaryDiffs bool + + // ArtifactUploader uploads oversized step artifacts when set. Small artifacts + // remain inline for backwards compatibility. + ArtifactUploader ArtifactUploader + // ArtifactUploadChunkSize is the number of bytes an artifact may contain + // before it is externalized through ArtifactUploader. + ArtifactUploadChunkSize int64 } func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution.AfterStepResult, err error) { @@ -175,7 +182,7 @@ func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution. return nil, err } - stdoutBuffer, stderrBuffer, err := executeSingleStep(ctx, opts, ws, i, step, digest, &stepContext) + stepOutput, err := executeSingleStep(ctx, opts, ws, i, step, digest, &stepContext) defer func() { if err != nil { exitCode := -1 @@ -189,6 +196,7 @@ func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution. if err != nil { return stepResults, err } + defer stepOutput.cleanup() // Get the current diff and store that away as the per-step result. stepDiff, err := ws.Diff(ctx) @@ -209,14 +217,20 @@ func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution. stepResult := execution.AfterStepResult{ Version: version, ChangedFiles: changes, - Stdout: stdoutBuffer.String(), - Stderr: stderrBuffer.String(), + Stdout: stepOutput.stdout.inline(), + Stderr: stepOutput.stderr.inline(), StepIndex: i, Diff: stepDiff, // Those will be set below. Outputs: make(map[string]any), } + if opts.ArtifactUploader != nil { + if err := uploadStepArtifacts(ctx, opts.ArtifactUploader, opts.ArtifactUploadChunkSize, i, &stepResult, stepOutput); err != nil { + return stepResults, err + } + } + // Set stepContext.Step to current step's results before rendering outputs. stepContext.Step = stepResult // Render and evaluate outputs. @@ -251,7 +265,20 @@ func executeSingleStep( step batcheslib.Step, imageDigest string, stepContext *template.StepContext, -) (stdout bytes.Buffer, stderr bytes.Buffer, err error) { +) (output *stepOutput, err error) { + artifactUploadThreshold := opts.ArtifactUploadChunkSize + if opts.ArtifactUploader == nil { + artifactUploadThreshold = maxInlineArtifactSize + } + output, err = newStepOutput(opts.TempDir, artifactUploadThreshold) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + output.cleanup() + } + }() // ---------- // PREPARATION // ---------- @@ -260,7 +287,7 @@ func executeSingleStep( cidFile, cleanup, err := createCidFile(ctx, opts.TempDir, util.SlugForRepo(opts.Task.Repository.Name, opts.Task.Repository.Rev())) if err != nil { opts.UI.StepPreparingFailed(stepIdx+1, err) - return bytes.Buffer{}, bytes.Buffer{}, err + return output, err } defer cleanup() @@ -269,13 +296,13 @@ func executeSingleStep( if err != nil { err = errors.Wrapf(err, "probing image %q for shell", step.Container) opts.UI.StepPreparingFailed(stepIdx+1, err) - return bytes.Buffer{}, bytes.Buffer{}, err + return output, err } runScriptFile, runScript, cleanup, err := createRunScriptFile(ctx, opts.TempDir, step.Run, stepContext) if err != nil { opts.UI.StepPreparingFailed(stepIdx+1, err) - return bytes.Buffer{}, bytes.Buffer{}, err + return output, err } defer cleanup() @@ -283,7 +310,7 @@ func executeSingleStep( filesToMount, cleanup, err := createFilesToMount(opts.TempDir, step, stepContext) if err != nil { opts.UI.StepPreparingFailed(stepIdx+1, err) - return bytes.Buffer{}, bytes.Buffer{}, err + return output, err } defer cleanup() @@ -292,7 +319,7 @@ func executeSingleStep( if err != nil { err = errors.Wrap(err, "resolving step environment") opts.UI.StepPreparingFailed(stepIdx+1, err) - return bytes.Buffer{}, bytes.Buffer{}, err + return output, err } // Render the step.Env variables as templates. @@ -300,7 +327,7 @@ func executeSingleStep( if err != nil { err = errors.Wrap(err, "parsing step environment") opts.UI.StepPreparingFailed(stepIdx+1, err) - return bytes.Buffer{}, bytes.Buffer{}, err + return output, err } opts.UI.StepPreparingSuccess(stepIdx + 1) @@ -312,7 +339,7 @@ func executeSingleStep( workspaceOpts, err := workspace.DockerRunOpts(ctx, workDir) if err != nil { - return bytes.Buffer{}, bytes.Buffer{}, errors.Wrap(err, "getting Docker options for workspace") + return output, errors.Wrap(err, "getting Docker options for workspace") } // Where should we execute the steps.run script? @@ -342,7 +369,7 @@ func executeSingleStep( for _, mount := range step.Mount { workspaceFilePath, err := getAbsoluteMountPath(opts.WorkingDirectory, mount.Path) if err != nil { - return bytes.Buffer{}, bytes.Buffer{}, err + return output, err } args = append(args, "--mount", fmt.Sprintf("type=bind,source=%s,target=%s,ro", workspaceFilePath, mount.Mountpoint)) } @@ -366,13 +393,13 @@ func executeSingleStep( outputWriter.Close() }() - stdoutWriter := io.MultiWriter(&stdout, outputWriter.StdoutWriter(), opts.Logger.PrefixWriter("stdout")) - stderrWriter := io.MultiWriter(&stderr, outputWriter.StderrWriter(), opts.Logger.PrefixWriter("stderr")) + stdoutWriter := io.MultiWriter(output.stdout.writer(), outputWriter.StdoutWriter(), opts.Logger.PrefixWriter("stdout")) + stderrWriter := io.MultiWriter(output.stderr.writer(), outputWriter.StderrWriter(), opts.Logger.PrefixWriter("stderr")) // Setup readers that pipe the output into the given buffers wg, err := process.PipeOutput(ctx, cmd, stdoutWriter, stderrWriter) if err != nil { - return stdout, stderr, errors.Wrap(err, "piping process output") + return output, errors.Wrap(err, "piping process output") } newStepFailedErr := func(wrappedErr error) stepFailedErr { @@ -388,8 +415,8 @@ func executeSingleStep( Run: runScript, Container: step.Container, TmpFilename: containerTemp, - Stdout: strings.TrimSpace(stdout.String()), - Stderr: strings.TrimSpace(stderr.String()), + Stdout: strings.TrimSpace(output.stdout.inline()), + Stderr: strings.TrimSpace(output.stderr.inline()), } } @@ -400,7 +427,7 @@ func executeSingleStep( t0 := time.Now() if err := cmd.Start(); err != nil { opts.Logger.Logf("[Step %d] error starting Docker container: %+v", stepIdx+1, err) - return stdout, stderr, newStepFailedErr(err) + return output, newStepFailedErr(err) } // Wait for the readers, because the pipes used by PipeOutput under the @@ -412,11 +439,11 @@ func executeSingleStep( elapsed := time.Since(t0).Round(time.Millisecond) if err != nil { opts.Logger.Logf("[Step %d] took %s; error running Docker container: %+v", stepIdx+1, elapsed, err) - return stdout, stderr, newStepFailedErr(err) + return output, newStepFailedErr(err) } opts.Logger.Logf("[Step %d] complete in %s", stepIdx+1, elapsed) - return stdout, stderr, nil + return output, nil } func setOutputs(stepOutputs batcheslib.Outputs, global map[string]any, stepCtx *template.StepContext) error { diff --git a/internal/batches/ui/json_lines.go b/internal/batches/ui/json_lines.go index d5c36c8da2..77867b5abd 100644 --- a/internal/batches/ui/json_lines.go +++ b/internal/batches/ui/json_lines.go @@ -24,7 +24,8 @@ import ( var _ ExecUI = &JSONLines{} type JSONLines struct { - BinaryDiffs bool + BinaryDiffs bool + SuppressStepOutput bool } func (ui *JSONLines) ParsingBatchSpec() { @@ -92,7 +93,8 @@ func (ui *JSONLines) CheckingCacheSuccess(cachedSpecsFound int, tasksToExecute i func (ui *JSONLines) ExecutingTasks(_ bool, _ int) executor.TaskExecutionUI { return &taskExecutionJSONLines{ - binaryDiffs: ui.BinaryDiffs, + binaryDiffs: ui.BinaryDiffs, + suppressStepOutput: ui.SuppressStepOutput, } } @@ -186,8 +188,9 @@ Error: %s } type taskExecutionJSONLines struct { - linesTasks map[*executor.Task]batcheslib.JSONLinesTask - binaryDiffs bool + linesTasks map[*executor.Task]batcheslib.JSONLinesTask + binaryDiffs bool + suppressStepOutput bool } // seededRand is used in randomID() to generate a "random" number. @@ -265,12 +268,13 @@ func (ui *taskExecutionJSONLines) StepsExecutionUI(task *executor.Task) executor panic("unknown task started") } - return &stepsExecutionJSONLines{linesTask: <} + return &stepsExecutionJSONLines{linesTask: <, binaryDiffs: ui.binaryDiffs, suppressStepOutput: ui.suppressStepOutput} } type stepsExecutionJSONLines struct { - linesTask *batcheslib.JSONLinesTask - binaryDiffs bool + linesTask *batcheslib.JSONLinesTask + binaryDiffs bool + suppressStepOutput bool } const stepFlushDuration = 500 * time.Millisecond @@ -319,6 +323,9 @@ func (ui *stepsExecutionJSONLines) StepStarted(step int, runScript string, env m } func (ui *stepsExecutionJSONLines) StepOutputWriter(ctx context.Context, task *executor.Task, step int) executor.StepOutputWriter { + if ui.suppressStepOutput { + return executor.NoopStepOutputWriter{} + } sink := func(data string) { logOperationProgress( batcheslib.LogEventOperationTaskStep, diff --git a/lib/batches/execution/results.go b/lib/batches/execution/results.go index 4426430708..53203f0367 100644 --- a/lib/batches/execution/results.go +++ b/lib/batches/execution/results.go @@ -14,29 +14,47 @@ type AfterStepResult struct { ChangedFiles git.Changes `json:"changedFiles"` // Stdout is the output produced by the step on standard out. Stdout string `json:"stdout"` + // StdoutArtifact points to externally stored standard output when Stdout is + // too large to inline. + StdoutArtifact *ArtifactReference `json:"stdoutArtifact,omitempty"` // Stderr is the output produced by the step on standard error. Stderr string `json:"stderr"` + // StderrArtifact points to externally stored standard error when Stderr is + // too large to inline. + StderrArtifact *ArtifactReference `json:"stderrArtifact,omitempty"` // StepIndex is the index of the step in the list of steps. StepIndex int `json:"stepIndex"` // Diff is the cumulative `git diff` after executing the Step. Diff []byte `json:"diff"` + // DiffArtifact points to an externally stored diff when Diff is too large to + // inline. + DiffArtifact *ArtifactReference `json:"diffArtifact,omitempty"` // Outputs is a copy of the Outputs after executing the Step. Outputs map[string]any `json:"outputs"` // Skipped determines whether the step was skipped. Skipped bool `json:"skipped"` } +type ArtifactReference struct { + URL string `json:"url,omitempty"` + ObjectStorageKey string `json:"objectStorageKey,omitempty"` + Size int64 `json:"size,omitempty"` +} + func (a AfterStepResult) MarshalJSON() ([]byte, error) { if a.Version == 2 { return json.Marshal(v2AfterStepResult(a)) } return json.Marshal(v1AfterStepResult{ - ChangedFiles: a.ChangedFiles, - Stdout: a.Stdout, - Stderr: a.Stderr, - StepIndex: a.StepIndex, - Diff: string(a.Diff), - Outputs: a.Outputs, + ChangedFiles: a.ChangedFiles, + Stdout: a.Stdout, + StdoutArtifact: a.StdoutArtifact, + Stderr: a.Stderr, + StderrArtifact: a.StderrArtifact, + StepIndex: a.StepIndex, + Diff: string(a.Diff), + DiffArtifact: a.DiffArtifact, + Outputs: a.Outputs, }) } @@ -53,9 +71,12 @@ func (a *AfterStepResult) UnmarshalJSON(data []byte) error { a.Version = v2.Version a.ChangedFiles = v2.ChangedFiles a.Stdout = v2.Stdout + a.StdoutArtifact = v2.StdoutArtifact a.Stderr = v2.Stderr + a.StderrArtifact = v2.StderrArtifact a.StepIndex = v2.StepIndex a.Diff = v2.Diff + a.DiffArtifact = v2.DiffArtifact a.Outputs = v2.Outputs a.Skipped = v2.Skipped return nil @@ -66,9 +87,12 @@ func (a *AfterStepResult) UnmarshalJSON(data []byte) error { } a.ChangedFiles = v1.ChangedFiles a.Stdout = v1.Stdout + a.StdoutArtifact = v1.StdoutArtifact a.Stderr = v1.Stderr + a.StderrArtifact = v1.StderrArtifact a.StepIndex = v1.StepIndex a.Diff = []byte(v1.Diff) + a.DiffArtifact = v1.DiffArtifact a.Outputs = v1.Outputs return nil } @@ -78,21 +102,27 @@ type versionAfterStepResult struct { } type v2AfterStepResult struct { - Version int `json:"version"` - ChangedFiles git.Changes `json:"changedFiles"` - Stdout string `json:"stdout"` - Stderr string `json:"stderr"` - StepIndex int `json:"stepIndex"` - Diff []byte `json:"diff"` - Outputs map[string]any `json:"outputs"` - Skipped bool `json:"skipped"` + Version int `json:"version"` + ChangedFiles git.Changes `json:"changedFiles"` + Stdout string `json:"stdout"` + StdoutArtifact *ArtifactReference `json:"stdoutArtifact,omitempty"` + Stderr string `json:"stderr"` + StderrArtifact *ArtifactReference `json:"stderrArtifact,omitempty"` + StepIndex int `json:"stepIndex"` + Diff []byte `json:"diff"` + DiffArtifact *ArtifactReference `json:"diffArtifact,omitempty"` + Outputs map[string]any `json:"outputs"` + Skipped bool `json:"skipped"` } type v1AfterStepResult struct { - ChangedFiles git.Changes `json:"changedFiles"` - Stdout string `json:"stdout"` - Stderr string `json:"stderr"` - StepIndex int `json:"stepIndex"` - Diff string `json:"diff"` - Outputs map[string]any `json:"outputs"` + ChangedFiles git.Changes `json:"changedFiles"` + Stdout string `json:"stdout"` + StdoutArtifact *ArtifactReference `json:"stdoutArtifact,omitempty"` + Stderr string `json:"stderr"` + StderrArtifact *ArtifactReference `json:"stderrArtifact,omitempty"` + StepIndex int `json:"stepIndex"` + Diff string `json:"diff"` + DiffArtifact *ArtifactReference `json:"diffArtifact,omitempty"` + Outputs map[string]any `json:"outputs"` }