这是indexloc提供的服务,不要输入任何密码
Skip to content

Move CLI to Vercel #52

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

Merged
merged 10 commits into from
Dec 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions cmd/turbo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func main() {
ui.Error(fmt.Sprintf("%s %s", uiPkg.ERROR_PREFIX, color.RedString(err.Error())))
os.Exit(1)
}
c.HiddenCommands = []string{"graph"}
c.HiddenCommands = []string{"graph", "login", "logout", "me"}
c.Commands = map[string]cli.CommandFactory{
"run": func() (cli.Command, error) {
return &run.RunCommand{Config: cf, Ui: ui},
Expand All @@ -74,21 +74,21 @@ func main() {
"prune": func() (cli.Command, error) {
return &prune.PruneCommand{Config: cf, Ui: ui}, nil
},
"link": func() (cli.Command, error) {
return &login.LinkCommand{Config: cf, Ui: ui}, nil
},
"unlink": func() (cli.Command, error) {
return &login.UnlinkCommand{Config: cf, Ui: ui}, nil
},
"graph": func() (cli.Command, error) {
return &info.GraphCommand{Config: cf, Ui: ui}, nil
},
"login": func() (cli.Command, error) {
return &login.LoginCommand{Config: cf, Ui: ui}, nil
},
"logout": func() (cli.Command, error) {
return &login.LogoutCommand{Config: cf, Ui: ui}, nil
},
"me": func() (cli.Command, error) {
return &login.MeCommand{Config: cf, Ui: ui}, nil
},
"link": func() (cli.Command, error) {
return &login.LinkCommand{Config: cf, Ui: ui}, nil
},
}

// Capture the defer statements below so the "done" message comes last
Expand Down
35 changes: 8 additions & 27 deletions docs/pages/docs/reference/command-line-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,6 @@ turbo run lint --parallel --no-cache
turbo run dev --parallel --no-cache
```

#### `--project`

Your turborepo.com project's slug. Useful for running in non-interactive shells in combination with `--token` and `--team` flags.

```sh
turbo run build --team=my-team --project=my-project
turbo run build --team=my-team --project=my-project --token=xxxxxxxxxxxxxxxxx
```

You can also set the value of the current project by setting an environment variable named `TURBO_PROJECT`. The flag will take precendence over the environment variable if both are present.

#### `--scope`

`type: string[]`
Expand Down Expand Up @@ -199,21 +188,21 @@ turbo run build --since=origin/main

#### `--token`

A turborepo.com personal access token. Useful for running in non-interactive shells (e.g. CI/CD) in combination with `--project` and `--team` flags.
A bearer token for remote caching. Useful for running in non-interactive shells (e.g. CI/CD) in combination with `--team` flags.

```sh
turbo run build --team=my-team --project=my-project --token=xxxxxxxxxxxxxxxxx
turbo run build --team=my-team --token=xxxxxxxxxxxxxxxxx
```

You can also set the value of the current token by setting an environment variable named `TURBO_TOKEN`. The flag will take precendence over the environment variable if both are present.

#### `--team`

The slug of the turborepo.com team. Useful for running in non-interactive shells in combination with `--token` and `--team` flags.
The slug of the remote cache team. Useful for running in non-interactive shells in combination with `--token` and `--team` flags.

```sh
turbo run build --team=my-team --project=my-project
turbo run build --team=my-team --project=my-project --token=xxxxxxxxxxxxxxxxx
turbo run build --team=my-team
turbo run build --team=my-team --token=xxxxxxxxxxxxxxxxx
```

You can also set the value of the current team by setting an environment variable named `TURBO_TEAM`. The flag will take precendence over the environment variable if both are present.
Expand Down Expand Up @@ -312,16 +301,8 @@ With the `--docker` flag. The `prune` command will generate folder called `out`

## `turbo link`

Link the current directory to a new or existing turborepo.com project. A project is used to to share [cache artifacts](../caching).

## `turbo login`

Login to your turborepo.com account and activate the current machine. If you haven't done so, visit [https://beta.turborepo.com/signup](https://beta.turborepo.com/signup) to sign up before hand.

## `turbo logout`

Logout of your turborepo.com account.
Link the current directory to an existing Vercel organization or user. The selected owner (either a user or and organization) will be able to share [cache artifacts](../caching).

## `turbo me`
## `turbo unlink`

Prints info about the currently logged in user.
Unlink the current directory from a Vercel organization or user.
2 changes: 1 addition & 1 deletion internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func newSyncCache(config *config.Config, remoteOnly bool) Cache {
if config.Cache.Dir != "" && !remoteOnly {
mplex.caches = append(mplex.caches, newFsCache(config))
}
if config.Token != "" && config.ProjectId != "" && config.TeamId != "" {
if config.Token != "" && config.TeamId != "" {
fmt.Println(ui.Dim("• Remote computation caching enabled (experimental)"))
mplex.caches = append(mplex.caches, newHTTPCache(config))
}
Expand Down
10 changes: 6 additions & 4 deletions internal/cache/cache_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
)

type httpCache struct {
cwd string
writable bool
config *config.Config
requestLimiter limiter
Expand Down Expand Up @@ -44,7 +43,7 @@ func (cache *httpCache) Put(target, hash string, files []string) error {
defer cache.requestLimiter.release()
r, w := io.Pipe()
go cache.write(w, hash, files)
return cache.config.ApiClient.PutArtifact(hash, cache.config.TeamId, cache.config.ProjectId, r)
return cache.config.ApiClient.PutArtifact(hash, cache.config.TeamId, cache.config.TeamSlug, r)
}

// write writes a series of files into the given Writer.
Expand Down Expand Up @@ -105,13 +104,16 @@ func (cache *httpCache) Fetch(target, key string, _unusedOutputGlobs []string) (
defer cache.requestLimiter.release()
m, files, err := cache.retrieve(key)
if err != nil {
return false, files, fmt.Errorf("Failed to retrieve files from HTTP cache: %w", err)
return false, files, fmt.Errorf("failed to retrieve files from HTTP cache: %w", err)
}
return m, files, err
}

func (cache *httpCache) retrieve(key string) (bool, []string, error) {
resp, err := cache.config.ApiClient.FetchArtifact(key, cache.config.TeamId, cache.config.ProjectId, nil)
resp, err := cache.config.ApiClient.FetchArtifact(key, cache.config.TeamId, cache.config.TeamSlug, nil)
if err != nil {
return false, nil, err
}
defer resp.Body.Close()
files := []string{}
if resp.StatusCode == http.StatusNotFound {
Expand Down
139 changes: 124 additions & 15 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"time"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-retryablehttp"
)

Expand All @@ -27,7 +28,7 @@ func (api *ApiClient) SetToken(token string) {
}

// New creates a new ApiClient
func NewClient(baseUrl string) *ApiClient {
func NewClient(baseUrl string, logger hclog.Logger) *ApiClient {
return &ApiClient{
baseUrl: baseUrl,
HttpClient: &retryablehttp.Client{
Expand All @@ -39,6 +40,7 @@ func NewClient(baseUrl string) *ApiClient {
RetryMax: 5,
CheckRetry: retryablehttp.DefaultRetryPolicy,
Backoff: retryablehttp.DefaultBackoff,
Logger: logger,
},
}
}
Expand All @@ -61,29 +63,37 @@ func (c *ApiClient) makeUrl(endpoint string) string {
return fmt.Sprintf("%v%v", c.baseUrl, endpoint)
}

func (c *ApiClient) PutArtifact(hash string, teamId string, projectId string, rawBody interface{}) error {
func (c *ApiClient) PutArtifact(hash string, teamId string, slug string, rawBody interface{}) error {
params := url.Values{}
params.Add("projectId", projectId)
params.Add("teamId", teamId)
req, err := retryablehttp.NewRequest(http.MethodPut, c.makeUrl("/artifact/"+hash+"?"+params.Encode()), rawBody)
if teamId != "" && strings.HasPrefix(teamId, "team_") {
params.Add("teamId", teamId)
}
if slug != "" {
params.Add("slug", slug)
}
req, err := retryablehttp.NewRequest(http.MethodPut, c.makeUrl("/v8/artifacts/"+hash+"?"+params.Encode()), rawBody)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Authorization", "Bearer "+c.Token)
if err != nil {
return fmt.Errorf("[WARNING] Invalid cache URL: %w", err)
}
if resp, err := c.HttpClient.Do(req); err != nil {
return fmt.Errorf("Failed to store files in HTTP cache: %w", err)
return fmt.Errorf("failed to store files in HTTP cache: %w", err)
} else {
resp.Body.Close()
}
return nil
}

func (c *ApiClient) FetchArtifact(hash string, teamId string, projectId string, rawBody interface{}) (*http.Response, error) {
func (c *ApiClient) FetchArtifact(hash string, teamId string, slug string, rawBody interface{}) (*http.Response, error) {
params := url.Values{}
params.Add("projectId", projectId)
params.Add("teamId", teamId)
req, err := retryablehttp.NewRequest(http.MethodGet, c.makeUrl("/artifact/"+hash+"?"+params.Encode()), nil)
if teamId != "" && strings.HasPrefix(teamId, "team_") {
params.Add("teamId", teamId)
}
if slug != "" {
params.Add("slug", slug)
}
req, err := retryablehttp.NewRequest(http.MethodGet, c.makeUrl("/v8/artifacts/"+hash+"?"+params.Encode()), nil)
req.Header.Set("Authorization", "Bearer "+c.Token)
if err != nil {
return nil, fmt.Errorf("[WARNING] Invalid cache URL: %w", err)
Expand All @@ -98,7 +108,7 @@ func (c *ApiClient) RequestDeviceToken() (*DeviceToken, error) {
return nil, err
}

req.Header.Set("User-Agent", fmt.Sprintf("Turbo CLI"))
req.Header.Set("User-Agent", "Turbo CLI")
req.Header.Set("Content-Type", "application/json")

resp, err := c.HttpClient.Do(req)
Expand All @@ -114,11 +124,11 @@ func (c *ApiClient) RequestDeviceToken() (*DeviceToken, error) {
}
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
return nil, fmt.Errorf("Could not read JSON response: %s", string(body))
return nil, fmt.Errorf("could not read JSON response: %s", string(body))
}
marshalErr := json.Unmarshal(body, deviceToken)
if marshalErr != nil {
return nil, fmt.Errorf("Could not parse JSON response: %s", string(body))
return nil, fmt.Errorf("could not parse JSON response: %s", string(body))
}
return deviceToken, nil
}
Expand Down Expand Up @@ -174,11 +184,11 @@ func (c *ApiClient) PollForAccessToken(deviceToken *DeviceToken) (*AccessToken,
}
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
return nil, fmt.Errorf("Could not read JSON response: %s", string(body))
return nil, fmt.Errorf("could not read JSON response: %s", string(body))
}
marshalErr := json.Unmarshal(body, &accessToken)
if marshalErr != nil {
return nil, fmt.Errorf("Could not parse JSON response: %s", string(body))
return nil, fmt.Errorf("could not parse JSON response: %s", string(body))
}
return accessToken, nil
}
Expand Down Expand Up @@ -223,3 +233,102 @@ func retryPolicy(resp *http.Response, err error) (bool, error) {

return false, nil
}

// Team is a Vercel Team object
type Team struct {
ID string `json:"id,omitempty"`
Slug string `json:"slug,omitempty"`
Name string `json:"name,omitempty"`
CreatedAt int `json:"createdAt,omitempty"`
Created string `json:"created,omitempty"`
}

// Pagination is a Vercel pagination object
type Pagination struct {
Count int `json:"count,omitempty"`
Next int `json:"next,omitempty"`
Prev int `json:"prev,omitempty"`
}

// TeamsResponse is a Vercel object containing a list of teams and pagination info
type TeamsResponse struct {
Teams []Team `json:"teams,omitempty"`
Pagination Pagination `json:"pagination,omitempty"`
}

// GetTeams returns a list of Vercel teams
func (c *ApiClient) GetTeams() (*TeamsResponse, error) {
teamsResponse := &TeamsResponse{}
req, err := retryablehttp.NewRequest(http.MethodGet, c.makeUrl("/v2/teams?limit=100"), nil)
if err != nil {
return nil, err
}

req.Header.Set("User-Agent", "Turbo CLI")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.Token)
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("404 - Not found") // doesn't exist - not an error
} else if resp.StatusCode != http.StatusOK {
b, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("%s", string(b))
}
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
return nil, fmt.Errorf("could not read JSON response: %s", string(body))
}
marshalErr := json.Unmarshal(body, teamsResponse)
if marshalErr != nil {
return nil, fmt.Errorf("could not parse JSON response: %s", string(body))
}
return teamsResponse, nil
}

type User struct {
ID string `json:"id,omitempty"`
Username string `json:"username,omitempty"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
CreatedAt int `json:"createdAt,omitempty"`
}
type UserResponse struct {
User User `json:"user,omitempty"`
}

// GetUser returns the current user
func (c *ApiClient) GetUser() (*UserResponse, error) {
userResponse := &UserResponse{}
req, err := retryablehttp.NewRequest(http.MethodGet, c.makeUrl("/v2/user"), nil)
if err != nil {
return nil, err
}

req.Header.Set("User-Agent", "Turbo CLI")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.Token)
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("404 - Not found") // doesn't exist - not an error
} else if resp.StatusCode != http.StatusOK {
b, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("%s", string(b))
}
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
return nil, fmt.Errorf("could not read JSON response: %s", string(body))
}
marshalErr := json.Unmarshal(body, userResponse)
if marshalErr != nil {
return nil, fmt.Errorf("could not parse JSON response: %s", string(body))
}
return userResponse, nil
}
Loading