Skip to content

Commit

Permalink
Generate a run summary file on turbo run (#4069)
Browse files Browse the repository at this point in the history
  • Loading branch information
mehulkar authored Mar 7, 2023
1 parent f6b980a commit 9d49e67
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 84 deletions.
1 change: 1 addition & 0 deletions cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
github.com/pyr-sh/dag v1.0.0
github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f
github.com/schollz/progressbar/v3 v3.9.0
github.com/segmentio/ksuid v1.0.4
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.0
Expand Down
2 changes: 2 additions & 0 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43
github.com/schollz/progressbar/v3 v3.9.0 h1:k9SRNQ8KZyibz1UZOaKxnkUE3iGtmGSDt1YY9KlCYQk=
github.com/schollz/progressbar/v3 v3.9.0/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
Expand Down
17 changes: 17 additions & 0 deletions cli/integration_tests/basic_monorepo/run_summary.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Setup
$ . ${TESTDIR}/../setup.sh
$ . ${TESTDIR}/setup.sh $(pwd)

$ rm -rf .turbo/runs
$ TURBO_RUN_SUMMARY=true ${TURBO} run build > /dev/null
# no output, just check for 0 status code
$ test -d .turbo/runs
$ ls .turbo/runs/*.json | wc -l
\s*1 (re)

# Without env var, no summary file is generated
$ rm -rf .turbo/runs
$ ${TURBO} run build > /dev/null
# validate with exit code so the test works on macOS and linux
$ test -d .turbo/runs
[1]
45 changes: 36 additions & 9 deletions cli/internal/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/pyr-sh/dag"
"github.com/vercel/turbo/cli/internal/fs"
"github.com/vercel/turbo/cli/internal/nodes"
"github.com/vercel/turbo/cli/internal/runsummary"
"github.com/vercel/turbo/cli/internal/taskhash"
"github.com/vercel/turbo/cli/internal/turbopath"
"github.com/vercel/turbo/cli/internal/util"
Expand Down Expand Up @@ -48,7 +49,7 @@ func (g *CompleteGraph) GetPackageTaskVisitor(
taskGraph *dag.AcyclicGraph,
getArgs func(taskID string) []string,
logger hclog.Logger,
visitor func(ctx gocontext.Context, packageTask *nodes.PackageTask) error,
visitor func(ctx gocontext.Context, packageTask *nodes.PackageTask, taskSummary *runsummary.TaskSummary) error,
) func(taskID string) error {
return func(taskID string) error {
packageName, taskName := util.GetPackageTaskFromId(taskID)
Expand All @@ -62,6 +63,7 @@ func (g *CompleteGraph) GetPackageTaskVisitor(
return fmt.Errorf("Could not find definition for task")
}

// TODO: maybe we can remove this PackageTask struct at some point
packageTask := &nodes.PackageTask{
TaskID: taskID,
Task: taskName,
Expand All @@ -80,22 +82,47 @@ func (g *CompleteGraph) GetPackageTaskVisitor(
getArgs(taskName),
)

// Not being able to construct the task hash is a hard error
if err != nil {
return fmt.Errorf("Hashing error: %v", err)
}

pkgDir := pkg.Dir
packageTask.Hash = hash
packageTask.HashedEnvVars = g.TaskHashTracker.GetEnvVars(packageTask.TaskID)
packageTask.ExpandedInputs = g.TaskHashTracker.GetExpandedInputs(packageTask)
packageTask.Framework = g.TaskHashTracker.GetFramework(packageTask.TaskID)
envVars := g.TaskHashTracker.GetEnvVars(taskID)
expandedInputs := g.TaskHashTracker.GetExpandedInputs(packageTask)
framework := g.TaskHashTracker.GetFramework(taskID)

// Assign remaining fields to packageTask
var command string
if cmd, ok := pkg.Scripts[taskName]; ok {
packageTask.Command = cmd
command = cmd
}

packageTask.LogFile = repoRelativeLogFile(packageTask)
logFile := repoRelativeLogFile(pkgDir, taskName)
packageTask.LogFile = logFile
packageTask.Command = command

summary := &runsummary.TaskSummary{
TaskID: taskID,
Task: taskName,
Hash: hash,
Package: packageName,
Dir: pkgDir.ToString(),
Outputs: taskDefinition.Outputs.Inclusions,
ExcludedOutputs: taskDefinition.Outputs.Exclusions,
LogFile: logFile,
ResolvedTaskDefinition: taskDefinition,
ExpandedInputs: expandedInputs,
Command: command,
Framework: framework,
EnvVars: runsummary.TaskEnvVarSummary{
Configured: envVars.BySource.Explicit.ToSecretHashable(),
Inferred: envVars.BySource.Prefixed.ToSecretHashable(),
},
}

return visitor(ctx, packageTask)
return visitor(ctx, packageTask, summary)
}
}

Expand Down Expand Up @@ -152,6 +179,6 @@ func (g *CompleteGraph) GetPackageJSONFromWorkspace(workspaceName string) (*fs.P

// repoRelativeLogFile returns the path to the log file for this task execution as a
// relative path from the root of the monorepo.
func repoRelativeLogFile(pt *nodes.PackageTask) string {
return filepath.Join(pt.Pkg.Dir.ToStringDuringMigration(), ".turbo", fmt.Sprintf("turbo-%v.log", pt.Task))
func repoRelativeLogFile(dir turbopath.AnchoredSystemPath, taskName string) string {
return filepath.Join(dir.ToStringDuringMigration(), ".turbo", fmt.Sprintf("turbo-%v.log", taskName))
}
5 changes: 0 additions & 5 deletions cli/internal/nodes/packagetask.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ package nodes
import (
"fmt"

"github.com/vercel/turbo/cli/internal/env"
"github.com/vercel/turbo/cli/internal/fs"
"github.com/vercel/turbo/cli/internal/turbopath"
)

// PackageTask represents running a particular task in a particular package
Expand All @@ -21,10 +19,7 @@ type PackageTask struct {
Outputs []string
ExcludedOutputs []string
LogFile string
ExpandedInputs map[turbopath.AnchoredUnixPath]string
Hash string
HashedEnvVars env.DetailedMap
Framework string
}

// OutputPrefix returns the prefix to be used for logging and ui for this task
Expand Down
66 changes: 21 additions & 45 deletions cli/internal/run/dry_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ import (
"github.com/vercel/turbo/cli/internal/util"
)

// 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>"

// DryRun gets all the info needed from tasks and prints out a summary, but doesn't actually
// execute the task.
func DryRun(
Expand Down Expand Up @@ -64,30 +59,20 @@ func DryRun(
if err != nil {
return err
}
base.UI.Output(rendered)
base.UI.Output(string(rendered))
return nil
}

return summary.FormatAndPrintText(base.UI, g.WorkspaceInfos, singlePackage)
}

func executeDryRun(ctx gocontext.Context, engine *core.Engine, g *graph.CompleteGraph, taskHashTracker *taskhash.Tracker, rs *runSpec, base *cmdutil.CmdBase, turboCache cache.Cache) ([]runsummary.TaskSummary, error) {
taskIDs := []runsummary.TaskSummary{}

dryRunExecFunc := func(ctx gocontext.Context, packageTask *nodes.PackageTask) error {
command := missingTaskLabel
if packageTask.Command != "" {
command = packageTask.Command
}

framework := runsummary.MissingFrameworkLabel
if packageTask.Framework != "" {
framework = packageTask.Framework
}
func executeDryRun(ctx gocontext.Context, engine *core.Engine, g *graph.CompleteGraph, taskHashTracker *taskhash.Tracker, rs *runSpec, base *cmdutil.CmdBase, turboCache cache.Cache) ([]*runsummary.TaskSummary, error) {
taskIDs := []*runsummary.TaskSummary{}

dryRunExecFunc := func(ctx gocontext.Context, packageTask *nodes.PackageTask, taskSummary *runsummary.TaskSummary) error {
isRootTask := packageTask.PackageName == util.RootPkgName
if isRootTask && commandLooksLikeTurbo(command) {
return fmt.Errorf("root task %v (%v) looks like it invokes turbo and might cause a loop", packageTask.Task, command)
if isRootTask && commandLooksLikeTurbo(taskSummary.Command) {
return fmt.Errorf("root task %v (%v) looks like it invokes turbo and might cause a loop", packageTask.Task, taskSummary.Command)
}

ancestors, err := engine.GetTaskGraphAncestors(packageTask.TaskID)
Expand All @@ -100,34 +85,25 @@ func executeDryRun(ctx gocontext.Context, engine *core.Engine, g *graph.Complete
return err
}

hash := packageTask.Hash
itemStatus, err := turboCache.Exists(hash)
itemStatus, err := turboCache.Exists(packageTask.Hash)
if err != nil {
return err
}

taskIDs = append(taskIDs, runsummary.TaskSummary{
TaskID: packageTask.TaskID,
Task: packageTask.Task,
Package: packageTask.PackageName,
Dir: packageTask.Dir,
Outputs: packageTask.Outputs,
ExcludedOutputs: packageTask.ExcludedOutputs,
LogFile: packageTask.LogFile,
ResolvedTaskDefinition: packageTask.TaskDefinition,
ExpandedInputs: packageTask.ExpandedInputs,
Command: command,
Framework: framework,
EnvVars: runsummary.TaskEnvVarSummary{
Configured: packageTask.HashedEnvVars.BySource.Explicit.ToSecretHashable(),
Inferred: packageTask.HashedEnvVars.BySource.Prefixed.ToSecretHashable(),
},

Hash: hash,
CacheState: itemStatus, // TODO(mehulkar): Move this to PackageTask
Dependencies: ancestors, // TODO(mehulkar): Move this to PackageTask
Dependents: descendents, // TODO(mehulkar): Move this to PackageTask
})
// Assign some fallbacks if they were missing
if taskSummary.Command == "" {
taskSummary.Command = runsummary.MissingTaskLabel
}

if taskSummary.Framework == "" {
taskSummary.Framework = runsummary.MissingFrameworkLabel
}

taskSummary.CacheState = itemStatus // TODO(mehulkar): Move this to PackageTask
taskSummary.Dependencies = ancestors // TODO(mehulkar): Move this to PackageTask
taskSummary.Dependents = descendents // TODO(mehulkar): Move this to PackageTask

taskIDs = append(taskIDs, taskSummary)

return nil
}
Expand Down
17 changes: 16 additions & 1 deletion cli/internal/run/real_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/vercel/turbo/cli/internal/packagemanager"
"github.com/vercel/turbo/cli/internal/process"
"github.com/vercel/turbo/cli/internal/runcache"
"github.com/vercel/turbo/cli/internal/runsummary"
"github.com/vercel/turbo/cli/internal/spinner"
"github.com/vercel/turbo/cli/internal/taskhash"
"github.com/vercel/turbo/cli/internal/turbopath"
Expand All @@ -40,6 +41,7 @@ func RealRun(
turboCache cache.Cache,
packagesInScope []string,
base *cmdutil.CmdBase,
runSummary *runsummary.RunSummary,
packageManager *packagemanager.PackageManager,
processes *process.Manager,
runState *RunState,
Expand Down Expand Up @@ -88,8 +90,10 @@ func RealRun(
Concurrency: rs.Opts.runOpts.concurrency,
}

execFunc := func(ctx gocontext.Context, packageTask *nodes.PackageTask) error {
taskSummaries := []*runsummary.TaskSummary{}
execFunc := func(ctx gocontext.Context, packageTask *nodes.PackageTask, taskSummary *runsummary.TaskSummary) error {
deps := engine.TaskGraph.DownEdges(packageTask.TaskID)
taskSummaries = append(taskSummaries, taskSummary)
// deps here are passed in to calculate the task hash
return ec.exec(ctx, packageTask, deps)
}
Expand All @@ -105,6 +109,9 @@ func RealRun(
exitCode := 0
exitCodeErr := &process.ChildExit{}

// Assign tasks after execution
runSummary.Tasks = taskSummaries

for _, err := range errs {
if errors.As(err, &exitCodeErr) {
if exitCodeErr.ExitCode > exitCode {
Expand All @@ -120,6 +127,14 @@ func RealRun(
if err := runState.Close(base.UI); err != nil {
return errors.Wrap(err, "error with profiler")
}

// Write Run Summary if we wanted to
if rs.Opts.runOpts.summarize {
if err := runSummary.Save(base.RepoRoot, singlePackage); err != nil {
base.UI.Warn(fmt.Sprintf("Failed to write run summary: %s", err))
}
}

if exitCode != 0 {
return &process.ChildExit{
ExitCode: exitCode,
Expand Down
36 changes: 19 additions & 17 deletions cli/internal/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ func configureRun(base *cmdutil.CmdBase, opts *Opts, signalWatcher *signals.Watc
opts.cacheOpts.SkipFilesystem = true
}

if os.Getenv("TURBO_RUN_SUMMARY") == "true" {
opts.runOpts.summarize = true
}

processes := process.NewManager(base.Logger.Named("processes"))
signalWatcher.AddOnClose(processes.Close)
return &run{
Expand Down Expand Up @@ -345,25 +349,22 @@ func (r *run) run(ctx gocontext.Context, targets []string) error {
}
}

// RunSummary contains information that is statically analyzable about
// the tasks that we expect to run based on the user command.
summary := runsummary.NewRunSummary(
r.base.TurboVersion,
packagesInScope,
runsummary.NewGlobalHashSummary(
globalHashable.globalFileHashMap,
globalHashable.rootExternalDepsHash,
globalHashable.hashedSortedEnvPairs,
globalHashable.globalCacheKey,
globalHashable.pipeline,
),
)

// Dry Run
if rs.Opts.runOpts.dryRun {
// dryRunSummary contains information that is statically analyzable about
// the tasks that we expect to run based on the user command.
// Currently, we only emit this on dry runs, but it may be useful for real runs later also.
summary := &runsummary.RunSummary{
TurboVersion: r.base.TurboVersion,
Packages: packagesInScope,
// TODO(mehulkar): passing the globalHashable struct directly caused a type mismatch compilation error
GlobalHashSummary: runsummary.NewGlobalHashSummary(
globalHashable.globalFileHashMap,
globalHashable.rootExternalDepsHash,
globalHashable.hashedSortedEnvPairs,
globalHashable.globalCacheKey,
globalHashable.pipeline,
),
Tasks: []runsummary.TaskSummary{},
}

return DryRun(
ctx,
g,
Expand All @@ -388,6 +389,7 @@ func (r *run) run(ctx gocontext.Context, targets []string) error {
turboCache,
packagesInScope,
r.base,
summary,
// Extra arg only for regular runs, dry-run doesn't get this
packageManager,
r.processes,
Expand Down
3 changes: 3 additions & 0 deletions cli/internal/run/run_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,7 @@ type runOpts struct {

// logPrefix controls whether we should print a prefix in task logs
logPrefix string

// Whether turbo should create a run summary
summarize bool
}
12 changes: 6 additions & 6 deletions cli/internal/runsummary/format_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import (
)

// FormatJSON returns a json string representing a RunSummary
func (summary *RunSummary) FormatJSON(singlePackage bool) (string, error) {
func (summary *RunSummary) FormatJSON(singlePackage bool) ([]byte, error) {
if singlePackage {
return summary.formatJSONSinglePackage()
}

bytes, err := json.MarshalIndent(summary, "", " ")
if err != nil {
return "", errors.Wrap(err, "failed to render JSON")
return nil, errors.Wrap(err, "failed to render JSON")
}
return string(bytes), nil
return bytes, nil
}

func (summary *RunSummary) formatJSONSinglePackage() (string, error) {
func (summary *RunSummary) formatJSONSinglePackage() ([]byte, error) {
singlePackageTasks := make([]singlePackageTaskSummary, len(summary.Tasks))

for i, task := range summary.Tasks {
Expand All @@ -30,8 +30,8 @@ func (summary *RunSummary) formatJSONSinglePackage() (string, error) {

bytes, err := json.MarshalIndent(spSummary, "", " ")
if err != nil {
return "", errors.Wrap(err, "failed to render JSON")
return nil, errors.Wrap(err, "failed to render JSON")
}

return string(bytes), nil
return bytes, nil
}
Loading

0 comments on commit 9d49e67

Please sign in to comment.