diff --git a/internal/runners/initialize/init.go b/internal/runners/initialize/init.go index caa393e997..7a61f94818 100644 --- a/internal/runners/initialize/init.go +++ b/internal/runners/initialize/init.go @@ -5,6 +5,8 @@ import ( "path/filepath" "strings" + "github.com/go-openapi/strfmt" + "github.com/ActiveState/cli/internal/analytics" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" @@ -86,6 +88,7 @@ func inferLanguage(config projectfile.ConfigGetter) (string, string, bool) { } func (r *Initialize) Run(params *RunParams) (rerr error) { + defer rationalizeError(&rerr) logging.Debug("Init: %s/%s %v", params.Namespace.Owner, params.Namespace.Project, params.Private) if !r.auth.Authenticated() { @@ -217,7 +220,29 @@ func (r *Initialize) Run(params *RunParams) (rerr error) { return err } - commitID, err := model.CommitInitial(model.HostPlatform, lang.Requirement(), version) + logging.Debug("Creating Platform project") + + platformID, err := model.PlatformNameToPlatformID(model.HostPlatform) + if err != nil { + return errs.Wrap(err, "Unable to determine Platform ID from %s", model.HostPlatform) + } + + timestamp, err := model.FetchLatestTimeStamp() + if err != nil { + return errs.Wrap(err, "Unable to fetch latest timestamp") + } + + bp := model.NewBuildPlannerModel(r.auth) + commitID, err := bp.CreateProject(&model.CreateProjectParams{ + Owner: namespace.Owner, + Project: namespace.Project, + PlatformID: strfmt.UUID(platformID), + Language: lang.Requirement(), + Version: version, + Private: params.Private, + Timestamp: *timestamp, + Description: locale.T("commit_message_add_initial"), + }) if err != nil { return locale.WrapError(err, "err_init_commit", "Could not create initial commit") } @@ -233,23 +258,6 @@ func (r *Initialize) Run(params *RunParams) (rerr error) { } } - logging.Debug("Creating Platform project and pushing it") - - platformProject, err := model.CreateEmptyProject(namespace.Owner, namespace.Project, params.Private) - if err != nil { - return locale.WrapInputError(err, "err_init_create_project", "Failed to create a Platform project at {{.V0}}.", namespace.String()) - } - - branch, err := model.DefaultBranchForProject(platformProject) // only one branch for newly created project - if err != nil { - return locale.NewInputError("err_no_default_branch") - } - - err = model.UpdateProjectBranchCommitWithModel(platformProject, branch.Label, commitID) - if err != nil { - return locale.WrapError(err, "err_init_push", "Failed to push to the newly created Platform project at {{.V0}}", namespace.String()) - } - err = runbits.RefreshRuntime(r.auth, r.out, r.analytics, proj, commitID, true, target.TriggerInit, r.svcModel) if err != nil { logging.Debug("Deleting remotely created project due to runtime setup error") diff --git a/internal/runners/initialize/rationalize.go b/internal/runners/initialize/rationalize.go new file mode 100644 index 0000000000..606fc6f3f7 --- /dev/null +++ b/internal/runners/initialize/rationalize.go @@ -0,0 +1,29 @@ +package initialize + +import ( + "errors" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + bpModel "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" +) + +func rationalizeError(err *error) { + if err == nil { + return + } + + pcErr := &bpModel.ProjectCreatedError{} + if !errors.As(*err, &pcErr) { + return + } + switch pcErr.Type { + case bpModel.AlreadyExistsErrorType: + *err = errs.NewUserFacing(locale.Tl("err_create_project_exists", "That project already exists."), errs.SetInput()) + case bpModel.ForbiddenErrorType: + *err = errs.NewUserFacing( + locale.Tl("err_create_project_forbidden", "You do not have permission to create that project"), + errs.SetInput(), + errs.SetTips(locale.T("err_init_authenticated"))) + } +} diff --git a/pkg/platform/api/buildplanner/model/buildplan.go b/pkg/platform/api/buildplanner/model/buildplan.go index 80982d95ec..303e84298d 100644 --- a/pkg/platform/api/buildplanner/model/buildplan.go +++ b/pkg/platform/api/buildplanner/model/buildplan.go @@ -333,6 +333,21 @@ func ProcessProjectError(project *Project, fallbackMessage string) error { return errs.New(fallbackMessage) } +type ProjectCreatedError struct { + Type string + Message string +} + +func (p *ProjectCreatedError) Error() string { return p.Message } + +func ProcessProjectCreatedError(pcErr *projectCreated, fallbackMessage string) error { + if pcErr.Type != "" { + // These will be handled individually per type as user-facing errors in DX-2300. + return &ProjectCreatedError{pcErr.Type, pcErr.Message} + } + return errs.New(fallbackMessage) +} + type BuildExpression struct { Type string `json:"__typename"` Commit *Commit `json:"commit"` @@ -355,6 +370,16 @@ type StageCommitResult struct { Commit *Commit `json:"stageCommit"` } +type projectCreated struct { + Type string `json:"__typename"` + Commit *Commit `json:"commit"` + *Error +} + +type CreateProjectResult struct { + ProjectCreated *projectCreated `json:"createProject"` +} + // Error contains an error message. type Error struct { Message string `json:"message"` diff --git a/pkg/platform/api/buildplanner/request/createproject.go b/pkg/platform/api/buildplanner/request/createproject.go new file mode 100644 index 0000000000..81551b7d8b --- /dev/null +++ b/pkg/platform/api/buildplanner/request/createproject.go @@ -0,0 +1,57 @@ +package request + +import "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" + +func CreateProject(owner, project string, private bool, expr *buildexpression.BuildExpression, description string) *createProject { + return &createProject{map[string]interface{}{ + "organization": owner, + "project": project, + "private": private, + "expr": expr, + "description": description, + }} +} + +type createProject struct { + vars map[string]interface{} +} + +func (c *createProject) Query() string { + return ` +mutation ($organization: String!, $project: String!, $private: Boolean!, $expr: BuildExpr!, $description: String!) { + createProject(input:{organization:$organization, project:$project, private:$private, expr:$expr, description:$description}) { + ... on ProjectCreated { + __typename + commit { + __typename + commitId + } + } + ... on AlreadyExists { + __typename + message + } + ... on NotFound { + __typename + message + } + ... on ParseError { + __typename + message + path + } + ... on ValidationError { + __typename + message + } + ... on Forbidden { + __typename + message + } + } +}` +} + +func (c *createProject) Vars() map[string]interface{} { + return c.vars +} diff --git a/pkg/platform/model/buildplanner.go b/pkg/platform/model/buildplanner.go index a5112a9256..6237d5410a 100644 --- a/pkg/platform/model/buildplanner.go +++ b/pkg/platform/model/buildplanner.go @@ -325,6 +325,66 @@ func (bp *BuildPlanner) GetBuildExpression(owner, project, commitID string) (*bu return expression, nil } +type CreateProjectParams struct { + Owner string + Project string + PlatformID strfmt.UUID + Language string + Version string + Private bool + Timestamp strfmt.DateTime + Description string +} + +func (bp *BuildPlanner) CreateProject(params *CreateProjectParams) (strfmt.UUID, error) { + logging.Debug("CreateProject, owner: %s, project: %s, language: %s, version: %s", params.Owner, params.Project, params.Language, params.Version) + + // Construct an initial buildexpression for the new project. + expr, err := buildexpression.NewEmpty() + if err != nil { + return "", errs.Wrap(err, "Unable to create initial buildexpression") + } + + // Add the platform. + expr.UpdatePlatform(model.OperationAdded, params.PlatformID) + + // Create a requirement for the given language and version. + versionRequirements, err := VersionStringToRequirements(params.Version) + if err != nil { + return "", errs.Wrap(err, "Unable to read version") + } + expr.UpdateRequirement(model.OperationAdded, bpModel.Requirement{ + Name: params.Language, + Namespace: "language", // TODO: make this a constant DX-1738 + VersionRequirement: versionRequirements, + }) + + // Add the timestamp. + expr.UpdateTimestamp(params.Timestamp) + + // Create the project. + request := request.CreateProject(params.Owner, params.Project, params.Private, expr, params.Description) + resp := &bpModel.CreateProjectResult{} + err = bp.client.Run(request, resp) + if err != nil { + return "", processBuildPlannerError(err, "Failed to create project") + } + + if resp.ProjectCreated == nil { + return "", errs.New("ProjectCreated is nil") + } + + if bpModel.IsErrorResponse(resp.ProjectCreated.Type) { + return "", bpModel.ProcessProjectCreatedError(resp.ProjectCreated, "Could not create project") + } + + if resp.ProjectCreated.Commit == nil { + return "", errs.New("ProjectCreated.Commit is nil") + } + + return resp.ProjectCreated.Commit.CommitID, nil +} + // processBuildPlannerError will check for special error types that should be // handled differently. If no special error type is found, the fallback message // will be used. diff --git a/pkg/platform/runtime/buildexpression/buildexpression.go b/pkg/platform/runtime/buildexpression/buildexpression.go index 2c5de9e9e0..9aebc10e84 100644 --- a/pkg/platform/runtime/buildexpression/buildexpression.go +++ b/pkg/platform/runtime/buildexpression/buildexpression.go @@ -84,7 +84,7 @@ type In struct { Name *string } -// NewBuildExpression creates a BuildExpression from a JSON byte array. +// New creates a BuildExpression from a JSON byte array. // The JSON must be a valid BuildExpression in the following format: // // { @@ -171,6 +171,33 @@ func New(data []byte) (*BuildExpression, error) { return expr, nil } +// NewEmpty creates a minimal, empty buildexpression. +func NewEmpty() (*BuildExpression, error) { + // At this time, there is no way to ask the Platform for an empty buildexpression, so build one + // manually. + expr, err := New([]byte(` + { + "let": { + "runtime": { + "solve_legacy": { + "at_time": "", + "build_flags": [], + "camel_flags": [], + "platforms": [], + "requirements": [], + "solver_version": null + } + }, + "in": "$runtime" + } + } + `)) + if err != nil { + return nil, errs.Wrap(err, "Unable to create initial buildexpression") + } + return expr, nil +} + func newLet(path []string, m map[string]interface{}) (*Let, error) { path = append(path, ctxLet) defer func() {