diff --git a/cli/integration_tests/monorepo_one_script_error/monorepo/apps/my-app/package.json b/cli/integration_tests/monorepo_one_script_error/monorepo/apps/my-app/package.json index e9a8240dfe9fb..1e0506a390ec7 100644 --- a/cli/integration_tests/monorepo_one_script_error/monorepo/apps/my-app/package.json +++ b/cli/integration_tests/monorepo_one_script_error/monorepo/apps/my-app/package.json @@ -1,6 +1,7 @@ { "name": "my-app", "scripts": { + "okay": "echo 'working'", "error": "echo 'intentionally failing' && exit 2" } } diff --git a/cli/integration_tests/monorepo_one_script_error/monorepo/turbo.json b/cli/integration_tests/monorepo_one_script_error/monorepo/turbo.json index 97c84fc604045..261c09d8d4b81 100644 --- a/cli/integration_tests/monorepo_one_script_error/monorepo/turbo.json +++ b/cli/integration_tests/monorepo_one_script_error/monorepo/turbo.json @@ -3,6 +3,9 @@ "pipeline": { "error": { "outputs": ["foo"] + }, + "okay": { + "outputs": [] } } } diff --git a/cli/integration_tests/monorepo_one_script_error/run.t b/cli/integration_tests/monorepo_one_script_error/run.t index 4b3adfdfc242a..2c7a9704b25b3 100644 --- a/cli/integration_tests/monorepo_one_script_error/run.t +++ b/cli/integration_tests/monorepo_one_script_error/run.t @@ -8,7 +8,7 @@ Note that npm reports any failed script as exit code 1, even though we "exit 2" \xe2\x80\xa2 Packages in scope: my-app (esc) \xe2\x80\xa2 Running error in 1 packages (esc) \xe2\x80\xa2 Remote caching disabled (esc) - my-app:error: cache miss, executing f66954acab3dda71 + my-app:error: cache miss, executing 15d3d7967bc433e3 my-app:error: my-app:error: > error my-app:error: > echo 'intentionally failing' && exit 2 @@ -33,7 +33,7 @@ Make sure it isn't cached \xe2\x80\xa2 Packages in scope: my-app (esc) \xe2\x80\xa2 Running error in 1 packages (esc) \xe2\x80\xa2 Remote caching disabled (esc) - my-app:error: cache miss, executing f66954acab3dda71 + my-app:error: cache miss, executing 15d3d7967bc433e3 my-app:error: my-app:error: > error my-app:error: > echo 'intentionally failing' && exit 2 @@ -52,3 +52,27 @@ Make sure it isn't cached ERROR run failed: command exited (1) [1] + +Running with --output-mode=errors-only gives error output only + $ ${TURBO} --output-logs=errors-only error okay + \xe2\x80\xa2 Packages in scope: my-app (esc) + \xe2\x80\xa2 Running error, okay in 1 packages (esc) + \xe2\x80\xa2 Remote caching disabled (esc) + my-app:error: ERROR: command finished with error: command \(.*/run\.t/apps/my-app\) npm run error exited \(1\) (re) + my-app:error: + my-app:error: > error + my-app:error: > echo 'intentionally failing' && exit 2 + my-app:error: + my-app:error: intentionally failing + my-app:error: npm ERR! Lifecycle script `error` failed with error: + my-app:error: npm ERR! Error: command failed + my-app:error: npm ERR! in workspace: my-app + my-app:error: npm ERR! at location: .*/run.t/apps/my-app (re) + command \(.*/run.t/apps/my-app\) npm run error exited \(1\) (re) + + Tasks: 1 successful, 2 total + Cached: 0 cached, 2 total + Time:\s*[\.0-9]+m?s (re) + + ERROR run failed: command exited (1) + [1] diff --git a/cli/integration_tests/single_package_deps/run.t b/cli/integration_tests/single_package_deps/run.t index f82c48d44b133..3475b6712f77d 100644 --- a/cli/integration_tests/single_package_deps/run.t +++ b/cli/integration_tests/single_package_deps/run.t @@ -39,4 +39,33 @@ Run a second time, verify caching works because there is a config Tasks: 2 successful, 2 total Cached: 2 cached, 2 total Time:\s*[\.0-9]+m?s >>> FULL TURBO (re) - \ No newline at end of file + +Run with --output-logs=hash-only + $ ${TURBO} run test --single-package --output-logs=hash-only + \xe2\x80\xa2 Running test (esc) + \xe2\x80\xa2 Remote caching disabled (esc) + build: cache hit, suppressing output e04d8bf0e58a455b + test: cache hit, suppressing output 804a9b713dc63f25 + + Tasks: 2 successful, 2 total + Cached: 2 cached, 2 total + Time:\s*[\.0-9]+m?s >>> FULL TURBO (re) + +Run with --output-logs=errors-only + $ ${TURBO} run test --single-package --output-logs=errors-only + \xe2\x80\xa2 Running test (esc) + \xe2\x80\xa2 Remote caching disabled (esc) + + Tasks: 2 successful, 2 total + Cached: 2 cached, 2 total + Time:\s*[\.0-9]+m?s >>> FULL TURBO (re) + +Run with --output-logs=none + $ ${TURBO} run test --single-package --output-logs=none + \xe2\x80\xa2 Running test (esc) + \xe2\x80\xa2 Remote caching disabled (esc) + + Tasks: 2 successful, 2 total + Cached: 2 cached, 2 total + Time:\s*[\.0-9]+m?s >>> FULL TURBO (re) + diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index afd33164ab527..8d066e6795a59 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -1124,6 +1124,9 @@ func (ec *execContext) exec(ctx gocontext.Context, packageTask *nodes.PackageTas } else { prefixedUI.Warn("command finished with error, but continuing...") } + + taskCache.OnError(prefixedUI, progressLogger) + return err } diff --git a/cli/internal/runcache/runcache.go b/cli/internal/runcache/runcache.go index 7250892f9a6ca..41e54bf329a38 100644 --- a/cli/internal/runcache/runcache.go +++ b/cli/internal/runcache/runcache.go @@ -154,7 +154,7 @@ type TaskCache struct { // Returns true if successful. func (tc TaskCache) RestoreOutputs(ctx context.Context, prefixedUI *cli.PrefixedUi, progressLogger hclog.Logger) (bool, error) { if tc.cachingDisabled || tc.rc.readsDisabled { - if tc.taskOutputMode != util.NoTaskOutput { + if tc.taskOutputMode != util.NoTaskOutput && tc.taskOutputMode != util.ErrorTaskOutput { prefixedUI.Output(fmt.Sprintf("cache bypass, force executing %s", ui.Dim(tc.hash))) } return false, nil @@ -175,7 +175,7 @@ func (tc TaskCache) RestoreOutputs(ctx context.Context, prefixedUI *cli.Prefixed if err != nil { return false, err } else if !hit { - if tc.taskOutputMode != util.NoTaskOutput { + if tc.taskOutputMode != util.NoTaskOutput && tc.taskOutputMode != util.ErrorTaskOutput { prefixedUI.Output(fmt.Sprintf("cache miss, executing %s", ui.Dim(tc.hash))) } return false, nil @@ -198,10 +198,9 @@ func (tc TaskCache) RestoreOutputs(ctx context.Context, prefixedUI *cli.Prefixed case util.FullTaskOutput: progressLogger.Debug("log file", "path", tc.LogFileName) prefixedUI.Info(fmt.Sprintf("cache hit, replaying output %s", ui.Dim(tc.hash))) - if tc.LogFileName.FileExists() { - - tc.rc.logReplayer(progressLogger, prefixedUI, tc.LogFileName) - } + tc.ReplayLogFile(prefixedUI, progressLogger) + case util.ErrorTaskOutput: + // The task succeeded, so we don't output anything in this case default: // NoLogs, do not output anything } @@ -209,6 +208,22 @@ func (tc TaskCache) RestoreOutputs(ctx context.Context, prefixedUI *cli.Prefixed return true, nil } +// ReplayLogFile writes out the stored logfile to the terminal +func (tc TaskCache) ReplayLogFile(prefixedUI *cli.PrefixedUi, progressLogger hclog.Logger) { + if tc.LogFileName.FileExists() { + tc.rc.logReplayer(progressLogger, prefixedUI, tc.LogFileName) + } +} + +// OnError +// If output-mode is set to "errors-only", we'll have buffered output +// to disk without logging it. Now we can log that here. +func (tc TaskCache) OnError(terminal *cli.PrefixedUi, logger hclog.Logger) { + if tc.taskOutputMode == util.ErrorTaskOutput { + tc.ReplayLogFile(terminal, logger) + } +} + // nopWriteCloser is modeled after io.NopCloser, which is for Readers type nopWriteCloser struct { io.Writer @@ -253,7 +268,7 @@ func (tc TaskCache) OutputWriter(prefix string) (io.WriteCloser, error) { file: output, bufio: bufWriter, } - if tc.taskOutputMode == util.NoTaskOutput || tc.taskOutputMode == util.HashTaskOutput { + if tc.taskOutputMode == util.NoTaskOutput || tc.taskOutputMode == util.HashTaskOutput || tc.taskOutputMode == util.ErrorTaskOutput { // only write to log file, not to stdout fwc.Writer = bufWriter } else { diff --git a/cli/internal/util/task_output_mode.go b/cli/internal/util/task_output_mode.go index ec6d7ee5178f4..bcc2abf6abc16 100644 --- a/cli/internal/util/task_output_mode.go +++ b/cli/internal/util/task_output_mode.go @@ -17,13 +17,16 @@ const ( HashTaskOutput // NewTaskOutput will show all new task output and turbo-computed task hashes for cached output NewTaskOutput + // ErrorTaskOutput will show task output for failures only; no cache miss/hit messages are emitted + ErrorTaskOutput ) const ( - fullTaskOutputString = "full" - noTaskOutputString = "none" - hashTaskOutputString = "hash-only" - newTaskOutputString = "new-only" + fullTaskOutputString = "full" + noTaskOutputString = "none" + hashTaskOutputString = "hash-only" + newTaskOutputString = "new-only" + errorTaskOutputString = "errors-only" ) // TaskOutputModeStrings is an array containing the string representations for task output modes @@ -32,6 +35,7 @@ var TaskOutputModeStrings = []string{ noTaskOutputString, hashTaskOutputString, newTaskOutputString, + errorTaskOutputString, } // FromTaskOutputModeString converts a task output mode's string representation into the enum value @@ -45,6 +49,8 @@ func FromTaskOutputModeString(value string) (TaskOutputMode, error) { return HashTaskOutput, nil case newTaskOutputString: return NewTaskOutput, nil + case errorTaskOutputString: + return ErrorTaskOutput, nil } return FullTaskOutput, fmt.Errorf("invalid task output mode: %v", value) @@ -61,6 +67,8 @@ func ToTaskOutputModeString(value TaskOutputMode) (string, error) { return hashTaskOutputString, nil case NewTaskOutput: return newTaskOutputString, nil + case ErrorTaskOutput: + return errorTaskOutputString, nil } return "", fmt.Errorf("invalid task output mode: %v", value)