diff --git a/client/environment_variable.go b/client/environment_variable.go index ff641529..bcdf7980 100644 --- a/client/environment_variable.go +++ b/client/environment_variable.go @@ -42,12 +42,71 @@ func (c *Client) CreateEnvironmentVariable(ctx context.Context, request CreateEn url: url, body: payload, }, &e) + if conflictingEnv, isConflicting, err2 := conflictingEnvVar(err); isConflicting { + if err2 != nil { + return e, err2 + } + id, err3 := c.findConflictingEnvID(ctx, request.TeamID, request.ProjectID, conflictingEnv) + if err3 != nil { + return e, fmt.Errorf("%w %s", err, err3) + } + return e, fmt.Errorf("%w the conflicting environment variable ID is %s", err, id) + } + if err != nil { + return e, err + } // The API response returns an encrypted environment variable, but we want to return the decrypted version. e.Value = request.EnvironmentVariable.Value e.TeamID = c.teamID(request.TeamID) return e, err } +func (c *Client) ListEnvironmentVariables(ctx context.Context, teamID, projectID string) (envs []EnvironmentVariable, err error) { + url := fmt.Sprintf("%s/v9/projects/%s/env", c.baseURL, projectID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + response := struct { + Envs []EnvironmentVariable `json:"envs"` + }{} + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + }, &response) + return response.Envs, err +} + +func overlaps(s []string, e []string) bool { + for _, a := range s { + for _, b := range e { + if a == b { + return true + } + } + } + return false +} + +func (c *Client) findConflictingEnvID(ctx context.Context, teamID, projectID string, envConflict EnvConflictError) (string, error) { + envs, err := c.ListEnvironmentVariables(ctx, teamID, projectID) + if err != nil { + return "", fmt.Errorf("unable to list environment variables to detect conflict: %w", err) + } + + for _, env := range envs { + if env.Key == envConflict.Key && overlaps(env.Target, envConflict.Target) && env.GitBranch == envConflict.GitBranch { + id := fmt.Sprintf("%s/%s", projectID, env.ID) + if teamID != "" { + id = fmt.Sprintf("%s/%s", teamID, id) + } + return id, nil + } + } + return "", fmt.Errorf("conflicting environment variable not found") +} + type CreateEnvironmentVariablesRequest struct { EnvironmentVariables []EnvironmentVariableRequest ProjectID string @@ -64,12 +123,27 @@ func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateE "url": url, "payload": payload, }) - return c.doRequest(clientRequest{ + err := c.doRequest(clientRequest{ ctx: ctx, method: "POST", url: url, body: payload, }, nil) + if conflictingEnv, isConflicting, err2 := conflictingEnvVar(err); isConflicting { + if err2 != nil { + return err2 + } + id, err3 := c.findConflictingEnvID(ctx, request.TeamID, request.ProjectID, conflictingEnv) + if err3 != nil { + return fmt.Errorf("%w %s", err, err3) + } + return fmt.Errorf("%w the conflicting environment variable ID is %s", err, id) + } + if err != nil { + return err + } + + return err } // UpdateEnvironmentVariableRequest defines the information that needs to be passed to Vercel in order to diff --git a/client/error.go b/client/error.go index 9cad6b4a..1e051171 100644 --- a/client/error.go +++ b/client/error.go @@ -1,6 +1,9 @@ package client -import "errors" +import ( + "encoding/json" + "errors" +) // NotFound detects if an error returned by the Vercel API was the result of an entity not existing. func NotFound(err error) bool { @@ -12,3 +15,30 @@ func noContent(err error) bool { var apiErr APIError return err != nil && errors.As(err, &apiErr) && apiErr.StatusCode == 204 } + +func conflictingSharedEnv(err error) bool { + var apiErr APIError + return err != nil && errors.As(err, &apiErr) && apiErr.StatusCode == 409 && apiErr.Code == "existing_key_and_target" +} + +type EnvConflictError struct { + Code string `json:"code"` + Message string `json:"message"` + Key string `json:"key"` + Target []string `json:"target"` + GitBranch *string `json:"gitBranch"` +} + +func conflictingEnvVar(e error) (envConflictError EnvConflictError, ok bool, err error) { + var apiErr APIError + conflict := e != nil && errors.As(e, &apiErr) && apiErr.StatusCode == 403 && apiErr.Code == "ENV_ALREADY_EXISTS" + if !conflict { + return envConflictError, false, err + } + + var conflictErr struct { + Error EnvConflictError `json:"error"` + } + _ = json.Unmarshal(apiErr.RawMessage, &conflictErr) + return conflictErr.Error, true, err +} diff --git a/client/shared_environment_variable.go b/client/shared_environment_variable.go index 49c1c132..7040f690 100644 --- a/client/shared_environment_variable.go +++ b/client/shared_environment_variable.go @@ -36,6 +36,28 @@ type CreateSharedEnvironmentVariableRequest struct { TeamID string } +func (c *Client) findConflictingSharedEnvID(ctx context.Context, request CreateSharedEnvironmentVariableRequest) (string, error) { + envs, err := c.ListSharedEnvironmentVariables(ctx, request.TeamID) + if err != nil { + return "", fmt.Errorf("unable to list shared environment variables to detect conflict: %w", err) + } + if len(request.EnvironmentVariable.EnvironmentVariables) != 1 { + return "", fmt.Errorf("cannot detect conflict for multiple shared environment variables") + } + requestedEnv := request.EnvironmentVariable.EnvironmentVariables[0] + + for _, env := range envs { + if env.Key == requestedEnv.Key && overlaps(env.Target, request.EnvironmentVariable.Target) { + id := env.ID + if request.TeamID != "" { + id = fmt.Sprintf("%s/%s", request.TeamID, id) + } + return id, nil + } + } + return "", fmt.Errorf("conflicting shared environment variable not found") +} + // CreateSharedEnvironmentVariable will create a brand new shared environment variable if one does not exist. func (c *Client) CreateSharedEnvironmentVariable(ctx context.Context, request CreateSharedEnvironmentVariableRequest) (e SharedEnvironmentVariableResponse, err error) { url := fmt.Sprintf("%s/v1/env", c.baseURL) @@ -56,6 +78,13 @@ func (c *Client) CreateSharedEnvironmentVariable(ctx context.Context, request Cr url: url, body: payload, }, &response) + if conflictingSharedEnv(err) { + id, err2 := c.findConflictingSharedEnvID(ctx, request) + if err2 != nil { + return e, fmt.Errorf("%w %s", err, err2) + } + return e, fmt.Errorf("%w the conflicting shared environment variable ID is %s", err, id) + } if err != nil { return e, err }