From c18ebeb395f4db74f52b65975af45cf77b1566f9 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 21 Jul 2023 13:49:17 -0700 Subject: [PATCH 01/46] Fix target PR failing due to apparent backward incompatible API change --- scripts/internal/workflow-helpers/github.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/internal/workflow-helpers/github.go b/scripts/internal/workflow-helpers/github.go index 26eb5956cf..eac5904273 100644 --- a/scripts/internal/workflow-helpers/github.go +++ b/scripts/internal/workflow-helpers/github.go @@ -312,7 +312,7 @@ func ActiveVersionsOnBranch(ghClient *github.Client, jiraClient *jira.Client, br func UpdatePRTargetBranch(client *github.Client, prnumber int, targetBranch string) error { _, _, err := client.PullRequests.Edit(context.Background(), "ActiveState", "cli", prnumber, &github.PullRequest{ Base: &github.PullRequestBranch{ - Ref: github.String(fmt.Sprintf("refs/heads/%s", targetBranch)), + Ref: github.String(targetBranch), }, }) if err != nil { From 7642d9755d5a215417240acf38647627fd382f82 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 21 Jul 2023 13:50:08 -0700 Subject: [PATCH 02/46] Fix target PR failing on version PRs --- scripts/ci/target-version-pr/main.go | 47 +++++++++++++++++----------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/scripts/ci/target-version-pr/main.go b/scripts/ci/target-version-pr/main.go index 7bcf0a9e88..59ff01c15c 100644 --- a/scripts/ci/target-version-pr/main.go +++ b/scripts/ci/target-version-pr/main.go @@ -28,8 +28,9 @@ type Meta struct { ActiveVersion wh.Version ActiveJiraVersion string VersionPRName string - VersionBranchName string + TargetBranchName string VersionPR *github.PullRequest + IsVersionPR bool } func (m Meta) GetVersion() semver.Version { @@ -41,7 +42,7 @@ func (m Meta) GetJiraVersion() string { } func (m Meta) GetVersionBranchName() string { - return m.VersionBranchName + return m.TargetBranchName } func (m Meta) GetVersionPRName() string { @@ -76,7 +77,7 @@ func run() error { finish() // Create version PR if it doesn't exist yet - if meta.VersionPR == nil && !meta.ActiveVersion.EQ(wh.VersionMaster) { + if !meta.IsVersionPR && meta.VersionPR == nil && !meta.ActiveVersion.EQ(wh.VersionMaster) { finish = wc.PrintStart("Creating version PR for fixVersion %s", meta.ActiveVersion) err := wc.CreateVersionPR(ghClient, jiraClient, meta) if err != nil { @@ -86,34 +87,36 @@ func run() error { } // Set the target branch for our PR - finish = wc.PrintStart("Setting target branch to %s", meta.VersionBranchName) - if strings.HasSuffix(meta.ActivePR.GetBase().GetRef(), meta.VersionBranchName) { - wc.Print("PR already targets version branch %s", meta.VersionBranchName) + finish = wc.PrintStart("Setting target branch to %s", meta.TargetBranchName) + if strings.HasSuffix(meta.ActivePR.GetBase().GetRef(), meta.TargetBranchName) { + wc.Print("PR already targets version branch %s", meta.TargetBranchName) } else { if os.Getenv("DRYRUN") != "true" { - if err := wh.UpdatePRTargetBranch(ghClient, meta.ActivePR.GetNumber(), meta.VersionBranchName); err != nil { + if err := wh.UpdatePRTargetBranch(ghClient, meta.ActivePR.GetNumber(), meta.TargetBranchName); err != nil { return errs.Wrap(err, "failed to update PR target branch") } } else { - wc.Print("DRYRUN: would update PR target branch to %s", meta.VersionBranchName) + wc.Print("DRYRUN: would update PR target branch to %s", meta.TargetBranchName) } } finish() // Set the fixVersion - finish = wc.PrintStart("Setting fixVersion to %s", meta.ActiveVersion) - if len(meta.ActiveStory.Fields.FixVersions) == 0 || meta.ActiveStory.Fields.FixVersions[0].ID != meta.ActiveVersion.JiraID { - if os.Getenv("DRYRUN") != "true" { - if err := wh.UpdateJiraFixVersion(jiraClient, meta.ActiveStory, meta.ActiveVersion.JiraID); err != nil { - return errs.Wrap(err, "failed to update Jira fixVersion") + if !meta.IsVersionPR { + finish = wc.PrintStart("Setting fixVersion to %s", meta.ActiveVersion) + if len(meta.ActiveStory.Fields.FixVersions) == 0 || meta.ActiveStory.Fields.FixVersions[0].ID != meta.ActiveVersion.JiraID { + if os.Getenv("DRYRUN") != "true" { + if err := wh.UpdateJiraFixVersion(jiraClient, meta.ActiveStory, meta.ActiveVersion.JiraID); err != nil { + return errs.Wrap(err, "failed to update Jira fixVersion") + } + } else { + wc.Print("DRYRUN: would set fixVersion to %s", meta.ActiveVersion.String()) } } else { - wc.Print("DRYRUN: would set fixVersion to %s", meta.ActiveVersion.String()) + wc.Print("Jira issue already has fixVersion %s", meta.ActiveVersion.String()) } - } else { - wc.Print("Jira issue already has fixVersion %s", meta.ActiveVersion.String()) + finish() } - finish() wc.Print("All Done") @@ -130,6 +133,14 @@ func fetchMeta(ghClient *github.Client, jiraClient *jira.Client, prNumber int) ( wc.Print("PR retrieved: %s", prBeingHandled.GetTitle()) finish() + if wh.IsVersionBranch(prBeingHandled.Head.GetRef()) { + return Meta{ + Repo: &github.Repository{}, + ActivePR: prBeingHandled, + TargetBranchName: "beta", + IsVersionPR: true, + }, nil + } finish = wc.PrintStart("Extracting Jira Issue ID from Active PR: %s", prBeingHandled.GetTitle()) jiraIssueID, err := wh.ExtractJiraIssueID(prBeingHandled) if err != nil { @@ -189,7 +200,7 @@ func fetchMeta(ghClient *github.Client, jiraClient *jira.Client, prNumber int) ( ActiveVersion: fixVersion, ActiveJiraVersion: jiraVersion.Name, VersionPRName: versionPRName, - VersionBranchName: wh.VersionedBranchName(fixVersion.Version), + TargetBranchName: wh.VersionedBranchName(fixVersion.Version), VersionPR: versionPR, } From de0559ada113f6f52e00b52f7c790c9042b36076 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 21 Jul 2023 13:50:29 -0700 Subject: [PATCH 03/46] Verify that we have all commits from the previous version --- scripts/ci/verify-pr/main.go | 36 ++++- scripts/internal/workflow-controllers/pr.go | 2 +- scripts/internal/workflow-helpers/github.go | 33 +++- .../internal/workflow-helpers/github_test.go | 143 ++++++++++++++++++ scripts/internal/workflow-helpers/workflow.go | 2 + 5 files changed, 208 insertions(+), 8 deletions(-) diff --git a/scripts/ci/verify-pr/main.go b/scripts/ci/verify-pr/main.go index d90c08d32f..252f7298ee 100644 --- a/scripts/ci/verify-pr/main.go +++ b/scripts/ci/verify-pr/main.go @@ -112,8 +112,34 @@ func verifyVersionRC(ghClient *github.Client, jiraClient *jira.Client, pr *githu } finish() + finish = wc.PrintStart("Fetching previous version PR") + prevVersionPR, err := wh.FetchVersionPR(ghClient, wh.AssertLT, version) + if err != nil { + return errs.Wrap(err, + "Failed to find previous version PR for %s.", version.String()) + } + wc.Print("Got: %s\n", prevVersionPR.GetTitle()) + finish() + + finish = wc.PrintStart("Verifying we have all the commits from the previous version PR, comparing %s to %s", pr.Head.GetRef(), prevVersionPR.Head.GetRef()) + behind, err := wh.GetCommitsBehind(ghClient, prevVersionPR.Head.GetRef(), pr.Head.GetRef()) + if err != nil { + return errs.Wrap(err, "Failed to compare to previous version PR") + } + if len(behind) > 0 { + commits := []string{} + for _, c := range behind { + commits = append(commits, c.GetSHA()+": "+c.GetCommit().GetMessage()) + } + return errs.New("PR is behind the previous version PR (%s) by %d commits, missing commits:\n%s", + prevVersionPR.GetTitle(), len(behind), strings.Join(commits, "\n")) + } + finish() + finish = wc.PrintStart("Fetching commits for PR %d", pr.GetNumber()) - commits, err := wh.FetchCommitsByRef(ghClient, pr.GetHead().GetSHA(), nil) + commits, err := wh.FetchCommitsByRef(ghClient, pr.GetHead().GetSHA(), func(commit *github.RepositoryCommit) bool { + return commit.GetSHA() == prevVersionPR.GetHead().GetSHA() + }) if err != nil { return errs.Wrap(err, "Failed to fetch commits") } @@ -138,9 +164,9 @@ func verifyVersionRC(ghClient *github.Client, jiraClient *jira.Client, pr *githu if !isFound { issue := jiraIDs[jiraID] if wh.IsMergedStatus(issue.Fields.Status.Name) { - notFoundCritical = append(notFoundCritical, issue.Key) + notFoundCritical = append(notFoundCritical, issue.Key+": "+jiraIDs[jiraID].Fields.Summary) } else { - notFound = append(notFound, issue.Key) + notFound = append(notFound, issue.Key+": "+jiraIDs[jiraID].Fields.Summary) } } } @@ -150,8 +176,8 @@ func verifyVersionRC(ghClient *github.Client, jiraClient *jira.Client, pr *githu if len(notFound) > 0 { return errs.New("PR not ready as it's still missing commits for the following JIRA issues:\n"+ - "Pending story completion: %s\n"+ - "Missing stories: %s", strings.Join(notFound, ", "), strings.Join(notFoundCritical, ", ")) + "Pending story completion:\n%s\n\n"+ + "Missing stories:\n%s", strings.Join(notFound, "\n"), strings.Join(notFoundCritical, "\n")) } finish() diff --git a/scripts/internal/workflow-controllers/pr.go b/scripts/internal/workflow-controllers/pr.go index 610a736952..6b852e0728 100644 --- a/scripts/internal/workflow-controllers/pr.go +++ b/scripts/internal/workflow-controllers/pr.go @@ -95,7 +95,7 @@ func CreateVersionPR(ghClient *github.Client, jiraClient *jira.Client, meta Meta // Create commit with version.txt change finish = PrintStart("Creating commit with version.txt change") - parentSha, err := wh.CreateFileUpdateCommit(ghClient, meta.GetVersionBranchName(), "version.txt", meta.GetVersion().String()) + parentSha, err := wh.CreateFileUpdateCommit(ghClient, meta.GetVersionBranchName(), "version.txt", meta.GetVersion().String(), wh.UpdateVersionCommitMessage) if err != nil { return errs.Wrap(err, "failed to create commit") } diff --git a/scripts/internal/workflow-helpers/github.go b/scripts/internal/workflow-helpers/github.go index eac5904273..2c7c8b0df2 100644 --- a/scripts/internal/workflow-helpers/github.go +++ b/scripts/internal/workflow-helpers/github.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/rtutils/ptr" @@ -321,6 +322,34 @@ func UpdatePRTargetBranch(client *github.Client, prnumber int, targetBranch stri return nil } +func GetCommitsBehind(client *github.Client, base, head string) ([]*github.RepositoryCommit, error) { + // Note we're swapping base and head when doing this because github responds with the commits that are ahead, rather than behind. + commits, _, err := client.Repositories.CompareCommits(context.Background(), "ActiveState", "cli", head, base, nil) + if err != nil { + return nil, errs.Wrap(err, "failed to compare commits") + } + result := []*github.RepositoryCommit{} + for _, commit := range commits.Commits { + msg := strings.Split(commit.GetCommit().GetMessage(), "\n")[0] // first line only + msgWords := strings.Split(msg, " ") + if msg == UpdateVersionCommitMessage { + // Updates to version.txt are not meant to be inherited + continue + } + suffix := strings.TrimPrefix(msgWords[len(msgWords)-1], "ActiveState/") + if (strings.HasPrefix(msg, "Merge pull request") && IsVersionBranch(suffix)) || + (strings.HasPrefix(msg, "Merge branch '"+constants.BetaBranch+"'") && IsVersionBranch(suffix)) { + // Git's compare commits is not smart enough to consider merge commits from other version branches equal + // This matches the following types of messages: + // Merge pull request #2531 from ActiveState/version/0-38-1-RC1 + // Merge branch 'beta' into version/0-40-0-RC1 + continue + } + result = append(result, commit) + } + return result, nil +} + func SetPRBody(client *github.Client, prnumber int, body string) error { _, _, err := client.PullRequests.Edit(context.Background(), "ActiveState", "cli", prnumber, &github.PullRequest{ Body: &body, @@ -344,7 +373,7 @@ func CreateBranch(ghClient *github.Client, branchName string, SHA string) error return nil } -func CreateFileUpdateCommit(ghClient *github.Client, branchName string, path string, contents string) (string, error) { +func CreateFileUpdateCommit(ghClient *github.Client, branchName string, path string, contents string, message string) (string, error) { fileContents, _, _, err := ghClient.Repositories.GetContents(context.Background(), "ActiveState", "cli", path, &github.RepositoryContentGetOptions{ Ref: branchName, }) @@ -358,7 +387,7 @@ func CreateFileUpdateCommit(ghClient *github.Client, branchName string, path str Email: ptr.To("support@activestate.com"), }, Branch: &branchName, - Message: ptr.To(fmt.Sprintf("Update %s", path)), + Message: ptr.To(message), Content: []byte(contents), SHA: fileContents.SHA, }) diff --git a/scripts/internal/workflow-helpers/github_test.go b/scripts/internal/workflow-helpers/github_test.go index c533cbef63..c31ad0fb75 100644 --- a/scripts/internal/workflow-helpers/github_test.go +++ b/scripts/internal/workflow-helpers/github_test.go @@ -1,12 +1,16 @@ package workflow_helpers import ( + "fmt" + "reflect" "testing" "time" "github.com/ActiveState/cli/internal/errs" + "github.com/blang/semver" "github.com/google/go-github/v45/github" "github.com/stretchr/testify/require" + "github.com/thoas/go-funk" ) func TestParseJiraKey(t *testing.T) { @@ -255,6 +259,30 @@ func TestFetchPRByTitle(t *testing.T) { }, want: "Version 0.34.0-RC1", }, + { + name: "Version 0.40.0-RC1", + args: args{ + ghClient: InitGHClient(), + prName: "Version 0.40.0-RC1", + }, + want: "Version 0.40.0-RC1", + }, + { + name: "Version 0.40.0-RC2", + args: args{ + ghClient: InitGHClient(), + prName: "Version 0.40.0-RC2", + }, + want: "Version 0.40.0-RC2", + }, + { + name: "Version 0.40.0-RC3", + args: args{ + ghClient: InitGHClient(), + prName: "Version 0.40.0-RC3", + }, + want: "Version 0.40.0-RC3", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -337,6 +365,23 @@ func TestFetchCommitsByShaRange(t *testing.T) { } } +func TestUpdatePRTargetBranch(t *testing.T) { + t.Skip("For debugging purposes, comment this line out if you want to test this locally") + + err := UpdatePRTargetBranch(InitGHClient(), 1985, "version/0-40-0-RC2") + require.NoError(t, err, errs.JoinMessage(err)) +} + +func TestCreateBranch(t *testing.T) { + t.Skip("For debugging purposes, comment this line out if you want to test this locally") + + prefix := funk.RandomString(10, []rune("abcdefghijklmnopqrstuvwxyz0123456789")) + name := prefix + "/" + funk.RandomString(10, []rune("abcdefghijklmnopqrstuvwxyz0123456789")) + fmt.Printf("Creating branch %s\n", name) + err := CreateBranch(InitGHClient(), name, "f8a9465c572ed7a26145c7ebf961554da9367ec7") + require.NoError(t, err, errs.JoinMessage(err)) +} + func validateCommits(t *testing.T, commits []*github.RepositoryCommit, wantSHAs []string, wantN int) { if wantN != -1 && len(commits) != wantN { t.Errorf("FetchCommitsByRef() has %d results, want %d", len(commits), wantN) @@ -354,3 +399,101 @@ func validateCommits(t *testing.T, commits []*github.RepositoryCommit, wantSHAs } } } + +func TestBehindBy(t *testing.T) { + t.Skip("For debugging purposes, comment this line out if you want to test this locally") + + type args struct { + client *github.Client + base string + head string + } + tests := []struct { + name string + args args + wantBehind bool + wantErr bool + }{ + { + "Should be behind", + args{ + InitGHClient(), + "version/0-39-0-RC2", + "version/0-39-0-RC1", + }, + true, + false, + }, + { + "Should not be behind", + args{ + InitGHClient(), + "version/0-39-0-RC1", + "version/0-39-0-RC2", + }, + false, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetCommitsBehind(tt.args.client, tt.args.base, tt.args.head) + if (err != nil) != tt.wantErr { + t.Errorf("BehindBy() error = %v, wantErr %v", err, tt.wantErr) + return + } + if (len(got) > 0) != tt.wantBehind { + t.Errorf("BehindBy() got = %v, want %v", len(got), tt.wantBehind) + } + }) + } +} + +func TestFetchVersionPR(t *testing.T) { + t.Skip("For debugging purposes, comment this line out if you want to test this locally") + + type args struct { + ghClient *github.Client + assert Assertion + versionToCompare semver.Version + } + tests := []struct { + name string + args args + wantTitle string + wantErr bool + }{ + { + "Previous Version", + args{ + InitGHClient(), + AssertLT, + semver.MustParse("0.39.0-RC2"), + }, + "Version 0.39.0-RC1", + false, + }, + { + "Next Version", + args{ + InitGHClient(), + AssertGT, + semver.MustParse("0.39.0-RC1"), + }, + "Version 0.39.0-RC2", + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := FetchVersionPR(tt.args.ghClient, tt.args.assert, tt.args.versionToCompare) + if (err != nil) != tt.wantErr { + t.Errorf("FetchVersionPR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got.GetTitle(), tt.wantTitle) { + t.Errorf("FetchVersionPR() got = %v, want %v", got.GetTitle(), tt.wantTitle) + } + }) + } +} diff --git a/scripts/internal/workflow-helpers/workflow.go b/scripts/internal/workflow-helpers/workflow.go index 6826190d16..6e1c9d1f4d 100644 --- a/scripts/internal/workflow-helpers/workflow.go +++ b/scripts/internal/workflow-helpers/workflow.go @@ -16,6 +16,8 @@ const ( ReleaseBranch = "release" ) +const UpdateVersionCommitMessage = "Update version.txt" + const VersionNextFeasible = "Next Feasible" const VersionNextUnscheduled = "Next Unscheduled" From 1b45eaa048557dbe29d64b5f4bce1ea8fe485f9f Mon Sep 17 00:00:00 2001 From: daved Date: Fri, 21 Jul 2023 17:15:51 -0700 Subject: [PATCH 04/46] Save existing refresh token during dcag instead of creating new one --- pkg/cmdlets/auth/login.go | 20 +++++++++++++++----- pkg/platform/authentication/auth.go | 27 +++++++++++++-------------- pkg/platform/model/auth/auth.go | 14 +++++++------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/pkg/cmdlets/auth/login.go b/pkg/cmdlets/auth/login.go index ac11a8bbe4..e6a30a8862 100644 --- a/pkg/cmdlets/auth/login.go +++ b/pkg/cmdlets/auth/login.go @@ -16,7 +16,7 @@ import ( "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets" "github.com/ActiveState/cli/pkg/platform/authentication" - "github.com/ActiveState/cli/pkg/platform/model/auth" + model "github.com/ActiveState/cli/pkg/platform/model/auth" "github.com/go-openapi/strfmt" ) @@ -30,7 +30,14 @@ func Authenticate(cfg keypairs.Configurable, out output.Outputer, prompt prompt. } // AuthenticateWithInput will prompt the user for authentication if the input doesn't already provide it -func AuthenticateWithInput(username, password, totp string, nonInteractive bool, cfg keypairs.Configurable, out output.Outputer, prompt prompt.Prompter, auth *authentication.Auth) error { +func AuthenticateWithInput( + username, password, totp string, + nonInteractive bool, + cfg keypairs.Configurable, + out output.Outputer, + prompt prompt.Prompter, + auth *authentication.Auth, +) error { logging.Debug("Authenticating with input") credentials := &mono_models.Credentials{Username: username, Password: password, Totp: totp} @@ -240,9 +247,11 @@ func AuthenticateWithBrowser(out output.Outputer, auth *authentication.Auth, pro out.Notice(locale.Tr("err_browser_open", *response.VerificationURIComplete)) } + var refreshToken string if !response.Nopoll { // Wait for user to complete authentication - if err := auth.AuthenticateWithDevicePolling(strfmt.UUID(*response.DeviceCode), time.Duration(response.Interval)*time.Second); err != nil { + refreshToken, err = auth.AuthenticateWithDevicePolling(strfmt.UUID(*response.DeviceCode), time.Duration(response.Interval)*time.Second) + if err != nil { return locale.WrapError(err, "err_auth_device") } } else { @@ -256,12 +265,13 @@ func AuthenticateWithBrowser(out output.Outputer, auth *authentication.Auth, pro return errs.Wrap(err, "Prompt failed") } } - if err := auth.AuthenticateWithDevice(strfmt.UUID(*response.DeviceCode)); err != nil { + refreshToken, err = auth.AuthenticateWithDevice(strfmt.UUID(*response.DeviceCode)) + if err != nil { return locale.WrapError(err, "err_auth_device") } } - if err := auth.CreateToken(); err != nil { + if err := auth.SaveToken(refreshToken); err != nil { return locale.WrapError(err, "err_auth_token", "Failed to create token after authenticating with browser.") } diff --git a/pkg/platform/authentication/auth.go b/pkg/platform/authentication/auth.go index cb9dd1b6c3..3979229a81 100644 --- a/pkg/platform/authentication/auth.go +++ b/pkg/platform/authentication/auth.go @@ -233,39 +233,38 @@ func (s *Auth) AuthenticateWithModel(credentials *mono_models.Credentials) error return nil } -func (s *Auth) AuthenticateWithDevice(deviceCode strfmt.UUID) error { +func (s *Auth) AuthenticateWithDevice(deviceCode strfmt.UUID) (refreshToken string, err error) { logging.Debug("AuthenticateWithDevice") - token, err := model.CheckDeviceAuthorization(deviceCode) + jwtToken, refToken, err := model.CheckDeviceAuthorization(deviceCode) if err != nil { - return errs.Wrap(err, "Authorization failed") + return "", errs.Wrap(err, "Authorization failed") } - if token == nil { - return errNotYetGranted + if jwtToken == nil { + return "", errNotYetGranted } - if err := s.updateSession(token); err != nil { - return errs.Wrap(err, "Storing JWT failed") + if err := s.updateSession(jwtToken); err != nil { + return "", errs.Wrap(err, "Storing JWT failed") } - return nil - + return refToken.Token, nil } -func (s *Auth) AuthenticateWithDevicePolling(deviceCode strfmt.UUID, interval time.Duration) error { +func (s *Auth) AuthenticateWithDevicePolling(deviceCode strfmt.UUID, interval time.Duration) (string, error) { logging.Debug("AuthenticateWithDevicePolling, polling: %v", interval.String()) for start := time.Now(); time.Since(start) < 5*time.Minute; { - err := s.AuthenticateWithDevice(deviceCode) + token, err := s.AuthenticateWithDevice(deviceCode) if err == nil { - return nil + return token, nil } else if !errors.Is(err, errNotYetGranted) { - return errs.Wrap(err, "Device authentication failed") + return "", errs.Wrap(err, "Device authentication failed") } time.Sleep(interval) // then try again } - return locale.NewInputError("err_auth_device_timeout") + return "", locale.NewInputError("err_auth_device_timeout") } // AuthenticateWithToken will try to authenticate using the given token diff --git a/pkg/platform/model/auth/auth.go b/pkg/platform/model/auth/auth.go index ec649f9f7b..cd2e377cb1 100644 --- a/pkg/platform/model/auth/auth.go +++ b/pkg/platform/model/auth/auth.go @@ -7,7 +7,7 @@ import ( "github.com/ActiveState/cli/pkg/platform/api" "github.com/ActiveState/cli/pkg/platform/api/mono" "github.com/ActiveState/cli/pkg/platform/api/mono/mono_client/oauth" - "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" + mms "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" "github.com/go-openapi/strfmt" ) @@ -15,7 +15,7 @@ import ( // returns the device code needed for authorization. // The user is subsequently required to visit the device code's URI and click the "Authorize" // button. -func RequestDeviceAuthorization() (*mono_models.DeviceCode, error) { +func RequestDeviceAuthorization() (*mms.DeviceCode, error) { postParams := oauth.NewAuthDevicePostParams() response, err := mono.Get().Oauth.AuthDevicePost(postParams) if err != nil { @@ -25,7 +25,7 @@ func RequestDeviceAuthorization() (*mono_models.DeviceCode, error) { return response.Payload, nil } -func CheckDeviceAuthorization(deviceCode strfmt.UUID) (*mono_models.JWT, error) { +func CheckDeviceAuthorization(deviceCode strfmt.UUID) (jwt *mms.JWT, refreshToken *mms.NewToken, err error) { getParams := oauth.NewAuthDeviceGetParams() getParams.SetDeviceCode(deviceCode) @@ -37,14 +37,14 @@ func CheckDeviceAuthorization(deviceCode strfmt.UUID) (*mono_models.JWT, error) switch *errorToken { case oauth.AuthDeviceGetBadRequestBodyErrorAuthorizationPending, oauth.AuthDeviceGetBadRequestBodyErrorSlowDown: logging.Debug("Authorization still pending") - return nil, nil + return nil, nil, nil case oauth.AuthDeviceGetBadRequestBodyErrorExpiredToken: - return nil, locale.WrapInputError(err, "auth_device_timeout") + return nil, nil, locale.WrapInputError(err, "auth_device_timeout") } } - return nil, errs.Wrap(err, api.ErrorMessageFromPayload(err)) + return nil, nil, errs.Wrap(err, api.ErrorMessageFromPayload(err)) } - return response.Payload.AccessToken, nil + return response.Payload.AccessToken, response.Payload.RefreshToken, nil } From 3eca6a7c43535c54269bca5d80085515383c2d78 Mon Sep 17 00:00:00 2001 From: daved Date: Fri, 21 Jul 2023 17:46:51 -0700 Subject: [PATCH 05/46] Lock down hello cmd to our ci/testing --- cmd/state/internal/cmdtree/cmdtree.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/state/internal/cmdtree/cmdtree.go b/cmd/state/internal/cmdtree/cmdtree.go index 55576386ad..cf74c84981 100644 --- a/cmd/state/internal/cmdtree/cmdtree.go +++ b/cmd/state/internal/cmdtree/cmdtree.go @@ -205,7 +205,7 @@ func New(prime *primer.Values, args ...string) *CmdTree { newTestCommand(prime), ) - if !condition.OnCI() { + if condition.OnCI() && condition.InTest() { helloCmd := newHelloCommand(prime) stateCmd.AddChildren(helloCmd) } From 6a52039d708893c92d74046e6bbd3fa1573379e1 Mon Sep 17 00:00:00 2001 From: daved Date: Mon, 24 Jul 2023 10:59:34 -0700 Subject: [PATCH 06/46] Rename refreshtoken vars to apikey --- pkg/cmdlets/auth/login.go | 8 ++++---- pkg/platform/authentication/auth.go | 6 +++--- pkg/platform/model/auth/auth.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmdlets/auth/login.go b/pkg/cmdlets/auth/login.go index e6a30a8862..108bcebeef 100644 --- a/pkg/cmdlets/auth/login.go +++ b/pkg/cmdlets/auth/login.go @@ -247,10 +247,10 @@ func AuthenticateWithBrowser(out output.Outputer, auth *authentication.Auth, pro out.Notice(locale.Tr("err_browser_open", *response.VerificationURIComplete)) } - var refreshToken string + var apiKey string if !response.Nopoll { // Wait for user to complete authentication - refreshToken, err = auth.AuthenticateWithDevicePolling(strfmt.UUID(*response.DeviceCode), time.Duration(response.Interval)*time.Second) + apiKey, err = auth.AuthenticateWithDevicePolling(strfmt.UUID(*response.DeviceCode), time.Duration(response.Interval)*time.Second) if err != nil { return locale.WrapError(err, "err_auth_device") } @@ -265,13 +265,13 @@ func AuthenticateWithBrowser(out output.Outputer, auth *authentication.Auth, pro return errs.Wrap(err, "Prompt failed") } } - refreshToken, err = auth.AuthenticateWithDevice(strfmt.UUID(*response.DeviceCode)) + apiKey, err = auth.AuthenticateWithDevice(strfmt.UUID(*response.DeviceCode)) if err != nil { return locale.WrapError(err, "err_auth_device") } } - if err := auth.SaveToken(refreshToken); err != nil { + if err := auth.SaveToken(apiKey); err != nil { return locale.WrapError(err, "err_auth_token", "Failed to create token after authenticating with browser.") } diff --git a/pkg/platform/authentication/auth.go b/pkg/platform/authentication/auth.go index 3979229a81..cb477c7c80 100644 --- a/pkg/platform/authentication/auth.go +++ b/pkg/platform/authentication/auth.go @@ -233,10 +233,10 @@ func (s *Auth) AuthenticateWithModel(credentials *mono_models.Credentials) error return nil } -func (s *Auth) AuthenticateWithDevice(deviceCode strfmt.UUID) (refreshToken string, err error) { +func (s *Auth) AuthenticateWithDevice(deviceCode strfmt.UUID) (apiKey string, err error) { logging.Debug("AuthenticateWithDevice") - jwtToken, refToken, err := model.CheckDeviceAuthorization(deviceCode) + jwtToken, apiKeyToken, err := model.CheckDeviceAuthorization(deviceCode) if err != nil { return "", errs.Wrap(err, "Authorization failed") } @@ -249,7 +249,7 @@ func (s *Auth) AuthenticateWithDevice(deviceCode strfmt.UUID) (refreshToken stri return "", errs.Wrap(err, "Storing JWT failed") } - return refToken.Token, nil + return apiKeyToken.Token, nil } func (s *Auth) AuthenticateWithDevicePolling(deviceCode strfmt.UUID, interval time.Duration) (string, error) { diff --git a/pkg/platform/model/auth/auth.go b/pkg/platform/model/auth/auth.go index cd2e377cb1..dd5ff67d6c 100644 --- a/pkg/platform/model/auth/auth.go +++ b/pkg/platform/model/auth/auth.go @@ -25,7 +25,7 @@ func RequestDeviceAuthorization() (*mms.DeviceCode, error) { return response.Payload, nil } -func CheckDeviceAuthorization(deviceCode strfmt.UUID) (jwt *mms.JWT, refreshToken *mms.NewToken, err error) { +func CheckDeviceAuthorization(deviceCode strfmt.UUID) (jwt *mms.JWT, apiKey *mms.NewToken, err error) { getParams := oauth.NewAuthDeviceGetParams() getParams.SetDeviceCode(deviceCode) From 482f5b400a61669637902a1ddfc14804903aa607 Mon Sep 17 00:00:00 2001 From: daved Date: Mon, 24 Jul 2023 11:21:42 -0700 Subject: [PATCH 07/46] Ensure example cmd easily runs locally --- cmd/state/internal/cmdtree/cmdtree.go | 5 ++++- internal/constants/constants.go | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/state/internal/cmdtree/cmdtree.go b/cmd/state/internal/cmdtree/cmdtree.go index cf74c84981..956b79d339 100644 --- a/cmd/state/internal/cmdtree/cmdtree.go +++ b/cmd/state/internal/cmdtree/cmdtree.go @@ -1,11 +1,13 @@ package cmdtree import ( + "os" "time" "github.com/ActiveState/cli/cmd/state/internal/cmdtree/exechandlers/cmdcall" "github.com/ActiveState/cli/internal/captain" "github.com/ActiveState/cli/internal/condition" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/primer" @@ -205,7 +207,8 @@ func New(prime *primer.Values, args ...string) *CmdTree { newTestCommand(prime), ) - if condition.OnCI() && condition.InTest() { + exampleEnabled := os.Getenv(constants.EnableExampleEnvVarName) == "true" + if exampleEnabled || (condition.OnCI() && condition.InTest()) { helloCmd := newHelloCommand(prime) stateCmd.AddChildren(helloCmd) } diff --git a/internal/constants/constants.go b/internal/constants/constants.go index d479164199..d731ed0261 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -47,6 +47,9 @@ const DisableRuntime = "ACTIVESTATE_CLI_DISABLE_RUNTIME" // DisableUpdates is the env var used to disable automatic updates const DisableUpdates = "ACTIVESTATE_CLI_DISABLE_UPDATES" +// EnableExampleEnvVarName is the env var to enable the example command +const EnableExampleEnvVarName = "ACTIVESTATE_CLI_ENABLE_EXAMPLE" + // UpdateBranchEnvVarName is the env var that is used to override which branch to pull the update from const UpdateBranchEnvVarName = "ACTIVESTATE_CLI_UPDATE_BRANCH" From 90575623686db12ebf53f6ba5df643e5db587386 Mon Sep 17 00:00:00 2001 From: daved Date: Mon, 24 Jul 2023 12:42:33 -0700 Subject: [PATCH 08/46] Add example notice to hello cmd --- internal/runners/hello/hello_example.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/runners/hello/hello_example.go b/internal/runners/hello/hello_example.go index 7d8b6b901b..d336a88640 100644 --- a/internal/runners/hello/hello_example.go +++ b/internal/runners/hello/hello_example.go @@ -60,6 +60,8 @@ func New(p primeable) *Hello { // Run contains the scope in which the hello runner logic is executed. func (h *Hello) Run(params *RunParams) error { + h.out.Print(locale.Tl("hello_notice", "This command is for example use only")) + if h.project == nil { err := locale.NewInputError( "hello_info_err_no_project", "Not in a project directory.", From 3d607b562ae78b5054f74bd11e8b1542b7e00731 Mon Sep 17 00:00:00 2001 From: daved Date: Mon, 24 Jul 2023 12:43:38 -0700 Subject: [PATCH 09/46] Ensure hello cmd is always setup, rename to _hello --- cmd/state/internal/cmdtree/cmdtree.go | 9 +-------- cmd/state/internal/cmdtree/hello_example.go | 6 +++--- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/cmd/state/internal/cmdtree/cmdtree.go b/cmd/state/internal/cmdtree/cmdtree.go index 956b79d339..4721bb1bba 100644 --- a/cmd/state/internal/cmdtree/cmdtree.go +++ b/cmd/state/internal/cmdtree/cmdtree.go @@ -1,13 +1,11 @@ package cmdtree import ( - "os" "time" "github.com/ActiveState/cli/cmd/state/internal/cmdtree/exechandlers/cmdcall" "github.com/ActiveState/cli/internal/captain" "github.com/ActiveState/cli/internal/condition" - "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/primer" @@ -160,6 +158,7 @@ func New(prime *primer.Values, args ...string) *CmdTree { stateCmd := newStateCommand(globals, prime) stateCmd.AddChildren( + newHelloCommand(prime), newActivateCommand(prime), newInitCommand(prime), newPushCommand(prime), @@ -207,12 +206,6 @@ func New(prime *primer.Values, args ...string) *CmdTree { newTestCommand(prime), ) - exampleEnabled := os.Getenv(constants.EnableExampleEnvVarName) == "true" - if exampleEnabled || (condition.OnCI() && condition.InTest()) { - helloCmd := newHelloCommand(prime) - stateCmd.AddChildren(helloCmd) - } - return &CmdTree{ cmd: stateCmd, } diff --git a/cmd/state/internal/cmdtree/hello_example.go b/cmd/state/internal/cmdtree/hello_example.go index 16fade05f5..9edd798d72 100644 --- a/cmd/state/internal/cmdtree/hello_example.go +++ b/cmd/state/internal/cmdtree/hello_example.go @@ -14,7 +14,7 @@ func newHelloCommand(prime *primer.Values) *captain.Command { cmd := captain.NewCommand( // The command's name should not be localized as we want commands to behave consistently regardless of localization. - "hello", + "_hello", // The title is printed with title formatting when running the command. Leave empty to disable. locale.Tl("hello_cmd_title", "Saying hello"), // The description is shown on --help output @@ -57,9 +57,9 @@ func newHelloCommand(prime *primer.Values) *captain.Command { // The group is used to group together commands in the --help output cmd.SetGroup(UtilsGroup) // Any new command should be marked unstable for the first release it goes out in. - // cmd.SetUnstable(true) + cmd.SetUnstable(true) // Certain commands like `state deploy` are there for backwards compatibility, but we don't want to show them in the --help output as they are not part of the happy path or our long term goals. - // cmd.SetHidden(true) + cmd.SetHidden(true) return cmd } From 7c969dc1cbb10bbc375bec3b59b3a426a8b540cf Mon Sep 17 00:00:00 2001 From: daved Date: Mon, 24 Jul 2023 12:46:32 -0700 Subject: [PATCH 10/46] Update e2e hello tests with new cmd name _hello --- test/integration/hello_int_example_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/integration/hello_int_example_test.go b/test/integration/hello_int_example_test.go index dff119d1bd..f58b70b284 100644 --- a/test/integration/hello_int_example_test.go +++ b/test/integration/hello_int_example_test.go @@ -22,25 +22,25 @@ func (suite *HelloIntegrationTestSuite) TestHello() { cp.Expect("Checked out project") cp.ExpectExitCode(0) - cp = ts.Spawn("hello") + cp = ts.Spawn("_hello") cp.Expect("Hello, Friend!") cp.ExpectExitCode(0) - cp = ts.Spawn("hello", "Person") + cp = ts.Spawn("_hello", "Person") cp.Expect("Hello, Person!") cp.ExpectExitCode(0) - cp = ts.Spawn("hello", "") - cp.Expect("Cannot say hello") + cp = ts.Spawn("_hello", "") + cp.Expect("Cannot say Hello") cp.Expect("No name provided") cp.ExpectNotExitCode(0) - cp = ts.Spawn("hello", "--extra") + cp = ts.Spawn("_hello", "--extra") cp.Expect("Project: ActiveState-CLI/small-python") cp.Expect("Current commit message:") cp.ExpectExitCode(0) - cp = ts.Spawn("hello", "--echo", "example") + cp = ts.Spawn("_hello", "--echo", "example") cp.Expect("Echoing: example") cp.ExpectExitCode(0) } From ada38bd327580b552170b122a2097f00855fc04f Mon Sep 17 00:00:00 2001 From: daved Date: Mon, 24 Jul 2023 12:48:08 -0700 Subject: [PATCH 11/46] Remove enable hello constant --- internal/constants/constants.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index d731ed0261..d479164199 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -47,9 +47,6 @@ const DisableRuntime = "ACTIVESTATE_CLI_DISABLE_RUNTIME" // DisableUpdates is the env var used to disable automatic updates const DisableUpdates = "ACTIVESTATE_CLI_DISABLE_UPDATES" -// EnableExampleEnvVarName is the env var to enable the example command -const EnableExampleEnvVarName = "ACTIVESTATE_CLI_ENABLE_EXAMPLE" - // UpdateBranchEnvVarName is the env var that is used to override which branch to pull the update from const UpdateBranchEnvVarName = "ACTIVESTATE_CLI_UPDATE_BRANCH" From 72b772668a4dfaaaef1d8d62892069bb06a28bde Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Mon, 24 Jul 2023 14:34:50 -0700 Subject: [PATCH 12/46] Add changelog for v0.40.0 --- changelog.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/changelog.md b/changelog.md index b03b87563e..bfc3292299 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,47 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +### 0.40.0 + +### Added + +- New command `state projects edit` which allows you to edit a projects name, + visibility, and linked git repository. You will need to opt-in to unstable + commands to use it. +- New command `state projects delete` which allows you to delete a project. + You will need to opt-in to unstable commands to use it. +- New command `state projects move` which allows you to move a project to a + different organization. You will need to opt-in to unstable commands to use + it. + +### Changed + +- Runtime installations have been updated to use our new buildplanner API. This + will enable us to develop new features in future versions. There should be no + impact to the user experience in this version. +- Runtime installation is now atomic, meaning that an interruption to the + installation progress will not leave you in a corrupt state. +- Requirement names are now normalized, avoiding requirement name collisions as + well as making it easier to install packages by their non-standard naming. +- Commands which do not produce JSON output will now error out and say they do + not support JSON, rather than produce empty output. +- When `state clean uninstall` cannot uninstall the State Tool because of third + party files in its installation dir it will now report what those files are. + +### Removed + +- The output format `--output=editor.v0` has been removed. Instead + use `--output=editor` or `--output=json`. + +### Fixed + +- Fixed issue where the `--namespace` flag on `state packages` was not + respected. +- Fixed issue where `PYTHONTPATH` would not be set to empty when sourcing a + runtime, making it so that a system runtime can contaminate the sourced + runtime. +- Several localization improvements. + ### 0.39.0 ### Added From 098f1126aa9aef952d2b64478a56c222eace25e7 Mon Sep 17 00:00:00 2001 From: mdrakos Date: Tue, 25 Jul 2023 13:25:44 -0700 Subject: [PATCH 13/46] Ensure RemediableSolverErrors are presented to users --- .../api/buildplanner/model/buildplan.go | 19 +++++++++++++++++++ pkg/platform/model/buildplanner.go | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/pkg/platform/api/buildplanner/model/buildplan.go b/pkg/platform/api/buildplanner/model/buildplan.go index 076a00a61e..6f3a42889a 100644 --- a/pkg/platform/api/buildplanner/model/buildplan.go +++ b/pkg/platform/api/buildplanner/model/buildplan.go @@ -61,6 +61,10 @@ const ( XActiveStateArtifactMimeType = "application/x-activestate-artifacts" XCamelInstallerMimeType = "application/x-camel-installer" XGozipInstallerMimeType = "application/x-gozip-installer" + + // Error types + RemediableSolveErrorType = "RemediableSolveError" + GenericSolveErrorType = "GenericSolveError" ) func IsStateToolArtifact(mimeType string) bool { @@ -94,6 +98,13 @@ func (e *BuildPlannerError) InputError() bool { return true } +// UserError returns the error message to be displayed to the user. +// This function is added so that BuildPlannerErrors will be displayed +// to the user +func (e *BuildPlannerError) UserError() string { + return e.Error() +} + func (e *BuildPlannerError) Error() string { // Append last five lines to error message offset := 0 @@ -176,6 +187,10 @@ func (b *BuildPlanByProject) Build() (*Build, error) { var errs []string var isTransient bool for _, se := range b.Project.Commit.Build.SubErrors { + if se.Type != RemediableSolveErrorType { + continue + } + if se.Message != "" { errs = append(errs, se.Message) isTransient = se.IsTransient @@ -253,6 +268,10 @@ func (b *BuildPlanByCommit) Build() (*Build, error) { var errs []string var isTransient bool for _, se := range b.Commit.Build.SubErrors { + if se.Type != RemediableSolveErrorType { + continue + } + if se.Message != "" { errs = append(errs, se.Message) isTransient = se.IsTransient diff --git a/pkg/platform/model/buildplanner.go b/pkg/platform/model/buildplanner.go index 753f541373..d2b881b2a9 100644 --- a/pkg/platform/model/buildplanner.go +++ b/pkg/platform/model/buildplanner.go @@ -305,6 +305,10 @@ func (bp *BuildPlanner) StageCommit(params StageCommitParams) (strfmt.UUID, erro var errs []string var isTransient bool for _, se := range resp.Commit.Build.SubErrors { + if se.Type != bpModel.RemediableSolveErrorType { + continue + } + if se.Message != "" { errs = append(errs, se.Message) isTransient = se.IsTransient From d8dd69919c7f2463142a6ed235215ddf85cce40e Mon Sep 17 00:00:00 2001 From: mdrakos Date: Tue, 25 Jul 2023 13:58:42 -0700 Subject: [PATCH 14/46] Remove generic error constant --- pkg/platform/api/buildplanner/model/buildplan.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/platform/api/buildplanner/model/buildplan.go b/pkg/platform/api/buildplanner/model/buildplan.go index 6f3a42889a..5bad6dc7ac 100644 --- a/pkg/platform/api/buildplanner/model/buildplan.go +++ b/pkg/platform/api/buildplanner/model/buildplan.go @@ -64,7 +64,6 @@ const ( // Error types RemediableSolveErrorType = "RemediableSolveError" - GenericSolveErrorType = "GenericSolveError" ) func IsStateToolArtifact(mimeType string) bool { From 48c9234f16061027d181da03517104945c66cf64 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Mon, 24 Jul 2023 13:56:14 -0700 Subject: [PATCH 15/46] WIP: Installer tests use genuine payload --- activestate.generators.yaml | 37 ++++++++------ .../test/integration/installer_int_test.go | 15 ++---- scripts/ci/payload-generator/main.go | 49 +++++++++++++++++++ 3 files changed, 74 insertions(+), 27 deletions(-) create mode 100644 scripts/ci/payload-generator/main.go diff --git a/activestate.generators.yaml b/activestate.generators.yaml index a27b5b7278..ae89111559 100644 --- a/activestate.generators.yaml +++ b/activestate.generators.yaml @@ -53,6 +53,26 @@ scripts: language: bash description: Detects new localisation calls and generates placeholder entries in en-us.yaml value: python3 scripts/locale-generator.py + - name: generate-payload + language: bash + description: Generate payload for installer / update archives + value: | + set -e + $constants.SET_ENV + + # TODO: Move all this logic to the payload-generator Go runner so we can employ our type system + + echo "# Create payload dir" + PAYLOADDIR=$BUILD_TARGET_DIR/payload + mkdir -p $PAYLOADDIR + + echo "# Copy targets $PAYLOADDIR" + cp -av $BUILD_TARGET_DIR/$constants.BUILD_INSTALLER_TARGET $PAYLOADDIR + BINDIR=$PAYLOADDIR/bin + mkdir -p $BINDIR + cp -av $BUILD_TARGET_DIR/$constants.BUILD_DAEMON_TARGET $BINDIR + cp -av $BUILD_TARGET_DIR/$constants.BUILD_EXEC_TARGET $BINDIR + cp -av $BUILD_TARGET_DIR/$constants.BUILD_TARGET $BINDIR - name: generate-update language: bash description: Generate update files @@ -61,26 +81,13 @@ scripts: export GOARCH=${1:-amd64} $constants.SET_ENV - echo "# Create temp dir to generate bits" - TEMPDIR=$BUILD_TARGET_DIR/state-install - mkdir -p $TEMPDIR - cp -a $BUILD_TARGET_DIR/$constants.BUILD_INSTALLER_TARGET $TEMPDIR - - echo "# Copy targets to temp dir" - BINDIR=$TEMPDIR/bin - mkdir -p $BINDIR - cp -a $BUILD_TARGET_DIR/$constants.BUILD_DAEMON_TARGET $BINDIR - cp -a $BUILD_TARGET_DIR/$constants.BUILD_EXEC_TARGET $BINDIR - cp -a $BUILD_TARGET_DIR/$constants.BUILD_TARGET $BINDIR + $scripts.generate-payload echo "# Create update dir" mkdir -p ./build/update echo "# Generate update from temp dir" - go run scripts/ci/update-generator/main.go -o ./build/update $TEMPDIR - - echo "# Remove temp dir" - rm -rf $TEMPDIR + go run scripts/ci/update-generator/main.go -o ./build/update $PAYLOADDIR - name: generate-remote-install-deployment language: bash value: go run scripts/ci/deploy-generator/remote-installer/main.go "$@" diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 1bdbc3deae..8b02fe092a 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -17,7 +17,6 @@ import ( "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/httputil" "github.com/ActiveState/cli/internal/installation" - "github.com/ActiveState/cli/internal/osutils" "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" @@ -33,7 +32,7 @@ type InstallerIntegrationTestSuite struct { func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { suite.OnlyRunForTags(tagsuite.Installer, tagsuite.Critical) - ts := e2e.New(suite.T(), false) + ts := e2e.New(suite.T(), true) defer ts.Close() suite.setupTest(ts) @@ -335,16 +334,8 @@ func (suite *InstallerIntegrationTestSuite) AssertConfig(ts *e2e.Session) { func (s *InstallerIntegrationTestSuite) setupTest(ts *e2e.Session) { root := environment.GetRootPathUnsafe() - buildDir := fileutils.Join(root, "build") - installerExe := filepath.Join(buildDir, constants.StateInstallerCmd+osutils.ExeExt) - if !fileutils.FileExists(installerExe) { - s.T().Fatal("E2E tests require a state-installer binary. Run `state run build-installer`.") - } - s.installerExe = ts.CopyExeToDir(installerExe, filepath.Join(ts.Dirs.Base, "installer")) - - payloadDir := filepath.Dir(s.installerExe) - ts.CopyExeToDir(ts.Exe, filepath.Join(payloadDir, installation.BinDirName)) - ts.CopyExeToDir(ts.SvcExe, filepath.Join(payloadDir, installation.BinDirName)) + payloadDir := fileutils.Join(root, "build", "payload") + // Todo: Copy payload files to test dirs (see ts.Dirs) } func TestInstallerIntegrationTestSuite(t *testing.T) { diff --git a/scripts/ci/payload-generator/main.go b/scripts/ci/payload-generator/main.go new file mode 100644 index 0000000000..0d7acfc378 --- /dev/null +++ b/scripts/ci/payload-generator/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/environment" + "github.com/ActiveState/cli/internal/exeutils" + "github.com/ActiveState/cli/internal/fileutils" +) + +// The payload-generator is an intentionally very dumb runner that just copies some files around. +// This could just be a bash script if not for the fact that bash can't be linked with our type system. + +func main() { + root := environment.GetRootPathUnsafe() + buildDir := filepath.Join(root, "build") + payloadDir := filepath.Join(buildDir, "payload") + payloadBinDir := filepath.Join(buildDir, "payload", "bin") + + if err := fileutils.MkdirUnlessExists(payloadBinDir); err != nil { + fmt.Printf("Error creating payload bin dir: %s\n", err.Error()) + os.Exit(1) + } + + if err := copyFiles(map[string]string{ + filepath.Join(buildDir, constants.StateInstallerCmd+exeutils.Extension): payloadDir, + filepath.Join(buildDir, constants.StateCmd+exeutils.Extension): payloadBinDir, + filepath.Join(buildDir, constants.StateSvcCmd+exeutils.Extension): payloadBinDir, + filepath.Join(buildDir, constants.StateExecutorCmd+exeutils.Extension): payloadBinDir, + }); err != nil { + fmt.Printf("Error copying files: %s\n", err.Error()) + os.Exit(1) + } + +} + +func copyFiles(files map[string]string) error { + for src, target := range files { + fmt.Printf("Copying %s to %s\n", src, target) + err := fileutils.CopyFile(src, filepath.Join(target, filepath.Base(src))) + if err != nil { + return err + } + } + return nil +} From 3e4ef0cdd0fed66db8d7c37d5f9616e2d0ce4c16 Mon Sep 17 00:00:00 2001 From: daved Date: Tue, 25 Jul 2023 15:09:22 -0700 Subject: [PATCH 16/46] Move payload generator logic to lib, call script from asy.gens. --- activestate.generators.yaml | 15 +---- scripts/ci/payload-generator/main.go | 45 +++++---------- scripts/ci/payload-generator/paygen/paygen.go | 56 +++++++++++++++++++ 3 files changed, 72 insertions(+), 44 deletions(-) create mode 100644 scripts/ci/payload-generator/paygen/paygen.go diff --git a/activestate.generators.yaml b/activestate.generators.yaml index ae89111559..93d5d38ba7 100644 --- a/activestate.generators.yaml +++ b/activestate.generators.yaml @@ -60,19 +60,8 @@ scripts: set -e $constants.SET_ENV - # TODO: Move all this logic to the payload-generator Go runner so we can employ our type system - - echo "# Create payload dir" - PAYLOADDIR=$BUILD_TARGET_DIR/payload - mkdir -p $PAYLOADDIR - - echo "# Copy targets $PAYLOADDIR" - cp -av $BUILD_TARGET_DIR/$constants.BUILD_INSTALLER_TARGET $PAYLOADDIR - BINDIR=$PAYLOADDIR/bin - mkdir -p $BINDIR - cp -av $BUILD_TARGET_DIR/$constants.BUILD_DAEMON_TARGET $BINDIR - cp -av $BUILD_TARGET_DIR/$constants.BUILD_EXEC_TARGET $BINDIR - cp -av $BUILD_TARGET_DIR/$constants.BUILD_TARGET $BINDIR + echo "# Generate payload" + go run ./scripts/ci/payload-generator/main.go - name: generate-update language: bash description: Generate update files diff --git a/scripts/ci/payload-generator/main.go b/scripts/ci/payload-generator/main.go index 0d7acfc378..762b086cbd 100644 --- a/scripts/ci/payload-generator/main.go +++ b/scripts/ci/payload-generator/main.go @@ -5,45 +5,28 @@ import ( "os" "path/filepath" - "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" - "github.com/ActiveState/cli/internal/exeutils" - "github.com/ActiveState/cli/internal/fileutils" + "github.com/ActiveState/cli/scripts/ci/payload-generator/paygen" ) +var logErr = func(msg string, vals ...any) { + fmt.Fprintf(os.Stderr, msg, vals...) + fmt.Fprintf(os.Stderr, "\n") +} + // The payload-generator is an intentionally very dumb runner that just copies some files around. // This could just be a bash script if not for the fact that bash can't be linked with our type system. - func main() { - root := environment.GetRootPathUnsafe() - buildDir := filepath.Join(root, "build") - payloadDir := filepath.Join(buildDir, "payload") - payloadBinDir := filepath.Join(buildDir, "payload", "bin") - - if err := fileutils.MkdirUnlessExists(payloadBinDir); err != nil { - fmt.Printf("Error creating payload bin dir: %s\n", err.Error()) - os.Exit(1) - } - - if err := copyFiles(map[string]string{ - filepath.Join(buildDir, constants.StateInstallerCmd+exeutils.Extension): payloadDir, - filepath.Join(buildDir, constants.StateCmd+exeutils.Extension): payloadBinDir, - filepath.Join(buildDir, constants.StateSvcCmd+exeutils.Extension): payloadBinDir, - filepath.Join(buildDir, constants.StateExecutorCmd+exeutils.Extension): payloadBinDir, - }); err != nil { - fmt.Printf("Error copying files: %s\n", err.Error()) + if err := run(); err != nil { + logErr("%s", err) os.Exit(1) } - } -func copyFiles(files map[string]string) error { - for src, target := range files { - fmt.Printf("Copying %s to %s\n", src, target) - err := fileutils.CopyFile(src, filepath.Join(target, filepath.Base(src))) - if err != nil { - return err - } - } - return nil +func run() error { + root := environment.GetRootPathUnsafe() + buildDir := filepath.Join(root, "build") + payloadDir := filepath.Join(buildDir, "payload") + + return paygen.GeneratePayload(buildDir, payloadDir) } diff --git a/scripts/ci/payload-generator/paygen/paygen.go b/scripts/ci/payload-generator/paygen/paygen.go new file mode 100644 index 0000000000..2389042719 --- /dev/null +++ b/scripts/ci/payload-generator/paygen/paygen.go @@ -0,0 +1,56 @@ +package paygen + +import ( + "fmt" + "os" + fp "path/filepath" + + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/exeutils" + "github.com/ActiveState/cli/internal/fileutils" + "github.com/ActiveState/cli/internal/installation" +) + +var log = func(msg string, vals ...any) { + fmt.Fprintf(os.Stdout, msg, vals...) + fmt.Fprintf(os.Stdout, "\n") +} + +func GeneratePayload(buildDir, payloadDir string) error { + emsg := "generate payload: %w" + + payloadBinDir := fp.Join(payloadDir, "bin") + + if err := fileutils.MkdirUnlessExists(payloadBinDir); err != nil { + return fmt.Errorf(emsg, err) + } + + installDirMarker := installation.InstallDirMarker + log("Creating install dir marker in %s", payloadDir) + if err := fileutils.Touch(fp.Join(payloadDir, installDirMarker)); err != nil { + return fmt.Errorf(emsg, err) + } + + files := map[string]string{ + fp.Join(buildDir, constants.StateInstallerCmd+exeutils.Extension): payloadDir, + fp.Join(buildDir, constants.StateCmd+exeutils.Extension): payloadBinDir, + fp.Join(buildDir, constants.StateSvcCmd+exeutils.Extension): payloadBinDir, + fp.Join(buildDir, constants.StateExecutorCmd+exeutils.Extension): payloadBinDir, + } + if err := copyFiles(files); err != nil { + return fmt.Errorf(emsg, err) + } + + return nil +} + +func copyFiles(files map[string]string) error { + for src, target := range files { + log("Copying %s to %s", src, target) + err := fileutils.CopyFile(src, fp.Join(target, fp.Base(src))) + if err != nil { + return fmt.Errorf("copy files (item %s to %s): %w", src, target, err) + } + } + return nil +} From fd3374f5e3b25449906eb5e727fb1aa099cd0a49 Mon Sep 17 00:00:00 2001 From: daved Date: Tue, 25 Jul 2023 16:31:41 -0700 Subject: [PATCH 17/46] Use paygen lib in installer e2e tests --- .../test/integration/installer_int_test.go | 74 +++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 8b02fe092a..57176fcc20 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -21,6 +21,7 @@ import ( "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" "github.com/ActiveState/cli/pkg/sysinfo" + "github.com/ActiveState/cli/scripts/ci/payload-generator/paygen" "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" ) @@ -35,19 +36,17 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { ts := e2e.New(suite.T(), true) defer ts.Close() - suite.setupTest(ts) + payload := suite.newPayload(ts) suite.SetupRCFile(ts) - suite.T().Setenv("ACTIVESTATE_HOME", ts.Dirs.HomeDir) - - target := filepath.Join(ts.Dirs.Work, "installation") + suite.T().Setenv(constants.HomeEnvVarName, ts.Dirs.HomeDir) dir, err := ioutil.TempDir("", "system*") suite.NoError(err) // Run installer with source-path flag (ie. install from this local path) cp := ts.SpawnCmdWithOpts( - suite.installerExe, - e2e.WithArgs(target), + payload.installer, + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(constants.DisableUpdates+"=false"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -66,8 +65,8 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Ensure installing overtop doesn't result in errors cp = ts.SpawnCmdWithOpts( - suite.installerExe, - e2e.WithArgs(target, "--force"), + payload.installer, + e2e.WithArgs(installationDir(ts), "--force"), e2e.AppendEnv(constants.DisableUpdates+"=false"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -75,14 +74,16 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Assert output cp.Expect("successfully installed") - stateExec, err := installation.StateExecFromDir(target) - suite.Contains(stateExec, target, "Ensure we're not grabbing state tool from integration test bin dir") + installDir := installationDir(ts) + + stateExec, err := installation.StateExecFromDir(installDir) + suite.Contains(stateExec, installDir, "Ensure we're not grabbing state tool from integration test bin dir") suite.NoError(err) stateExecResolved, err := fileutils.ResolvePath(stateExec) suite.Require().NoError(err) - serviceExec, err := installation.ServiceExecFromDir(target) + serviceExec, err := installation.ServiceExecFromDir(installDir) suite.NoError(err) // Verify that launched subshell has State tool on PATH @@ -125,14 +126,12 @@ func (suite *InstallerIntegrationTestSuite) TestInstallIncompatible() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.setupTest(ts) - - target := filepath.Join(ts.Dirs.Work, "installation") + payload := suite.newPayload(ts) // Run installer with source-path flag (ie. install from this local path) cp := ts.SpawnCmdWithOpts( - suite.installerExe, - e2e.WithArgs(target), + payload.installer, + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(constants.DisableUpdates+"=false", sysinfo.VersionOverrideEnvVar+"=10.0.0"), ) @@ -146,16 +145,14 @@ func (suite *InstallerIntegrationTestSuite) TestInstallNoErrorTips() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.setupTest(ts) - - target := filepath.Join(ts.Dirs.Work, "installation") + payload := suite.newPayload(ts) dir, err := ioutil.TempDir("", "system*") suite.NoError(err) cp := ts.SpawnCmdWithOpts( - suite.installerExe, - e2e.WithArgs(target, "--activate", "ActiveState/DoesNotExist"), + payload.installer, + e2e.WithArgs(installationDir(ts), "--activate", "ActiveState/DoesNotExist"), e2e.AppendEnv(constants.DisableUpdates+"=true"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -169,16 +166,14 @@ func (suite *InstallerIntegrationTestSuite) TestInstallErrorTips() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.setupTest(ts) - - target := filepath.Join(ts.Dirs.Work, "installation") + payload := suite.newPayload(ts) dir, err := ioutil.TempDir("", "system*") suite.NoError(err) cp := ts.SpawnCmdWithOpts( - suite.installerExe, - e2e.WithArgs(target, "--activate", "ActiveState-CLI/Python3"), + payload.installer, + e2e.WithArgs(installationDir(ts), "--activate", "ActiveState-CLI/Python3"), e2e.AppendEnv(constants.DisableUpdates+"=true"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -196,9 +191,9 @@ func (suite *InstallerIntegrationTestSuite) TestStateTrayRemoval() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.setupTest(ts) + _ = suite.newPayload(ts) - dir := filepath.Join(ts.Dirs.Work, "installation") + dir := installationDir(ts) // Install a release version that still has state-tray. version := "0.35.0-SHAb78e2a4" @@ -332,10 +327,27 @@ func (suite *InstallerIntegrationTestSuite) AssertConfig(ts *e2e.Session) { } } -func (s *InstallerIntegrationTestSuite) setupTest(ts *e2e.Session) { +func installationDir(ts *e2e.Session) string { + return filepath.Join(ts.Dirs.Work, "installation") +} + +type payload struct { + installer string +} + +func (suite *InstallerIntegrationTestSuite) newPayload(ts *e2e.Session) *payload { root := environment.GetRootPathUnsafe() - payloadDir := fileutils.Join(root, "build", "payload") - // Todo: Copy payload files to test dirs (see ts.Dirs) + buildDir := filepath.Join(root, "build") + payDir := filepath.Join(ts.Dirs.Work, "payload") + + suite.Assert().NoError(fileutils.MkdirUnlessExists(payDir)) + + err := paygen.GeneratePayload(buildDir, payDir) + suite.Assert().NoError(err, "generate payload") + + return &payload{ + installer: filepath.Join(payDir, constants.StateInstallerCmd), + } } func TestInstallerIntegrationTestSuite(t *testing.T) { From eeb12f9c3b19628bd8d3b7adb857172083f9048d Mon Sep 17 00:00:00 2001 From: daved Date: Tue, 25 Jul 2023 17:00:30 -0700 Subject: [PATCH 18/46] Ensure paygen preserves perms during file copy --- scripts/ci/payload-generator/paygen/paygen.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/ci/payload-generator/paygen/paygen.go b/scripts/ci/payload-generator/paygen/paygen.go index 2389042719..0aaeaf8041 100644 --- a/scripts/ci/payload-generator/paygen/paygen.go +++ b/scripts/ci/payload-generator/paygen/paygen.go @@ -44,13 +44,26 @@ func GeneratePayload(buildDir, payloadDir string) error { return nil } +// copyFiles will copy the given files while preserving permissions. func copyFiles(files map[string]string) error { + emsg := "copy files (%s to %s): %w" + for src, target := range files { log("Copying %s to %s", src, target) - err := fileutils.CopyFile(src, fp.Join(target, fp.Base(src))) + dest := fp.Join(target, fp.Base(src)) + err := fileutils.CopyFile(src, dest) + if err != nil { + return fmt.Errorf(emsg, src, target, err) + } + srcStat, err := os.Stat(src) if err != nil { - return fmt.Errorf("copy files (item %s to %s): %w", src, target, err) + return fmt.Errorf(emsg, src, target, err) + } + + if err := os.Chmod(dest, srcStat.Mode().Perm()); err != nil { + return fmt.Errorf(emsg, src, target, err) } } + return nil } From e28e4b7d421a47f5f5e9ed5cb091db2b077ab08e Mon Sep 17 00:00:00 2001 From: daved Date: Tue, 25 Jul 2023 20:36:42 -0700 Subject: [PATCH 19/46] Fix installation exec refs in state-installer e2e tests --- .../test/integration/installer_int_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 57176fcc20..064ecbc617 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -28,7 +28,6 @@ import ( type InstallerIntegrationTestSuite struct { tagsuite.Suite - installerExe string } func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { @@ -191,8 +190,7 @@ func (suite *InstallerIntegrationTestSuite) TestStateTrayRemoval() { ts := e2e.New(suite.T(), false) defer ts.Close() - _ = suite.newPayload(ts) - + payload := suite.newPayload(ts) dir := installationDir(ts) // Install a release version that still has state-tray. @@ -228,7 +226,7 @@ func (suite *InstallerIntegrationTestSuite) TestStateTrayRemoval() { // Run the installer, which should remove state-tray and clean up after it. cp = ts.SpawnCmdWithOpts( - suite.installerExe, + payload.installer, e2e.WithArgs("-f", "-n", "-t", dir), e2e.AppendEnv(constants.UpdateBranchEnvVarName+"=release"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), @@ -257,13 +255,15 @@ func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { ts := e2e.New(suite.T(), false) defer ts.Close() + payload := suite.newPayload(ts) + appInstallDir := filepath.Join(ts.Dirs.Work, "app") err := fileutils.Mkdir(appInstallDir) suite.Require().NoError(err) cp := ts.SpawnCmdWithOpts( - suite.installerExe, - e2e.WithArgs(filepath.Join(ts.Dirs.Work, "installation")), + payload.installer, + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), ) cp.Expect("Done") @@ -272,8 +272,8 @@ func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { // State Service.app should be overwritten cleanly without error. cp = ts.SpawnCmdWithOpts( - suite.installerExe, - e2e.WithArgs(filepath.Join(ts.Dirs.Work, "installation2")), + payload.installer, + e2e.WithArgs(installationDir(ts)+"2"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), ) cp.Expect("Done") From fd33579bc9da4f4799da1ce50a0245656a298212 Mon Sep 17 00:00:00 2001 From: daved Date: Tue, 25 Jul 2023 21:32:32 -0700 Subject: [PATCH 20/46] Ensure installer exec has file ext on windows in e2e tests --- cmd/state-installer/test/integration/installer_int_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 064ecbc617..0945f6382a 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -17,6 +17,7 @@ import ( "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/httputil" "github.com/ActiveState/cli/internal/installation" + "github.com/ActiveState/cli/internal/osutils" "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" @@ -346,7 +347,7 @@ func (suite *InstallerIntegrationTestSuite) newPayload(ts *e2e.Session) *payload suite.Assert().NoError(err, "generate payload") return &payload{ - installer: filepath.Join(payDir, constants.StateInstallerCmd), + installer: filepath.Join(payDir, constants.StateInstallerCmd) + osutils.ExeExt, } } From 95e819ec1bf9a274927fe1ec402032e2113d7a3e Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 26 Jul 2023 13:08:41 -0700 Subject: [PATCH 21/46] Allow installing overtop previous state tool as long as the branch matches --- cmd/state-installer/cmd.go | 22 ++++++++++++++++++- cmd/state-installer/installer.go | 15 +++++++++++-- .../test/integration/installer_int_test.go | 18 ++++++++++++--- internal/installation/paths.go | 5 +++++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/cmd/state-installer/cmd.go b/cmd/state-installer/cmd.go index 58d3b7556e..a69b8b564b 100644 --- a/cmd/state-installer/cmd.go +++ b/cmd/state-installer/cmd.go @@ -236,6 +236,26 @@ func execute(out output.Outputer, cfg *config.Instance, an analytics.Dispatcher, params.path = installPath } + // Detect if target dir is existing install of same target branch + var installedBranch string + marker := filepath.Join(installPath, installation.InstallDirMarker) + if stateToolInstalled && fileutils.TargetExists(marker) { + markerContents, err := fileutils.ReadFile(marker) + if err != nil { + return errs.Wrap(err, "Could not read marker file") + } + // The marker file is empty for versions prior to v0.40.0-RC3 + if len(markerContents) > 0 { + var markerMeta installation.InstallMarkerMeta + if err := json.Unmarshal(markerContents, &markerMeta); err != nil { + return errs.Wrap(err, "Could not parse install marker file") + } + installedBranch = markerMeta.Branch + } + } + // Older state tools did not bake in meta information, in this case we allow overwriting regardless of branch + targetingSameBranch := installedBranch == "" || installedBranch == constants.BranchName + // If this is a fresh installation we ensure that the target directory is empty if !stateToolInstalled && fileutils.DirExists(params.path) && !params.force { empty, err := fileutils.IsEmptyDir(params.path) @@ -257,7 +277,7 @@ func execute(out output.Outputer, cfg *config.Instance, an analytics.Dispatcher, an.Event(AnalyticsFunnelCat, route) // Check if state tool already installed - if !params.isUpdate && !params.force && stateToolInstalled { + if !params.isUpdate && !params.force && stateToolInstalled && !targetingSameBranch { logging.Debug("Cancelling out because State Tool is already installed") out.Print(fmt.Sprintf("State Tool Package Manager is already installed at [NOTICE]%s[/RESET]. To reinstall use the [ACTIONABLE]--force[/RESET] flag.", installPath)) an.Event(AnalyticsFunnelCat, "already-installed") diff --git a/cmd/state-installer/installer.go b/cmd/state-installer/installer.go index d8ea8633a7..7f13e41be9 100644 --- a/cmd/state-installer/installer.go +++ b/cmd/state-installer/installer.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "errors" "io/ioutil" "os" @@ -49,7 +50,17 @@ func NewInstaller(cfg *config.Instance, out output.Outputer, payloadPath string, } func (i *Installer) Install() (rerr error) { - if err := fileutils.Touch(filepath.Join(i.path, installation.InstallDirMarker)); err != nil { + // Create install marker + installMarkerContents := installation.InstallMarkerMeta{ + Branch: constants.BranchName, + Version: constants.Version, + } + b, err := json.Marshal(installMarkerContents) + if err != nil { + return errs.Wrap(err, "Could not marshal install marker contents") + } + + if err := fileutils.WriteFile(filepath.Join(i.path, installation.InstallDirMarker), b); err != nil { return errs.Wrap(err, "Could not place install dir marker") } @@ -73,7 +84,7 @@ func (i *Installer) Install() (rerr error) { } // Detect if existing installation needs to be cleaned - err := detectCorruptedInstallDir(i.path) + err = detectCorruptedInstallDir(i.path) if errors.Is(err, errCorruptedInstall) { err = i.sanitizeInstallPath() if err != nil { diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 1bdbc3deae..63d58bf071 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -33,7 +33,7 @@ type InstallerIntegrationTestSuite struct { func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { suite.OnlyRunForTags(tagsuite.Installer, tagsuite.Critical) - ts := e2e.New(suite.T(), false) + ts := e2e.New(suite.T(), true) defer ts.Close() suite.setupTest(ts) @@ -68,12 +68,24 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Ensure installing overtop doesn't result in errors cp = ts.SpawnCmdWithOpts( suite.installerExe, - e2e.WithArgs(target, "--force"), + e2e.WithArgs(target), e2e.AppendEnv(constants.DisableUpdates+"=false"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) + cp.Expect("successfully installed") + cp.WaitForInput() + cp.SendLine("exit") + cp.ExpectExitCode(0) - // Assert output + // Again ensure installing overtop doesn't result in errors, but mock an older state tool format where + // the marker has no contents + suite.Require().NoError(fileutils.WriteFile(filepath.Join(target, installation.InstallDirMarker), []byte{})) + cp = ts.SpawnCmdWithOpts( + suite.installerExe, + e2e.WithArgs(target), + e2e.AppendEnv(constants.DisableUpdates+"=false"), + e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), + ) cp.Expect("successfully installed") stateExec, err := installation.StateExecFromDir(target) diff --git a/internal/installation/paths.go b/internal/installation/paths.go index 514877504d..4a7c61a630 100644 --- a/internal/installation/paths.go +++ b/internal/installation/paths.go @@ -21,6 +21,11 @@ const ( InstallDirMarker = ".state_install_root" ) +type InstallMarkerMeta struct { + Branch string `json:"branch"` + Version string `json:"version"` +} + func DefaultInstallPath() (string, error) { return InstallPathForBranch(constants.BranchName) } From 5311f916e5afe3ccab58becb9d2df0383992e75b Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 26 Jul 2023 13:30:53 -0700 Subject: [PATCH 22/46] Update test --- test/integration/install_scripts_int_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/install_scripts_int_test.go b/test/integration/install_scripts_int_test.go index aa55e19993..eda10afa73 100644 --- a/test/integration/install_scripts_int_test.go +++ b/test/integration/install_scripts_int_test.go @@ -133,7 +133,7 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall() { suite.assertCorrectVersion(ts, installDir, tt.Version, tt.Channel) suite.DirExists(ts.Dirs.Config) - // Verify that we don't try to install it again + // Verify that can install overtop if runtime.GOOS != "windows" { cp = ts.SpawnCmdWithOpts("bash", e2e.WithArgs(argsPlain...)) } else { @@ -141,7 +141,7 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall() { e2e.AppendEnv("SHELL="), ) } - cp.Expect("already installed") + cp.Expect("successfully installed") cp.ExpectExitCode(0) }) } From 3661c529e33e0b9da387105be99488de18bac4ba Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 26 Jul 2023 13:55:50 -0700 Subject: [PATCH 23/46] Terminate shell when done asserting --- test/integration/install_scripts_int_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/install_scripts_int_test.go b/test/integration/install_scripts_int_test.go index eda10afa73..11ad87142d 100644 --- a/test/integration/install_scripts_int_test.go +++ b/test/integration/install_scripts_int_test.go @@ -142,6 +142,8 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall() { ) } cp.Expect("successfully installed") + cp.WaitForInput() + cp.SendLine("exit") cp.ExpectExitCode(0) }) } From 8893e737914cb61e805a7c462be6ba7109d6206b Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 26 Jul 2023 14:48:48 -0700 Subject: [PATCH 24/46] Increase logging to help us diagnose API token issues --- cmd/state/main.go | 2 +- pkg/platform/authentication/auth.go | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/state/main.go b/cmd/state/main.go index e0fc4e650a..c90ac8ad50 100644 --- a/cmd/state/main.go +++ b/cmd/state/main.go @@ -198,7 +198,7 @@ func run(args []string, isInteractive bool, cfg *config.Instance, out output.Out defer events.Close("auth", auth.Close) if err := auth.Sync(); err != nil { - logging.Warning("Could not sync authenticated state: %s", err.Error()) + logging.Warning("Could not sync authenticated state: %s", errs.JoinMessage(err)) } an := anAsync.New(svcmodel, cfg, auth, out, pjNamespace) diff --git a/pkg/platform/authentication/auth.go b/pkg/platform/authentication/auth.go index cb9dd1b6c3..2836cdda07 100644 --- a/pkg/platform/authentication/auth.go +++ b/pkg/platform/authentication/auth.go @@ -116,8 +116,8 @@ func (s *Auth) SyncRequired() bool { func (s *Auth) Sync() error { defer profile.Measure("auth:Sync", time.Now()) - if s.AvailableAPIToken() != "" { - logging.Debug("Authenticating with stored API token") + if token := s.AvailableAPIToken(); token != "" { + logging.Debug("Authenticating with stored API token: %s..", token[0:2]) if err := s.Authenticate(); err != nil { return errs.Wrap(err, "Failed to authenticate with API token") } @@ -387,6 +387,7 @@ func (s *Auth) CreateToken() error { for _, token := range tokensOK.Payload { if token.Name == constants.APITokenName { + logging.Debug("Deleting stale token") params := authentication.NewDeleteTokenParams() params.SetTokenID(token.TokenID) _, err := client.Authentication.DeleteToken(params, s.ClientAuth()) @@ -413,6 +414,7 @@ func (s *Auth) CreateToken() error { // SaveToken will save an API token func (s *Auth) SaveToken(token string) error { + logging.Debug("Saving token: %s..", token[0:2]) err := s.cfg.Set(ApiTokenConfigKey, token) if err != nil { return locale.WrapError(err, "err_set_token", "Could not set token in config") @@ -436,6 +438,8 @@ func (s *Auth) NewAPIKey(name string) (string, error) { return "", locale.WrapError(err, "err_token_create", "", err.Error()) } + logging.Debug("Created token: %s..", tokenOK.Payload.Token[0:2]) + return tokenOK.Payload.Token, nil } From eacc3ce2765f66e459407edd25a21340ec4344b6 Mon Sep 17 00:00:00 2001 From: daved Date: Wed, 26 Jul 2023 15:03:24 -0700 Subject: [PATCH 25/46] Use script-generated assets in installer e2e tests --- .../test/integration/installer_int_test.go | 57 ++++++------- scripts/ci/payload-generator/main.go | 79 ++++++++++++++++--- scripts/ci/payload-generator/paygen/paygen.go | 69 ---------------- 3 files changed, 96 insertions(+), 109 deletions(-) delete mode 100644 scripts/ci/payload-generator/paygen/paygen.go diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 0945f6382a..2d9e9bff82 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -22,7 +22,6 @@ import ( "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" "github.com/ActiveState/cli/pkg/sysinfo" - "github.com/ActiveState/cli/scripts/ci/payload-generator/paygen" "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" ) @@ -36,7 +35,8 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { ts := e2e.New(suite.T(), true) defer ts.Close() - payload := suite.newPayload(ts) + payload := newPayloadAssets(suite) + suite.SetupRCFile(ts) suite.T().Setenv(constants.HomeEnvVarName, ts.Dirs.HomeDir) @@ -46,7 +46,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Run installer with source-path flag (ie. install from this local path) cp := ts.SpawnCmdWithOpts( payload.installer, - e2e.WithArgs(installationDir(ts)), + e2e.WithArgs(payload.dir), e2e.AppendEnv(constants.DisableUpdates+"=false"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -66,7 +66,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Ensure installing overtop doesn't result in errors cp = ts.SpawnCmdWithOpts( payload.installer, - e2e.WithArgs(installationDir(ts), "--force"), + e2e.WithArgs(payload.dir, "--force"), e2e.AppendEnv(constants.DisableUpdates+"=false"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -74,7 +74,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Assert output cp.Expect("successfully installed") - installDir := installationDir(ts) + installDir := payload.dir stateExec, err := installation.StateExecFromDir(installDir) suite.Contains(stateExec, installDir, "Ensure we're not grabbing state tool from integration test bin dir") @@ -126,12 +126,12 @@ func (suite *InstallerIntegrationTestSuite) TestInstallIncompatible() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := suite.newPayload(ts) + payload := newPayloadAssets(suite) // Run installer with source-path flag (ie. install from this local path) cp := ts.SpawnCmdWithOpts( payload.installer, - e2e.WithArgs(installationDir(ts)), + e2e.WithArgs(payload.dir), e2e.AppendEnv(constants.DisableUpdates+"=false", sysinfo.VersionOverrideEnvVar+"=10.0.0"), ) @@ -145,14 +145,14 @@ func (suite *InstallerIntegrationTestSuite) TestInstallNoErrorTips() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := suite.newPayload(ts) + payload := newPayloadAssets(suite) dir, err := ioutil.TempDir("", "system*") suite.NoError(err) cp := ts.SpawnCmdWithOpts( payload.installer, - e2e.WithArgs(installationDir(ts), "--activate", "ActiveState/DoesNotExist"), + e2e.WithArgs(payload.dir, "--activate", "ActiveState/DoesNotExist"), e2e.AppendEnv(constants.DisableUpdates+"=true"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -166,14 +166,14 @@ func (suite *InstallerIntegrationTestSuite) TestInstallErrorTips() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := suite.newPayload(ts) + payload := newPayloadAssets(suite) dir, err := ioutil.TempDir("", "system*") suite.NoError(err) cp := ts.SpawnCmdWithOpts( payload.installer, - e2e.WithArgs(installationDir(ts), "--activate", "ActiveState-CLI/Python3"), + e2e.WithArgs(payload.dir, "--activate", "ActiveState-CLI/Python3"), e2e.AppendEnv(constants.DisableUpdates+"=true"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -191,8 +191,8 @@ func (suite *InstallerIntegrationTestSuite) TestStateTrayRemoval() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := suite.newPayload(ts) - dir := installationDir(ts) + payload := newPayloadAssets(suite) + dir := payload.dir // Install a release version that still has state-tray. version := "0.35.0-SHAb78e2a4" @@ -256,7 +256,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := suite.newPayload(ts) + payload := newPayloadAssets(suite) appInstallDir := filepath.Join(ts.Dirs.Work, "app") err := fileutils.Mkdir(appInstallDir) @@ -264,7 +264,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { cp := ts.SpawnCmdWithOpts( payload.installer, - e2e.WithArgs(installationDir(ts)), + e2e.WithArgs(payload.dir), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), ) cp.Expect("Done") @@ -274,7 +274,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { // State Service.app should be overwritten cleanly without error. cp = ts.SpawnCmdWithOpts( payload.installer, - e2e.WithArgs(installationDir(ts)+"2"), + e2e.WithArgs(payload.dir+"2"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), ) cp.Expect("Done") @@ -328,26 +328,21 @@ func (suite *InstallerIntegrationTestSuite) AssertConfig(ts *e2e.Session) { } } -func installationDir(ts *e2e.Session) string { - return filepath.Join(ts.Dirs.Work, "installation") -} - -type payload struct { +type payloadAssets struct { + dir string installer string } -func (suite *InstallerIntegrationTestSuite) newPayload(ts *e2e.Session) *payload { - root := environment.GetRootPathUnsafe() - buildDir := filepath.Join(root, "build") - payDir := filepath.Join(ts.Dirs.Work, "payload") - - suite.Assert().NoError(fileutils.MkdirUnlessExists(payDir)) +func newPayloadAssets(suite *InstallerIntegrationTestSuite) *payloadAssets { + dir := filepath.Join(environment.GetRootPathUnsafe(), "build", "payload") + suite.Assert().DirExists(dir, "locally generated payload exists") - err := paygen.GeneratePayload(buildDir, payDir) - suite.Assert().NoError(err, "generate payload") + installer := filepath.Join(dir, constants.StateInstallerCmd+osutils.ExeExt) + suite.Assert().FileExists(installer, "locally generated installer exists") - return &payload{ - installer: filepath.Join(payDir, constants.StateInstallerCmd) + osutils.ExeExt, + return &payloadAssets{ + dir: dir, + installer: installer, } } diff --git a/scripts/ci/payload-generator/main.go b/scripts/ci/payload-generator/main.go index 762b086cbd..60989c259c 100644 --- a/scripts/ci/payload-generator/main.go +++ b/scripts/ci/payload-generator/main.go @@ -3,16 +3,25 @@ package main import ( "fmt" "os" - "path/filepath" + fp "path/filepath" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" - "github.com/ActiveState/cli/scripts/ci/payload-generator/paygen" + "github.com/ActiveState/cli/internal/exeutils" + "github.com/ActiveState/cli/internal/fileutils" + "github.com/ActiveState/cli/internal/installation" ) -var logErr = func(msg string, vals ...any) { - fmt.Fprintf(os.Stderr, msg, vals...) - fmt.Fprintf(os.Stderr, "\n") -} +var ( + log = func(msg string, vals ...any) { + fmt.Fprintf(os.Stdout, msg, vals...) + fmt.Fprintf(os.Stdout, "\n") + } + logErr = func(msg string, vals ...any) { + fmt.Fprintf(os.Stderr, msg, vals...) + fmt.Fprintf(os.Stderr, "\n") + } +) // The payload-generator is an intentionally very dumb runner that just copies some files around. // This could just be a bash script if not for the fact that bash can't be linked with our type system. @@ -25,8 +34,60 @@ func main() { func run() error { root := environment.GetRootPathUnsafe() - buildDir := filepath.Join(root, "build") - payloadDir := filepath.Join(buildDir, "payload") + buildDir := fp.Join(root, "build") + payloadDir := fp.Join(buildDir, "payload") + + return generatePayload(buildDir, payloadDir) +} + +func generatePayload(buildDir, payloadDir string) error { + emsg := "generate payload: %w" + + payloadBinDir := fp.Join(payloadDir, "bin") + + if err := fileutils.MkdirUnlessExists(payloadBinDir); err != nil { + return fmt.Errorf(emsg, err) + } + + installDirMarker := installation.InstallDirMarker + log("Creating install dir marker in %s", payloadDir) + if err := fileutils.Touch(fp.Join(payloadDir, installDirMarker)); err != nil { + return fmt.Errorf(emsg, err) + } + + files := map[string]string{ + fp.Join(buildDir, constants.StateInstallerCmd+exeutils.Extension): payloadDir, + fp.Join(buildDir, constants.StateCmd+exeutils.Extension): payloadBinDir, + fp.Join(buildDir, constants.StateSvcCmd+exeutils.Extension): payloadBinDir, + fp.Join(buildDir, constants.StateExecutorCmd+exeutils.Extension): payloadBinDir, + } + if err := copyFiles(files); err != nil { + return fmt.Errorf(emsg, err) + } + + return nil +} + +// copyFiles will copy the given files while preserving permissions. +func copyFiles(files map[string]string) error { + emsg := "copy files (%s to %s): %w" + + for src, target := range files { + log("Copying %s to %s", src, target) + dest := fp.Join(target, fp.Base(src)) + err := fileutils.CopyFile(src, dest) + if err != nil { + return fmt.Errorf(emsg, src, target, err) + } + srcStat, err := os.Stat(src) + if err != nil { + return fmt.Errorf(emsg, src, target, err) + } + + if err := os.Chmod(dest, srcStat.Mode().Perm()); err != nil { + return fmt.Errorf(emsg, src, target, err) + } + } - return paygen.GeneratePayload(buildDir, payloadDir) + return nil } diff --git a/scripts/ci/payload-generator/paygen/paygen.go b/scripts/ci/payload-generator/paygen/paygen.go deleted file mode 100644 index 0aaeaf8041..0000000000 --- a/scripts/ci/payload-generator/paygen/paygen.go +++ /dev/null @@ -1,69 +0,0 @@ -package paygen - -import ( - "fmt" - "os" - fp "path/filepath" - - "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/exeutils" - "github.com/ActiveState/cli/internal/fileutils" - "github.com/ActiveState/cli/internal/installation" -) - -var log = func(msg string, vals ...any) { - fmt.Fprintf(os.Stdout, msg, vals...) - fmt.Fprintf(os.Stdout, "\n") -} - -func GeneratePayload(buildDir, payloadDir string) error { - emsg := "generate payload: %w" - - payloadBinDir := fp.Join(payloadDir, "bin") - - if err := fileutils.MkdirUnlessExists(payloadBinDir); err != nil { - return fmt.Errorf(emsg, err) - } - - installDirMarker := installation.InstallDirMarker - log("Creating install dir marker in %s", payloadDir) - if err := fileutils.Touch(fp.Join(payloadDir, installDirMarker)); err != nil { - return fmt.Errorf(emsg, err) - } - - files := map[string]string{ - fp.Join(buildDir, constants.StateInstallerCmd+exeutils.Extension): payloadDir, - fp.Join(buildDir, constants.StateCmd+exeutils.Extension): payloadBinDir, - fp.Join(buildDir, constants.StateSvcCmd+exeutils.Extension): payloadBinDir, - fp.Join(buildDir, constants.StateExecutorCmd+exeutils.Extension): payloadBinDir, - } - if err := copyFiles(files); err != nil { - return fmt.Errorf(emsg, err) - } - - return nil -} - -// copyFiles will copy the given files while preserving permissions. -func copyFiles(files map[string]string) error { - emsg := "copy files (%s to %s): %w" - - for src, target := range files { - log("Copying %s to %s", src, target) - dest := fp.Join(target, fp.Base(src)) - err := fileutils.CopyFile(src, dest) - if err != nil { - return fmt.Errorf(emsg, src, target, err) - } - srcStat, err := os.Stat(src) - if err != nil { - return fmt.Errorf(emsg, src, target, err) - } - - if err := os.Chmod(dest, srcStat.Mode().Perm()); err != nil { - return fmt.Errorf(emsg, src, target, err) - } - } - - return nil -} From 25e6e7e3b3a59b5dbc7a14dc17fe3c091b9d3535 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 26 Jul 2023 15:09:30 -0700 Subject: [PATCH 26/46] Centralize logic to desensitize token --- pkg/platform/authentication/auth.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/platform/authentication/auth.go b/pkg/platform/authentication/auth.go index 2836cdda07..09ad700552 100644 --- a/pkg/platform/authentication/auth.go +++ b/pkg/platform/authentication/auth.go @@ -117,7 +117,7 @@ func (s *Auth) Sync() error { defer profile.Measure("auth:Sync", time.Now()) if token := s.AvailableAPIToken(); token != "" { - logging.Debug("Authenticating with stored API token: %s..", token[0:2]) + logging.Debug("Authenticating with stored API token: %s..", desensitizeToken(token)) if err := s.Authenticate(); err != nil { return errs.Wrap(err, "Failed to authenticate with API token") } @@ -414,7 +414,7 @@ func (s *Auth) CreateToken() error { // SaveToken will save an API token func (s *Auth) SaveToken(token string) error { - logging.Debug("Saving token: %s..", token[0:2]) + logging.Debug("Saving token: %s..", desensitizeToken(token)) err := s.cfg.Set(ApiTokenConfigKey, token) if err != nil { return locale.WrapError(err, "err_set_token", "Could not set token in config") @@ -438,7 +438,7 @@ func (s *Auth) NewAPIKey(name string) (string, error) { return "", locale.WrapError(err, "err_token_create", "", err.Error()) } - logging.Debug("Created token: %s..", tokenOK.Payload.Token[0:2]) + logging.Debug("Created token: %s..", desensitizeToken(tokenOK.Payload.Token)) return tokenOK.Payload.Token, nil } @@ -450,3 +450,10 @@ func (s *Auth) AvailableAPIToken() (v string) { } return s.cfg.GetString(ApiTokenConfigKey) } + +func desensitizeToken(v string) string { + if len(v) <= 2 { + return "invalid token value" + } + return v[0:2] +} From b55782e5eba5f34c6c87a60ca26dd2ea7bdbf524 Mon Sep 17 00:00:00 2001 From: daved Date: Wed, 26 Jul 2023 15:44:10 -0700 Subject: [PATCH 27/46] Reduce noise in installer e2e tests --- .../test/integration/installer_int_test.go | 72 ++++++++----------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 3c72df165f..7c9155d415 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -28,6 +28,7 @@ import ( type InstallerIntegrationTestSuite struct { tagsuite.Suite + installerExe string } func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { @@ -35,8 +36,6 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { ts := e2e.New(suite.T(), true) defer ts.Close() - payload := newPayloadAssets(suite) - suite.SetupRCFile(ts) suite.T().Setenv(constants.HomeEnvVarName, ts.Dirs.HomeDir) @@ -45,8 +44,8 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Run installer with source-path flag (ie. install from this local path) cp := ts.SpawnCmdWithOpts( - payload.installer, - e2e.WithArgs(payload.dir), + suite.installerExe, + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(constants.DisableUpdates+"=false"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -65,8 +64,8 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Ensure installing overtop doesn't result in errors cp = ts.SpawnCmdWithOpts( - payload.installer, - e2e.WithArgs(payload.dir), + suite.installerExe, + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(constants.DisableUpdates+"=false"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -77,16 +76,16 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Again ensure installing overtop doesn't result in errors, but mock an older state tool format where // the marker has no contents - suite.Require().NoError(fileutils.WriteFile(filepath.Join(target, installation.InstallDirMarker), []byte{})) + suite.Require().NoError(fileutils.WriteFile(filepath.Join(installationDir(ts), installation.InstallDirMarker), []byte{})) cp = ts.SpawnCmdWithOpts( suite.installerExe, - e2e.WithArgs(target), + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(constants.DisableUpdates+"=false"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) cp.Expect("successfully installed") - installDir := payload.dir + installDir := installationDir(ts) stateExec, err := installation.StateExecFromDir(installDir) suite.Contains(stateExec, installDir, "Ensure we're not grabbing state tool from integration test bin dir") @@ -138,12 +137,10 @@ func (suite *InstallerIntegrationTestSuite) TestInstallIncompatible() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := newPayloadAssets(suite) - // Run installer with source-path flag (ie. install from this local path) cp := ts.SpawnCmdWithOpts( - payload.installer, - e2e.WithArgs(payload.dir), + suite.installerExe, + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(constants.DisableUpdates+"=false", sysinfo.VersionOverrideEnvVar+"=10.0.0"), ) @@ -157,14 +154,12 @@ func (suite *InstallerIntegrationTestSuite) TestInstallNoErrorTips() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := newPayloadAssets(suite) - dir, err := ioutil.TempDir("", "system*") suite.NoError(err) cp := ts.SpawnCmdWithOpts( - payload.installer, - e2e.WithArgs(payload.dir, "--activate", "ActiveState/DoesNotExist"), + suite.installerExe, + e2e.WithArgs(installationDir(ts), "--activate", "ActiveState/DoesNotExist"), e2e.AppendEnv(constants.DisableUpdates+"=true"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -178,14 +173,12 @@ func (suite *InstallerIntegrationTestSuite) TestInstallErrorTips() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := newPayloadAssets(suite) - dir, err := ioutil.TempDir("", "system*") suite.NoError(err) cp := ts.SpawnCmdWithOpts( - payload.installer, - e2e.WithArgs(payload.dir, "--activate", "ActiveState-CLI/Python3"), + suite.installerExe, + e2e.WithArgs(installationDir(ts), "--activate", "ActiveState-CLI/Python3"), e2e.AppendEnv(constants.DisableUpdates+"=true"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -203,8 +196,7 @@ func (suite *InstallerIntegrationTestSuite) TestStateTrayRemoval() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := newPayloadAssets(suite) - dir := payload.dir + dir := installationDir(ts) // Install a release version that still has state-tray. version := "0.35.0-SHAb78e2a4" @@ -239,7 +231,7 @@ func (suite *InstallerIntegrationTestSuite) TestStateTrayRemoval() { // Run the installer, which should remove state-tray and clean up after it. cp = ts.SpawnCmdWithOpts( - payload.installer, + suite.installerExe, e2e.WithArgs("-f", "-n", "-t", dir), e2e.AppendEnv(constants.UpdateBranchEnvVarName+"=release"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), @@ -268,15 +260,13 @@ func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { ts := e2e.New(suite.T(), false) defer ts.Close() - payload := newPayloadAssets(suite) - appInstallDir := filepath.Join(ts.Dirs.Work, "app") err := fileutils.Mkdir(appInstallDir) suite.Require().NoError(err) cp := ts.SpawnCmdWithOpts( - payload.installer, - e2e.WithArgs(payload.dir), + suite.installerExe, + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), ) cp.Expect("Done") @@ -285,8 +275,8 @@ func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { // State Service.app should be overwritten cleanly without error. cp = ts.SpawnCmdWithOpts( - payload.installer, - e2e.WithArgs(payload.dir+"2"), + suite.installerExe, + e2e.WithArgs(installationDir(ts)+"2"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), ) cp.Expect("Done") @@ -340,24 +330,20 @@ func (suite *InstallerIntegrationTestSuite) AssertConfig(ts *e2e.Session) { } } -type payloadAssets struct { - dir string - installer string +func installationDir(ts *e2e.Session) string { + return filepath.Join(ts.Dirs.Work, "installation") } -func newPayloadAssets(suite *InstallerIntegrationTestSuite) *payloadAssets { +func TestInstallerIntegrationTestSuite(t *testing.T) { + s := new(InstallerIntegrationTestSuite) + dir := filepath.Join(environment.GetRootPathUnsafe(), "build", "payload") - suite.Assert().DirExists(dir, "locally generated payload exists") + s.Assert().DirExists(dir, "locally generated payload exists") installer := filepath.Join(dir, constants.StateInstallerCmd+osutils.ExeExt) - suite.Assert().FileExists(installer, "locally generated installer exists") + s.Assert().FileExists(installer, "locally generated installer exists") - return &payloadAssets{ - dir: dir, - installer: installer, - } -} + s.installerExe = installer -func TestInstallerIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(InstallerIntegrationTestSuite)) + suite.Run(t, s) } From 990e1e3178d2f35be3769c922dca0be37556da44 Mon Sep 17 00:00:00 2001 From: Daved Date: Wed, 26 Jul 2023 15:55:25 -0700 Subject: [PATCH 28/46] Don't store e2e session dirs in installer e2e test Co-authored-by: Nathan Rijksen --- cmd/state-installer/test/integration/installer_int_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 7c9155d415..64adb0911f 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -33,7 +33,7 @@ type InstallerIntegrationTestSuite struct { func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { suite.OnlyRunForTags(tagsuite.Installer, tagsuite.Critical) - ts := e2e.New(suite.T(), true) + ts := e2e.New(suite.T(), false) defer ts.Close() suite.SetupRCFile(ts) From c6a5565c1eb184571bca3833bc8ccac7aac81eb8 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 26 Jul 2023 15:59:54 -0700 Subject: [PATCH 29/46] Fix bad merge --- cmd/state-installer/installer.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/state-installer/installer.go b/cmd/state-installer/installer.go index 36bcf0ceff..eccf65709b 100644 --- a/cmd/state-installer/installer.go +++ b/cmd/state-installer/installer.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "errors" "io/ioutil" "os" @@ -70,7 +69,7 @@ func (i *Installer) Install() (rerr error) { } // Detect if existing installation needs to be cleaned - err = detectCorruptedInstallDir(i.path) + err := detectCorruptedInstallDir(i.path) if errors.Is(err, errCorruptedInstall) { err = i.sanitizeInstallPath() if err != nil { From 824d8a2e2e5906f66a86195fe4c95883fabb1b25 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 26 Jul 2023 16:56:55 -0700 Subject: [PATCH 30/46] Fix bad merge --- scripts/ci/update-generator/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/update-generator/main.go b/scripts/ci/update-generator/main.go index 73cd1fae22..57b60b2ac6 100644 --- a/scripts/ci/update-generator/main.go +++ b/scripts/ci/update-generator/main.go @@ -111,7 +111,7 @@ func createUpdate(outputPath, channel, version, platform, target string) error { } up := updater.NewAvailableUpdate(version, channel, platform, filepath.ToSlash(relArchivePath), generateSha256(archivePath), "") - b, err := json.MarshalIndent(up, "", " ") + b, err = json.MarshalIndent(up, "", " ") if err != nil { return errs.Wrap(err, "Failed to marshal AvailableUpdate information.") } From 857d1f585a221cebb16f0bd4ede0cbf4dfbff165 Mon Sep 17 00:00:00 2001 From: daved Date: Thu, 27 Jul 2023 10:18:58 -0700 Subject: [PATCH 31/46] Move common installer e2e test assertions to setup func --- .../test/integration/installer_int_test.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 64adb0911f..b4933f615d 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -28,6 +28,7 @@ import ( type InstallerIntegrationTestSuite struct { tagsuite.Suite + localPayload string installerExe string } @@ -334,16 +335,18 @@ func installationDir(ts *e2e.Session) string { return filepath.Join(ts.Dirs.Work, "installation") } -func TestInstallerIntegrationTestSuite(t *testing.T) { - s := new(InstallerIntegrationTestSuite) +func (suite *InstallerIntegrationTestSuite) SetupSuite() { + suite.Assert().DirExists(suite.localPayload, "locally generated payload exists") + suite.Assert().FileExists(suite.installerExe, "locally generated installer exists") +} +func TestInstallerIntegrationTestSuite(t *testing.T) { dir := filepath.Join(environment.GetRootPathUnsafe(), "build", "payload") - s.Assert().DirExists(dir, "locally generated payload exists") - installer := filepath.Join(dir, constants.StateInstallerCmd+osutils.ExeExt) - s.Assert().FileExists(installer, "locally generated installer exists") - - s.installerExe = installer + s := &InstallerIntegrationTestSuite{ + localPayload: dir, + installerExe: filepath.Join(dir, constants.StateInstallerCmd+osutils.ExeExt), + } suite.Run(t, s) } From e5e1aff5ef49c8a602f9303d9f3452833b748489 Mon Sep 17 00:00:00 2001 From: daved Date: Thu, 27 Jul 2023 11:04:29 -0700 Subject: [PATCH 32/46] Fix typo in hello cmd e2e test --- test/integration/hello_int_example_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/hello_int_example_test.go b/test/integration/hello_int_example_test.go index f58b70b284..b69435c625 100644 --- a/test/integration/hello_int_example_test.go +++ b/test/integration/hello_int_example_test.go @@ -31,7 +31,7 @@ func (suite *HelloIntegrationTestSuite) TestHello() { cp.ExpectExitCode(0) cp = ts.Spawn("_hello", "") - cp.Expect("Cannot say Hello") + cp.Expect("Cannot say hello") cp.Expect("No name provided") cp.ExpectNotExitCode(0) From a53bf4f14736ac4fa6d871f5910fc669d5830687 Mon Sep 17 00:00:00 2001 From: daved Date: Thu, 27 Jul 2023 12:26:10 -0700 Subject: [PATCH 33/46] Move installer e2e suite construction to setupsuite func --- .../test/integration/installer_int_test.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index b4933f615d..9aebf9dc8f 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -28,7 +28,6 @@ import ( type InstallerIntegrationTestSuite struct { tagsuite.Suite - localPayload string installerExe string } @@ -336,17 +335,15 @@ func installationDir(ts *e2e.Session) string { } func (suite *InstallerIntegrationTestSuite) SetupSuite() { - suite.Assert().DirExists(suite.localPayload, "locally generated payload exists") + localPayload := filepath.Join(environment.GetRootPathUnsafe(), "build", "payload") + suite.Assert().DirExists(localPayload, "locally generated payload exists") + + installerExe := filepath.Join(localPayload, constants.StateInstallerCmd+osutils.ExeExt) suite.Assert().FileExists(suite.installerExe, "locally generated installer exists") + + suite.installerExe = installerExe } func TestInstallerIntegrationTestSuite(t *testing.T) { - dir := filepath.Join(environment.GetRootPathUnsafe(), "build", "payload") - - s := &InstallerIntegrationTestSuite{ - localPayload: dir, - installerExe: filepath.Join(dir, constants.StateInstallerCmd+osutils.ExeExt), - } - - suite.Run(t, s) + suite.Run(t, new(InstallerIntegrationTestSuite)) } From 39ccebc4945e6677d51836e860cc6f09c33c32c7 Mon Sep 17 00:00:00 2001 From: ActiveState CLI Automation Date: Thu, 27 Jul 2023 13:04:36 -0700 Subject: [PATCH 34/46] Update version.txt --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index f39757351a..7cb078fe8d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.40.0-RC3 \ No newline at end of file +0.40.0-RC4 \ No newline at end of file From f23cca448692f3fad5a81a4dcda8460317b96b23 Mon Sep 17 00:00:00 2001 From: daved Date: Thu, 27 Jul 2023 13:49:24 -0700 Subject: [PATCH 35/46] Move misplaced install marker setup from update-gen to payload-gen (bad merge) --- scripts/ci/payload-generator/main.go | 40 +++++++++++++++++++++++++--- scripts/ci/update-generator/main.go | 23 +++------------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/scripts/ci/payload-generator/main.go b/scripts/ci/payload-generator/main.go index 60989c259c..1b014f5b97 100644 --- a/scripts/ci/payload-generator/main.go +++ b/scripts/ci/payload-generator/main.go @@ -1,6 +1,8 @@ package main import ( + "encoding/json" + "flag" "fmt" "os" fp "path/filepath" @@ -33,14 +35,23 @@ func main() { } func run() error { + var ( + branch = constants.BranchName + version = constants.Version + ) + + flag.StringVar(&branch, "b", branch, "Override target branch. (Branch to receive update.)") + flag.StringVar(&version, "v", version, "Override version number for this update.") + flag.Parse() + root := environment.GetRootPathUnsafe() buildDir := fp.Join(root, "build") payloadDir := fp.Join(buildDir, "payload") - return generatePayload(buildDir, payloadDir) + return generatePayload(buildDir, payloadDir, branch, version) } -func generatePayload(buildDir, payloadDir string) error { +func generatePayload(buildDir, payloadDir, branch, version string) error { emsg := "generate payload: %w" payloadBinDir := fp.Join(payloadDir, "bin") @@ -49,9 +60,8 @@ func generatePayload(buildDir, payloadDir string) error { return fmt.Errorf(emsg, err) } - installDirMarker := installation.InstallDirMarker log("Creating install dir marker in %s", payloadDir) - if err := fileutils.Touch(fp.Join(payloadDir, installDirMarker)); err != nil { + if err := createInstallMarker(payloadDir, branch, version); err != nil { return fmt.Errorf(emsg, err) } @@ -68,6 +78,28 @@ func generatePayload(buildDir, payloadDir string) error { return nil } +func createInstallMarker(payloadDir, branch, version string) error { + emsg := "create install marker: %w" + + markerPath := fp.Join(payloadDir, installation.InstallDirMarker) + f, err := os.Create(markerPath) + if err != nil { + return fmt.Errorf(emsg, err) + } + defer f.Close() + + markerContents := installation.InstallMarkerMeta{ + Branch: branch, + Version: version, + } + + if err := json.NewEncoder(f).Encode(markerContents); err != nil { + return fmt.Errorf(emsg, err) + } + + return nil +} + // copyFiles will copy the given files while preserving permissions. func copyFiles(files map[string]string) error { emsg := "copy files (%s to %s): %w" diff --git a/scripts/ci/update-generator/main.go b/scripts/ci/update-generator/main.go index 57b60b2ac6..22f2950a02 100644 --- a/scripts/ci/update-generator/main.go +++ b/scripts/ci/update-generator/main.go @@ -19,7 +19,6 @@ import ( "github.com/ActiveState/cli/internal/environment" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/fileutils" - "github.com/ActiveState/cli/internal/installation" "github.com/ActiveState/cli/internal/osutils" "github.com/ActiveState/cli/internal/updater" ) @@ -80,38 +79,24 @@ func archiveMeta() (archiveMethod archiver.Archiver, ext string) { func createUpdate(outputPath, channel, version, platform, target string) error { relChannelPath := filepath.Join(channel, platform) relVersionedPath := filepath.Join(channel, version, platform) - os.MkdirAll(filepath.Join(outputPath, relChannelPath), 0755) - os.MkdirAll(filepath.Join(outputPath, relVersionedPath), 0755) + _ = os.MkdirAll(filepath.Join(outputPath, relChannelPath), 0755) + _ = os.MkdirAll(filepath.Join(outputPath, relVersionedPath), 0755) archive, archiveExt := archiveMeta() relArchivePath := filepath.Join(relVersionedPath, fmt.Sprintf("state-%s-%s%s", platform, version, archiveExt)) archivePath := filepath.Join(outputPath, relArchivePath) - // Create install marker - installMarkerContents := installation.InstallMarkerMeta{ - Branch: channel, - Version: version, - } - b, err := json.Marshal(installMarkerContents) - if err != nil { - return errs.Wrap(err, "Could not marshal install marker contents") - } - - if err := fileutils.WriteFile(filepath.Join(target, installation.InstallDirMarker), b); err != nil { - return errs.Wrap(err, "Could not place install dir marker") - } - // Remove archive path if it already exists _ = os.Remove(archivePath) // Create main archive fmt.Printf("Creating %s\n", archivePath) - err = archive.Archive([]string{target}, archivePath) + err := archive.Archive([]string{target}, archivePath) if err != nil { return errs.Wrap(err, "Archiving failed") } up := updater.NewAvailableUpdate(version, channel, platform, filepath.ToSlash(relArchivePath), generateSha256(archivePath), "") - b, err = json.MarshalIndent(up, "", " ") + b, err := json.MarshalIndent(up, "", " ") if err != nil { return errs.Wrap(err, "Failed to marshal AvailableUpdate information.") } From 548439b141a5b44a70f9ddb28d30a137e0b84567 Mon Sep 17 00:00:00 2001 From: daved Date: Thu, 27 Jul 2023 14:22:21 -0700 Subject: [PATCH 36/46] Use correct var in installer e2e setup assertion --- cmd/state-installer/test/integration/installer_int_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 9aebf9dc8f..194bb7fce8 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -339,7 +339,7 @@ func (suite *InstallerIntegrationTestSuite) SetupSuite() { suite.Assert().DirExists(localPayload, "locally generated payload exists") installerExe := filepath.Join(localPayload, constants.StateInstallerCmd+osutils.ExeExt) - suite.Assert().FileExists(suite.installerExe, "locally generated installer exists") + suite.Assert().FileExists(installerExe, "locally generated installer exists") suite.installerExe = installerExe } From c5223bca3502525506fabc5d83eb884552cb9905 Mon Sep 17 00:00:00 2001 From: mdrakos Date: Thu, 27 Jul 2023 14:31:41 -0700 Subject: [PATCH 37/46] Add parse error to buildplanner --- pkg/platform/api/buildplanner/model/buildplan.go | 9 +++++++++ .../api/buildplanner/request/stagecommit.go | 9 ++++++++- pkg/platform/model/buildplanner.go | 13 +++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg/platform/api/buildplanner/model/buildplan.go b/pkg/platform/api/buildplanner/model/buildplan.go index 5bad6dc7ac..0125571300 100644 --- a/pkg/platform/api/buildplanner/model/buildplan.go +++ b/pkg/platform/api/buildplanner/model/buildplan.go @@ -324,10 +324,12 @@ type PushCommitResult struct { type StageCommitResult struct { Commit *Commit `json:"stageCommit"` *Error + *ParseError } // Error contains an error message. type Error struct { + Type string `json:"__typename"` Message string `json:"message"` } @@ -495,6 +497,13 @@ type PlanningError struct { SubErrors []*BuildExprLocation `json:"subErrors"` } +// ParseError is an error that occurred while parsing the build expression. +type ParseError struct { + Type string `json:"__typename"` + Message string `json:"message"` + Path string `json:"path"` +} + // BuildExprLocation represents a location in the build script where an error occurred. type BuildExprLocation struct { Type string `json:"__typename"` diff --git a/pkg/platform/api/buildplanner/request/stagecommit.go b/pkg/platform/api/buildplanner/request/stagecommit.go index b7641e20a6..30d33c0bd9 100644 --- a/pkg/platform/api/buildplanner/request/stagecommit.go +++ b/pkg/platform/api/buildplanner/request/stagecommit.go @@ -156,10 +156,17 @@ mutation ($organization: String!, $project: String!, $parentCommit: ID, $expr:Bu } } } + ... on ParseError { + __typename + message + path + } ... on NotFound { + __typename message } - ... on Error{ + ... on Error { + __typename message } } diff --git a/pkg/platform/model/buildplanner.go b/pkg/platform/model/buildplanner.go index d2b881b2a9..7cb43a0e1f 100644 --- a/pkg/platform/model/buildplanner.go +++ b/pkg/platform/model/buildplanner.go @@ -286,6 +286,19 @@ func (bp *BuildPlanner) StageCommit(params StageCommitParams) (strfmt.UUID, erro return "", processBuildPlannerError(err, "failed to stage commit") } + if resp.Error != nil { + return "", errs.New("Failed to stage commit: " + resp.Error.Message) + } + + if resp.ParseError != nil { + return "", locale.NewInputError( + "err_stage_commit_parse", + "The platform failed to parse the build expression, recieved the following message: {{.V0}}@{{.V1}}", + resp.ParseError.Message, + resp.ParseError.Path, + ) + } + if resp.Commit == nil { return "", errs.New("Staged commit is nil") } From 101a526ff8103764cc5e20f82bc0bf182a2bd959 Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 27 Jul 2023 17:33:31 -0400 Subject: [PATCH 38/46] Removed references to personal orgs. --- internal/access/access.go | 16 ---------------- internal/runners/invite/org.go | 5 ----- pkg/platform/model/organizations.go | 2 -- 3 files changed, 23 deletions(-) diff --git a/internal/access/access.go b/internal/access/access.go index 4ca3f6fa61..1a675a5f2f 100644 --- a/internal/access/access.go +++ b/internal/access/access.go @@ -10,21 +10,6 @@ import ( // Secrets determines whether the authorized user has access // to the current project's secrets func Secrets(orgName string, auth *authentication.Auth) (bool, error) { - if isProjectOwner(orgName, auth) { - return true, nil - } - - return isOrgMember(orgName, auth) -} - -func isProjectOwner(orgName string, auth *authentication.Auth) bool { - if orgName != auth.WhoAmI() { - return false - } - return true -} - -func isOrgMember(orgName string, auth *authentication.Auth) (bool, error) { _, err := model.FetchOrgMember(orgName, auth.WhoAmI(), auth) if err != nil { if errors.Is(err, model.ErrMemberNotFound) { @@ -32,6 +17,5 @@ func isOrgMember(orgName string, auth *authentication.Auth) (bool, error) { } return false, err } - return true, nil } diff --git a/internal/runners/invite/org.go b/internal/runners/invite/org.go index 60ab545412..7df24fddb7 100644 --- a/internal/runners/invite/org.go +++ b/internal/runners/invite/org.go @@ -31,11 +31,6 @@ func (o *Org) Set(v string) error { } func (o *Org) CanInvite(numInvites int) error { - // don't allow personal organizations - if o.Personal { - return locale.NewInputError("err_invite_personal", "This project does not belong to any organization and so cannot have any users invited to it. To invite users create an organization.") - } - limits, err := model.FetchOrganizationLimits(o.URLname) if err != nil { return locale.WrapError(err, "err_invite_fetchlimits", "Could not detect member limits for organization.") diff --git a/pkg/platform/model/organizations.go b/pkg/platform/model/organizations.go index 8afdfc8bcc..242194e900 100644 --- a/pkg/platform/model/organizations.go +++ b/pkg/platform/model/organizations.go @@ -24,9 +24,7 @@ var ErrMemberNotFound = errs.New("member not found") func FetchOrganizations() ([]*mono_models.Organization, error) { params := clientOrgs.NewListOrganizationsParams() memberOnly := true - personal := false params.SetMemberOnly(&memberOnly) - params.SetPersonal(&personal) res, err := authentication.Client().Organizations.ListOrganizations(params, authentication.ClientAuth()) if err != nil { From 1fa2e0cb7e2417134932b400f814b4ebb6b70beb Mon Sep 17 00:00:00 2001 From: daved Date: Thu, 27 Jul 2023 15:25:40 -0700 Subject: [PATCH 39/46] Use fileutils in paygen script, drop filepath import alias --- scripts/ci/payload-generator/main.go | 32 +++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/scripts/ci/payload-generator/main.go b/scripts/ci/payload-generator/main.go index 1b014f5b97..9b96ad597d 100644 --- a/scripts/ci/payload-generator/main.go +++ b/scripts/ci/payload-generator/main.go @@ -5,7 +5,7 @@ import ( "flag" "fmt" "os" - fp "path/filepath" + "path/filepath" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" @@ -45,8 +45,8 @@ func run() error { flag.Parse() root := environment.GetRootPathUnsafe() - buildDir := fp.Join(root, "build") - payloadDir := fp.Join(buildDir, "payload") + buildDir := filepath.Join(root, "build") + payloadDir := filepath.Join(buildDir, "payload") return generatePayload(buildDir, payloadDir, branch, version) } @@ -54,7 +54,7 @@ func run() error { func generatePayload(buildDir, payloadDir, branch, version string) error { emsg := "generate payload: %w" - payloadBinDir := fp.Join(payloadDir, "bin") + payloadBinDir := filepath.Join(payloadDir, "bin") if err := fileutils.MkdirUnlessExists(payloadBinDir); err != nil { return fmt.Errorf(emsg, err) @@ -66,10 +66,10 @@ func generatePayload(buildDir, payloadDir, branch, version string) error { } files := map[string]string{ - fp.Join(buildDir, constants.StateInstallerCmd+exeutils.Extension): payloadDir, - fp.Join(buildDir, constants.StateCmd+exeutils.Extension): payloadBinDir, - fp.Join(buildDir, constants.StateSvcCmd+exeutils.Extension): payloadBinDir, - fp.Join(buildDir, constants.StateExecutorCmd+exeutils.Extension): payloadBinDir, + filepath.Join(buildDir, constants.StateInstallerCmd+exeutils.Extension): payloadDir, + filepath.Join(buildDir, constants.StateCmd+exeutils.Extension): payloadBinDir, + filepath.Join(buildDir, constants.StateSvcCmd+exeutils.Extension): payloadBinDir, + filepath.Join(buildDir, constants.StateExecutorCmd+exeutils.Extension): payloadBinDir, } if err := copyFiles(files); err != nil { return fmt.Errorf(emsg, err) @@ -81,19 +81,17 @@ func generatePayload(buildDir, payloadDir, branch, version string) error { func createInstallMarker(payloadDir, branch, version string) error { emsg := "create install marker: %w" - markerPath := fp.Join(payloadDir, installation.InstallDirMarker) - f, err := os.Create(markerPath) - if err != nil { - return fmt.Errorf(emsg, err) - } - defer f.Close() - markerContents := installation.InstallMarkerMeta{ Branch: branch, Version: version, } + b, err := json.Marshal(markerContents) + if err != nil { + return fmt.Errorf(emsg, err) + } - if err := json.NewEncoder(f).Encode(markerContents); err != nil { + markerPath := filepath.Join(payloadDir, installation.InstallDirMarker) + if err := fileutils.WriteFile(markerPath, b); err != nil { return fmt.Errorf(emsg, err) } @@ -106,7 +104,7 @@ func copyFiles(files map[string]string) error { for src, target := range files { log("Copying %s to %s", src, target) - dest := fp.Join(target, fp.Base(src)) + dest := filepath.Join(target, filepath.Base(src)) err := fileutils.CopyFile(src, dest) if err != nil { return fmt.Errorf(emsg, src, target, err) From a7034a68d3caeb9d577960b3257f9aa1d639a4c7 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 28 Jul 2023 09:49:43 -0700 Subject: [PATCH 40/46] Report buildplan failures to DWH --- internal/analytics/client/sync/reporters/ga-state.go | 1 + internal/analytics/dimensions/dimensions.go | 5 +++++ pkg/platform/runtime/runtime.go | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/internal/analytics/client/sync/reporters/ga-state.go b/internal/analytics/client/sync/reporters/ga-state.go index d742edfae9..40f7fe217b 100644 --- a/internal/analytics/client/sync/reporters/ga-state.go +++ b/internal/analytics/client/sync/reporters/ga-state.go @@ -98,5 +98,6 @@ func legacyDimensionMap(d *dimensions.Values) map[string]string { "23": strconv.FormatBool(ptr.From(d.Interactive, false)), "24": ptr.From(d.TargetVersion, ""), "25": ptr.From(d.Error, ""), + "26": ptr.From(d.Message, ""), } } diff --git a/internal/analytics/dimensions/dimensions.go b/internal/analytics/dimensions/dimensions.go index f45711751f..8565ac2998 100644 --- a/internal/analytics/dimensions/dimensions.go +++ b/internal/analytics/dimensions/dimensions.go @@ -42,6 +42,7 @@ type Values struct { Sequence *int TargetVersion *string Error *string + Message *string CI *bool Interactive *bool @@ -123,6 +124,7 @@ func (v *Values) Clone() *Values { Sequence: ptr.Clone(v.Sequence), TargetVersion: ptr.Clone(v.TargetVersion), Error: ptr.Clone(v.Error), + Message: ptr.Clone(v.Message), CI: ptr.Clone(v.CI), Interactive: ptr.Clone(v.Interactive), preProcessor: v.preProcessor, @@ -196,6 +198,9 @@ func (m *Values) Merge(mergeWith ...*Values) { if dim.Error != nil { m.Error = dim.Error } + if dim.Message != nil { + m.Message = dim.Message + } if dim.CI != nil { m.CI = dim.CI } diff --git a/pkg/platform/runtime/runtime.go b/pkg/platform/runtime/runtime.go index 7dbf772004..09aea4d693 100644 --- a/pkg/platform/runtime/runtime.go +++ b/pkg/platform/runtime/runtime.go @@ -21,6 +21,7 @@ import ( "github.com/ActiveState/cli/internal/multilog" "github.com/ActiveState/cli/internal/osutils" "github.com/ActiveState/cli/internal/rtutils/ptr" + bpModel "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime/envdef" @@ -193,6 +194,8 @@ func (r *Runtime) recordCompletion(err error) { errorType = "solve" case errs.Matches(err, &setup.BuildError{}) || errs.Matches(err, &buildlog.BuildError{}): errorType = "build" + case errs.Matches(err, bpModel.BuildPlannerError{}): + errorType = "buildplan" case errs.Matches(err, &setup.ArtifactSetupErrors{}): if setupErrors := (&setup.ArtifactSetupErrors{}); errors.As(err, &setupErrors) { for _, err := range setupErrors.Errors() { @@ -212,11 +215,17 @@ func (r *Runtime) recordCompletion(err error) { errorType = "progress" } + var message string + if err != nil { + message = errs.JoinMessage(err) + } + r.analytics.Event(anaConsts.CatRuntime, action, &dimensions.Values{ CommitID: ptr.To(r.target.CommitUUID().String()), // Note: ProjectID is set by state-svc since ProjectNameSpace is specified. ProjectNameSpace: ptr.To(ns.String()), Error: ptr.To(errorType), + Message: &message, }) } From 87afc03f132c6d51b133e77519b8860bf6386d61 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 28 Jul 2023 09:56:29 -0700 Subject: [PATCH 41/46] Fix build failure --- internal/analytics/dimensions/dimensions.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/analytics/dimensions/dimensions.go b/internal/analytics/dimensions/dimensions.go index 8565ac2998..37c5669b63 100644 --- a/internal/analytics/dimensions/dimensions.go +++ b/internal/analytics/dimensions/dimensions.go @@ -95,6 +95,7 @@ func NewDefaultDimensions(pjNamespace, sessionToken, updateTag string) *Values { ptr.To(0), ptr.To(""), ptr.To(""), + ptr.To(""), ptr.To(false), ptr.To(false), nil, From 2be1944c1fb3a758b81b30e9ec4ec609ed251d09 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 28 Jul 2023 12:33:40 -0700 Subject: [PATCH 42/46] Make this a pointer so it's consistent with how the error is fired --- pkg/platform/runtime/runtime.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/platform/runtime/runtime.go b/pkg/platform/runtime/runtime.go index 09aea4d693..95a2a7b813 100644 --- a/pkg/platform/runtime/runtime.go +++ b/pkg/platform/runtime/runtime.go @@ -194,7 +194,7 @@ func (r *Runtime) recordCompletion(err error) { errorType = "solve" case errs.Matches(err, &setup.BuildError{}) || errs.Matches(err, &buildlog.BuildError{}): errorType = "build" - case errs.Matches(err, bpModel.BuildPlannerError{}): + case errs.Matches(err, &bpModel.BuildPlannerError{}): errorType = "buildplan" case errs.Matches(err, &setup.ArtifactSetupErrors{}): if setupErrors := (&setup.ArtifactSetupErrors{}); errors.As(err, &setupErrors) { From 64f10e576d9c85f42330c4fa5dac1bdb342f2327 Mon Sep 17 00:00:00 2001 From: mdrakos Date: Fri, 28 Jul 2023 14:32:40 -0700 Subject: [PATCH 43/46] Improve error messaging --- pkg/platform/model/buildplanner.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/platform/model/buildplanner.go b/pkg/platform/model/buildplanner.go index 7cb43a0e1f..4090c000bf 100644 --- a/pkg/platform/model/buildplanner.go +++ b/pkg/platform/model/buildplanner.go @@ -287,13 +287,13 @@ func (bp *BuildPlanner) StageCommit(params StageCommitParams) (strfmt.UUID, erro } if resp.Error != nil { - return "", errs.New("Failed to stage commit: " + resp.Error.Message) + return "", locale.NewInputError("Failed to stage commit, API returned message: {{.V0}}", resp.Error.Message) } if resp.ParseError != nil { return "", locale.NewInputError( "err_stage_commit_parse", - "The platform failed to parse the build expression, recieved the following message: {{.V0}}@{{.V1}}", + "The platform failed to parse the build expression, recieved the following message: {{.V0}}\n Path: {{.V1}}", resp.ParseError.Message, resp.ParseError.Path, ) From 91fd6c6f8bf40b20aa350e4377479ab371bcc2a6 Mon Sep 17 00:00:00 2001 From: Mike Drakos Date: Fri, 28 Jul 2023 14:37:32 -0700 Subject: [PATCH 44/46] Update pkg/platform/model/buildplanner.go Co-authored-by: Nathan Rijksen --- pkg/platform/model/buildplanner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/platform/model/buildplanner.go b/pkg/platform/model/buildplanner.go index 4090c000bf..7aa1b4d8ef 100644 --- a/pkg/platform/model/buildplanner.go +++ b/pkg/platform/model/buildplanner.go @@ -293,7 +293,7 @@ func (bp *BuildPlanner) StageCommit(params StageCommitParams) (strfmt.UUID, erro if resp.ParseError != nil { return "", locale.NewInputError( "err_stage_commit_parse", - "The platform failed to parse the build expression, recieved the following message: {{.V0}}\n Path: {{.V1}}", + "The platform failed to parse the build expression, received the following message: {{.V0}}. Path: {{.V1}}", resp.ParseError.Message, resp.ParseError.Path, ) From 76977cf697c98ee6d70dd69272699b642b582295 Mon Sep 17 00:00:00 2001 From: Mike Drakos Date: Fri, 28 Jul 2023 14:39:40 -0700 Subject: [PATCH 45/46] Update pkg/platform/model/buildplanner.go Co-authored-by: Nathan Rijksen --- pkg/platform/model/buildplanner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/platform/model/buildplanner.go b/pkg/platform/model/buildplanner.go index 7aa1b4d8ef..9319b567df 100644 --- a/pkg/platform/model/buildplanner.go +++ b/pkg/platform/model/buildplanner.go @@ -287,7 +287,7 @@ func (bp *BuildPlanner) StageCommit(params StageCommitParams) (strfmt.UUID, erro } if resp.Error != nil { - return "", locale.NewInputError("Failed to stage commit, API returned message: {{.V0}}", resp.Error.Message) + return "", locale.NewError("Failed to stage commit, API returned message: {{.V0}}", resp.Error.Message) } if resp.ParseError != nil { From 0e326d12c7bc13c9053e93492be8e1715b81bbe2 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 31 Jul 2023 11:58:21 -0400 Subject: [PATCH 46/46] Added `state checkout --no-clone` option for not cloning a project's associated git repo. --- cmd/state/internal/cmdtree/checkout.go | 5 +++++ internal/runners/activate/activate.go | 2 +- internal/runners/checkout/checkout.go | 3 ++- pkg/cmdlets/checkout/checkout.go | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cmd/state/internal/cmdtree/checkout.go b/cmd/state/internal/cmdtree/checkout.go index e70befdaf3..f8f03613a5 100644 --- a/cmd/state/internal/cmdtree/checkout.go +++ b/cmd/state/internal/cmdtree/checkout.go @@ -29,6 +29,11 @@ func newCheckoutCommand(prime *primer.Values) *captain.Command { Description: locale.Tl("flag_state_checkout_runtime-path_description", "Path to store the runtime files"), Value: ¶ms.RuntimePath, }, + { + Name: "no-clone", + Description: locale.Tl("flag_state_checkout_no_clone_description", "Do not clone the github repository associated with this project (if any)"), + Value: ¶ms.NoClone, + }, }, []*captain.Argument{ { diff --git a/internal/runners/activate/activate.go b/internal/runners/activate/activate.go index 2145d42c28..14fd6ba218 100644 --- a/internal/runners/activate/activate.go +++ b/internal/runners/activate/activate.go @@ -94,7 +94,7 @@ func (r *Activate) Run(params *ActivateParams) error { } // Perform fresh checkout - pathToUse, err := r.activateCheckout.Run(params.Namespace, params.Branch, "", params.PreferredPath) + pathToUse, err := r.activateCheckout.Run(params.Namespace, params.Branch, "", params.PreferredPath, false) if err != nil { return locale.WrapError(err, "err_activate_pathtouse", "Could not figure out what path to use.") } diff --git a/internal/runners/checkout/checkout.go b/internal/runners/checkout/checkout.go index 3aa8c6f1a1..678fb6663f 100644 --- a/internal/runners/checkout/checkout.go +++ b/internal/runners/checkout/checkout.go @@ -25,6 +25,7 @@ type Params struct { PreferredPath string Branch string RuntimePath string + NoClone bool } type primeable interface { @@ -66,7 +67,7 @@ func (u *Checkout) Run(params *Params) error { logging.Debug("Checking out %s to %s", params.Namespace.String(), params.PreferredPath) var err error - projectDir, err := u.checkout.Run(params.Namespace, params.Branch, params.RuntimePath, params.PreferredPath) + projectDir, err := u.checkout.Run(params.Namespace, params.Branch, params.RuntimePath, params.PreferredPath, params.NoClone) if err != nil { return locale.WrapError(err, "err_checkout_project", "", params.Namespace.String()) } diff --git a/pkg/cmdlets/checkout/checkout.go b/pkg/cmdlets/checkout/checkout.go index 0bb472fc88..2ee2f493db 100644 --- a/pkg/cmdlets/checkout/checkout.go +++ b/pkg/cmdlets/checkout/checkout.go @@ -44,7 +44,7 @@ func New(repo git.Repository, prime primeable) *Checkout { return &Checkout{repo, prime.Output(), prime.Config(), prime.Analytics(), "", prime.Auth()} } -func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath string) (string, error) { +func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath string, noClone bool) (string, error) { path, err := r.pathToUse(ns, targetPath) if err != nil { return "", errs.Wrap(err, "Could not get path to use") @@ -84,7 +84,7 @@ func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath } // Clone the related repo, if it is defined - if pj.RepoURL != nil && *pj.RepoURL != "" { + if !noClone && pj.RepoURL != nil && *pj.RepoURL != "" { err := r.repo.CloneProject(ns.Owner, ns.Project, path, r.Outputer, r.analytics) if err != nil { return "", locale.WrapError(err, "err_clone_project", "Could not clone associated git repository")