From ccbbe1005061e43da545e6a3703128f0514bbefb Mon Sep 17 00:00:00 2001 From: Mehul Kar Date: Thu, 6 Apr 2023 17:03:15 -0700 Subject: [PATCH 1/6] Update data sent to vercel for runs - Add context and command when starting a Run - Update data sent for Tasks - Fix PATCH endpoint to mark Run as done - Improve error propagation --- cli/internal/runsummary/run_summary.go | 35 +++++++--- cli/internal/runsummary/vercel.go | 89 ++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 16 deletions(-) diff --git a/cli/internal/runsummary/run_summary.go b/cli/internal/runsummary/run_summary.go index eed6e2c9ad672..f9167297f79f0 100644 --- a/cli/internal/runsummary/run_summary.go +++ b/cli/internal/runsummary/run_summary.go @@ -26,6 +26,7 @@ const MissingFrameworkLabel = "" const runSummarySchemaVersion = "0" const runsEndpoint = "/v0/spaces/%s/runs" +const runsPatchEndpoint = "/v0/spaces/%s/runs/%s" const tasksEndpoint = "/v0/spaces/%s/runs/%s/tasks" type runType int @@ -144,8 +145,11 @@ func (rsm *Meta) Close(exitCode int, workspaceInfos workspace.Catalog) error { if rsm.shouldSave { if rsm.spaceID != "" && rsm.apiClient.IsLinked() { - if err := rsm.record(); err != nil { - rsm.ui.Warn(fmt.Sprintf("Error recording Run to Vercel: %v", err)) + if errs := rsm.record(); len(errs) > 0 { + rsm.ui.Warn("Errors recording run to Vercel") + for _, err := range errs { + rsm.ui.Warn(fmt.Sprintf("%v", err)) + } } } } @@ -207,23 +211,25 @@ func (rsm *Meta) record() []error { payload := newVercelRunCreatePayload(rsm.RunSummary) if startPayload, err := json.Marshal(payload); err == nil { if resp, err := rsm.apiClient.JSONPost(runsURL, startPayload); err != nil { - errs = append(errs, err) + errs = append(errs, fmt.Errorf("Failed to POST to /run: %v", err)) } else { vercelRunResponse := &vercelRunResponse{} if err := json.Unmarshal(resp, vercelRunResponse); err != nil { - errs = append(errs, err) + errs = append(errs, fmt.Errorf("Failed to unmarshal response: %v", err)) } else { + runID = vercelRunResponse.ID + } } } if runID != "" { rsm.postTaskSummaries(runID) - if donePayload, err := json.Marshal(newVercelDonePayload(rsm.RunSummary)); err == nil { - if _, err := rsm.apiClient.JSONPatch(runsURL, donePayload); err != nil { - errs = append(errs, err) + patchURL := fmt.Sprintf(runsPatchEndpoint, rsm.spaceID, runID) + if _, err := rsm.apiClient.JSONPatch(patchURL, donePayload); err != nil { + errs = append(errs, fmt.Errorf("Failed to post PATCH: %s", err)) } } } @@ -235,7 +241,8 @@ func (rsm *Meta) record() []error { return nil } -func (rsm *Meta) postTaskSummaries(runID string) { +func (rsm *Meta) postTaskSummaries(runID string) []error { + errs := []error{} // We make at most 8 requests at a time. maxParallelRequests := 8 taskSummaries := rsm.RunSummary.Tasks @@ -256,9 +263,11 @@ func (rsm *Meta) postTaskSummaries(runID string) { defer wg.Done() for index := range queue { task := taskSummaries[index] - if taskPayload, err := json.Marshal(task); err == nil { + payload := newVercelTaskPayload(task) + if taskPayload, err := json.Marshal(payload); err == nil { + if _, err := rsm.apiClient.JSONPost(taskURL, taskPayload); err != nil { - rsm.ui.Warn(fmt.Sprintf("Eror uploading summary of %s", task.TaskID)) + errs = append(errs, fmt.Errorf("Eror uploading summary of %s", task.TaskID)) } } } @@ -270,4 +279,10 @@ func (rsm *Meta) postTaskSummaries(runID string) { } close(queue) wg.Wait() + + if len(errs) > 0 { + return errs + } + + return nil } diff --git a/cli/internal/runsummary/vercel.go b/cli/internal/runsummary/vercel.go index d0533600e7d40..77bc39ad0a0b7 100644 --- a/cli/internal/runsummary/vercel.go +++ b/cli/internal/runsummary/vercel.go @@ -1,5 +1,13 @@ package runsummary +import ( + "fmt" + "strings" + + "github.com/vercel/turbo/cli/internal/ci" + "github.com/vercel/turbo/cli/internal/util" +) + type vercelRunResponse struct { ID string } @@ -22,28 +30,97 @@ type vercelRunPayload struct { // ExitCode is the exit code for the full run ExitCode int `json:"exitCode,omitempty"` + + // The command that kicked off the turbo run + Command string `json:"command,omitempty"` + + Context string `json:"context,omitempy"` + // TODO: we need to add these in // originationUser string // gitBranch string // gitSha string - // context string // command string } +type vercelCacheStatus struct { + Status string `json:"status,omitempty"` + Source string `json:"source,omitempty"` +} + +type vercelTask struct { + // id string + // log string + // TODO: add in command + + Key string `json:"key,omitempty"` + Name string `json:"name,omitempty"` + Workspace string `json:"workspace,omitempty"` + Hash string `json:"hash,omitempty"` + StartTime int `json:"startTime,omitempty"` + EndTime int `json:"endTime,omitempty"` + Cache vercelCacheStatus `json:"cache,omitempty"` + ExitCode int `json:"exitCode,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` + Dependents []string `json:"dependents,omitempty"` +} + func newVercelRunCreatePayload(runsummary *RunSummary) *vercelRunPayload { - startTime := int(runsummary.ExecutionSummary.startedAt.UnixMilli()) + startTime := runsummary.ExecutionSummary.startedAt.UnixMilli() + taskNames := make(util.Set, len(runsummary.Tasks)) + for _, task := range runsummary.Tasks { + taskNames.Add(task.Task) + } return &vercelRunPayload{ - StartTime: startTime, - Status: "started", + StartTime: int(startTime), + Status: "running", + Command: fmt.Sprintf("turbo run %s", strings.Join(taskNames.UnsafeListOfStrings(), " ")), Type: "TURBO", + Context: getContext(), + } +} + +func getContext() string { + name := ci.Constant() + if name == "" { + return "LOCAL" } + + return name + } func newVercelDonePayload(runsummary *RunSummary) *vercelRunPayload { - endTime := int(runsummary.ExecutionSummary.endedAt.UnixMilli()) + endTime := runsummary.ExecutionSummary.endedAt.UnixMilli() return &vercelRunPayload{ Status: "completed", - EndTime: endTime, + EndTime: int(endTime), ExitCode: runsummary.ExecutionSummary.exitCode, } } + +func newVercelTaskPayload(taskSummary *TaskSummary) *vercelTask { + hit := taskSummary.CacheState.Local || taskSummary.CacheState.Remote + status := "MISS" + var source string + if hit { + source = "REMOTE" + status = "HIT" + } + + return &vercelTask{ + Key: taskSummary.TaskID, + Name: taskSummary.Task, + Workspace: taskSummary.Package, + Hash: taskSummary.Hash, + StartTime: int(taskSummary.Execution.startAt.UnixMilli()), + EndTime: int(taskSummary.Execution.startAt.Add(taskSummary.Execution.Duration).UnixMilli()), + Cache: vercelCacheStatus{ + Status: status, + Source: source, + }, + ExitCode: *taskSummary.Execution.exitCode, + Dependencies: taskSummary.Dependencies, + Dependents: taskSummary.Dependents, + } +} From 823eb41e25479934d43d2f55fb148965ab8258ab Mon Sep 17 00:00:00 2001 From: Mehul Kar Date: Thu, 6 Apr 2023 17:17:24 -0700 Subject: [PATCH 2/6] updates --- cli/internal/runsummary/execution_summary.go | 6 ++- cli/internal/runsummary/run_summary.go | 21 ++++++--- cli/internal/runsummary/vercel.go | 47 +++++++------------- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/cli/internal/runsummary/execution_summary.go b/cli/internal/runsummary/execution_summary.go index 083dbf6769f28..72ec29710998d 100644 --- a/cli/internal/runsummary/execution_summary.go +++ b/cli/internal/runsummary/execution_summary.go @@ -72,6 +72,10 @@ type TaskExecutionSummary struct { exitCode *int // pointer so we can distinguish between 0 and unknown. } +func (ts *TaskExecutionSummary) endTime() time.Time { + return ts.startAt.Add(ts.Duration) +} + // MarshalJSON munges the TaskExecutionSummary into a format we want // We'll use an anonmyous, private struct for this, so it's not confusingly duplicated func (ts *TaskExecutionSummary) MarshalJSON() ([]byte, error) { @@ -82,7 +86,7 @@ func (ts *TaskExecutionSummary) MarshalJSON() ([]byte, error) { ExitCode *int `json:"exitCode"` }{ Start: ts.startAt.UnixMilli(), - End: ts.startAt.Add(ts.Duration).UnixMilli(), + End: ts.endTime().UnixMilli(), Err: ts.err, ExitCode: ts.exitCode, } diff --git a/cli/internal/runsummary/run_summary.go b/cli/internal/runsummary/run_summary.go index f9167297f79f0..286a801df42a9 100644 --- a/cli/internal/runsummary/run_summary.go +++ b/cli/internal/runsummary/run_summary.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "path/filepath" + "strings" "sync" "time" @@ -180,6 +181,16 @@ func (summary *RunSummary) TrackTask(taskID string) (func(outcome executionEvent return summary.ExecutionSummary.run(taskID) } +// command returns a best guess command for the entire Run. +// TODO: we should thread this through from the entry point rather than make it up +func (summary *RunSummary) command() string { + taskNames := make(util.Set, len(summary.Tasks)) + for _, task := range summary.Tasks { + taskNames.Add(task.Task) + } + return fmt.Sprintf("turbo run %s", strings.Join(taskNames.UnsafeListOfStrings(), " ")) +} + // Save saves the run summary to a file func (rsm *Meta) save() error { json, err := rsm.FormatJSON() @@ -211,25 +222,24 @@ func (rsm *Meta) record() []error { payload := newVercelRunCreatePayload(rsm.RunSummary) if startPayload, err := json.Marshal(payload); err == nil { if resp, err := rsm.apiClient.JSONPost(runsURL, startPayload); err != nil { - errs = append(errs, fmt.Errorf("Failed to POST to /run: %v", err)) + errs = append(errs, err) } else { vercelRunResponse := &vercelRunResponse{} if err := json.Unmarshal(resp, vercelRunResponse); err != nil { - errs = append(errs, fmt.Errorf("Failed to unmarshal response: %v", err)) + errs = append(errs, err) } else { - runID = vercelRunResponse.ID - } } } if runID != "" { rsm.postTaskSummaries(runID) + if donePayload, err := json.Marshal(newVercelDonePayload(rsm.RunSummary)); err == nil { patchURL := fmt.Sprintf(runsPatchEndpoint, rsm.spaceID, runID) if _, err := rsm.apiClient.JSONPatch(patchURL, donePayload); err != nil { - errs = append(errs, fmt.Errorf("Failed to post PATCH: %s", err)) + errs = append(errs, err) } } } @@ -265,7 +275,6 @@ func (rsm *Meta) postTaskSummaries(runID string) []error { task := taskSummaries[index] payload := newVercelTaskPayload(task) if taskPayload, err := json.Marshal(payload); err == nil { - if _, err := rsm.apiClient.JSONPost(taskURL, taskPayload); err != nil { errs = append(errs, fmt.Errorf("Eror uploading summary of %s", task.TaskID)) } diff --git a/cli/internal/runsummary/vercel.go b/cli/internal/runsummary/vercel.go index 77bc39ad0a0b7..2a2b0363e5bd5 100644 --- a/cli/internal/runsummary/vercel.go +++ b/cli/internal/runsummary/vercel.go @@ -1,11 +1,7 @@ package runsummary import ( - "fmt" - "strings" - "github.com/vercel/turbo/cli/internal/ci" - "github.com/vercel/turbo/cli/internal/util" ) type vercelRunResponse struct { @@ -34,13 +30,13 @@ type vercelRunPayload struct { // The command that kicked off the turbo run Command string `json:"command,omitempty"` - Context string `json:"context,omitempy"` + // Context is the host on which this Run was executed (e.g. Vercel) + Context string `json:"context,omitempty"` // TODO: we need to add these in // originationUser string // gitBranch string // gitSha string - // command string } type vercelCacheStatus struct { @@ -49,10 +45,6 @@ type vercelCacheStatus struct { } type vercelTask struct { - // id string - // log string - // TODO: add in command - Key string `json:"key,omitempty"` Name string `json:"name,omitempty"` Workspace string `json:"workspace,omitempty"` @@ -66,35 +58,25 @@ type vercelTask struct { } func newVercelRunCreatePayload(runsummary *RunSummary) *vercelRunPayload { - startTime := runsummary.ExecutionSummary.startedAt.UnixMilli() - taskNames := make(util.Set, len(runsummary.Tasks)) - for _, task := range runsummary.Tasks { - taskNames.Add(task.Task) + startTime := int(runsummary.ExecutionSummary.startedAt.UnixMilli()) + var context = "LOCAL" + if name := ci.Constant(); name != "" { + context = name } return &vercelRunPayload{ - StartTime: int(startTime), + StartTime: startTime, Status: "running", - Command: fmt.Sprintf("turbo run %s", strings.Join(taskNames.UnsafeListOfStrings(), " ")), + Command: runsummary.command(), Type: "TURBO", - Context: getContext(), - } -} - -func getContext() string { - name := ci.Constant() - if name == "" { - return "LOCAL" + Context: context, } - - return name - } func newVercelDonePayload(runsummary *RunSummary) *vercelRunPayload { - endTime := runsummary.ExecutionSummary.endedAt.UnixMilli() + endTime := int(runsummary.ExecutionSummary.endedAt.UnixMilli()) return &vercelRunPayload{ Status: "completed", - EndTime: int(endTime), + EndTime: endTime, ExitCode: runsummary.ExecutionSummary.exitCode, } } @@ -108,13 +90,16 @@ func newVercelTaskPayload(taskSummary *TaskSummary) *vercelTask { status = "HIT" } + startTime := int(taskSummary.Execution.startAt.UnixMilli()) + endTime := int(taskSummary.Execution.endTime().UnixMilli()) + return &vercelTask{ Key: taskSummary.TaskID, Name: taskSummary.Task, Workspace: taskSummary.Package, Hash: taskSummary.Hash, - StartTime: int(taskSummary.Execution.startAt.UnixMilli()), - EndTime: int(taskSummary.Execution.startAt.Add(taskSummary.Execution.Duration).UnixMilli()), + StartTime: startTime, + EndTime: endTime, Cache: vercelCacheStatus{ Status: status, Source: source, From 99a8b684608e91084393650042ae7271721657e1 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Mon, 10 Apr 2023 10:47:08 -0700 Subject: [PATCH 3/6] Synthesize a command for a Run (#4499) --- .../single_package/run-summary.t | 2 + cli/internal/run/run.go | 18 +-- cli/internal/run/run_spec.go | 32 ++++++ cli/internal/run/run_spec_test.go | 107 ++++++++++++++++++ cli/internal/runsummary/execution_summary.go | 33 ++++-- cli/internal/runsummary/run_summary.go | 50 ++++---- cli/internal/runsummary/vercel.go | 35 +++--- cli/internal/scope/scope.go | 38 +++++-- cli/internal/scope/scope_test.go | 6 +- 9 files changed, 240 insertions(+), 81 deletions(-) create mode 100644 cli/internal/run/run_spec_test.go diff --git a/cli/integration_tests/single_package/run-summary.t b/cli/integration_tests/single_package/run-summary.t index bc1d6d59d232b..5eefab3220e57 100644 --- a/cli/integration_tests/single_package/run-summary.t +++ b/cli/integration_tests/single_package/run-summary.t @@ -20,9 +20,11 @@ Check [ "attempted", "cached", + "command", "endTime", "exitCode", "failed", + "repoPath", "startTime", "success" ] diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index 5d087345600f9..4c48408e84503 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -30,18 +30,6 @@ import ( "github.com/pkg/errors" ) -var _cmdLong = ` -Run tasks across projects in your monorepo. - -By default, turbo executes tasks in topological order (i.e. -dependencies first) and then caches the results. Re-running commands for -tasks already in the cache will skip re-execution and immediately move -artifacts from the cache into the correct output folders (as if the task -occurred again). - -Arguments passed after '--' will be passed through to the named tasks. -` - // ExecuteRun executes the run command func ExecuteRun(ctx gocontext.Context, helper *cmdutil.Helper, signalWatcher *signals.Watcher, args *turbostate.ParsedArgsFromRust) error { base, err := helper.GetCmdBase(args) @@ -73,7 +61,9 @@ func optsFromArgs(args *turbostate.ParsedArgsFromRust) (*Opts, error) { opts := getDefaultOptions() // aliases := make(map[string]string) - scope.OptsFromArgs(&opts.scopeOpts, args) + if err := scope.OptsFromArgs(&opts.scopeOpts, args); err != nil { + return nil, err + } // Cache flags opts.clientOpts.Timeout = args.RemoteCacheTimeout @@ -358,6 +348,7 @@ func (r *run) run(ctx gocontext.Context, targets []string) error { startAt, r.base.UI, r.base.RepoRoot, + rs.Opts.scopeOpts.PackageInferenceRoot, r.base.TurboVersion, r.base.APIClient, rs.Opts.runOpts, @@ -369,6 +360,7 @@ func (r *run) run(ctx gocontext.Context, targets []string) error { globalHashable.globalCacheKey, globalHashable.pipeline, ), + rs.Opts.SynthesizeCommand(rs.Targets), ) // Dry Run diff --git a/cli/internal/run/run_spec.go b/cli/internal/run/run_spec.go index 0b37303607d18..14402d39b5c83 100644 --- a/cli/internal/run/run_spec.go +++ b/cli/internal/run/run_spec.go @@ -3,6 +3,8 @@ package run import ( + "strings" + "github.com/vercel/turbo/cli/internal/cache" "github.com/vercel/turbo/cli/internal/client" "github.com/vercel/turbo/cli/internal/runcache" @@ -45,6 +47,36 @@ type Opts struct { scopeOpts scope.Opts } +// SynthesizeCommand produces a command that produces an equivalent set of packages, tasks, +// and task arguments to what the current set of opts selects. +func (o *Opts) SynthesizeCommand(tasks []string) string { + cmd := "turbo run" + cmd += " " + strings.Join(tasks, " ") + for _, filterPattern := range o.scopeOpts.FilterPatterns { + cmd += " --filter=" + filterPattern + } + for _, filterPattern := range o.scopeOpts.LegacyFilter.AsFilterPatterns() { + cmd += " --filter=" + filterPattern + } + if o.runOpts.Parallel { + cmd += " --parallel" + } + if o.runOpts.ContinueOnError { + cmd += " --continue" + } + if o.runOpts.DryRun { + if o.runOpts.DryRunJSON { + cmd += " --dry=json" + } else { + cmd += " --dry" + } + } + if len(o.runOpts.PassThroughArgs) > 0 { + cmd += " -- " + strings.Join(o.runOpts.PassThroughArgs, " ") + } + return cmd +} + // getDefaultOptions returns the default set of Opts for every run func getDefaultOptions() *Opts { return &Opts{ diff --git a/cli/internal/run/run_spec_test.go b/cli/internal/run/run_spec_test.go new file mode 100644 index 0000000000000..2bcfe2b2475d2 --- /dev/null +++ b/cli/internal/run/run_spec_test.go @@ -0,0 +1,107 @@ +package run + +import ( + "testing" + + "github.com/vercel/turbo/cli/internal/scope" + "github.com/vercel/turbo/cli/internal/util" +) + +func TestSynthesizeCommand(t *testing.T) { + testCases := []struct { + filterPatterns []string + legacyFilter scope.LegacyFilter + passThroughArgs []string + parallel bool + continueOnError bool + dryRun bool + dryRunJSON bool + tasks []string + expected string + }{ + { + filterPatterns: []string{"my-app"}, + tasks: []string{"build"}, + expected: "turbo run build --filter=my-app", + }, + { + filterPatterns: []string{"my-app"}, + tasks: []string{"build"}, + passThroughArgs: []string{"-v", "--foo=bar"}, + expected: "turbo run build --filter=my-app -- -v --foo=bar", + }, + { + legacyFilter: scope.LegacyFilter{ + Entrypoints: []string{"my-app"}, + SkipDependents: true, + }, + tasks: []string{"build"}, + passThroughArgs: []string{"-v", "--foo=bar"}, + expected: "turbo run build --filter=my-app -- -v --foo=bar", + }, + { + legacyFilter: scope.LegacyFilter{ + Entrypoints: []string{"my-app"}, + SkipDependents: true, + }, + filterPatterns: []string{"other-app"}, + tasks: []string{"build"}, + passThroughArgs: []string{"-v", "--foo=bar"}, + expected: "turbo run build --filter=other-app --filter=my-app -- -v --foo=bar", + }, + { + legacyFilter: scope.LegacyFilter{ + Entrypoints: []string{"my-app"}, + IncludeDependencies: true, + Since: "some-ref", + }, + filterPatterns: []string{"other-app"}, + tasks: []string{"build"}, + expected: "turbo run build --filter=other-app --filter=...my-app...[some-ref]...", + }, + { + filterPatterns: []string{"my-app"}, + tasks: []string{"build"}, + parallel: true, + continueOnError: true, + expected: "turbo run build --filter=my-app --parallel --continue", + }, + { + filterPatterns: []string{"my-app"}, + tasks: []string{"build"}, + dryRun: true, + expected: "turbo run build --filter=my-app --dry", + }, + { + filterPatterns: []string{"my-app"}, + tasks: []string{"build"}, + dryRun: true, + dryRunJSON: true, + expected: "turbo run build --filter=my-app --dry=json", + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.expected, func(t *testing.T) { + o := Opts{ + scopeOpts: scope.Opts{ + FilterPatterns: testCase.filterPatterns, + LegacyFilter: testCase.legacyFilter, + }, + runOpts: util.RunOpts{ + PassThroughArgs: testCase.passThroughArgs, + Parallel: testCase.parallel, + ContinueOnError: testCase.continueOnError, + DryRun: testCase.dryRun, + DryRunJSON: testCase.dryRunJSON, + }, + } + cmd := o.SynthesizeCommand(testCase.tasks) + if cmd != testCase.expected { + t.Errorf("SynthesizeCommand() got %v, want %v", cmd, testCase.expected) + } + }) + } + +} diff --git a/cli/internal/runsummary/execution_summary.go b/cli/internal/runsummary/execution_summary.go index 72ec29710998d..77a2c1325f0a2 100644 --- a/cli/internal/runsummary/execution_summary.go +++ b/cli/internal/runsummary/execution_summary.go @@ -9,6 +9,7 @@ import ( "github.com/vercel/turbo/cli/internal/chrometracing" "github.com/vercel/turbo/cli/internal/fs" + "github.com/vercel/turbo/cli/internal/turbopath" "github.com/mitchellh/cli" ) @@ -112,10 +113,12 @@ type executionSummary struct { profileFilename string // These get serialized to JSON - success int // number of tasks that exited successfully (does not include cache hits) - failure int // number of tasks that exited with failure - cached int // number of tasks that had a cache hit - attempted int // number of tasks that started + command string // a synthesized turbo command to produce this invocation + repoPath turbopath.RelativeSystemPath // the (possibly empty) path from the turborepo root to where the command was run + success int // number of tasks that exited successfully (does not include cache hits) + failure int // number of tasks that exited with failure + cached int // number of tasks that had a cache hit + attempted int // number of tasks that started startedAt time.Time endedAt time.Time exitCode int @@ -125,14 +128,18 @@ type executionSummary struct { // We'll use an anonmyous, private struct for this, so it's not confusingly duplicated. func (es *executionSummary) MarshalJSON() ([]byte, error) { serializable := struct { - Success int `json:"success"` - Failure int `json:"failed"` - Cached int `json:"cached"` - Attempted int `json:"attempted"` - StartTime int64 `json:"startTime"` - EndTime int64 `json:"endTime"` - ExitCode int `json:"exitCode"` + Command string `json:"command"` + RepoPath string `json:"repoPath"` + Success int `json:"success"` + Failure int `json:"failed"` + Cached int `json:"cached"` + Attempted int `json:"attempted"` + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime"` + ExitCode int `json:"exitCode"` }{ + Command: es.command, + RepoPath: es.repoPath.ToString(), StartTime: es.startedAt.UnixMilli(), EndTime: es.endedAt.UnixMilli(), Success: es.success, @@ -146,12 +153,14 @@ func (es *executionSummary) MarshalJSON() ([]byte, error) { } // newExecutionSummary creates a executionSummary instance to track events in a `turbo run`.` -func newExecutionSummary(start time.Time, tracingProfile string) *executionSummary { +func newExecutionSummary(command string, repoPath turbopath.RelativeSystemPath, start time.Time, tracingProfile string) *executionSummary { if tracingProfile != "" { chrometracing.EnableTracing() } return &executionSummary{ + command: command, + repoPath: repoPath, success: 0, failure: 0, cached: 0, diff --git a/cli/internal/runsummary/run_summary.go b/cli/internal/runsummary/run_summary.go index 286a801df42a9..7563fb3840d7a 100644 --- a/cli/internal/runsummary/run_summary.go +++ b/cli/internal/runsummary/run_summary.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "path/filepath" - "strings" "sync" "time" @@ -41,14 +40,16 @@ const ( // Meta is a wrapper around the serializable RunSummary, with some extra information // about the Run and references to other things that we need. type Meta struct { - RunSummary *RunSummary - ui cli.Ui - repoRoot turbopath.AbsoluteSystemPath // used to write run summary - singlePackage bool - shouldSave bool - apiClient *client.APIClient - spaceID string - runType runType + RunSummary *RunSummary + ui cli.Ui + repoRoot turbopath.AbsoluteSystemPath // used to write run summary + repoPath turbopath.RelativeSystemPath + singlePackage bool + shouldSave bool + apiClient *client.APIClient + spaceID string + runType runType + synthesizedCommand string } // RunSummary contains a summary of what happens in the `turbo run` command and why. @@ -67,11 +68,13 @@ func NewRunSummary( startAt time.Time, ui cli.Ui, repoRoot turbopath.AbsoluteSystemPath, + repoPath turbopath.RelativeSystemPath, turboVersion string, apiClient *client.APIClient, runOpts util.RunOpts, packages []string, globalHashSummary *GlobalHashSummary, + synthesizedCommand string, ) Meta { singlePackage := runOpts.SinglePackage profile := runOpts.Profile @@ -86,7 +89,7 @@ func NewRunSummary( } } - executionSummary := newExecutionSummary(startAt, profile) + executionSummary := newExecutionSummary(synthesizedCommand, repoPath, startAt, profile) return Meta{ RunSummary: &RunSummary{ @@ -98,13 +101,14 @@ func NewRunSummary( Tasks: []*TaskSummary{}, GlobalHashSummary: globalHashSummary, }, - ui: ui, - runType: runType, - repoRoot: repoRoot, - singlePackage: singlePackage, - shouldSave: shouldSave, - apiClient: apiClient, - spaceID: spaceID, + ui: ui, + runType: runType, + repoRoot: repoRoot, + singlePackage: singlePackage, + shouldSave: shouldSave, + apiClient: apiClient, + spaceID: spaceID, + synthesizedCommand: synthesizedCommand, } } @@ -181,16 +185,6 @@ func (summary *RunSummary) TrackTask(taskID string) (func(outcome executionEvent return summary.ExecutionSummary.run(taskID) } -// command returns a best guess command for the entire Run. -// TODO: we should thread this through from the entry point rather than make it up -func (summary *RunSummary) command() string { - taskNames := make(util.Set, len(summary.Tasks)) - for _, task := range summary.Tasks { - taskNames.Add(task.Task) - } - return fmt.Sprintf("turbo run %s", strings.Join(taskNames.UnsafeListOfStrings(), " ")) -} - // Save saves the run summary to a file func (rsm *Meta) save() error { json, err := rsm.FormatJSON() @@ -219,7 +213,7 @@ func (rsm *Meta) record() []error { // can happen when the Run actually starts, so we can send updates to Vercel as the tasks progress. runsURL := fmt.Sprintf(runsEndpoint, rsm.spaceID) var runID string - payload := newVercelRunCreatePayload(rsm.RunSummary) + payload := rsm.newVercelRunCreatePayload() if startPayload, err := json.Marshal(payload); err == nil { if resp, err := rsm.apiClient.JSONPost(runsURL, startPayload); err != nil { errs = append(errs, err) diff --git a/cli/internal/runsummary/vercel.go b/cli/internal/runsummary/vercel.go index 2a2b0363e5bd5..be3f3a6e86f91 100644 --- a/cli/internal/runsummary/vercel.go +++ b/cli/internal/runsummary/vercel.go @@ -13,10 +13,10 @@ type vercelRunPayload struct { ID string `json:"vercelId,omitempty"` // StartTime is when this run was started - StartTime int `json:"startTime,omitempty"` + StartTime int64 `json:"startTime,omitempty"` // EndTime is when this run ended. We will never be submitting start and endtime at the same time. - EndTime int `json:"endTime,omitempty"` + EndTime int64 `json:"endTime,omitempty"` // Status is Status string `json:"status,omitempty"` @@ -30,6 +30,10 @@ type vercelRunPayload struct { // The command that kicked off the turbo run Command string `json:"command,omitempty"` + // RepositoryPath is the relative directory from the turborepo root to where + // the command was invoked. + RepositoryPath string `json:"repositoryPath,omitempty"` + // Context is the host on which this Run was executed (e.g. Vercel) Context string `json:"context,omitempty"` @@ -49,31 +53,32 @@ type vercelTask struct { Name string `json:"name,omitempty"` Workspace string `json:"workspace,omitempty"` Hash string `json:"hash,omitempty"` - StartTime int `json:"startTime,omitempty"` - EndTime int `json:"endTime,omitempty"` + StartTime int64 `json:"startTime,omitempty"` + EndTime int64 `json:"endTime,omitempty"` Cache vercelCacheStatus `json:"cache,omitempty"` ExitCode int `json:"exitCode,omitempty"` Dependencies []string `json:"dependencies,omitempty"` Dependents []string `json:"dependents,omitempty"` } -func newVercelRunCreatePayload(runsummary *RunSummary) *vercelRunPayload { - startTime := int(runsummary.ExecutionSummary.startedAt.UnixMilli()) - var context = "LOCAL" +func (rsm *Meta) newVercelRunCreatePayload() *vercelRunPayload { + startTime := rsm.RunSummary.ExecutionSummary.startedAt.UnixMilli() + context := "LOCAL" if name := ci.Constant(); name != "" { context = name } return &vercelRunPayload{ - StartTime: startTime, - Status: "running", - Command: runsummary.command(), - Type: "TURBO", - Context: context, + StartTime: startTime, + Status: "running", + Command: rsm.synthesizedCommand, + RepositoryPath: rsm.repoPath.ToString(), + Type: "TURBO", + Context: context, } } func newVercelDonePayload(runsummary *RunSummary) *vercelRunPayload { - endTime := int(runsummary.ExecutionSummary.endedAt.UnixMilli()) + endTime := runsummary.ExecutionSummary.endedAt.UnixMilli() return &vercelRunPayload{ Status: "completed", EndTime: endTime, @@ -90,8 +95,8 @@ func newVercelTaskPayload(taskSummary *TaskSummary) *vercelTask { status = "HIT" } - startTime := int(taskSummary.Execution.startAt.UnixMilli()) - endTime := int(taskSummary.Execution.endTime().UnixMilli()) + startTime := taskSummary.Execution.startAt.UnixMilli() + endTime := taskSummary.Execution.endTime().UnixMilli() return &vercelTask{ Key: taskSummary.TaskID, diff --git a/cli/internal/scope/scope.go b/cli/internal/scope/scope.go index df40b7366f0ac..b5ed4e7074949 100644 --- a/cli/internal/scope/scope.go +++ b/cli/internal/scope/scope.go @@ -55,7 +55,7 @@ type Opts struct { // Patterns are the filter patterns supplied to --filter on the commandline FilterPatterns []string - PackageInferenceRoot string + PackageInferenceRoot turbopath.RelativeSystemPath } var ( @@ -70,17 +70,35 @@ match any filter will be included.` in the root directory. Includes turbo.json, root package.json, and the root lockfile by default.` ) +// normalize package inference path. We compare against "" in several places, so maintain +// that behavior. In a post-rust-port world, this should more properly be an Option +func resolvePackageInferencePath(raw string) (turbopath.RelativeSystemPath, error) { + pkgInferenceRoot, err := turbopath.CheckedToRelativeSystemPath(raw) + if err != nil { + return "", errors.Wrapf(err, "invalid package inference root %v", raw) + } + if pkgInferenceRoot == "." { + return "", nil + } + return pkgInferenceRoot, nil +} + // OptsFromArgs adds the settings relevant to this package to the given Opts -func OptsFromArgs(opts *Opts, args *turbostate.ParsedArgsFromRust) { +func OptsFromArgs(opts *Opts, args *turbostate.ParsedArgsFromRust) error { opts.FilterPatterns = args.Command.Run.Filter opts.IgnorePatterns = args.Command.Run.Ignore opts.GlobalDepPatterns = args.Command.Run.GlobalDeps - opts.PackageInferenceRoot = args.Command.Run.PkgInferenceRoot + pkgInferenceRoot, err := resolvePackageInferencePath(args.Command.Run.PkgInferenceRoot) + if err != nil { + return err + } + opts.PackageInferenceRoot = pkgInferenceRoot addLegacyFlagsFromArgs(&opts.LegacyFilter, args) + return nil } -// asFilterPatterns normalizes legacy selectors to filter syntax -func (l *LegacyFilter) asFilterPatterns() []string { +// AsFilterPatterns normalizes legacy selectors to filter syntax +func (l *LegacyFilter) AsFilterPatterns() []string { var patterns []string prefix := "" if !l.SkipDependents { @@ -131,7 +149,7 @@ func ResolvePackages(opts *Opts, repoRoot turbopath.AbsoluteSystemPath, scm scm. PackagesChangedInRange: opts.getPackageChangeFunc(scm, repoRoot, ctx), } filterPatterns := opts.FilterPatterns - legacyFilterPatterns := opts.LegacyFilter.asFilterPatterns() + legacyFilterPatterns := opts.LegacyFilter.AsFilterPatterns() filterPatterns = append(filterPatterns, legacyFilterPatterns...) isAllPackages := len(filterPatterns) == 0 && opts.PackageInferenceRoot == "" filteredPkgs, err := filterResolver.GetPackagesFromPatterns(filterPatterns) @@ -149,15 +167,11 @@ func ResolvePackages(opts *Opts, repoRoot turbopath.AbsoluteSystemPath, scm scm. return filteredPkgs, isAllPackages, nil } -func calculateInference(repoRoot turbopath.AbsoluteSystemPath, rawPkgInferenceDir string, packageInfos workspace.Catalog, logger hclog.Logger) (*scope_filter.PackageInference, error) { - if rawPkgInferenceDir == "" { +func calculateInference(repoRoot turbopath.AbsoluteSystemPath, pkgInferencePath turbopath.RelativeSystemPath, packageInfos workspace.Catalog, logger hclog.Logger) (*scope_filter.PackageInference, error) { + if pkgInferencePath == "" { // No inference specified, no need to calculate anything return nil, nil } - pkgInferencePath, err := turbopath.CheckedToRelativeSystemPath(rawPkgInferenceDir) - if err != nil { - return nil, err - } logger.Debug(fmt.Sprintf("Using %v as a basis for selecting packages", pkgInferencePath)) fullInferencePath := repoRoot.Join(pkgInferencePath) for _, pkgInfo := range packageInfos.PackageJSONs { diff --git a/cli/internal/scope/scope_test.go b/cli/internal/scope/scope_test.go index 0bfe1ac198e8f..216984dca3152 100644 --- a/cli/internal/scope/scope_test.go +++ b/cli/internal/scope/scope_test.go @@ -510,6 +510,10 @@ func TestResolvePackages(t *testing.T) { readLockfile := func(_rootPackageJSON *fs.PackageJSON, content []byte) (lockfile.Lockfile, error) { return tc.prevLockfile, nil } + pkgInferenceRoot, err := resolvePackageInferencePath(tc.inferPkgPath) + if err != nil { + t.Errorf("bad inference path (%v): %v", tc.inferPkgPath, err) + } pkgs, isAllPackages, err := ResolvePackages(&Opts{ LegacyFilter: LegacyFilter{ Entrypoints: tc.scope, @@ -519,7 +523,7 @@ func TestResolvePackages(t *testing.T) { }, IgnorePatterns: []string{tc.ignore}, GlobalDepPatterns: tc.globalDeps, - PackageInferenceRoot: tc.inferPkgPath, + PackageInferenceRoot: pkgInferenceRoot, }, root, scm, &context.Context{ WorkspaceInfos: workspaceInfos, WorkspaceNames: packageNames, From 4ecd64d113996cb88ee2c4a1e4f5b727c28de64f Mon Sep 17 00:00:00 2001 From: Mehul Kar Date: Mon, 10 Apr 2023 13:42:53 -0700 Subject: [PATCH 4/6] Print warning when needs linking --- cli/internal/runsummary/run_summary.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cli/internal/runsummary/run_summary.go b/cli/internal/runsummary/run_summary.go index 7563fb3840d7a..3b79b08bf19d1 100644 --- a/cli/internal/runsummary/run_summary.go +++ b/cli/internal/runsummary/run_summary.go @@ -149,12 +149,16 @@ func (rsm *Meta) Close(exitCode int, workspaceInfos workspace.Catalog) error { rsm.printExecutionSummary() if rsm.shouldSave { - if rsm.spaceID != "" && rsm.apiClient.IsLinked() { - if errs := rsm.record(); len(errs) > 0 { - rsm.ui.Warn("Errors recording run to Vercel") - for _, err := range errs { - rsm.ui.Warn(fmt.Sprintf("%v", err)) + if rsm.spaceID != "" { + if rsm.apiClient.IsLinked() { + if errs := rsm.record(); len(errs) > 0 { + rsm.ui.Warn("Errors recording run to Vercel") + for _, err := range errs { + rsm.ui.Warn(fmt.Sprintf("%v", err)) + } } + } else { + rsm.ui.Warn("Failed to post to space because repo is not linked to Vercel. Run `turbo link` first.") } } } From 494ead8ff5c86a738bbd1548bb1978a25124b6d0 Mon Sep 17 00:00:00 2001 From: Mehul Kar Date: Mon, 10 Apr 2023 15:39:26 -0700 Subject: [PATCH 5/6] Set cache source correctly on local cache hits --- cli/internal/runsummary/vercel.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/internal/runsummary/vercel.go b/cli/internal/runsummary/vercel.go index be3f3a6e86f91..160b9e01db72b 100644 --- a/cli/internal/runsummary/vercel.go +++ b/cli/internal/runsummary/vercel.go @@ -87,11 +87,15 @@ func newVercelDonePayload(runsummary *RunSummary) *vercelRunPayload { } func newVercelTaskPayload(taskSummary *TaskSummary) *vercelTask { - hit := taskSummary.CacheState.Local || taskSummary.CacheState.Remote - status := "MISS" + // Set the cache source. Local and Remote shouldn't _both_ be true. var source string - if hit { + if taskSummary.CacheState.Local { + source = "LOCAL" + } else if taskSummary.CacheState.Remote { source = "REMOTE" + } + status := "MISS" + if source != "" { status = "HIT" } From cb1c9ccad2f706cc0ad63d3c4a78e0c406157fb0 Mon Sep 17 00:00:00 2001 From: Mehul Kar Date: Tue, 11 Apr 2023 11:32:16 -0700 Subject: [PATCH 6/6] Rename references to spaces (#4525) --- cli/internal/runsummary/run_summary.go | 21 +++++++------- .../runsummary/{vercel.go => spaces.go} | 29 +++++++++---------- 2 files changed, 24 insertions(+), 26 deletions(-) rename cli/internal/runsummary/{vercel.go => spaces.go} (80%) diff --git a/cli/internal/runsummary/run_summary.go b/cli/internal/runsummary/run_summary.go index b721d0098f768..1c6062a3c7c56 100644 --- a/cli/internal/runsummary/run_summary.go +++ b/cli/internal/runsummary/run_summary.go @@ -152,13 +152,13 @@ func (rsm *Meta) Close(exitCode int, workspaceInfos workspace.Catalog) error { if rsm.spaceID != "" { if rsm.apiClient.IsLinked() { if errs := rsm.record(); len(errs) > 0 { - rsm.ui.Warn("Errors recording run to Vercel") + rsm.ui.Warn("Errors recording run to Spaces") for _, err := range errs { rsm.ui.Warn(fmt.Sprintf("%v", err)) } } } else { - rsm.ui.Warn("Failed to post to space because repo is not linked to Vercel. Run `turbo link` first.") + rsm.ui.Warn("Failed to post to space because repo is not linked to a Space. Run `turbo link` first.") } } } @@ -213,20 +213,21 @@ func (rsm *Meta) record() []error { errs := []error{} // Right now we'll send the POST to create the Run and the subsequent task payloads - // when everything after all execution is done, but in the future, this first POST request - // can happen when the Run actually starts, so we can send updates to Vercel as the tasks progress. + // after all execution is done, but in the future, this first POST request + // can happen when the Run actually starts, so we can send updates to the associated Space + // as tasks complete. runsURL := fmt.Sprintf(runsEndpoint, rsm.spaceID) var runID string - payload := rsm.newVercelRunCreatePayload() + payload := rsm.newSpacesRunCreatePayload() if startPayload, err := json.Marshal(payload); err == nil { if resp, err := rsm.apiClient.JSONPost(runsURL, startPayload); err != nil { errs = append(errs, err) } else { - vercelRunResponse := &vercelRunResponse{} - if err := json.Unmarshal(resp, vercelRunResponse); err != nil { + spacesRunResponse := &spacesRunResponse{} + if err := json.Unmarshal(resp, spacesRunResponse); err != nil { errs = append(errs, err) } else { - runID = vercelRunResponse.ID + runID = spacesRunResponse.ID } } } @@ -234,7 +235,7 @@ func (rsm *Meta) record() []error { if runID != "" { rsm.postTaskSummaries(runID) - if donePayload, err := json.Marshal(newVercelDonePayload(rsm.RunSummary)); err == nil { + if donePayload, err := json.Marshal(newSpacesDonePayload(rsm.RunSummary)); err == nil { patchURL := fmt.Sprintf(runsPatchEndpoint, rsm.spaceID, runID) if _, err := rsm.apiClient.JSONPatch(patchURL, donePayload); err != nil { errs = append(errs, err) @@ -271,7 +272,7 @@ func (rsm *Meta) postTaskSummaries(runID string) []error { defer wg.Done() for index := range queue { task := taskSummaries[index] - payload := newVercelTaskPayload(task) + payload := newSpacesTaskPayload(task) if taskPayload, err := json.Marshal(payload); err == nil { if _, err := rsm.apiClient.JSONPost(taskURL, taskPayload); err != nil { errs = append(errs, fmt.Errorf("Eror uploading summary of %s", task.TaskID)) diff --git a/cli/internal/runsummary/vercel.go b/cli/internal/runsummary/spaces.go similarity index 80% rename from cli/internal/runsummary/vercel.go rename to cli/internal/runsummary/spaces.go index 160b9e01db72b..65c4957985150 100644 --- a/cli/internal/runsummary/vercel.go +++ b/cli/internal/runsummary/spaces.go @@ -4,14 +4,11 @@ import ( "github.com/vercel/turbo/cli/internal/ci" ) -type vercelRunResponse struct { +type spacesRunResponse struct { ID string } -type vercelRunPayload struct { - // ID is set by the backend, including it here for completeness, but we never fill this in. - ID string `json:"vercelId,omitempty"` - +type spacesRunPayload struct { // StartTime is when this run was started StartTime int64 `json:"startTime,omitempty"` @@ -34,7 +31,7 @@ type vercelRunPayload struct { // the command was invoked. RepositoryPath string `json:"repositoryPath,omitempty"` - // Context is the host on which this Run was executed (e.g. Vercel) + // Context is the host on which this Run was executed (e.g. Github Action, Vercel, etc) Context string `json:"context,omitempty"` // TODO: we need to add these in @@ -43,31 +40,31 @@ type vercelRunPayload struct { // gitSha string } -type vercelCacheStatus struct { +type spacesCacheStatus struct { Status string `json:"status,omitempty"` Source string `json:"source,omitempty"` } -type vercelTask struct { +type spacesTask struct { Key string `json:"key,omitempty"` Name string `json:"name,omitempty"` Workspace string `json:"workspace,omitempty"` Hash string `json:"hash,omitempty"` StartTime int64 `json:"startTime,omitempty"` EndTime int64 `json:"endTime,omitempty"` - Cache vercelCacheStatus `json:"cache,omitempty"` + Cache spacesCacheStatus `json:"cache,omitempty"` ExitCode int `json:"exitCode,omitempty"` Dependencies []string `json:"dependencies,omitempty"` Dependents []string `json:"dependents,omitempty"` } -func (rsm *Meta) newVercelRunCreatePayload() *vercelRunPayload { +func (rsm *Meta) newSpacesRunCreatePayload() *spacesRunPayload { startTime := rsm.RunSummary.ExecutionSummary.startedAt.UnixMilli() context := "LOCAL" if name := ci.Constant(); name != "" { context = name } - return &vercelRunPayload{ + return &spacesRunPayload{ StartTime: startTime, Status: "running", Command: rsm.synthesizedCommand, @@ -77,16 +74,16 @@ func (rsm *Meta) newVercelRunCreatePayload() *vercelRunPayload { } } -func newVercelDonePayload(runsummary *RunSummary) *vercelRunPayload { +func newSpacesDonePayload(runsummary *RunSummary) *spacesRunPayload { endTime := runsummary.ExecutionSummary.endedAt.UnixMilli() - return &vercelRunPayload{ + return &spacesRunPayload{ Status: "completed", EndTime: endTime, ExitCode: runsummary.ExecutionSummary.exitCode, } } -func newVercelTaskPayload(taskSummary *TaskSummary) *vercelTask { +func newSpacesTaskPayload(taskSummary *TaskSummary) *spacesTask { // Set the cache source. Local and Remote shouldn't _both_ be true. var source string if taskSummary.CacheState.Local { @@ -102,14 +99,14 @@ func newVercelTaskPayload(taskSummary *TaskSummary) *vercelTask { startTime := taskSummary.Execution.startAt.UnixMilli() endTime := taskSummary.Execution.endTime().UnixMilli() - return &vercelTask{ + return &spacesTask{ Key: taskSummary.TaskID, Name: taskSummary.Task, Workspace: taskSummary.Package, Hash: taskSummary.Hash, StartTime: startTime, EndTime: endTime, - Cache: vercelCacheStatus{ + Cache: spacesCacheStatus{ Status: status, Source: source, },