From ed092cc4c6701eed87bd4e0a2c39dfeb53b9f714 Mon Sep 17 00:00:00 2001 From: timhuynh94 Date: Wed, 15 Apr 2026 16:06:38 -0500 Subject: [PATCH 1/2] fix(artifacts): stream log to UI --- executor/linux/outputs.go | 92 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/executor/linux/outputs.go b/executor/linux/outputs.go index db700355..ac7de239 100644 --- a/executor/linux/outputs.go +++ b/executor/linux/outputs.go @@ -225,20 +225,76 @@ func (o *outputSvc) pollFiles(ctx context.Context, ctn *pipeline.Container, _ste // https://pkg.go.dev/github.com/sirupsen/logrus#Entry.WithField logger := o.client.Logger.WithField("artifact-outputs", ctn.Name) + // load the step log so artifact messages are streamed to the UI. + // Wait briefly to allow the StreamStep goroutine to finish its final + // log upload so that we do not race with it. Then re-fetch the log + // from the server to get the authoritative copy that includes all + // container output — this prevents artifact messages from overwriting + // the step's own logs. + time.Sleep(2 * time.Second) + + _log, _, err := o.client.Vela.Log.GetStep(ctx, + b.GetRepo().GetOrg(), b.GetRepo().GetName(), + b.GetNumber(), _step.Number) + if err != nil { + logger.Warnf("unable to fetch step log for artifact streaming: %v", err) + } + + // store back into the map so future references are consistent + if _log != nil { + o.client.stepLogs.Store(_step.ID, _log) + } + + // streamLog appends a message to the step log and pushes it to the server + // so that artifact progress is visible in the UI on the attached step. + streamLog := func(msg string) { + logger.Info(msg) + + if _log == nil { + return + } + + _log.AppendData([]byte(fmt.Sprintf("[artifact] %s\n", msg))) + + _, err := o.client.Vela.Log.UpdateStep(ctx, + b.GetRepo().GetOrg(), b.GetRepo().GetName(), + b.GetNumber(), _step.Number, _log) + if err != nil { + logger.Errorf("unable to update step log: %v", err) + } + } + + streamLog(fmt.Sprintf("starting artifact upload for step %s (build: %d, repo: %s/%s)", + _step.Name, b.GetNumber(), b.GetRepo().GetOrg(), b.GetRepo().GetName())) + streamLog(fmt.Sprintf("configured artifact paths: %v", _step.Artifacts.Paths)) + // grab file paths from the container filesPath, err := o.client.Runtime.PollFileNames(ctx, ctn, _step) if err != nil { + streamLog(fmt.Sprintf("failed to discover artifact files: %v", err)) return fmt.Errorf("unable to poll file names: %w", err) } + streamLog(fmt.Sprintf("discovered %d artifact file(s) matching configured paths", len(filesPath))) + if len(filesPath) == 0 { + streamLog(fmt.Sprintf("no files found matching artifact paths: %v — ensure your step produces files at the expected locations", _step.Artifacts.Paths)) return fmt.Errorf("no files found for file list: %v", _step.Artifacts.Paths) } + logger.Debugf("matched files: %v", filesPath) + // create http client for uploading files to storage putClient := http.DefaultClient putClient.Timeout = time.Second * 30 + // track upload statistics + var ( + uploaded int + skipped int + failed int + ) + // process each file found for _, filePath := range filesPath { fileName := filepath.Base(filePath) @@ -247,31 +303,47 @@ func (o *outputSvc) pollFiles(ctx context.Context, ctn *pipeline.Container, _ste // skip hidden files and files within hidden directories if isHidden(filePath) { logger.Debugf("skipping hidden file or directory: %s", filePath) + skipped++ + continue } url, _, err := o.client.Vela.Build.GetPresignedPutURL(ctx, fileName, b.GetRepo().GetOrg(), b.GetRepo().GetName(), b.GetNumber()) if err != nil { - logger.Errorf("unable to get presigned put url: %v", err) + streamLog(fmt.Sprintf("artifact %q could not be uploaded — the server did not provide an upload URL. "+ + "This may indicate that artifact storage is not configured or the server encountered an error. "+ + "Please contact your Vela administrator if this persists. (error: %v)", fileName, err)) + failed++ + continue } // get file content from container reader, size, err := o.client.Runtime.PollFileContent(ctx, ctn, filePath) if err != nil { - logger.Errorf("unable to poll file content for %s: %v", filePath, err) + streamLog(fmt.Sprintf("unable to read artifact file %q from container: %v", filePath, err)) + failed++ + continue } + logger.Infof("artifact file %q size: %d bytes", fileName, size) + // TODO: surface this skip to the user if o.client.fileSizeLimit > 0 && size > o.client.fileSizeLimit { - logger.Infof("skipping file %s due to file size limit", filePath) + streamLog(fmt.Sprintf("skipping artifact %q — file size (%d bytes) exceeds the per-file limit (%d bytes)", + fileName, size, o.client.fileSizeLimit)) + skipped++ + continue } if o.client.buildFileSizeLimit > 0 && size+o.client.Uploaded > o.client.buildFileSizeLimit { - logger.Infof("skipping file %s due to build file size limit", filePath) + streamLog(fmt.Sprintf("skipping artifact %q — uploading this file would exceed the per-build size limit (%d bytes). "+ + "Total uploaded so far: %d bytes", fileName, o.client.buildFileSizeLimit, o.client.Uploaded)) + skipped++ + continue } @@ -282,17 +354,25 @@ func (o *outputSvc) pollFiles(ctx context.Context, ctn *pipeline.Container, _ste strconv.FormatInt(b.GetNumber(), 10), fileName) - logger.Debugf("uploading file %s to storage with object name %s", filePath, objectName) + streamLog(fmt.Sprintf("uploading artifact %q to storage (object: %s, size: %d bytes)", fileName, objectName, size)) err = uploadObject(ctx, putClient, reader, size, fileName, url.URL) if err != nil { - logger.Errorf("unable to upload object %s: %v", fileName, err) + streamLog(fmt.Sprintf("failed to upload artifact %q: %v", fileName, err)) + failed++ + continue } o.client.Uploaded += size + uploaded++ + + streamLog(fmt.Sprintf("successfully uploaded artifact %q (%d bytes)", fileName, size)) } + streamLog(fmt.Sprintf("artifact upload complete — uploaded: %d, skipped: %d, failed: %d (total files: %d)", + uploaded, skipped, failed, len(filesPath))) + return nil } From 6adc8389a7692266310c368d34f82fe4b2fb639a Mon Sep 17 00:00:00 2001 From: timhuynh94 Date: Thu, 16 Apr 2026 08:56:10 -0500 Subject: [PATCH 2/2] fix linters --- executor/linux/outputs.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/executor/linux/outputs.go b/executor/linux/outputs.go index ac7de239..d447e162 100644 --- a/executor/linux/outputs.go +++ b/executor/linux/outputs.go @@ -303,6 +303,7 @@ func (o *outputSvc) pollFiles(ctx context.Context, ctn *pipeline.Container, _ste // skip hidden files and files within hidden directories if isHidden(filePath) { logger.Debugf("skipping hidden file or directory: %s", filePath) + skipped++ continue @@ -314,6 +315,7 @@ func (o *outputSvc) pollFiles(ctx context.Context, ctn *pipeline.Container, _ste streamLog(fmt.Sprintf("artifact %q could not be uploaded — the server did not provide an upload URL. "+ "This may indicate that artifact storage is not configured or the server encountered an error. "+ "Please contact your Vela administrator if this persists. (error: %v)", fileName, err)) + failed++ continue @@ -323,6 +325,7 @@ func (o *outputSvc) pollFiles(ctx context.Context, ctn *pipeline.Container, _ste reader, size, err := o.client.Runtime.PollFileContent(ctx, ctn, filePath) if err != nil { streamLog(fmt.Sprintf("unable to read artifact file %q from container: %v", filePath, err)) + failed++ continue @@ -334,6 +337,7 @@ func (o *outputSvc) pollFiles(ctx context.Context, ctn *pipeline.Container, _ste if o.client.fileSizeLimit > 0 && size > o.client.fileSizeLimit { streamLog(fmt.Sprintf("skipping artifact %q — file size (%d bytes) exceeds the per-file limit (%d bytes)", fileName, size, o.client.fileSizeLimit)) + skipped++ continue @@ -342,6 +346,7 @@ func (o *outputSvc) pollFiles(ctx context.Context, ctn *pipeline.Container, _ste if o.client.buildFileSizeLimit > 0 && size+o.client.Uploaded > o.client.buildFileSizeLimit { streamLog(fmt.Sprintf("skipping artifact %q — uploading this file would exceed the per-build size limit (%d bytes). "+ "Total uploaded so far: %d bytes", fileName, o.client.buildFileSizeLimit, o.client.Uploaded)) + skipped++ continue @@ -359,6 +364,7 @@ func (o *outputSvc) pollFiles(ctx context.Context, ctn *pipeline.Container, _ste err = uploadObject(ctx, putClient, reader, size, fileName, url.URL) if err != nil { streamLog(fmt.Sprintf("failed to upload artifact %q: %v", fileName, err)) + failed++ continue