diff --git a/activestate.generators.yaml b/activestate.generators.yaml index a27b5b7278..93d5d38ba7 100644 --- a/activestate.generators.yaml +++ b/activestate.generators.yaml @@ -53,6 +53,15 @@ 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 + + echo "# Generate payload" + go run ./scripts/ci/payload-generator/main.go - name: generate-update language: bash description: Generate update files @@ -61,26 +70,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/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 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/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index 1bdbc3deae..194bb7fce8 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -36,11 +36,8 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.setupTest(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) @@ -48,7 +45,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Run installer with source-path flag (ie. install from this local path) 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)), ) @@ -68,22 +65,36 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { // Ensure installing overtop doesn't result in errors cp = ts.SpawnCmdWithOpts( suite.installerExe, - e2e.WithArgs(target, "--force"), + e2e.WithArgs(installationDir(ts)), 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(installationDir(ts), installation.InstallDirMarker), []byte{})) + cp = ts.SpawnCmdWithOpts( + suite.installerExe, + e2e.WithArgs(installationDir(ts)), + e2e.AppendEnv(constants.DisableUpdates+"=false"), + e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), + ) 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 @@ -126,14 +137,10 @@ func (suite *InstallerIntegrationTestSuite) TestInstallIncompatible() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.setupTest(ts) - - target := filepath.Join(ts.Dirs.Work, "installation") - // Run installer with source-path flag (ie. install from this local path) cp := ts.SpawnCmdWithOpts( suite.installerExe, - e2e.WithArgs(target), + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(constants.DisableUpdates+"=false", sysinfo.VersionOverrideEnvVar+"=10.0.0"), ) @@ -147,16 +154,12 @@ func (suite *InstallerIntegrationTestSuite) TestInstallNoErrorTips() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.setupTest(ts) - - target := filepath.Join(ts.Dirs.Work, "installation") - dir, err := ioutil.TempDir("", "system*") suite.NoError(err) cp := ts.SpawnCmdWithOpts( suite.installerExe, - e2e.WithArgs(target, "--activate", "ActiveState/DoesNotExist"), + e2e.WithArgs(installationDir(ts), "--activate", "ActiveState/DoesNotExist"), e2e.AppendEnv(constants.DisableUpdates+"=true"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -170,16 +173,12 @@ func (suite *InstallerIntegrationTestSuite) TestInstallErrorTips() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.setupTest(ts) - - target := filepath.Join(ts.Dirs.Work, "installation") - dir, err := ioutil.TempDir("", "system*") suite.NoError(err) cp := ts.SpawnCmdWithOpts( suite.installerExe, - e2e.WithArgs(target, "--activate", "ActiveState-CLI/Python3"), + e2e.WithArgs(installationDir(ts), "--activate", "ActiveState-CLI/Python3"), e2e.AppendEnv(constants.DisableUpdates+"=true"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), ) @@ -197,9 +196,7 @@ func (suite *InstallerIntegrationTestSuite) TestStateTrayRemoval() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.setupTest(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" @@ -269,7 +266,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { cp := ts.SpawnCmdWithOpts( suite.installerExe, - e2e.WithArgs(filepath.Join(ts.Dirs.Work, "installation")), + e2e.WithArgs(installationDir(ts)), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), ) cp.Expect("Done") @@ -279,7 +276,7 @@ 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")), + e2e.WithArgs(installationDir(ts)+"2"), e2e.AppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), ) cp.Expect("Done") @@ -333,18 +330,18 @@ 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")) +func installationDir(ts *e2e.Session) string { + return filepath.Join(ts.Dirs.Work, "installation") +} + +func (suite *InstallerIntegrationTestSuite) SetupSuite() { + 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(installerExe, "locally generated installer exists") - payloadDir := filepath.Dir(s.installerExe) - ts.CopyExeToDir(ts.Exe, filepath.Join(payloadDir, installation.BinDirName)) - ts.CopyExeToDir(ts.SvcExe, filepath.Join(payloadDir, installation.BinDirName)) + suite.installerExe = installerExe } func TestInstallerIntegrationTestSuite(t *testing.T) { 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/cmd/state/internal/cmdtree/cmdtree.go b/cmd/state/internal/cmdtree/cmdtree.go index f6d1e0c877..7314194e80 100644 --- a/cmd/state/internal/cmdtree/cmdtree.go +++ b/cmd/state/internal/cmdtree/cmdtree.go @@ -158,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), @@ -206,11 +207,6 @@ func New(prime *primer.Values, args ...string) *CmdTree { newPublish(prime), ) - if !condition.OnCI() { - 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 } 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/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/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..37c5669b63 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 @@ -94,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, @@ -123,6 +125,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 +199,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/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) } 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/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.", 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/cmdlets/auth/login.go b/pkg/cmdlets/auth/login.go index ac11a8bbe4..108bcebeef 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 apiKey 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 { + apiKey, 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 { + apiKey, 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(apiKey); err != nil { return locale.WrapError(err, "err_auth_token", "Failed to create token after authenticating with browser.") } 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") diff --git a/pkg/platform/api/buildplanner/model/buildplan.go b/pkg/platform/api/buildplanner/model/buildplan.go index 076a00a61e..0125571300 100644 --- a/pkg/platform/api/buildplanner/model/buildplan.go +++ b/pkg/platform/api/buildplanner/model/buildplan.go @@ -61,6 +61,9 @@ const ( XActiveStateArtifactMimeType = "application/x-activestate-artifacts" XCamelInstallerMimeType = "application/x-camel-installer" XGozipInstallerMimeType = "application/x-gozip-installer" + + // Error types + RemediableSolveErrorType = "RemediableSolveError" ) func IsStateToolArtifact(mimeType string) bool { @@ -94,6 +97,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 +186,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 +267,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 @@ -306,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"` } @@ -477,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 0a728be4be..9ab4485a4c 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/authentication/auth.go b/pkg/platform/authentication/auth.go index cb9dd1b6c3..2823ac02b5 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..", desensitizeToken(token)) if err := s.Authenticate(); err != nil { return errs.Wrap(err, "Failed to authenticate with API token") } @@ -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) (apiKey string, err error) { logging.Debug("AuthenticateWithDevice") - token, err := model.CheckDeviceAuthorization(deviceCode) + jwtToken, apiKeyToken, 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 apiKeyToken.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 @@ -387,6 +386,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 +413,7 @@ func (s *Auth) CreateToken() error { // SaveToken will save an API token func (s *Auth) SaveToken(token string) error { + 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") @@ -436,6 +437,8 @@ func (s *Auth) NewAPIKey(name string) (string, error) { return "", locale.WrapError(err, "err_token_create", "", err.Error()) } + logging.Debug("Created token: %s..", desensitizeToken(tokenOK.Payload.Token)) + return tokenOK.Payload.Token, nil } @@ -446,3 +449,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] +} diff --git a/pkg/platform/model/auth/auth.go b/pkg/platform/model/auth/auth.go index ec649f9f7b..dd5ff67d6c 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, apiKey *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 } diff --git a/pkg/platform/model/buildplanner.go b/pkg/platform/model/buildplanner.go index 1c029fcd2b..85037949c9 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 "", locale.NewError("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, received the following message: {{.V0}}. Path: {{.V1}}", + resp.ParseError.Message, + resp.ParseError.Path, + ) + } + if resp.Commit == nil { return "", errs.New("Staged commit is nil") } @@ -305,6 +318,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 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 { diff --git a/pkg/platform/runtime/runtime.go b/pkg/platform/runtime/runtime.go index 7dbf772004..95a2a7b813 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, }) } diff --git a/scripts/ci/payload-generator/main.go b/scripts/ci/payload-generator/main.go new file mode 100644 index 0000000000..9b96ad597d --- /dev/null +++ b/scripts/ci/payload-generator/main.go @@ -0,0 +1,123 @@ +package main + +import ( + "encoding/json" + "flag" + "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" + "github.com/ActiveState/cli/internal/installation" +) + +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. +func main() { + if err := run(); err != nil { + logErr("%s", err) + os.Exit(1) + } +} + +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 := filepath.Join(root, "build") + payloadDir := filepath.Join(buildDir, "payload") + + return generatePayload(buildDir, payloadDir, branch, version) +} + +func generatePayload(buildDir, payloadDir, branch, version string) error { + emsg := "generate payload: %w" + + payloadBinDir := filepath.Join(payloadDir, "bin") + + if err := fileutils.MkdirUnlessExists(payloadBinDir); err != nil { + return fmt.Errorf(emsg, err) + } + + log("Creating install dir marker in %s", payloadDir) + if err := createInstallMarker(payloadDir, branch, version); err != nil { + return fmt.Errorf(emsg, err) + } + + files := 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, + } + if err := copyFiles(files); err != nil { + return fmt.Errorf(emsg, err) + } + + return nil +} + +func createInstallMarker(payloadDir, branch, version string) error { + emsg := "create install marker: %w" + + markerContents := installation.InstallMarkerMeta{ + Branch: branch, + Version: version, + } + b, err := json.Marshal(markerContents) + if err != nil { + return fmt.Errorf(emsg, err) + } + + markerPath := filepath.Join(payloadDir, installation.InstallDirMarker) + if err := fileutils.WriteFile(markerPath, b); 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 := filepath.Join(target, filepath.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 +} 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, } diff --git a/scripts/ci/update-generator/main.go b/scripts/ci/update-generator/main.go index 70c0a53a43..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,18 +79,13 @@ 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) - installDirMarker := installation.InstallDirMarker - if err := fileutils.Touch(filepath.Join(target, installDirMarker)); 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 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 26eb5956cf..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" @@ -312,7 +313,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 { @@ -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" diff --git a/test/integration/hello_int_example_test.go b/test/integration/hello_int_example_test.go index dff119d1bd..b69435c625 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 = 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) } diff --git a/test/integration/install_scripts_int_test.go b/test/integration/install_scripts_int_test.go index aa55e19993..11ad87142d 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,9 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall() { e2e.AppendEnv("SHELL="), ) } - cp.Expect("already installed") + cp.Expect("successfully installed") + cp.WaitForInput() + cp.SendLine("exit") cp.ExpectExitCode(0) }) } 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