aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/runsummary
diff options
context:
space:
mode:
Diffstat (limited to 'cli/internal/runsummary')
-rw-r--r--cli/internal/runsummary/execution_summary.go282
-rw-r--r--cli/internal/runsummary/format_execution_summary.go70
-rw-r--r--cli/internal/runsummary/format_json.go66
-rw-r--r--cli/internal/runsummary/format_text.go100
-rw-r--r--cli/internal/runsummary/globalhash_summary.go38
-rw-r--r--cli/internal/runsummary/run_summary.go320
-rw-r--r--cli/internal/runsummary/spaces.go96
-rw-r--r--cli/internal/runsummary/task_summary.go117
8 files changed, 0 insertions, 1089 deletions
diff --git a/cli/internal/runsummary/execution_summary.go b/cli/internal/runsummary/execution_summary.go
deleted file mode 100644
index fabb690..0000000
--- a/cli/internal/runsummary/execution_summary.go
+++ /dev/null
@@ -1,282 +0,0 @@
-package runsummary
-
-import (
- "encoding/json"
- "fmt"
- "os"
- "sync"
- "time"
-
- "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"
-)
-
-// executionEvent represents a single event in the build process, i.e. a target starting or finishing
-// building, or reaching some milestone within those steps.
-type executionEvent struct {
- // Timestamp of this event
- Time time.Time
- // Duration of this event
- Duration time.Duration
- // Target which has just changed
- Label string
- // Its current status
- Status executionEventName
- // Error, only populated for failure statuses
- Err string
-
- exitCode *int
-}
-
-// executionEventName represents the status of a target when we log a build result.
-type executionEventName int
-
-// The collection of expected build result statuses.
-const (
- targetInitialized executionEventName = iota
- TargetBuilding
- TargetBuildStopped
- TargetExecuted
- TargetBuilt
- TargetCached
- TargetBuildFailed
-)
-
-func (en executionEventName) toString() string {
- switch en {
- case targetInitialized:
- return "initialized"
- case TargetBuilding:
- return "building"
- case TargetBuildStopped:
- return "buildStopped"
- case TargetExecuted:
- return "executed"
- case TargetBuilt:
- return "built"
- case TargetCached:
- return "cached"
- case TargetBuildFailed:
- return "buildFailed"
- }
-
- return ""
-}
-
-// TaskExecutionSummary contains data about the state of a single task in a turbo run.
-// Some fields are updated over time as the task prepares to execute and finishes execution.
-type TaskExecutionSummary struct {
- startAt time.Time // set once
- status executionEventName // current status, updated during execution
- err string // only populated for failure statuses
- Duration time.Duration // updated during the task execution
- 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) {
- serializable := struct {
- Start int64 `json:"startTime"`
- End int64 `json:"endTime"`
- Err string `json:"error,omitempty"`
- ExitCode *int `json:"exitCode"`
- }{
- Start: ts.startAt.UnixMilli(),
- End: ts.endTime().UnixMilli(),
- Err: ts.err,
- ExitCode: ts.exitCode,
- }
-
- return json.Marshal(&serializable)
-}
-
-// ExitCode access exit code nil means no exit code was received
-func (ts *TaskExecutionSummary) ExitCode() *int {
- var exitCode int
- if ts.exitCode == nil {
- return nil
- }
- exitCode = *ts.exitCode
- return &exitCode
-}
-
-// executionSummary is the state of the entire `turbo run`. Individual task state in `Tasks` field
-type executionSummary struct {
- // mu guards reads/writes to the `state` field
- mu sync.Mutex
- tasks map[string]*TaskExecutionSummary // key is a taskID
- profileFilename string
-
- // These get serialized to JSON
- 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
-}
-
-// MarshalJSON munges the executionSummary into a format we want
-// We'll use an anonmyous, private struct for this, so it's not confusingly duplicated.
-func (es *executionSummary) MarshalJSON() ([]byte, error) {
- serializable := struct {
- 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,
- Failure: es.failure,
- Cached: es.cached,
- Attempted: es.attempted,
- ExitCode: es.exitCode,
- }
-
- return json.Marshal(&serializable)
-}
-
-// newExecutionSummary creates a executionSummary instance to track events in a `turbo run`.`
-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,
- attempted: 0,
- tasks: make(map[string]*TaskExecutionSummary),
- startedAt: start,
- profileFilename: tracingProfile,
- }
-}
-
-// Run starts the Execution of a single task. It returns a function that can
-// be used to update the state of a given taskID with the executionEventName enum
-func (es *executionSummary) run(taskID string) (func(outcome executionEventName, err error, exitCode *int), *TaskExecutionSummary) {
- start := time.Now()
- taskExecutionSummary := es.add(&executionEvent{
- Time: start,
- Label: taskID,
- Status: targetInitialized,
- })
-
- tracer := chrometracing.Event(taskID)
-
- // This function can be called with an enum and an optional error to update
- // the state of a given taskID.
- tracerFn := func(outcome executionEventName, err error, exitCode *int) {
- defer tracer.Done()
- now := time.Now()
- result := &executionEvent{
- Time: now,
- Duration: now.Sub(start),
- Label: taskID,
- Status: outcome,
- // We'll assign this here regardless of whether it is nil, but we'll check for nil
- // when we assign it to the taskExecutionSummary.
- exitCode: exitCode,
- }
-
- if err != nil {
- result.Err = err.Error()
- }
-
- // Ignore the return value here
- es.add(result)
- }
-
- return tracerFn, taskExecutionSummary
-}
-
-func (es *executionSummary) add(event *executionEvent) *TaskExecutionSummary {
- es.mu.Lock()
- defer es.mu.Unlock()
-
- var taskExecSummary *TaskExecutionSummary
- if ts, ok := es.tasks[event.Label]; ok {
- // If we already know about this task, we'll update it with the new event
- taskExecSummary = ts
- } else {
- // If we don't know about it yet, init and add it into the parent struct
- // (event.Status should always be `targetBuilding` here.)
- taskExecSummary = &TaskExecutionSummary{startAt: event.Time}
- es.tasks[event.Label] = taskExecSummary
- }
-
- // Update the Status, Duration, and Err fields
- taskExecSummary.status = event.Status
- taskExecSummary.err = event.Err
- taskExecSummary.Duration = event.Duration
-
- if event.exitCode != nil {
- taskExecSummary.exitCode = event.exitCode
- }
-
- switch {
- case event.Status == TargetBuilding:
- es.attempted++
- case event.Status == TargetBuildFailed:
- es.failure++
- case event.Status == TargetCached:
- es.cached++
- case event.Status == TargetBuilt:
- es.success++
- }
-
- return es.tasks[event.Label]
-}
-
-// writeChromeTracing writes to a profile name if the `--profile` flag was passed to turbo run
-func writeChrometracing(filename string, terminal cli.Ui) error {
- outputPath := chrometracing.Path()
- if outputPath == "" {
- // tracing wasn't enabled
- return nil
- }
-
- name := fmt.Sprintf("turbo-%s.trace", time.Now().Format(time.RFC3339))
- if filename != "" {
- name = filename
- }
- if err := chrometracing.Close(); err != nil {
- terminal.Warn(fmt.Sprintf("Failed to flush tracing data: %v", err))
- }
- cwdRaw, err := os.Getwd()
- if err != nil {
- return err
- }
- root, err := fs.GetCwd(cwdRaw)
- if err != nil {
- return err
- }
- // chrometracing.Path() is absolute by default, but can still be relative if overriden via $CHROMETRACING_DIR
- // so we have to account for that before converting to turbopath.AbsoluteSystemPath
- if err := fs.CopyFile(&fs.LstatCachedFile{Path: fs.ResolveUnknownPath(root, outputPath)}, name); err != nil {
- return err
- }
- return nil
-}
diff --git a/cli/internal/runsummary/format_execution_summary.go b/cli/internal/runsummary/format_execution_summary.go
deleted file mode 100644
index 37092be..0000000
--- a/cli/internal/runsummary/format_execution_summary.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package runsummary
-
-import (
- "os"
- "time"
-
- "github.com/fatih/color"
- internalUI "github.com/vercel/turbo/cli/internal/ui"
- "github.com/vercel/turbo/cli/internal/util"
-)
-
-func (rsm *Meta) printExecutionSummary() {
- maybeFullTurbo := ""
- summary := rsm.RunSummary
- ui := rsm.ui
-
- attempted := summary.ExecutionSummary.attempted
- successful := summary.ExecutionSummary.cached + summary.ExecutionSummary.success
- cached := summary.ExecutionSummary.cached
- // TODO: can we use a method on ExecutionSummary here?
- duration := time.Since(summary.ExecutionSummary.startedAt).Truncate(time.Millisecond)
-
- if cached == attempted && attempted > 0 {
- terminalProgram := os.Getenv("TERM_PROGRAM")
- // On the macOS Terminal, the rainbow colors show up as a magenta background
- // with a gray background on a single letter. Instead, we print in bold magenta
- if terminalProgram == "Apple_Terminal" {
- fallbackTurboColor := color.New(color.FgHiMagenta, color.Bold).SprintFunc()
- maybeFullTurbo = fallbackTurboColor(">>> FULL TURBO")
- } else {
- maybeFullTurbo = internalUI.Rainbow(">>> FULL TURBO")
- }
- }
-
- if attempted == 0 {
- ui.Output("") // Clear the line
- ui.Warn("No tasks were executed as part of this run.")
- }
-
- ui.Output("") // Clear the line
- spacer := " " // 4 chars
-
- var lines []string
-
- // The only difference between these two branches is that when there is a run summary
- // we print the path to that file and we adjust the whitespace in the printed text so it aligns.
- // We could just always align to account for the summary line, but that would require a whole
- // bunch of test output assertions to change.
- if rsm.getPath().FileExists() {
- lines = []string{
- util.Sprintf("${BOLD} Tasks:${BOLD_GREEN}%s%v successful${RESET}${GRAY}, %v total${RESET}", spacer, successful, attempted),
- util.Sprintf("${BOLD} Cached:%s%v cached${RESET}${GRAY}, %v total${RESET}", spacer, cached, attempted),
- util.Sprintf("${BOLD} Time:%s%v${RESET} %v${RESET}", spacer, duration, maybeFullTurbo),
- util.Sprintf("${BOLD}Summary:%s%s${RESET}", spacer, rsm.getPath()),
- }
- } else {
- lines = []string{
- util.Sprintf("${BOLD} Tasks:${BOLD_GREEN}%s%v successful${RESET}${GRAY}, %v total${RESET}", spacer, successful, attempted),
- util.Sprintf("${BOLD}Cached:%s%v cached${RESET}${GRAY}, %v total${RESET}", spacer, cached, attempted),
- util.Sprintf("${BOLD} Time:%s%v${RESET} %v${RESET}", spacer, duration, maybeFullTurbo),
- }
- }
-
- // Print the real thing
- for _, line := range lines {
- ui.Output(line)
- }
-
- ui.Output("")
-}
diff --git a/cli/internal/runsummary/format_json.go b/cli/internal/runsummary/format_json.go
deleted file mode 100644
index 76a0a40..0000000
--- a/cli/internal/runsummary/format_json.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package runsummary
-
-import (
- "encoding/json"
-
- "github.com/pkg/errors"
- "github.com/segmentio/ksuid"
- "github.com/vercel/turbo/cli/internal/util"
-)
-
-// FormatJSON returns a json string representing a RunSummary
-func (rsm *Meta) FormatJSON() ([]byte, error) {
- rsm.normalize() // normalize data
-
- var bytes []byte
- var err error
-
- if rsm.singlePackage {
- bytes, err = json.MarshalIndent(nonMonorepoRunSummary(*rsm.RunSummary), "", " ")
- } else {
- bytes, err = json.MarshalIndent(rsm.RunSummary, "", " ")
- }
-
- if err != nil {
- return nil, errors.Wrap(err, "failed to render JSON")
- }
- return bytes, nil
-}
-
-func (rsm *Meta) normalize() {
- for _, t := range rsm.RunSummary.Tasks {
- t.EnvVars.Global = rsm.RunSummary.GlobalHashSummary.envVars
- t.EnvVars.GlobalPassthrough = rsm.RunSummary.GlobalHashSummary.passthroughEnvVars
- }
-
- // Remove execution summary for dry runs
- if rsm.runType == runTypeDryJSON {
- rsm.RunSummary.ExecutionSummary = nil
- }
-
- // For single packages, we don't need the Packages
- // and each task summary needs some cleaning.
- if rsm.singlePackage {
- rsm.RunSummary.Packages = []string{}
-
- for _, task := range rsm.RunSummary.Tasks {
- task.cleanForSinglePackage()
- }
- }
-}
-
-// nonMonorepoRunSummary is an exact copy of RunSummary, but the JSON tags are structured
-// for rendering a single-package run of turbo. Notably, we want to always omit packages
-// since there is no concept of packages in a single-workspace repo.
-// This struct exists solely for the purpose of serializing to JSON and should not be
-// used anywhere else.
-type nonMonorepoRunSummary struct {
- ID ksuid.KSUID `json:"id"`
- Version string `json:"version"`
- TurboVersion string `json:"turboVersion"`
- GlobalHashSummary *GlobalHashSummary `json:"globalCacheInputs"`
- Packages []string `json:"-"`
- EnvMode util.EnvMode `json:"envMode"`
- ExecutionSummary *executionSummary `json:"execution,omitempty"`
- Tasks []*TaskSummary `json:"tasks"`
-}
diff --git a/cli/internal/runsummary/format_text.go b/cli/internal/runsummary/format_text.go
deleted file mode 100644
index 28b1638..0000000
--- a/cli/internal/runsummary/format_text.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package runsummary
-
-import (
- "encoding/json"
- "fmt"
- "os"
- "strconv"
- "strings"
- "text/tabwriter"
-
- "github.com/vercel/turbo/cli/internal/util"
- "github.com/vercel/turbo/cli/internal/workspace"
-)
-
-// FormatAndPrintText prints a Run Summary to the Terminal UI
-func (rsm Meta) FormatAndPrintText(workspaceInfos workspace.Catalog) error {
- ui := rsm.ui
- summary := rsm.RunSummary
-
- rsm.normalize() // normalize data
-
- if !rsm.singlePackage {
- ui.Output("")
- ui.Info(util.Sprintf("${CYAN}${BOLD}Packages in Scope${RESET}"))
- p := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
- fmt.Fprintln(p, "Name\tPath\t")
- for _, pkg := range summary.Packages {
- fmt.Fprintf(p, "%s\t%s\t\n", pkg, workspaceInfos.PackageJSONs[pkg].Dir)
- }
- if err := p.Flush(); err != nil {
- return err
- }
- }
-
- fileCount := 0
- for range summary.GlobalHashSummary.GlobalFileHashMap {
- fileCount = fileCount + 1
- }
- w1 := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
- ui.Output("")
- ui.Info(util.Sprintf("${CYAN}${BOLD}Global Hash Inputs${RESET}"))
- fmt.Fprintln(w1, util.Sprintf(" ${GREY}Global Files\t=\t%d${RESET}", fileCount))
- fmt.Fprintln(w1, util.Sprintf(" ${GREY}External Dependencies Hash\t=\t%s${RESET}", summary.GlobalHashSummary.RootExternalDepsHash))
- fmt.Fprintln(w1, util.Sprintf(" ${GREY}Global Cache Key\t=\t%s${RESET}", summary.GlobalHashSummary.GlobalCacheKey))
- if bytes, err := json.Marshal(summary.GlobalHashSummary.Pipeline); err == nil {
- fmt.Fprintln(w1, util.Sprintf(" ${GREY}Root pipeline\t=\t%s${RESET}", bytes))
- }
- if err := w1.Flush(); err != nil {
- return err
- }
-
- ui.Output("")
- ui.Info(util.Sprintf("${CYAN}${BOLD}Tasks to Run${RESET}"))
-
- for _, task := range summary.Tasks {
- taskName := task.TaskID
-
- if rsm.singlePackage {
- taskName = task.Task
- }
-
- ui.Info(util.Sprintf("${BOLD}%s${RESET}", taskName))
- w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Task\t=\t%s\t${RESET}", task.Task))
-
- if !rsm.singlePackage {
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Package\t=\t%s\t${RESET}", task.Package))
- }
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Hash\t=\t%s\t${RESET}", task.Hash))
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Cached (Local)\t=\t%s\t${RESET}", strconv.FormatBool(task.CacheSummary.Local)))
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Cached (Remote)\t=\t%s\t${RESET}", strconv.FormatBool(task.CacheSummary.Remote)))
-
- if !rsm.singlePackage {
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Directory\t=\t%s\t${RESET}", task.Dir))
- }
-
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Command\t=\t%s\t${RESET}", task.Command))
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Outputs\t=\t%s\t${RESET}", strings.Join(task.Outputs, ", ")))
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Log File\t=\t%s\t${RESET}", task.LogFile))
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Dependencies\t=\t%s\t${RESET}", strings.Join(task.Dependencies, ", ")))
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Dependendents\t=\t%s\t${RESET}", strings.Join(task.Dependents, ", ")))
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Inputs Files Considered\t=\t%d\t${RESET}", len(task.ExpandedInputs)))
-
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Configured Environment Variables\t=\t%s\t${RESET}", strings.Join(task.EnvVars.Configured, ", ")))
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Inferred Environment Variables\t=\t%s\t${RESET}", strings.Join(task.EnvVars.Inferred, ", ")))
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Global Environment Variables\t=\t%s\t${RESET}", strings.Join(task.EnvVars.Global, ", ")))
-
- bytes, err := json.Marshal(task.ResolvedTaskDefinition)
- // If there's an error, we can silently ignore it, we don't need to block the entire print.
- if err == nil {
- fmt.Fprintln(w, util.Sprintf(" ${GREY}ResolvedTaskDefinition\t=\t%s\t${RESET}", string(bytes)))
- }
-
- fmt.Fprintln(w, util.Sprintf(" ${GREY}Framework\t=\t%s\t${RESET}", task.Framework))
- if err := w.Flush(); err != nil {
- return err
- }
- }
- return nil
-}
diff --git a/cli/internal/runsummary/globalhash_summary.go b/cli/internal/runsummary/globalhash_summary.go
deleted file mode 100644
index e24976d5..0000000
--- a/cli/internal/runsummary/globalhash_summary.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package runsummary
-
-import (
- "github.com/vercel/turbo/cli/internal/env"
- "github.com/vercel/turbo/cli/internal/fs"
- "github.com/vercel/turbo/cli/internal/turbopath"
-)
-
-// GlobalHashSummary contains the pieces of data that impacted the global hash (then then impacted the task hash)
-type GlobalHashSummary struct {
- GlobalCacheKey string `json:"rootKey"`
- GlobalFileHashMap map[turbopath.AnchoredUnixPath]string `json:"files"`
- RootExternalDepsHash string `json:"hashOfExternalDependencies"`
- Pipeline fs.PristinePipeline `json:"rootPipeline"`
-
- // This is a private field because and not in JSON, because we'll add it to each task
- envVars env.EnvironmentVariablePairs
- passthroughEnvVars env.EnvironmentVariablePairs
-}
-
-// NewGlobalHashSummary creates a GlobalHashSummary struct from a set of fields.
-func NewGlobalHashSummary(
- fileHashMap map[turbopath.AnchoredUnixPath]string,
- rootExternalDepsHash string,
- envVars env.DetailedMap,
- passthroughEnvVars env.EnvironmentVariableMap,
- globalCacheKey string,
- pipeline fs.PristinePipeline,
-) *GlobalHashSummary {
- return &GlobalHashSummary{
- envVars: envVars.All.ToSecretHashable(),
- passthroughEnvVars: passthroughEnvVars.ToSecretHashable(),
- GlobalFileHashMap: fileHashMap,
- RootExternalDepsHash: rootExternalDepsHash,
- GlobalCacheKey: globalCacheKey,
- Pipeline: pipeline,
- }
-}
diff --git a/cli/internal/runsummary/run_summary.go b/cli/internal/runsummary/run_summary.go
deleted file mode 100644
index a297114..0000000
--- a/cli/internal/runsummary/run_summary.go
+++ /dev/null
@@ -1,320 +0,0 @@
-// Package runsummary implements structs that report on a `turbo run` and `turbo run --dry`
-package runsummary
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "path/filepath"
- "sync"
- "time"
-
- "github.com/mitchellh/cli"
- "github.com/segmentio/ksuid"
- "github.com/vercel/turbo/cli/internal/client"
- "github.com/vercel/turbo/cli/internal/spinner"
- "github.com/vercel/turbo/cli/internal/turbopath"
- "github.com/vercel/turbo/cli/internal/util"
- "github.com/vercel/turbo/cli/internal/workspace"
-)
-
-// MissingTaskLabel is printed when a package is missing a definition for a task that is supposed to run
-// E.g. if `turbo run build --dry` is run, and package-a doesn't define a `build` script in package.json,
-// the RunSummary will print this, instead of the script (e.g. `next build`).
-const MissingTaskLabel = "<NONEXISTENT>"
-
-// MissingFrameworkLabel is a string to identify when a workspace doesn't detect a framework
-const MissingFrameworkLabel = "<NO FRAMEWORK DETECTED>"
-
-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
-
-const (
- runTypeReal runType = iota
- runTypeDryText
- runTypeDryJSON
-)
-
-// 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
- 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.
-type RunSummary struct {
- ID ksuid.KSUID `json:"id"`
- Version string `json:"version"`
- TurboVersion string `json:"turboVersion"`
- GlobalHashSummary *GlobalHashSummary `json:"globalCacheInputs"`
- Packages []string `json:"packages"`
- EnvMode util.EnvMode `json:"envMode"`
- ExecutionSummary *executionSummary `json:"execution,omitempty"`
- Tasks []*TaskSummary `json:"tasks"`
-}
-
-// NewRunSummary returns a RunSummary instance
-func NewRunSummary(
- startAt time.Time,
- ui cli.Ui,
- repoRoot turbopath.AbsoluteSystemPath,
- repoPath turbopath.RelativeSystemPath,
- turboVersion string,
- apiClient *client.APIClient,
- runOpts util.RunOpts,
- packages []string,
- globalEnvMode util.EnvMode,
- globalHashSummary *GlobalHashSummary,
- synthesizedCommand string,
-) Meta {
- singlePackage := runOpts.SinglePackage
- profile := runOpts.Profile
- shouldSave := runOpts.Summarize
- spaceID := runOpts.ExperimentalSpaceID
-
- runType := runTypeReal
- if runOpts.DryRun {
- runType = runTypeDryText
- if runOpts.DryRunJSON {
- runType = runTypeDryJSON
- }
- }
-
- executionSummary := newExecutionSummary(synthesizedCommand, repoPath, startAt, profile)
-
- return Meta{
- RunSummary: &RunSummary{
- ID: ksuid.New(),
- Version: runSummarySchemaVersion,
- ExecutionSummary: executionSummary,
- TurboVersion: turboVersion,
- Packages: packages,
- EnvMode: globalEnvMode,
- Tasks: []*TaskSummary{},
- GlobalHashSummary: globalHashSummary,
- },
- ui: ui,
- runType: runType,
- repoRoot: repoRoot,
- singlePackage: singlePackage,
- shouldSave: shouldSave,
- apiClient: apiClient,
- spaceID: spaceID,
- synthesizedCommand: synthesizedCommand,
- }
-}
-
-// getPath returns a path to where the runSummary is written.
-// The returned path will always be relative to the dir passsed in.
-// We don't do a lot of validation, so `../../` paths are allowed.
-func (rsm *Meta) getPath() turbopath.AbsoluteSystemPath {
- filename := fmt.Sprintf("%s.json", rsm.RunSummary.ID)
- return rsm.repoRoot.UntypedJoin(filepath.Join(".turbo", "runs"), filename)
-}
-
-// Close wraps up the RunSummary at the end of a `turbo run`.
-func (rsm *Meta) Close(ctx context.Context, exitCode int, workspaceInfos workspace.Catalog) error {
- if rsm.runType == runTypeDryJSON || rsm.runType == runTypeDryText {
- return rsm.closeDryRun(workspaceInfos)
- }
-
- rsm.RunSummary.ExecutionSummary.exitCode = exitCode
- rsm.RunSummary.ExecutionSummary.endedAt = time.Now()
-
- summary := rsm.RunSummary
- if err := writeChrometracing(summary.ExecutionSummary.profileFilename, rsm.ui); err != nil {
- rsm.ui.Error(fmt.Sprintf("Error writing tracing data: %v", err))
- }
-
- // TODO: printing summary to local, writing to disk, and sending to API
- // are all the same thng, we should use a strategy similar to cache save/upload to
- // do this in parallel.
-
- // Otherwise, attempt to save the summary
- // Warn on the error, but we don't need to throw an error
- if rsm.shouldSave {
- if err := rsm.save(); err != nil {
- rsm.ui.Warn(fmt.Sprintf("Error writing run summary: %v", err))
- }
- }
-
- rsm.printExecutionSummary()
-
- // If we're not supposed to save or if there's no spaceID
- if !rsm.shouldSave || rsm.spaceID == "" {
- return nil
- }
-
- if !rsm.apiClient.IsLinked() {
- rsm.ui.Warn("Failed to post to space because repo is not linked to a Space. Run `turbo link` first.")
- return nil
- }
-
- // Wrap the record function so we can hoist out url/errors but keep
- // the function signature/type the spinner.WaitFor expects.
- var url string
- var errs []error
- record := func() {
- url, errs = rsm.record()
- }
-
- func() {
- _ = spinner.WaitFor(ctx, record, rsm.ui, "...sending run summary...", 1000*time.Millisecond)
- }()
-
- // After the spinner is done, print any errors and the url
- if len(errs) > 0 {
- rsm.ui.Warn("Errors recording run to Spaces")
- for _, err := range errs {
- rsm.ui.Warn(fmt.Sprintf("%v", err))
- }
- }
-
- if url != "" {
- rsm.ui.Output(fmt.Sprintf("Run: %s", url))
- rsm.ui.Output("")
- }
-
- return nil
-}
-
-// closeDryRun wraps up the Run Summary at the end of `turbo run --dry`.
-// Ideally this should be inlined into Close(), but RunSummary doesn't currently
-// have context about whether a run was real or dry.
-func (rsm *Meta) closeDryRun(workspaceInfos workspace.Catalog) error {
- // Render the dry run as json
- if rsm.runType == runTypeDryJSON {
- rendered, err := rsm.FormatJSON()
- if err != nil {
- return err
- }
-
- rsm.ui.Output(string(rendered))
- return nil
- }
-
- return rsm.FormatAndPrintText(workspaceInfos)
-}
-
-// TrackTask makes it possible for the consumer to send information about the execution of a task.
-func (summary *RunSummary) TrackTask(taskID string) (func(outcome executionEventName, err error, exitCode *int), *TaskExecutionSummary) {
- return summary.ExecutionSummary.run(taskID)
-}
-
-// Save saves the run summary to a file
-func (rsm *Meta) save() error {
- json, err := rsm.FormatJSON()
- if err != nil {
- return err
- }
-
- // summaryPath will always be relative to the dir passsed in.
- // We don't do a lot of validation, so `../../` paths are allowed
- summaryPath := rsm.getPath()
-
- if err := summaryPath.EnsureDir(); err != nil {
- return err
- }
-
- return summaryPath.WriteFile(json, 0644)
-}
-
-// record sends the summary to the API
-func (rsm *Meta) record() (string, []error) {
- errs := []error{}
-
- // Right now we'll send the POST to create the Run and the subsequent task payloads
- // 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.
- createRunEndpoint := fmt.Sprintf(runsEndpoint, rsm.spaceID)
- response := &spacesRunResponse{}
-
- payload := rsm.newSpacesRunCreatePayload()
- if startPayload, err := json.Marshal(payload); err == nil {
- if resp, err := rsm.apiClient.JSONPost(createRunEndpoint, startPayload); err != nil {
- errs = append(errs, fmt.Errorf("POST %s: %w", createRunEndpoint, err))
- } else {
- if err := json.Unmarshal(resp, response); err != nil {
- errs = append(errs, fmt.Errorf("Error unmarshaling response: %w", err))
- }
- }
- }
-
- if response.ID != "" {
- if taskErrs := rsm.postTaskSummaries(response.ID); len(taskErrs) > 0 {
- errs = append(errs, taskErrs...)
- }
-
- if donePayload, err := json.Marshal(newSpacesDonePayload(rsm.RunSummary)); err == nil {
- patchURL := fmt.Sprintf(runsPatchEndpoint, rsm.spaceID, response.ID)
- if _, err := rsm.apiClient.JSONPatch(patchURL, donePayload); err != nil {
- errs = append(errs, fmt.Errorf("PATCH %s: %w", patchURL, err))
- }
- }
- }
-
- if len(errs) > 0 {
- return response.URL, errs
- }
-
- return response.URL, nil
-}
-
-func (rsm *Meta) postTaskSummaries(runID string) []error {
- errs := []error{}
- // We make at most 8 requests at a time.
- maxParallelRequests := 8
- taskSummaries := rsm.RunSummary.Tasks
- taskCount := len(taskSummaries)
- taskURL := fmt.Sprintf(tasksEndpoint, rsm.spaceID, runID)
-
- parallelRequestCount := maxParallelRequests
- if taskCount < maxParallelRequests {
- parallelRequestCount = taskCount
- }
-
- queue := make(chan int, taskCount)
-
- wg := &sync.WaitGroup{}
- for i := 0; i < parallelRequestCount; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for index := range queue {
- task := taskSummaries[index]
- 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("Error sending %s summary to space: %w", task.TaskID, err))
- }
- }
- }
- }()
- }
-
- for index := range taskSummaries {
- queue <- index
- }
- close(queue)
- wg.Wait()
-
- if len(errs) > 0 {
- return errs
- }
-
- return nil
-}
diff --git a/cli/internal/runsummary/spaces.go b/cli/internal/runsummary/spaces.go
deleted file mode 100644
index bf19941..0000000
--- a/cli/internal/runsummary/spaces.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package runsummary
-
-import (
- "github.com/vercel/turbo/cli/internal/ci"
-)
-
-// spacesRunResponse deserialized the response from POST Run endpoint
-type spacesRunResponse struct {
- ID string
- URL string
-}
-
-type spacesRunPayload struct {
- StartTime int64 `json:"startTime,omitempty"` // when the run was started
- EndTime int64 `json:"endTime,omitempty"` // when the run ended. we should never submit start and end at the same time.
- Status string `json:"status,omitempty"` // Status is "running" or "completed"
- Type string `json:"type,omitempty"` // hardcoded to "TURBO"
- ExitCode int `json:"exitCode,omitempty"` // exit code for the full run
- Command string `json:"command,omitempty"` // the thing that kicked off the turbo run
- RepositoryPath string `json:"repositoryPath,omitempty"` // where the command was invoked from
- Context string `json:"context,omitempty"` // the host on which this Run was executed (e.g. Github Action, Vercel, etc)
-
- // TODO: we need to add these in
- // originationUser string
- // gitBranch string
- // gitSha string
-}
-
-// spacesCacheStatus is the same as TaskCacheSummary so we can convert
-// spacesCacheStatus(cacheSummary), but change the json tags, to omit local and remote fields
-type spacesCacheStatus struct {
- // omitted fields, but here so we can convert from TaskCacheSummary easily
- Local bool `json:"-"`
- Remote bool `json:"-"`
- Status string `json:"status"` // should always be there
- Source string `json:"source,omitempty"`
- TimeSaved int `json:"timeSaved"`
-}
-
-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 spacesCacheStatus `json:"cache,omitempty"`
- ExitCode int `json:"exitCode,omitempty"`
- Dependencies []string `json:"dependencies,omitempty"`
- Dependents []string `json:"dependents,omitempty"`
- Logs string `json:"log"`
-}
-
-func (rsm *Meta) newSpacesRunCreatePayload() *spacesRunPayload {
- startTime := rsm.RunSummary.ExecutionSummary.startedAt.UnixMilli()
- context := "LOCAL"
- if name := ci.Constant(); name != "" {
- context = name
- }
- return &spacesRunPayload{
- StartTime: startTime,
- Status: "running",
- Command: rsm.synthesizedCommand,
- RepositoryPath: rsm.repoPath.ToString(),
- Type: "TURBO",
- Context: context,
- }
-}
-
-func newSpacesDonePayload(runsummary *RunSummary) *spacesRunPayload {
- endTime := runsummary.ExecutionSummary.endedAt.UnixMilli()
- return &spacesRunPayload{
- Status: "completed",
- EndTime: endTime,
- ExitCode: runsummary.ExecutionSummary.exitCode,
- }
-}
-
-func newSpacesTaskPayload(taskSummary *TaskSummary) *spacesTask {
- startTime := taskSummary.Execution.startAt.UnixMilli()
- endTime := taskSummary.Execution.endTime().UnixMilli()
-
- return &spacesTask{
- Key: taskSummary.TaskID,
- Name: taskSummary.Task,
- Workspace: taskSummary.Package,
- Hash: taskSummary.Hash,
- StartTime: startTime,
- EndTime: endTime,
- Cache: spacesCacheStatus(taskSummary.CacheSummary), // wrapped so we can remove fields
- ExitCode: *taskSummary.Execution.exitCode,
- Dependencies: taskSummary.Dependencies,
- Dependents: taskSummary.Dependents,
- Logs: string(taskSummary.GetLogs()),
- }
-}
diff --git a/cli/internal/runsummary/task_summary.go b/cli/internal/runsummary/task_summary.go
deleted file mode 100644
index fb0cb30..0000000
--- a/cli/internal/runsummary/task_summary.go
+++ /dev/null
@@ -1,117 +0,0 @@
-package runsummary
-
-import (
- "os"
-
- "github.com/vercel/turbo/cli/internal/cache"
- "github.com/vercel/turbo/cli/internal/fs"
- "github.com/vercel/turbo/cli/internal/turbopath"
- "github.com/vercel/turbo/cli/internal/util"
-)
-
-// TaskCacheSummary is an extended version of cache.ItemStatus
-// that includes TimeSaved and some better data.
-type TaskCacheSummary struct {
- Local bool `json:"local"` // Deprecated, but keeping around for --dry=json
- Remote bool `json:"remote"` // Deprecated, but keeping around for --dry=json
- Status string `json:"status"` // should always be there
- Source string `json:"source,omitempty"` // can be empty on status:miss
- TimeSaved int `json:"timeSaved"` // always include, but can be 0
-}
-
-// NewTaskCacheSummary decorates a cache.ItemStatus into a TaskCacheSummary
-// Importantly, it adds the derived keys of `source` and `status` based on
-// the local/remote booleans. It would be nice if these were just included
-// from upstream, but that is a more invasive change.
-func NewTaskCacheSummary(itemStatus cache.ItemStatus, timeSaved *int) TaskCacheSummary {
- status := cache.CacheEventMiss
- if itemStatus.Local || itemStatus.Remote {
- status = cache.CacheEventHit
- }
-
- var source string
- if itemStatus.Local {
- source = cache.CacheSourceFS
- } else if itemStatus.Remote {
- source = cache.CacheSourceRemote
- }
-
- cs := TaskCacheSummary{
- // copy these over
- Local: itemStatus.Local,
- Remote: itemStatus.Remote,
- Status: status,
- Source: source,
- }
- // add in a dereferences timeSaved, should be 0 if nil
- if timeSaved != nil {
- cs.TimeSaved = *timeSaved
- }
- return cs
-}
-
-// TaskSummary contains information about the task that was about to run
-// TODO(mehulkar): `Outputs` and `ExcludedOutputs` are slightly redundant
-// as the information is also available in ResolvedTaskDefinition. We could remove them
-// and favor a version of Outputs that is the fully expanded list of files.
-type TaskSummary struct {
- TaskID string `json:"taskId,omitempty"`
- Task string `json:"task"`
- Package string `json:"package,omitempty"`
- Hash string `json:"hash"`
- ExpandedInputs map[turbopath.AnchoredUnixPath]string `json:"inputs"`
- ExternalDepsHash string `json:"hashOfExternalDependencies"`
- CacheSummary TaskCacheSummary `json:"cache"`
- Command string `json:"command"`
- CommandArguments []string `json:"cliArguments"`
- Outputs []string `json:"outputs"`
- ExcludedOutputs []string `json:"excludedOutputs"`
- LogFile string `json:"logFile"`
- Dir string `json:"directory,omitempty"`
- Dependencies []string `json:"dependencies"`
- Dependents []string `json:"dependents"`
- ResolvedTaskDefinition *fs.TaskDefinition `json:"resolvedTaskDefinition"`
- ExpandedOutputs []turbopath.AnchoredSystemPath `json:"expandedOutputs"`
- Framework string `json:"framework"`
- EnvMode util.EnvMode `json:"envMode"`
- EnvVars TaskEnvVarSummary `json:"environmentVariables"`
- Execution *TaskExecutionSummary `json:"execution,omitempty"` // omit when it's not set
-}
-
-// GetLogs reads the Logfile and returns the data
-func (ts *TaskSummary) GetLogs() []byte {
- bytes, err := os.ReadFile(ts.LogFile)
- if err != nil {
- return []byte{}
- }
- return bytes
-}
-
-// TaskEnvVarSummary contains the environment variables that impacted a task's hash
-type TaskEnvVarSummary struct {
- Configured []string `json:"configured"`
- Inferred []string `json:"inferred"`
- Global []string `json:"global"`
- Passthrough []string `json:"passthrough"`
- GlobalPassthrough []string `json:"globalPassthrough"`
-}
-
-// cleanForSinglePackage converts a TaskSummary to remove references to workspaces
-func (ts *TaskSummary) cleanForSinglePackage() {
- dependencies := make([]string, len(ts.Dependencies))
- for i, dependency := range ts.Dependencies {
- dependencies[i] = util.StripPackageName(dependency)
- }
- dependents := make([]string, len(ts.Dependents))
- for i, dependent := range ts.Dependents {
- dependents[i] = util.StripPackageName(dependent)
- }
- task := util.StripPackageName(ts.TaskID)
-
- ts.TaskID = task
- ts.Task = task
- ts.Dependencies = dependencies
- ts.Dependents = dependents
- ts.Dir = ""
- ts.Package = ""
-}