Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send run summary to API #4250

Merged
merged 13 commits into from
Mar 23, 2023
2 changes: 1 addition & 1 deletion cli/integration_tests/bad_flag.t
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Bad flag with an implied run command should display run flags

note: to pass '--bad-flag' as a value, use '-- --bad-flag'

Usage: turbo <--cache-dir <CACHE_DIR>|--cache-workers <CACHE_WORKERS>|--concurrency <CONCURRENCY>|--continue|--dry-run [<DRY_RUN>]|--single-package|--filter <FILTER>|--force|--global-deps <GLOBAL_DEPS>|--graph [<GRAPH>]|--ignore <IGNORE>|--include-dependencies|--no-cache|--no-daemon|--no-deps|--output-logs <OUTPUT_LOGS>|--only|--parallel|--pkg-inference-root <PKG_INFERENCE_ROOT>|--profile <PROFILE>|--remote-only|--scope <SCOPE>|--since <SINCE>|--log-prefix <LOG_PREFIX>|TASKS|PASS_THROUGH_ARGS>
Usage: turbo <--cache-dir <CACHE_DIR>|--cache-workers <CACHE_WORKERS>|--concurrency <CONCURRENCY>|--continue|--dry-run [<DRY_RUN>]|--single-package|--filter <FILTER>|--force|--global-deps <GLOBAL_DEPS>|--graph [<GRAPH>]|--ignore <IGNORE>|--include-dependencies|--no-cache|--no-daemon|--no-deps|--output-logs <OUTPUT_LOGS>|--only|--parallel|--pkg-inference-root <PKG_INFERENCE_ROOT>|--profile <PROFILE>|--remote-only|--scope <SCOPE>|--since <SINCE>|--log-prefix <LOG_PREFIX>|TASKS|PASS_THROUGH_ARGS|--experimental-space-id <EXPERIMENTAL_SPACE_ID>>

For more information, try '--help'.

Expand Down
48 changes: 4 additions & 44 deletions cli/internal/client/analytics.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,20 @@ package client

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"

"github.com/hashicorp/go-retryablehttp"
"github.com/vercel/turbo/cli/internal/ci"
)

// RecordAnalyticsEvents is a specific method for POSTing events to Vercel
func (c *APIClient) RecordAnalyticsEvents(events []map[string]interface{}) error {
if err := c.okToRequest(); err != nil {
return err
}
params := url.Values{}
c.addTeamParam(&params)
encoded := params.Encode()
if encoded != "" {
encoded = "?" + encoded
}
body, err := json.Marshal(events)
if err != nil {
return err
}

requestURL := c.makeURL("/v8/artifacts/events" + encoded)
allowAuth := true
if c.usePreflight {
resp, latestRequestURL, err := c.doPreflight(requestURL, http.MethodPost, "Content-Type, Authorization, User-Agent")
if err != nil {
return fmt.Errorf("pre-flight request failed before trying to store in HTTP cache: %w", err)
}
requestURL = latestRequestURL
headers := resp.Header.Get("Access-Control-Allow-Headers")
allowAuth = strings.Contains(strings.ToLower(headers), strings.ToLower("Authorization"))
}

req, err := retryablehttp.NewRequest(http.MethodPost, requestURL, body)
if err != nil {
// We don't care about the response here
if _, err := c.JSONPost("/v8/artifacts/events", body); err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if allowAuth {
req.Header.Set("Authorization", "Bearer "+c.token)
}
req.Header.Set("User-Agent", c.userAgent())
if ci.IsCi() {
req.Header.Set("x-artifact-client-ci", ci.Constant())
}
resp, err := c.HTTPClient.Do(req)
if resp != nil && resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
b, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("%s", string(b))
}
return err

return nil
}
96 changes: 96 additions & 0 deletions cli/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"runtime"
Expand All @@ -15,6 +16,7 @@ import (

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-retryablehttp"
"github.com/vercel/turbo/cli/internal/ci"
)

// APIClient is the main interface for making network requests to Vercel
Expand Down Expand Up @@ -211,3 +213,97 @@ func (c *APIClient) addTeamParam(params *url.Values) {
params.Add("slug", c.teamSlug)
}
}

// JSONPatch sends a byte array (json.marshalled payload) to a given endpoint with PATCH
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth renaming this, as I was expecting https://jsonpatch.com/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have a better name? I considered just Post and Patch, but since there are other methods here that use, application/octect-stream for example, I wanted to be clear that I'm not attempting to co-opt.

func (c *APIClient) JSONPatch(endpoint string, body []byte) ([]byte, error) {
resp, err := c.request(endpoint, http.MethodPatch, body)
if err != nil {
return nil, err
}

rawResponse, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s", string(rawResponse))
}

return rawResponse, nil
}

// JSONPost sends a byte array (json.marshalled payload) to a given endpoint with POST
func (c *APIClient) JSONPost(endpoint string, body []byte) ([]byte, error) {
resp, err := c.request(endpoint, http.MethodPost, body)
if err != nil {
return nil, err
}

rawResponse, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response %v", err)
}

// For non 200/201 status codes, return the response body as an error
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("%s", string(rawResponse))
}

return rawResponse, nil
}

func (c *APIClient) request(endpoint string, method string, body []byte) (*http.Response, error) {
if err := c.okToRequest(); err != nil {
return nil, err
}

params := url.Values{}
c.addTeamParam(&params)
encoded := params.Encode()
if encoded != "" {
encoded = "?" + encoded
}

requestURL := c.makeURL(endpoint + encoded)

allowAuth := false
if c.usePreflight {
resp, latestRequestURL, err := c.doPreflight(requestURL, method, "Authorization, User-Agent")
if err != nil {
return nil, fmt.Errorf("pre-flight request failed before trying to fetch files in HTTP cache: %w", err)
}

requestURL = latestRequestURL
headers := resp.Header.Get("Access-Control-Allow-Headers")
allowAuth = strings.Contains(strings.ToLower(headers), strings.ToLower("Authorization"))
}

req, err := retryablehttp.NewRequest(method, requestURL, body)
if err != nil {
return nil, err
}

// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", c.userAgent())

if allowAuth {
req.Header.Set("Authorization", "Bearer "+c.token)
}

if ci.IsCi() {
req.Header.Set("x-artifact-client-ci", ci.Constant())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know headers are case-insensitive but we should go all-camel or no-camel 🐫

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I think there's more work to be done in the ApiClient, but since this is the existing code, I'll keep it as-is

}

resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}

// If there isn't a response, something else probably went wrong
if resp == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems ok, we just need to make sure we don't end up getting 204 No Content back from a request

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Ideally we should just be checking for 2xx, not specifically 200/201 , but also going to defer this since I've just used existing code and I don't want to affect what's going on

return nil, fmt.Errorf("response from %s is nil, something went wrong", requestURL)
}

return resp, nil
}
9 changes: 4 additions & 5 deletions cli/internal/run/dry_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@ func DryRun(
taskHashTracker *taskhash.Tracker,
turboCache cache.Cache,
base *cmdutil.CmdBase,
summary *runsummary.RunSummary,
summary runsummary.Meta,
) error {
defer turboCache.Shutdown()

dryRunJSON := rs.Opts.runOpts.dryRunJSON
singlePackage := rs.Opts.runOpts.singlePackage

taskSummaries, err := executeDryRun(
ctx,
Expand All @@ -52,19 +51,19 @@ func DryRun(
populateCacheState(turboCache, taskSummaries)

// Assign the Task Summaries to the main summary
summary.Tasks = taskSummaries
summary.RunSummary.Tasks = taskSummaries

// Render the dry run as json
if dryRunJSON {
rendered, err := summary.FormatJSON(singlePackage)
rendered, err := summary.FormatJSON()
if err != nil {
return err
}
base.UI.Output(string(rendered))
return nil
}

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

func executeDryRun(ctx gocontext.Context, engine *core.Engine, g *graph.CompleteGraph, taskHashTracker *taskhash.Tracker, rs *runSpec, base *cmdutil.CmdBase) ([]*runsummary.TaskSummary, error) {
Expand Down
17 changes: 5 additions & 12 deletions cli/internal/run/real_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func RealRun(
turboCache cache.Cache,
packagesInScope []string,
base *cmdutil.CmdBase,
runSummary *runsummary.RunSummary,
runSummary runsummary.Meta,
packageManager *packagemanager.PackageManager,
processes *process.Manager,
) error {
Expand Down Expand Up @@ -129,7 +129,7 @@ func RealRun(
exitCodeErr := &process.ChildExit{}

// Assign tasks after execution
runSummary.Tasks = taskSummaries
runSummary.RunSummary.Tasks = taskSummaries

for _, err := range errs {
if errors.As(err, &exitCodeErr) {
Expand All @@ -150,14 +150,7 @@ func RealRun(
base.UI.Error(err.Error())
}

runSummary.Close(base.UI)

// 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))
}
}
runSummary.Close(base.RepoRoot)

if exitCode != 0 {
return &process.ChildExit{
Expand All @@ -169,7 +162,7 @@ func RealRun(

type execContext struct {
colorCache *colorcache.ColorCache
runSummary *runsummary.RunSummary
runSummary runsummary.Meta
rs *runSpec
ui cli.Ui
runCache *runcache.RunCache
Expand Down Expand Up @@ -198,7 +191,7 @@ func (ec *execContext) exec(ctx gocontext.Context, packageTask *nodes.PackageTas
progressLogger.Debug("start")

// Setup tracer
tracer, taskExecutionSummary := ec.runSummary.TrackTask(packageTask.TaskID)
tracer, taskExecutionSummary := ec.runSummary.RunSummary.TrackTask(packageTask.TaskID)

passThroughArgs := ec.rs.ArgsForTask(packageTask.Task)
hash := packageTask.Hash
Expand Down
6 changes: 6 additions & 0 deletions cli/internal/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func optsFromArgs(args *turbostate.ParsedArgsFromRust) (*Opts, error) {
opts.cacheOpts.OverrideDir = runPayload.CacheDir
opts.cacheOpts.Workers = runPayload.CacheWorkers
opts.runOpts.logPrefix = runPayload.LogPrefix
opts.runOpts.experimentalSpaceID = runPayload.ExperimentalSpaceID

// Runcache flags
opts.runcacheOpts.SkipReads = runPayload.Force
Expand Down Expand Up @@ -353,6 +354,8 @@ func (r *run) run(ctx gocontext.Context, targets []string) error {
// the tasks that we expect to run based on the user command.
summary := runsummary.NewRunSummary(
startAt,
r.base.UI,
rs.Opts.runOpts.singlePackage,
rs.Opts.runOpts.profile,
r.base.TurboVersion,
packagesInScope,
Expand All @@ -363,6 +366,9 @@ func (r *run) run(ctx gocontext.Context, targets []string) error {
globalHashable.globalCacheKey,
globalHashable.pipeline,
),
rs.Opts.runOpts.summarize,
r.base.APIClient,
rs.Opts.runOpts.experimentalSpaceID,
)

// Dry Run
Expand Down
2 changes: 2 additions & 0 deletions cli/internal/run/run_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,6 @@ type runOpts struct {

// Whether turbo should create a run summary
summarize bool

experimentalSpaceID string
}
6 changes: 4 additions & 2 deletions cli/internal/runsummary/format_execution_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"time"

"github.com/fatih/color"
"github.com/mitchellh/cli"
internalUI "github.com/vercel/turbo/cli/internal/ui"
"github.com/vercel/turbo/cli/internal/util"
)

func (summary *RunSummary) printExecutionSummary(ui cli.Ui) {
func (rsm *Meta) printExecutionSummary() {
maybeFullTurbo := ""
summary := rsm.RunSummary
ui := rsm.ui

if summary.ExecutionSummary.Cached == summary.ExecutionSummary.Attempted && summary.ExecutionSummary.Attempted > 0 {
terminalProgram := os.Getenv("TERM_PROGRAM")
// On the macOS Terminal, the rainbow colors show up as a magenta background
Expand Down
16 changes: 8 additions & 8 deletions cli/internal/runsummary/format_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@ import (
)

// FormatJSON returns a json string representing a RunSummary
func (summary *RunSummary) FormatJSON(singlePackage bool) ([]byte, error) {
summary.normalize() // normalize data
func (rsm *Meta) FormatJSON() ([]byte, error) {
rsm.RunSummary.normalize() // normalize data

if singlePackage {
return summary.formatJSONSinglePackage()
if rsm.singlePackage {
return rsm.formatJSONSinglePackage()
}

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

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

for i, task := range summary.Tasks {
for i, task := range rsm.RunSummary.Tasks {
singlePackageTasks[i] = task.toSinglePackageTask()
}

Expand Down
Loading