diff --git a/.github/deps/Linux/.state_install_root b/.github/deps/Linux/.state_install_root deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.github/deps/Linux/bin/state b/.github/deps/Linux/bin/state deleted file mode 100755 index 420ba98518..0000000000 Binary files a/.github/deps/Linux/bin/state and /dev/null differ diff --git a/.github/deps/Linux/bin/state-svc b/.github/deps/Linux/bin/state-svc deleted file mode 100755 index ca577d583b..0000000000 Binary files a/.github/deps/Linux/bin/state-svc and /dev/null differ diff --git a/.github/deps/Windows/.state_install_root b/.github/deps/Windows/.state_install_root deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.github/deps/Windows/bin/state-svc.exe b/.github/deps/Windows/bin/state-svc.exe deleted file mode 100644 index c9f3a46223..0000000000 Binary files a/.github/deps/Windows/bin/state-svc.exe and /dev/null differ diff --git a/.github/deps/Windows/bin/state.exe b/.github/deps/Windows/bin/state.exe deleted file mode 100644 index 555fdf8423..0000000000 Binary files a/.github/deps/Windows/bin/state.exe and /dev/null differ diff --git a/.github/deps/macOS/.state_install_root b/.github/deps/macOS/.state_install_root deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.github/deps/macOS/bin/state b/.github/deps/macOS/bin/state deleted file mode 100755 index 2ec3b34c38..0000000000 Binary files a/.github/deps/macOS/bin/state and /dev/null differ diff --git a/.github/deps/macOS/bin/state-svc b/.github/deps/macOS/bin/state-svc deleted file mode 100755 index 5a85857d29..0000000000 Binary files a/.github/deps/macOS/bin/state-svc and /dev/null differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5d0c2ca18..abb1f20291 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,6 +77,10 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} + - # === Install State Tool === + name: Install State Tool + uses: ActiveState/setup-state-tool@v1 + - # === Setup === name: Setup shell: bash @@ -413,6 +417,10 @@ jobs: with: go-version: ${{ matrix.go-version }} + - # === Install State Tool === + name: Install State Tool + uses: ActiveState/setup-state-tool@v1 + - # === Setup === name: Setup shell: bash @@ -462,6 +470,11 @@ jobs: aws-region: ${{ env.AWS_REGION }} mask-aws-account-id: true + - # === Generate updated master versions.json if necessary === + name: Generate version list + shell: bash + run: state run generate-versions-list + - # === Deploy === name: Deploy shell: bash diff --git a/.github/workflows/propagate.yml b/.github/workflows/propagate.yml index b5d5048b08..7c32c260c6 100644 --- a/.github/workflows/propagate.yml +++ b/.github/workflows/propagate.yml @@ -42,6 +42,10 @@ jobs: with: go-version: ${{ matrix.go-version }} + - # === Install State Tool === + name: Install State Tool + uses: ActiveState/setup-state-tool@v1 + - # === Setup === name: Setup shell: bash @@ -85,6 +89,13 @@ jobs: "type": "mrkdwn", "text": "Propagate-PR failed for *<${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}>*\nFailure should be addressed by *${{ github.event.pull_request.user.login }}*" } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Failed job: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } } ] } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 188adc8487..68350a8700 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,10 @@ jobs: with: go-version: ${{ matrix.go-version }} + - # === Install State Tool === + name: Install State Tool + uses: ActiveState/setup-state-tool@v1 + - # === Setup === name: Setup shell: bash diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 78fde2fe8b..0fd24fd16e 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -39,6 +39,10 @@ jobs: with: go-version: '1.20.x' + - # === Install State Tool === + name: Install State Tool + uses: ActiveState/setup-state-tool@v1 + - # === Setup === name: Setup shell: bash diff --git a/activestate.generators.yaml b/activestate.generators.yaml index f7e639c843..4096dbbb11 100644 --- a/activestate.generators.yaml +++ b/activestate.generators.yaml @@ -59,7 +59,7 @@ scripts: value: | set -e $constants.SET_ENV - + echo "# Generate payload" go run ./scripts/ci/payload-generator/main.go "$@" - name: generate-update @@ -120,3 +120,12 @@ scripts: go run scripts/ci/payload-generator/main.go -b ${TEST_CHANNEL} -v ${TEST_VERSION} copy_test_payload go run scripts/ci/update-generator/main.go -b ${TEST_CHANNEL} -v ${TEST_VERSION} -o ${TEST_ARCHIVE_DIR} + - name: generate-versions-list + language: bash + standalone: true + description: Generates master versions.json from S3 and info.json's from generate-update + value: | + set -e + $constants.SET_ENV + + go run scripts/ci/update-version-list/main.go ./build/update diff --git a/activestate.offlineinstall.yaml b/activestate.offlineinstall.yaml deleted file mode 100644 index 8e3b1d7867..0000000000 --- a/activestate.offlineinstall.yaml +++ /dev/null @@ -1,37 +0,0 @@ -constants: - - name: BUILD_OFFINSTALL_TARGET - if: ne .OS.Name "Windows" - value: offline-installer - - name: BUILD_OFFINSTALL_TARGET - if: eq .OS.Name "Windows" - value: offline-installer.exe - - name: OFFINSTALL_PKGS - value: ./cmd/state-offline-installer - - name: BUILD_OFFUNINSTALL_TARGET - if: ne .OS.Name "Windows" - value: uninstall - - name: BUILD_OFFUNINSTALL_TARGET - if: eq .OS.Name "Windows" - value: uninstall.exe - - name: OFFUNINSTALL_PKGS - value: ./cmd/state-offline-uninstaller -scripts: - - name: build-offline-installer - language: bash - description: Builds the project with the host OS as the target OS. - value: | - set -e - $constants.SET_ENV - - rm $BUILD_TARGET_DIR/offline/${constants.BUILD_OFFINSTALL_TARGET} || : - go build -tags "$GO_BUILD_TAGS" -o $BUILD_TARGET_DIR/offline/$constants.BUILD_OFFINSTALL_TARGET $constants.CLI_BUILDFLAGS $constants.OFFINSTALL_PKGS - go build -tags "$GO_BUILD_TAGS" -o $BUILD_TARGET_DIR/offline/$constants.BUILD_OFFUNINSTALL_TARGET $constants.CLI_BUILDFLAGS $constants.OFFUNINSTALL_PKGS - - name: pkg-offline-installer - language: bash - description: Packages the installer / uninstaller with the assets it requires - value: | - set -e - $constants.SET_ENV - - cd $BUILD_TARGET_DIR/offline/ - gozip -c ${constants.BUILD_OFFINSTALL_TARGET} artifacts.tar.gz LICENSE.txt installer_config.json $constants.BUILD_OFFUNINSTALL_TARGET diff --git a/architecture.md b/architecture.md index 43d0c374de..c3bbdd2153 100644 --- a/architecture.md +++ b/architecture.md @@ -64,8 +64,7 @@ from use by external code. #### internal/runbits/ Packages that are made available for use by "runner" packages. In essence, -`internal/runners/internal/runbits`. A synonymous and deprecated directory -exists at `pkg/cmdlets/`. +`internal/runners/internal/runbits`. #### internal/runners/ diff --git a/cmd/state-exec/cmd.go b/cmd/state-exec/cmd.go index 2b7595384f..e7a40a8720 100644 --- a/cmd/state-exec/cmd.go +++ b/cmd/state-exec/cmd.go @@ -6,7 +6,7 @@ import ( "os/exec" ) -func runCmd(meta *executorMeta) error { +func runCmd(meta *executorMeta) (int, error) { userArgs := os.Args[1:] cmd := exec.Command(meta.MatchingBin, userArgs...) cmd.Stdin = os.Stdin @@ -15,8 +15,8 @@ func runCmd(meta *executorMeta) error { cmd.Env = meta.TransformedEnv if err := cmd.Run(); err != nil { - return fmt.Errorf("command %q failed: %w", meta.MatchingBin, err) + return -1, fmt.Errorf("command %q failed: %w", meta.MatchingBin, err) } - return nil + return cmd.ProcessState.ExitCode(), nil } diff --git a/cmd/state-exec/comm.go b/cmd/state-exec/comm.go index 49ee0a974d..8e5a5a3584 100644 --- a/cmd/state-exec/comm.go +++ b/cmd/state-exec/comm.go @@ -12,14 +12,14 @@ const ( msgWidth = 1024 ) -func sendMsgToService(sockPath string, hb *svcmsg.Heartbeat) error { +func sendMsgToService(sockPath string, msg svcmsg.Messager) error { conn, err := net.Dial(network, sockPath) if err != nil { return fmt.Errorf("dial failed: %w", err) } defer conn.Close() - _, err = conn.Write([]byte(hb.SvcMsg())) + _, err = conn.Write([]byte(msg.SvcMsg())) if err != nil { return fmt.Errorf("write to connection failed: %w", err) } diff --git a/cmd/state-exec/condition.go b/cmd/state-exec/condition.go index 74649a66d9..29ed298a2d 100644 --- a/cmd/state-exec/condition.go +++ b/cmd/state-exec/condition.go @@ -2,7 +2,7 @@ package main import "os" -// onCI is copied from the internal/condition package (to minimize depdencies). -func onCI() bool { - return os.Getenv("CI") != "" || os.Getenv("BUILDER_OUTPUT") != "" +// inActiveStateCI is copied from the internal/condition package (to minimize dependencies). +func inActiveStateCI() bool { + return os.Getenv("ACTIVESTATE_CI") == "true" } diff --git a/cmd/state-exec/exitcode.go b/cmd/state-exec/exitcode.go new file mode 100644 index 0000000000..f0b2d0586c --- /dev/null +++ b/cmd/state-exec/exitcode.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "os" + "strconv" + + "github.com/ActiveState/cli/internal/svcctl/svcmsg" +) + +func newExitCodeMessage(exitCode int) (*svcmsg.ExitCode, error) { + execPath, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("cannot get executable info: %w", err) + } + return &svcmsg.ExitCode{execPath, strconv.Itoa(exitCode)}, nil +} diff --git a/cmd/state-exec/main.go b/cmd/state-exec/main.go index 47c375daa3..a8853b63fd 100644 --- a/cmd/state-exec/main.go +++ b/cmd/state-exec/main.go @@ -87,16 +87,31 @@ func run() error { if err := sendMsgToService(meta.SockPath, hb); err != nil { logr.Debug(" sock - error: %v", err) - if onCI() { // halt control flow on CI only + if inActiveStateCI() { // halt control flow on CI only return fmt.Errorf("cannot send message to service (this error is handled in CI only): %w", err) } } logr.Debug("cmd - running: %s", meta.MatchingBin) - if err := runCmd(meta); err != nil { + exitCode, err := runCmd(meta) + if err != nil { logr.Debug(" running - failed: bins (%v)", meta.ExecMeta.Bins) return fmt.Errorf("cannot run command: %w", err) } + msg, err := newExitCodeMessage(exitCode) + if err != nil { + return fmt.Errorf("cannot create new exit code message: %w", err) + } + logr.Debug("message data - exec: %s, exit code: %s", msg.ExecPath, msg.ExitCode) + + if err := sendMsgToService(meta.SockPath, msg); err != nil { + logr.Debug(" sock - error: %v", err) + + if inActiveStateCI() { // halt control flow on CI only + return fmt.Errorf("cannot send message to service (this error is handled in CI only): %w", err) + } + } + return nil } diff --git a/cmd/state-installer/cmd.go b/cmd/state-installer/cmd.go index 462444eff5..3c7e13d26d 100644 --- a/cmd/state-installer/cmd.go +++ b/cmd/state-installer/cmd.go @@ -29,10 +29,10 @@ import ( "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/rollbar" + "github.com/ActiveState/cli/internal/runbits/errors" "github.com/ActiveState/cli/internal/runbits/panics" "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/cli/internal/subshell/bash" - "github.com/ActiveState/cli/pkg/cmdlets/errors" "github.com/ActiveState/cli/pkg/project" "github.com/ActiveState/cli/pkg/sysinfo" "golang.org/x/crypto/ssh/terminal" @@ -123,6 +123,19 @@ func main() { logging.Debug("Original Args: %v", os.Args) logging.Debug("Processed Args: %v", processedArgs) + // Store sessionToken to config + for _, envVar := range []string{constants.OverrideSessionTokenEnvVarName, constants.SessionTokenEnvVarName} { + sessionToken, ok := os.LookupEnv(envVar) + if !ok { + continue + } + err := cfg.Set(anaConst.CfgSessionToken, sessionToken) + if err != nil { + multilog.Error("Unable to set session token: " + errs.JoinMessage(err)) + } + break + } + an = sync.New(anaConst.SrcStateInstaller, cfg, nil, out) an.Event(anaConst.CatInstallerFunnel, "start") diff --git a/cmd/state-installer/installer.go b/cmd/state-installer/installer.go index eccf65709b..480b64f7a8 100644 --- a/cmd/state-installer/installer.go +++ b/cmd/state-installer/installer.go @@ -9,7 +9,6 @@ import ( svcApp "github.com/ActiveState/cli/cmd/state-svc/app" svcAutostart "github.com/ActiveState/cli/cmd/state-svc/autostart" - anaConst "github.com/ActiveState/cli/internal/analytics/constants" "github.com/ActiveState/cli/internal/config" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" @@ -30,10 +29,9 @@ import ( ) type Installer struct { - out output.Outputer - cfg *config.Instance - payloadPath string - sessionToken string + out output.Outputer + cfg *config.Instance + payloadPath string *Params } @@ -49,13 +47,6 @@ func NewInstaller(cfg *config.Instance, out output.Outputer, payloadPath string, } func (i *Installer) Install() (rerr error) { - // Store sessionToken to config - if i.sessionToken != "" && i.cfg.GetString(anaConst.CfgSessionToken) == "" { - if err := i.cfg.Set(anaConst.CfgSessionToken, i.sessionToken); err != nil { - return errs.Wrap(err, "Failed to set session token") - } - } - // Store update tag if i.updateTag != "" { if err := i.cfg.Set(updater.CfgUpdateTag, i.updateTag); err != nil { @@ -145,9 +136,6 @@ func (i *Installer) InstallPath() string { // sanitizeInput cleans up the input and inserts fallback values func (i *Installer) sanitizeInput() error { - if sessionToken, ok := os.LookupEnv(constants.SessionTokenEnvVarName); ok { - i.sessionToken = sessionToken - } if tag, ok := os.LookupEnv(constants.UpdateTagEnvVarName); ok { i.updateTag = tag } diff --git a/cmd/state-installer/test/integration/installer_int_test.go b/cmd/state-installer/test/integration/installer_int_test.go index e65da195e2..5b4547e49b 100644 --- a/cmd/state-installer/test/integration/installer_int_test.go +++ b/cmd/state-installer/test/integration/installer_int_test.go @@ -8,9 +8,7 @@ import ( "runtime" "strings" "testing" - "time" - "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" "github.com/ActiveState/cli/internal/condition" @@ -18,7 +16,6 @@ import ( "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" "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" @@ -37,7 +34,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.SetupRCFile(ts) + ts.SetupRCFile() suite.T().Setenv(constants.HomeEnvVarName, ts.Dirs.HomeDir) dir, err := ioutil.TempDir("", "system*") @@ -193,66 +190,6 @@ func (suite *InstallerIntegrationTestSuite) TestInstallErrorTips() { "error tips should be displayed in shell created by installer") } -func (suite *InstallerIntegrationTestSuite) TestStateTrayRemoval() { - suite.OnlyRunForTags(tagsuite.Installer, tagsuite.Critical) - ts := e2e.New(suite.T(), false) - defer ts.Close() - - dir := installationDir(ts) - - // Install a release version that still has state-tray. - version := "0.35.0-SHAb78e2a4" - var cp *e2e.SpawnedCmd - if runtime.GOOS != "windows" { - oneLiner := fmt.Sprintf("sh <(curl -q https://platform.activestate.com/dl/cli/pdli01/install.sh) -f -n -t %s -v %s", dir, version) - cp = ts.SpawnCmdWithOpts( - "bash", e2e.OptArgs("-c", oneLiner), - e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), - ) - } else { - b, err := httputil.GetDirect("https://platform.activestate.com/dl/cli/pdli01/install.ps1") - suite.Require().NoError(err) - - ps1File := filepath.Join(ts.Dirs.Work, "install.ps1") - suite.Require().NoError(fileutils.WriteFile(ps1File, b)) - - cp = ts.SpawnCmdWithOpts("powershell.exe", e2e.OptArgs(ps1File, "-f", "-n", "-t", dir, "-v", version), - e2e.OptAppendEnv("SHELL="), - e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), - ) - } - cp.Expect("Installation Complete", termtest.OptExpectTimeout(5*time.Minute)) - - // Verify state-tray is there. - svcExec, err := installation.ServiceExecFromDir(dir) - suite.Require().NoError(err) - trayExec := strings.Replace(svcExec, constants.StateSvcCmd, "state-tray", 1) - suite.FileExists(trayExec) - updateDialogExec := strings.Replace(svcExec, constants.StateSvcCmd, "state-update-dialog", 1) - // suite.FileExists(updateDialogExec) // this is not actually installed... - - // Run the installer, which should remove state-tray and clean up after it. - cp = ts.SpawnCmdWithOpts( - suite.installerExe, - e2e.OptArgs("-f", "-n", "-t", dir), - e2e.OptAppendEnv(constants.UpdateBranchEnvVarName+"=release"), - e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), - ) - cp.Expect("Installing", termtest.OptExpectTimeout(10*time.Second)) - cp.Expect("Done", termtest.OptExpectTimeout(30*time.Second)) - - // Verify state-tray is no longer there. - suite.NoFileExists(trayExec) - suite.NoFileExists(updateDialogExec) - - // Verify state can still be run and has a newly updated version. - stateExec, err := installation.StateExecFromDir(dir) - suite.Require().NoError(err) - cp = ts.SpawnCmdWithOpts(stateExec, e2e.OptArgs("--version")) - suite.Assert().NotContains(cp.Output(), version) - cp.ExpectExitCode(0) -} - func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { suite.OnlyRunForTags(tagsuite.Installer) if runtime.GOOS != "darwin" { @@ -286,22 +223,6 @@ func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { cp.ExpectExitCode(0) } -func (suite *InstallerIntegrationTestSuite) SetupRCFile(ts *e2e.Session) { - if runtime.GOOS == "windows" { - return - } - - cfg, err := config.New() - suite.Require().NoError(err) - - subshell := subshell.New(cfg) - rcFile, err := subshell.RcFile() - suite.Require().NoError(err) - - err = fileutils.CopyFile(rcFile, filepath.Join(ts.Dirs.HomeDir, filepath.Base(rcFile))) - suite.Require().NoError(err) -} - func (suite *InstallerIntegrationTestSuite) AssertConfig(ts *e2e.Session) { if runtime.GOOS != "windows" { // Test bashrc diff --git a/cmd/state-offline-installer/install.go b/cmd/state-offline-installer/install.go deleted file mode 100644 index 25dc4db130..0000000000 --- a/cmd/state-offline-installer/install.go +++ /dev/null @@ -1,492 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - rt "runtime" - - "github.com/ActiveState/cli/internal/analytics" - ac "github.com/ActiveState/cli/internal/analytics/constants" - "github.com/ActiveState/cli/internal/analytics/dimensions" - "github.com/ActiveState/cli/internal/assets" - "github.com/ActiveState/cli/internal/config" - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/exeutils" - "github.com/ActiveState/cli/internal/fileutils" - "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/offinstall" - "github.com/ActiveState/cli/internal/osutils" - "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/internal/prompt" - "github.com/ActiveState/cli/internal/rtutils/ptr" - "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/internal/subshell/sscommon" - "github.com/ActiveState/cli/internal/testhelpers/outputhelper" - "github.com/ActiveState/cli/internal/unarchiver" - "github.com/ActiveState/cli/pkg/platform/runtime" - "github.com/ActiveState/cli/pkg/platform/runtime/setup/events" - "github.com/ActiveState/cli/pkg/platform/runtime/target" - "github.com/ActiveState/cli/pkg/project" - "github.com/vbauerster/mpb/v7" - "github.com/vbauerster/mpb/v7/decor" -) - -const artifactsTarGZName = "artifacts.tar.gz" -const assetsPathName = "assets" -const artifactsPathName = "artifacts" -const licenseFileName = "LICENSE.txt" -const installerConfigFileName = "installer_config.json" -const uninstallerFileNameRoot = "uninstall" + exeutils.Extension - -type runner struct { - out output.Outputer - prompt prompt.Prompter - analytics analytics.Dispatcher - cfg *config.Instance - shell subshell.SubShell - icfg InstallerConfig -} - -type primeable interface { - primer.Outputer - primer.Prompter - primer.Analyticer - primer.Configurer - primer.Subsheller -} - -func NewRunner(prime primeable) *runner { - return &runner{ - prime.Output(), - prime.Prompt(), - prime.Analytics(), - prime.Config(), - prime.Subshell(), - InstallerConfig{}, - } -} - -type InstallerConfig struct { - OrgName string `json:"org_name"` - ProjectID string `json:"project_id"` - ProjectName string `json:"project_name"` - CommitID string `json:"commit_id"` -} - -type Params struct { - path string -} - -func newParams() *Params { - return &Params{} -} - -func (r *runner) Run(params *Params) (rerr error) { - var installerDimensions *dimensions.Values - defer func() { - if rerr == nil { - return - } - if locale.IsInputError(rerr) { - r.analytics.EventWithLabel(ac.CatOfflineInstaller, ac.ActOfflineInstallerAbort, errs.JoinMessage(rerr), installerDimensions) - } else { - r.analytics.EventWithLabel(ac.CatOfflineInstaller, ac.ActOfflineInstallerFailure, errs.JoinMessage(rerr), installerDimensions) - } - }() - - tempDir, err := ioutil.TempDir("", "artifacts-") - if err != nil { - return errs.Wrap(err, "Unable to create temporary directory") - } - defer os.RemoveAll(tempDir) - - /* Extract Assets */ - backpackZipFile := os.Args[0] - assetsPath := filepath.Join(tempDir, assetsPathName) - if err := r.extractAssets(assetsPath, backpackZipFile); err != nil { - return errs.Wrap(err, "Could not extract assets") - } - - err = r.prepareInstallerConfig(assetsPath) - if err != nil { - return errs.Wrap(err, "Could not read installer config, this installer appears to be corrupted.") - } - - namespace := project.NewNamespace(r.icfg.OrgName, r.icfg.ProjectName, "") - installerDimensions = &dimensions.Values{ - ProjectNameSpace: ptr.To(namespace.String()), - CommitID: &r.icfg.CommitID, - Trigger: ptr.To(target.TriggerOfflineInstaller.String()), - } - r.analytics.Event(ac.CatOfflineInstaller, "start", installerDimensions) - - // Detect target path - targetPath, err := r.getTargetPath(params.path) - if err != nil { - return errs.Wrap(err, "Could not determine target path") - } - - /* Validate Target Path */ - if err := r.validateTargetPath(targetPath); err != nil { - return errs.Wrap(err, "Could not validate target path") - } - - /* Prompt for License */ - accepted, err := r.promptLicense(assetsPath) - if err != nil { - return errs.Wrap(err, "Could not prompt for license") - } - if !accepted { - return locale.NewInputError("License not accepted") - } - - /* Extract Artifacts */ - artifactsPath := filepath.Join(tempDir, artifactsPathName) - if err := r.extractArtifacts(artifactsPath, assetsPath); err != nil { - return errs.Wrap(err, "Could not extract artifacts") - } - - /* Install Artifacts */ - asrt, err := r.setupRuntime(artifactsPath, targetPath) - if err != nil { - return errs.Wrap(err, "Could not setup runtime") - } - - /* Manually Install License File */ - { - err = fileutils.CopyFile(filepath.Join(assetsPath, licenseFileName), filepath.Join(targetPath, licenseFileName)) - if err != nil { - return errs.Wrap(err, "Error copying license file") - } - } - - /* Manually Install config File */ - { - err = fileutils.CopyFile( - filepath.Join(assetsPath, installerConfigFileName), - filepath.Join(targetPath, installerConfigFileName), - ) - if err != nil { - return errs.Wrap(err, "Error copying config file") - } - } - - var uninstallerSrc string - var uninstallerDest string - - /* Manually Install uninstaller */ - if rt.GOOS == "windows" { - /* shenanigans because windows won't let you delete an executable that's running */ - installDir, err := filepath.Abs(targetPath) - if err != nil { - return errs.Wrap(err, "Error determining absolute install directory") - } - uninstallDir := filepath.Join(installDir, "uninstall-data") - if fileutils.DirExists(uninstallDir) { - if err := os.RemoveAll(uninstallDir); err != nil { - return errs.Wrap(err, "Error removing uninstall directory") - } - } - if err := os.Mkdir(uninstallDir, os.ModeDir); err != nil { - return errs.Wrap(err, "Error creating uninstall directory") - } - - uninstallerSrc = filepath.Join(assetsPath, uninstallerFileNameRoot) - uninstallerDest = filepath.Join(uninstallDir, uninstallerFileNameRoot) - - // create batch script which copies the uninstaller to a temp dir and runs it from there this is necessary - // because windows won't let you delete an executable that's running - // The last message about ignoring the error is because the uninstaller will delete the directory the batch file - // is in, which unlike with the exe is fine because batch files are "special", but it does result in a benign - // "File not Found" error - batch := fmt.Sprintf( - ` - @echo off - copy %[1]s\%[2]s %%TEMP%%\%[2]s >nul 2>&1 - %%TEMP%%\%[2]s %[3]s - del %%TEMP%%\%[2]s >nul 2>&1 - echo You can safely ignore any File not Found errors following this message. - `, - uninstallDir, - uninstallerFileNameRoot, - installDir, - ) - err = os.WriteFile(filepath.Join(installDir, "uninstall.bat"), []byte(batch), 0755) - if err != nil { - return errs.Wrap(err, "Error creating uninstall script") - } - } else { - uninstallerSrc = filepath.Join(assetsPath, uninstallerFileNameRoot) - uninstallerDest = filepath.Join(targetPath, uninstallerFileNameRoot) - } - { - if fileutils.TargetExists(uninstallerDest) { - err := os.Remove(uninstallerDest) - if err != nil { - return errs.Wrap(err, "Error removing existing uninstaller") - } - } - err = fileutils.CopyFile( - uninstallerSrc, - uninstallerDest, - ) - if err != nil { - return errs.Wrap(err, "Error copying uninstaller") - } - err = os.Chmod(uninstallerDest, 0555) - if err != nil { - return errs.Wrap(err, "Error making uninstaller executable") - } - } - - /* Configure Environment */ - if err := r.configureEnvironment(targetPath, namespace, asrt); err != nil { - return errs.Wrap(err, "Could not configure environment") - } - - r.analytics.Event(ac.CatOfflineInstaller, ac.ActOfflineInstallerSuccess, installerDimensions) - - r.out.Print(fmt.Sprintf(`Installation complete. -Your language runtime has been installed in [ACTIONABLE]%s[/RESET].`, targetPath)) - - return nil -} - -func (r *runner) prepareInstallerConfig(assetsPath string) error { - icfg := InstallerConfig{} - installerConfigPath := filepath.Join(assetsPath, installerConfigFileName) - configData, err := os.ReadFile(installerConfigPath) - if err != nil { - return errs.Wrap(err, "Failed to read config_file") - } - if err := json.Unmarshal(configData, &icfg); err != nil { - return errs.Wrap(err, "Failed to decode config_file") - } - - if icfg.ProjectName == "" { - return errs.New("ProjectName is empty") - } - - if icfg.OrgName == "" { - return errs.New("OrgName is empty") - } - - if icfg.CommitID == "" { - return errs.New("CommitID is empty") - } - - r.icfg = icfg - - return nil -} - -func (r *runner) setupRuntime(artifactsPath string, targetPath string) (*runtime.Runtime, error) { - logfile, err := buildlogfile.New(outputhelper.NewCatcher()) - if err != nil { - return nil, errs.Wrap(err, "Unable to create new logfile object") - } - - ns := project.NewNamespace(r.icfg.OrgName, r.icfg.ProjectName, r.icfg.CommitID) - offlineTarget := target.NewOfflineTarget(ns, targetPath, artifactsPath) - offlineTarget.SetTrigger(target.TriggerOfflineInstaller) - - offlineProgress := newOfflineProgressOutput(r.out) - eventHandler := events.NewRuntimeEventHandler(offlineProgress, nil, logfile) - - rti, err := runtime.New(offlineTarget, r.analytics, nil, nil) - if err != nil { - if !runtime.IsNeedsUpdateError(err) { - return nil, errs.Wrap(err, "Could not create runtime") - } - if err = rti.Update(eventHandler); err != nil { - return nil, errs.Wrap(err, "Had an installation error") - } - } - return rti, nil -} - -func (r *runner) extractArtifacts(artifactsPath, assetsPath string) error { - if err := os.Mkdir(artifactsPath, os.ModePerm); err != nil { - return errs.Wrap(err, "Unable to create artifactsPath directory") - } - - archivePath := filepath.Join(assetsPath, artifactsTarGZName) - ua := unarchiver.NewTarGz() - f, siz, err := ua.PrepareUnpacking(archivePath, artifactsPath) - if err != nil { - return errs.Wrap(err, "Unable to prepare unpacking of artifact tarball") - } - - pb := mpb.New( - mpb.WithWidth(40), - ) - barName := "Extracting" - bar := pb.AddBar( - siz, - mpb.PrependDecorators(decor.Name(barName, decor.WC{W: len(barName) + 1, C: decor.DidentRight})), - ) - - ua.SetNotifier(func(filename string, _ int64, isDir bool) { - if !isDir { - bar.Increment() - } - }) - - err = ua.Unarchive(f, siz, artifactsPath) - if err != nil { - return errs.Wrap(err, "Unable to unarchive artifacts to artifactsPath") - } - - bar.SetTotal(0, true) - bar.Abort(true) - pb.Wait() - - return nil -} - -func (r *runner) extractAssets(assetsPath string, backpackZipFile string) error { - if err := os.Mkdir(assetsPath, os.ModePerm); err != nil { - return errs.Wrap(err, "Unable to create assetsPath") - } - - ua := unarchiver.NewZip() - f, siz, err := ua.PrepareUnpacking(backpackZipFile, assetsPath) - if err != nil { - return errs.Wrap(err, "Unable to prepare unpacking of backpack") - } - - err = ua.Unarchive(f, siz, assetsPath) - if err != nil { - return errs.Wrap(err, "Unable to unarchive Assets to assetsPath") - } - - return nil -} - -func (r *runner) configureEnvironment(path string, namespace *project.Namespaced, asrt *runtime.Runtime) error { - env, err := asrt.Env(false, false) - if err != nil { - return errs.Wrap(err, "Error setting environment") - } - - if rt.GOOS == "windows" { - contents, err := assets.ReadFileBytes("scripts/setenv.bat") - if err != nil { - return errs.Wrap(err, "Error reading file bytes") - } - err = fileutils.WriteFile(filepath.Join(path, "setenv.bat"), contents) - if err != nil { - return locale.WrapError(err, - "err_deploy_write_setenv", - "Could not create setenv batch scriptfile at path: {{.V0}}", - path) - } - } - - // Configure available shells - isAdmin, err := osutils.IsAdmin() - if err != nil { - return errs.Wrap(err, "Could not determine if running as Windows administrator") - } - - id := sscommon.ProjectRCIdentifier(sscommon.OfflineInstallID, namespace) - err = subshell.ConfigureAvailableShells(r.shell, r.cfg, env, id, !isAdmin) - if err != nil { - return locale.WrapError(err, - "err_deploy_subshell_write", - "Could not write environment information to your shell configuration.") - } - - binPath := filepath.Join(path, "bin") - if err := fileutils.MkdirUnlessExists(binPath); err != nil { - return locale.WrapError(err, "err_deploy_binpath", "Could not create bin directory.") - } - - // Write global env file - err = r.shell.SetupShellRcFile(binPath, env, nil) - if err != nil { - return locale.WrapError(err, "err_deploy_subshell_rc_file", "Could not create environment script.") - } - - return nil -} - -func (r *runner) getTargetPath(inputPath string) (string, error) { - var targetPath string - if inputPath != "" { - targetPath = inputPath - } else { - parentDir, err := offinstall.DefaultInstallParentDir() - if err != nil { - return "", errs.Wrap(err, "Could not determine default install path") - } - targetPath = filepath.Join(parentDir, r.icfg.ProjectName) - - targetPath, err = r.prompt.Input("", "Enter an installation directory", &targetPath) - if err != nil { - return "", errs.Wrap(err, "Could not retrieve installation directory") - } - } - return targetPath, nil -} - -func (r *runner) validateTargetPath(path string) error { - if !fileutils.IsWritable(path) { - return errs.New( - "Cannot write to [ACTIONABLE]%s[/RESET]. Please ensure that the directory is writeable without "+ - "needing admin privileges or run this installer with Admin.", path) - } - - if fileutils.TargetExists(path) { - if !fileutils.IsDir(path) { - return errs.New("Target path [ACTIONABLE]%s[/RESET] is not a directory", path) - } - - empty, err := fileutils.IsEmptyDir(path) - if err != nil { - return errs.Wrap(err, "Test for directory empty failed") - } - if !empty { - installNonEmpty, err := r.prompt.Confirm( - "Setup", - "Installation directory is not empty, install anyway?", - ptr.To(true)) - if err != nil { - return errs.Wrap(err, "Unable to get confirmation to install into non-empty directory") - } - - if !installNonEmpty { - return locale.NewInputError( - "offline_installer_err_installdir_notempty", - "Installation directory ({{.V0}}) not empty, installation aborted", - path) - } - } - } - - return nil -} - -func (r *runner) promptLicense(assetsPath string) (bool, error) { - licenseFileAssetPath := filepath.Join(assetsPath, licenseFileName) - licenseContents, err := fileutils.ReadFile(licenseFileAssetPath) - if err != nil { - return false, errs.Wrap(err, "Unable to open License file") - } - r.out.Print(licenseContents) - - choice, err := r.prompt.Confirm("", "Do you accept the ActiveState Runtime Installer License Agreement?", ptr.To(false)) - if err != nil { - return false, err - } - - if err != nil { - return false, errs.Wrap(err, "Unable to confirm license") - } - - return choice, nil -} diff --git a/cmd/state-offline-installer/main.go b/cmd/state-offline-installer/main.go deleted file mode 100644 index 82b8cd86e7..0000000000 --- a/cmd/state-offline-installer/main.go +++ /dev/null @@ -1,137 +0,0 @@ -package main - -import ( - "fmt" - "os" - "runtime/debug" - "time" - - "github.com/ActiveState/cli/internal/analytics" - "github.com/ActiveState/cli/internal/analytics/client/sync" - anaConst "github.com/ActiveState/cli/internal/analytics/constants" - "github.com/ActiveState/cli/internal/captain" - "github.com/ActiveState/cli/internal/config" - "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/events" - "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/multilog" - "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/internal/prompt" - "github.com/ActiveState/cli/internal/rollbar" - "github.com/ActiveState/cli/internal/rtutils/ptr" - "github.com/ActiveState/cli/internal/runbits/panics" - "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/pkg/cmdlets/errors" -) - -func main() { - var exitCode int - - var an analytics.Dispatcher - var cfg *config.Instance - rollbar.SetupRollbar(constants.OfflineInstallerRollbarToken) - - // Allow starting the installer via a double click - captain.DisableMousetrap() - - // Handle things like panics, exit codes and the closing of globals - defer func() { - if panics.HandlePanics(recover(), debug.Stack()) { - exitCode = 1 - } - - if err := cfg.Close(); err != nil { - logging.Error("Failed to close config: %w", err) - } - - if err := events.WaitForEvents(5*time.Second, rollbar.Wait, an.Wait, logging.Close); err != nil { - logging.Warning("state-remote-installer failed to wait for events: %v", err) - } - os.Exit(exitCode) - }() - - if os.Getenv("VERBOSE") == "true" { - logging.CurrentHandler().SetVerbose(true) - } - - // Set up configuration handler - cfg, err := config.New() - if err != nil { - logging.Critical("Could not set up configuration handler: " + errs.JoinMessage(err)) - fmt.Fprintln(os.Stderr, errs.JoinMessage(err)) - exitCode = 1 - return - } - - rollbar.SetConfig(cfg) - - out, err := output.New("", &output.Config{ - OutWriter: os.Stdout, - ErrWriter: os.Stderr, - }) - if err != nil { - logging.Critical("Could not set up outputter: " + errs.JoinMessage(err)) - fmt.Fprintln(os.Stderr, errs.JoinMessage(err)) - exitCode = 1 - return - } - - an = sync.New(anaConst.SrcOfflineInstaller, cfg, nil, out) - - prime := primer.New( - nil, out, nil, - prompt.New(true, an), - subshell.New(cfg), nil, cfg, - nil, nil, an) - - if err := run(prime); err != nil { - if locale.IsInputError(err) { - logging.Debug("state-offline-installer errored out due to input: %s", errs.JoinMessage(err)) - } else { - multilog.Critical("state-offline-installer errored out: %s", errs.JoinMessage(err)) - } - - exitCode, _ = errors.ParseUserFacing(err) - if err != nil { - fmt.Fprintln(os.Stderr, errs.JoinMessage(err)) - } - } - out.Print("Press enter to exit.") - fmt.Scanln(ptr.To("")) // Wait for input from user -} - -func run(prime *primer.Values) error { - params := newParams() - - cmd := captain.NewCommand( - "install", - "Doing offline installation", - "Do an offline installation", - prime, nil, - []*captain.Argument{ - { - Name: "path", - Description: "Install into target directory ", - Value: ¶ms.path, - Required: false, - }, - }, - func(ccmd *captain.Command, args []string) error { - logging.Debug("Running CmdInstall") - runner := NewRunner(prime) - return runner.Run(params) - }, - ) - - err := cmd.Execute(os.Args[1:]) - if err != nil { - errors.PanicOnMissingLocale = false - errors.ReportError(err, cmd, prime.Analytics()) - return err - } - - return nil -} diff --git a/cmd/state-offline-installer/progress.go b/cmd/state-offline-installer/progress.go deleted file mode 100644 index 81515452dd..0000000000 --- a/cmd/state-offline-installer/progress.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/platform/model" - "github.com/ActiveState/cli/pkg/platform/runtime/artifact" - "github.com/vbauerster/mpb/v7" - "github.com/vbauerster/mpb/v7/decor" -) - -// New returns an error with the supplied message. -// New also records the stack trace at the point it was called. -// func New(out output.Outputer) *offlineProgressOutput { -// -// return &offlineProgressOutput{ -// out: out, -// } -// } - -type offlineProgressOutput struct { - out output.Outputer - pb *mpb.Progress - bar *mpb.Bar -} - -func newOfflineProgressOutput(out output.Outputer) *offlineProgressOutput { - return &offlineProgressOutput{out: out} -} - -func (mpo *offlineProgressOutput) BuildStarted(total int64) error { - return nil -} -func (mpo *offlineProgressOutput) BuildCompleted(bool) error { - return nil -} - -func (mpo *offlineProgressOutput) BuildArtifactStarted(artifactID artifact.ArtifactID, artifactName string) error { - return nil -} -func (mpo *offlineProgressOutput) BuildArtifactCompleted(artifactID artifact.ArtifactID, artifactName, logURI string, cachedBuild bool) error { - return nil -} -func (mpo *offlineProgressOutput) BuildArtifactFailure(artifactID artifact.ArtifactID, artifactName, logURI string, errorMessage string, cachedBuild bool) error { - return nil -} -func (mpo *offlineProgressOutput) BuildArtifactProgress(artifactID artifact.ArtifactID, artifactName, timeStamp, message, facility, pipeName, source string) error { - return nil -} - -func (mpo *offlineProgressOutput) InstallationCompleted(withFailures bool) error { - mpo.bar.SetTotal(0, true) - mpo.bar.Abort(true) - mpo.pb.Wait() - return nil -} -func (mpo *offlineProgressOutput) InstallationStarted(total int64) error { - mpo.pb = mpb.New(mpb.WithWidth(40)) - barName := "Installing" - mpo.bar = mpo.pb.AddBar(total, mpb.PrependDecorators(decor.Name(barName, decor.WC{W: len(barName) + 1, C: decor.DidentRight}))) - return nil -} -func (mpo *offlineProgressOutput) InstallationStatusUpdate(current, total int64) error { - mpo.bar.SetTotal(total, false) - mpo.bar.SetCurrent(current) - return nil -} -func (mpo *offlineProgressOutput) ArtifactStepStarted(artifactID artifact.ArtifactID, artifactName string, title string, total int64, counterCountsBytes bool) error { - return nil -} -func (mpo *offlineProgressOutput) ArtifactStepIncrement(artifactID artifact.ArtifactID, artifactName string, title string, total int64) error { - return nil -} -func (mpo *offlineProgressOutput) ArtifactStepCompleted(artifactID artifact.ArtifactID, artifactName string, title string) error { - return nil -} -func (mpo *offlineProgressOutput) ArtifactStepFailure(artifact.ArtifactID, string, string, string) error { - return nil -} -func (mpo *offlineProgressOutput) StillBuilding(numCompleted, numTotal int) error { - return nil -} -func (mpo *offlineProgressOutput) SolverStart() error { - return nil -} - -func (mpo *offlineProgressOutput) SolverSuccess() error { - return nil -} -func (mpo *offlineProgressOutput) SolverError(serr *model.SolverError) error { - return nil -} -func (mpo *offlineProgressOutput) Close() error { return nil } diff --git a/cmd/state-offline-uninstaller/main.go b/cmd/state-offline-uninstaller/main.go deleted file mode 100644 index 0ba17527c5..0000000000 --- a/cmd/state-offline-uninstaller/main.go +++ /dev/null @@ -1,136 +0,0 @@ -package main - -import ( - "fmt" - "os" - "runtime/debug" - "time" - - "github.com/ActiveState/cli/internal/analytics" - "github.com/ActiveState/cli/internal/analytics/client/sync" - anaConst "github.com/ActiveState/cli/internal/analytics/constants" - "github.com/ActiveState/cli/internal/captain" - "github.com/ActiveState/cli/internal/config" - "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/events" - "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/multilog" - "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/internal/prompt" - "github.com/ActiveState/cli/internal/rollbar" - "github.com/ActiveState/cli/internal/rtutils/ptr" - "github.com/ActiveState/cli/internal/runbits/panics" - "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/pkg/cmdlets/errors" -) - -func main() { - var exitCode int - - var an analytics.Dispatcher - var cfg *config.Instance - rollbar.SetupRollbar(constants.OfflineInstallerRollbarToken) - - // Allow starting the installer via a double click - captain.DisableMousetrap() - - // Handle things like panics, exit codes and the closing of globals - defer func() { - if panics.HandlePanics(recover(), debug.Stack()) { - exitCode = 1 - } - - if err := cfg.Close(); err != nil { - logging.Error("Failed to close config: %w", err) - } - - if err := events.WaitForEvents(5*time.Second, rollbar.Wait, an.Wait, logging.Close); err != nil { - logging.Warning("state-remote-installer failed to wait for events: %v", err) - } - os.Exit(exitCode) - }() - - if os.Getenv("VERBOSE") == "true" { - logging.CurrentHandler().SetVerbose(true) - } - - // Set up configuration handler - cfg, err := config.New() - if err != nil { - logging.Critical("Could not set up configuration handler: " + errs.JoinMessage(err)) - fmt.Fprintln(os.Stderr, errs.JoinMessage(err)) - exitCode = 1 - return - } - - rollbar.SetConfig(cfg) - - out, err := output.New("", &output.Config{ - OutWriter: os.Stdout, - ErrWriter: os.Stderr, - }) - if err != nil { - logging.Critical("Could not set up outputter: " + errs.JoinMessage(err)) - fmt.Fprintln(os.Stderr, errs.JoinMessage(err)) - exitCode = 1 - return - } - - an = sync.New(anaConst.SrcOfflineInstaller, cfg, nil, out) - - prime := primer.New( - nil, out, nil, - prompt.New(true, an), - subshell.New(cfg), nil, cfg, - nil, nil, an) - - if err := run(prime); err != nil { - if locale.IsInputError(err) { - logging.Debug("state-offline-uninstaller errored out due to input: %s", errs.JoinMessage(err)) - } else { - multilog.Critical("state-offline-uninstaller errored out: %s", errs.JoinMessage(err)) - } - - exitCode, _ = errors.ParseUserFacing(err) - if err != nil { - fmt.Fprintln(os.Stderr, errs.JoinMessage(err)) - } - } - out.Print("Press enter to exit.") - fmt.Scanln(ptr.To("")) // Wait for input from user -} - -func run(prime *primer.Values) error { - params := newParams() - cmd := captain.NewCommand( - "uninstall", - "Doing offline un-installation", - "Do an offline un-installation", - prime, nil, - []*captain.Argument{ - { - Name: "path", - Description: "Directory to uninstall ", - Value: ¶ms.path, - Required: false, - }, - }, - func(ccmd *captain.Command, args []string) error { - logging.Debug("Running CmdUnInstall") - runner := NewRunner(prime) - return runner.Run(params) - }, - ) - - err := cmd.Execute(os.Args[1:]) - if err != nil { - errors.PanicOnMissingLocale = false - errors.ReportError(err, cmd, prime.Analytics()) - return err - } - - return nil -} diff --git a/cmd/state-offline-uninstaller/uninstall.go b/cmd/state-offline-uninstaller/uninstall.go deleted file mode 100644 index 08a86065fc..0000000000 --- a/cmd/state-offline-uninstaller/uninstall.go +++ /dev/null @@ -1,233 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/ActiveState/cli/internal/analytics" - ac "github.com/ActiveState/cli/internal/analytics/constants" - "github.com/ActiveState/cli/internal/analytics/dimensions" - "github.com/ActiveState/cli/internal/config" - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/fileutils" - "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/internal/rtutils/ptr" - "github.com/ActiveState/cli/pkg/project" - - "github.com/ActiveState/cli/internal/osutils" - "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/internal/prompt" - "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/internal/subshell/sscommon" - "github.com/ActiveState/cli/pkg/platform/runtime/target" -) - -const licenseFileName = "LICENSE.txt" - -type runner struct { - out output.Outputer - prompt prompt.Prompter - analytics analytics.Dispatcher - cfg *config.Instance - shell subshell.SubShell - icfg InstallerConfig -} - -type primeable interface { - primer.Outputer - primer.Prompter - primer.Analyticer - primer.Configurer - primer.Subsheller -} - -func NewRunner(prime primeable) *runner { - return &runner{ - prime.Output(), - prime.Prompt(), - prime.Analytics(), - prime.Config(), - prime.Subshell(), - InstallerConfig{}, - } -} - -const installerConfigFileName = "installer_config.json" - -type InstallerConfig struct { - OrgName string `json:"org_name"` - ProjectID string `json:"project_id"` - ProjectName string `json:"project_name"` - CommitID string `json:"commit_id"` -} - -type Params struct { - path string -} - -func newParams() *Params { - return &Params{} -} - -func (r *runner) Run(params *Params) (rerr error) { - var installerDimensions *dimensions.Values - defer func() { - if rerr == nil { - return - } - if locale.IsInputError(rerr) { - r.analytics.EventWithLabel(ac.CatOfflineInstaller, ac.ActOfflineInstallerAbort, errs.JoinMessage(rerr), installerDimensions) - } else { - r.analytics.EventWithLabel(ac.CatOfflineInstaller, ac.ActOfflineInstallerFailure, errs.JoinMessage(rerr), installerDimensions) - } - }() - - // Detect target path - targetPath, err := r.getTargetPath(params.path) - if err != nil { - return errs.Wrap(err, "Could not determine target path") - } - - /* Validate Target Path */ - if err := r.validateTargetPath(targetPath); err != nil { - return errs.Wrap(err, "Could not validate target path") - } - - if err := r.prepareInstallerConfig(targetPath); err != nil { - return errs.Wrap(err, "Could not read installer config, this installer appears to be corrupted.") - } - - cont, err := r.prompt.Confirm("", - fmt.Sprintf("You are about to uninstall the runtime installed at [[ACTIONABLE]%s[/RESET], continue?", targetPath), - ptr.To(false)) - if err != nil { - return errs.Wrap(err, "Could not confirm uninstall") - } - if !cont { - return locale.NewInputError("err_uninstall_abort", "Uninstall aborted") - } - - namespace := project.NewNamespace(r.icfg.OrgName, r.icfg.ProjectName, "") - installerDimensions = &dimensions.Values{ - ProjectNameSpace: ptr.To(namespace.String()), - CommitID: &r.icfg.CommitID, - Trigger: ptr.To(target.TriggerOfflineUninstaller.String()), - } - r.analytics.Event(ac.CatOfflineInstaller, ac.ActOfflineInstallerStart, installerDimensions) - - r.out.Print("Removing environment configuration") - err = r.removeEnvPaths(namespace) - if err != nil { - return errs.Wrap(err, "Error removing environment path") - } - - r.out.Print("Removing installation directory") - err = os.RemoveAll(targetPath) - if err != nil { - return errs.Wrap(err, "Error removing installation directory") - } - - r.analytics.Event(ac.CatOfflineInstaller, ac.ActOfflineInstallerSuccess, installerDimensions) - r.analytics.Event(ac.CatRuntimeUsage, ac.ActRuntimeDelete, installerDimensions) - - r.out.Print("Uninstall Complete") - - return nil -} - -func (r *runner) prepareInstallerConfig(assetsPath string) error { - icfg := InstallerConfig{} - installerConfigPath := filepath.Join(assetsPath, installerConfigFileName) - - configData, err := os.ReadFile(installerConfigPath) - if err != nil { - return errs.Wrap(err, "Failed to read config_file") - } - if err := json.Unmarshal(configData, &icfg); err != nil { - return errs.Wrap(err, "Failed to decode config_file") - } - - if icfg.ProjectName == "" { - return errs.New("ProjectName is empty") - } - - if icfg.OrgName == "" { - return errs.New("OrgName is empty") - } - - if icfg.CommitID == "" { - return errs.New("CommitID is empty") - } - - r.icfg = icfg - - return nil -} - -func (r *runner) getTargetPath(inputPath string) (string, error) { - if inputPath != "" { - return inputPath, nil - } - - cwd, err := os.Getwd() - if err != nil { - return "", errs.Wrap(err, "Could not determine current working directory") - } - - var targetPath string - if fileutils.TargetExists(filepath.Join(cwd, installerConfigFileName)) { - targetPath = cwd - } - - if targetPath != "" { - targetPath, err = r.prompt.Input("", "Enter an installation directory to uninstall", &targetPath) - } else { - targetPath, err = r.prompt.Input("", "Enter an installation directory to uninstall", nil, prompt.InputRequired) - } - if err != nil { - return "", errs.Wrap(err, "Could not retrieve installation directory") - } - return targetPath, nil -} - -func (r *runner) validateTargetPath(path string) error { - if !fileutils.IsWritable(path) { - return errs.New( - "Cannot write to [ACTIONABLE]%s[/RESET]. Please ensure that the directory is writeable without "+ - "needing admin privileges or run this installer with Admin.", path) - } - - if !fileutils.IsDir(path) { - return errs.New("Target path [ACTIONABLE]%s[/RESET] is not a directory", path) - } - - installerConfigPath := filepath.Join(path, installerConfigFileName) - if !fileutils.FileExists(installerConfigPath) { - return errs.New( - "The target directory does not appear to contain an ActiveState Runtime installation. Expected to find: %s.", - installerConfigPath) - } - - return nil -} - -func (r *runner) removeEnvPaths(namespace *project.Namespaced) error { - isAdmin, err := osutils.IsAdmin() - if err != nil { - return errs.Wrap(err, "Could not determine if running as Windows administrator") - } - - // remove shell file additions - id := sscommon.ProjectRCIdentifier(sscommon.OfflineInstallID, namespace) - if err := r.shell.CleanUserEnv(r.cfg, id, !isAdmin); err != nil { - return errs.Wrap(err, "Failed to remove runtime PATH") - } - if err := r.shell.CleanUserEnv(r.cfg, sscommon.AutostartID, !isAdmin); err != nil { - return errs.Wrap(err, "Failed to remove runtime PATH") - } - - return nil -} diff --git a/cmd/state-remote-installer/main.go b/cmd/state-remote-installer/main.go index bf2f913a43..3b0713db2e 100644 --- a/cmd/state-remote-installer/main.go +++ b/cmd/state-remote-installer/main.go @@ -25,9 +25,9 @@ import ( "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/rollbar" "github.com/ActiveState/cli/internal/rtutils/ptr" + "github.com/ActiveState/cli/internal/runbits/errors" "github.com/ActiveState/cli/internal/runbits/panics" "github.com/ActiveState/cli/internal/updater" - "github.com/ActiveState/cli/pkg/cmdlets/errors" ) type Params struct { @@ -95,6 +95,12 @@ func main() { return } + // Store sessionToken to config + err = cfg.Set(anaConst.CfgSessionToken, "remote_"+constants.RemoteInstallerVersion) + if err != nil { + logging.Error("Unable to set session token: " + errs.JoinMessage(err)) + } + an = sync.New(anaConst.SrcStateRemoteInstaller, cfg, nil, out) // Set up prompter diff --git a/cmd/state-svc/internal/resolver/resolver.go b/cmd/state-svc/internal/resolver/resolver.go index cdfc5d1a99..92b3bba7a5 100644 --- a/cmd/state-svc/internal/resolver/resolver.go +++ b/cmd/state-svc/internal/resolver/resolver.go @@ -10,7 +10,6 @@ import ( "time" "github.com/ActiveState/cli/cmd/state-svc/internal/messages" - "github.com/ActiveState/cli/cmd/state-svc/internal/rtusage" "github.com/ActiveState/cli/cmd/state-svc/internal/rtwatcher" genserver "github.com/ActiveState/cli/cmd/state-svc/internal/server/generated" "github.com/ActiveState/cli/internal/analytics/client/sync" @@ -36,7 +35,6 @@ type Resolver struct { messages *messages.Messages updatePoller *poller.Poller authPoller *poller.Poller - usageChecker *rtusage.Checker projectIDCache *projectcache.ID an *sync.Client anForClient *sync.Client // Use separate client for events sent through service so we don't contaminate one with the other @@ -73,8 +71,6 @@ func New(cfg *config.Instance, an *sync.Client, auth *authentication.Auth) (*Res return nil, nil }) - usageChecker := rtusage.NewChecker(cfg, auth) - // Note: source does not matter here, as analytics sent via the resolver have a source // (e.g. State Tool or Executor), and that source will be used. anForClient := sync.New(anaConsts.SrcStateTool, cfg, auth, nil) @@ -83,7 +79,6 @@ func New(cfg *config.Instance, an *sync.Client, auth *authentication.Auth) (*Res msg, pollUpdate, pollAuth, - usageChecker, projectcache.NewID(), an, anForClient, @@ -235,29 +230,6 @@ func (r *Resolver) ReportRuntimeUsage(_ context.Context, pid int, exec, source s return &graph.ReportRuntimeUsageResponse{Received: true}, nil } -func (r *Resolver) CheckRuntimeUsage(_ context.Context, organizationName string) (*graph.CheckRuntimeUsageResponse, error) { - defer func() { handlePanics(recover(), debug.Stack()) }() - - logging.Debug("CheckRuntimeUsage resolver") - - usage, err := r.usageChecker.Check(organizationName) - if err != nil { - return nil, errs.Wrap(err, "Could not check runtime usage: %s", errs.JoinMessage(err)) - } - - if usage == nil { - return &graph.CheckRuntimeUsageResponse{ - Limit: 1, - Usage: 0, - }, nil - } - - return &graph.CheckRuntimeUsageResponse{ - Limit: int(usage.LimitDynamicRuntimes), - Usage: int(usage.ActiveDynamicRuntimes), - }, nil -} - func (r *Resolver) CheckMessages(ctx context.Context, command string, flags []string) ([]*graph.MessageInfo, error) { defer func() { handlePanics(recover(), debug.Stack()) }() logging.Debug("Check messages resolver") diff --git a/cmd/state-svc/internal/rtusage/rtusage.go b/cmd/state-svc/internal/rtusage/rtusage.go deleted file mode 100644 index a2404ccc62..0000000000 --- a/cmd/state-svc/internal/rtusage/rtusage.go +++ /dev/null @@ -1,84 +0,0 @@ -package rtusage - -import ( - "time" - - "github.com/patrickmn/go-cache" - - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/pkg/platform/api/graphql" - "github.com/ActiveState/cli/pkg/platform/api/graphql/model" - "github.com/ActiveState/cli/pkg/platform/api/graphql/request" - "github.com/ActiveState/cli/pkg/platform/authentication" -) - -const cacheKey = "runtime-usage-" - -// Checker is the struct that we use to do checks with -type Checker struct { - config configurable - cache *cache.Cache - auth *authentication.Auth -} - -// configurable defines the configuration function used by the functions in this package -type configurable interface { - ConfigPath() string - GetTime(key string) time.Time - Set(key string, value interface{}) error - Close() error -} - -// NewChecker returns a new instance of the Checker struct -func NewChecker(configuration configurable, auth *authentication.Auth) *Checker { - checker := &Checker{ - configuration, - cache.New(1*time.Hour, 1*time.Hour), - auth, - } - - return checker -} - -// Check will check the runtime usage for the given organization, it may return a cached result -func (c *Checker) Check(organizationName string) (*model.RuntimeUsage, error) { - if cached, ok := c.cache.Get(cacheKey + organizationName); ok { - return cached.(*model.RuntimeUsage), nil - } - - if err := c.auth.Refresh(); err != nil { - return nil, errs.Wrap(err, "Could not refresh authentication") - } - - if !c.auth.Authenticated() { - // Usage information can only be given to authenticated users, and the API doesn't support authentication errors - // so we just don't even attempt it if not authenticated. - return nil, nil - } - - client := graphql.New() - - orgsResponse := model.Organizations{} - if err := client.Run(request.OrganizationsByName(organizationName), &orgsResponse); err != nil { - return nil, errs.Wrap(err, "Could not fetch organization: %s", organizationName) - } - if len(orgsResponse.Organizations) == 0 { - return nil, errs.New("Could not find organization: %s", organizationName) - } - org := orgsResponse.Organizations[0] - - usageResponse := model.RuntimeUsageResponse{} - if err := client.Run(request.RuntimeUsage(org.ID), &usageResponse); err != nil { - return nil, errs.Wrap(err, "Could not fetch runtime usage information") - } - - if len(usageResponse.Usage) == 0 { - logging.Debug("No runtime usage information found for organization: %s", organizationName) - return nil, nil - } - - c.cache.Set(cacheKey+organizationName, &usageResponse.Usage[0], 0) - - return &usageResponse.Usage[0], nil -} diff --git a/cmd/state-svc/internal/server/generated/generated.go b/cmd/state-svc/internal/server/generated/generated.go index 59cc4344fc..4c20a755d2 100644 --- a/cmd/state-svc/internal/server/generated/generated.go +++ b/cmd/state-svc/internal/server/generated/generated.go @@ -54,11 +54,6 @@ type ComplexityRoot struct { Version func(childComplexity int) int } - CheckRuntimeUsageResponse struct { - Limit func(childComplexity int) int - Usage func(childComplexity int) int - } - ConfigChangedResponse struct { Received func(childComplexity int) int } @@ -81,7 +76,6 @@ type ComplexityRoot struct { AnalyticsEvent func(childComplexity int, category string, action string, source string, label *string, dimensionsJSON string) int AvailableUpdate func(childComplexity int, desiredChannel string, desiredVersion string) int CheckMessages func(childComplexity int, command string, flags []string) int - CheckRuntimeUsage func(childComplexity int, organizationName string) int ConfigChanged func(childComplexity int, key string) int FetchLogTail func(childComplexity int) int Projects func(childComplexity int) int @@ -112,7 +106,6 @@ type QueryResolver interface { Projects(ctx context.Context) ([]*graph.Project, error) AnalyticsEvent(ctx context.Context, category string, action string, source string, label *string, dimensionsJSON string) (*graph.AnalyticsEventResponse, error) ReportRuntimeUsage(ctx context.Context, pid int, exec string, source string, dimensionsJSON string) (*graph.ReportRuntimeUsageResponse, error) - CheckRuntimeUsage(ctx context.Context, organizationName string) (*graph.CheckRuntimeUsageResponse, error) CheckMessages(ctx context.Context, command string, flags []string) ([]*graph.MessageInfo, error) ConfigChanged(ctx context.Context, key string) (*graph.ConfigChangedResponse, error) FetchLogTail(ctx context.Context) (string, error) @@ -175,20 +168,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AvailableUpdate.Version(childComplexity), true - case "CheckRuntimeUsageResponse.limit": - if e.complexity.CheckRuntimeUsageResponse.Limit == nil { - break - } - - return e.complexity.CheckRuntimeUsageResponse.Limit(childComplexity), true - - case "CheckRuntimeUsageResponse.usage": - if e.complexity.CheckRuntimeUsageResponse.Usage == nil { - break - } - - return e.complexity.CheckRuntimeUsageResponse.Usage(childComplexity), true - case "ConfigChangedResponse.received": if e.complexity.ConfigChangedResponse.Received == nil { break @@ -288,18 +267,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.CheckMessages(childComplexity, args["command"].(string), args["flags"].([]string)), true - case "Query.checkRuntimeUsage": - if e.complexity.Query.CheckRuntimeUsage == nil { - break - } - - args, err := ec.field_Query_checkRuntimeUsage_args(context.TODO(), rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Query.CheckRuntimeUsage(childComplexity, args["organizationName"].(string)), true - case "Query.configChanged": if e.complexity.Query.ConfigChanged == nil { break @@ -479,11 +446,6 @@ type ReportRuntimeUsageResponse { received: Boolean! } -type CheckRuntimeUsageResponse { - limit: Int! - usage: Int! -} - enum MessageRepeatType { Disabled Constantly @@ -519,7 +481,6 @@ type Query { projects: [Project]! analyticsEvent(category: String!, action: String!, source: String!, label: String, dimensionsJson: String!): AnalyticsEventResponse reportRuntimeUsage(pid: Int!, exec: String!, source: String!, dimensionsJson: String!): ReportRuntimeUsageResponse - checkRuntimeUsage(organizationName: String!): CheckRuntimeUsageResponse checkMessages(command: String!, flags: [String!]!): [MessageInfo!]! configChanged(key: String!): ConfigChangedResponse fetchLogTail: String! @@ -650,21 +611,6 @@ func (ec *executionContext) field_Query_checkMessages_args(ctx context.Context, return args, nil } -func (ec *executionContext) field_Query_checkRuntimeUsage_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { - var err error - args := map[string]interface{}{} - var arg0 string - if tmp, ok := rawArgs["organizationName"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationName")) - arg0, err = ec.unmarshalNString2string(ctx, tmp) - if err != nil { - return nil, err - } - } - args["organizationName"] = arg0 - return args, nil -} - func (ec *executionContext) field_Query_configChanged_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1024,94 +970,6 @@ func (ec *executionContext) fieldContext_AvailableUpdate_sha256(ctx context.Cont return fc, nil } -func (ec *executionContext) _CheckRuntimeUsageResponse_limit(ctx context.Context, field graphql.CollectedField, obj *graph.CheckRuntimeUsageResponse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_CheckRuntimeUsageResponse_limit(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Limit, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_CheckRuntimeUsageResponse_limit(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "CheckRuntimeUsageResponse", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _CheckRuntimeUsageResponse_usage(ctx context.Context, field graphql.CollectedField, obj *graph.CheckRuntimeUsageResponse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_CheckRuntimeUsageResponse_usage(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Usage, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_CheckRuntimeUsageResponse_usage(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "CheckRuntimeUsageResponse", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} - func (ec *executionContext) _ConfigChangedResponse_received(ctx context.Context, field graphql.CollectedField, obj *graph.ConfigChangedResponse) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ConfigChangedResponse_received(ctx, field) if err != nil { @@ -1774,63 +1632,6 @@ func (ec *executionContext) fieldContext_Query_reportRuntimeUsage(ctx context.Co return fc, nil } -func (ec *executionContext) _Query_checkRuntimeUsage(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_checkRuntimeUsage(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().CheckRuntimeUsage(rctx, fc.Args["organizationName"].(string)) - }) - if err != nil { - ec.Error(ctx, err) - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*graph.CheckRuntimeUsageResponse) - fc.Result = res - return ec.marshalOCheckRuntimeUsageResponse2ᚖgithubᚗcomᚋActiveStateᚋcliᚋinternalᚋgraphᚐCheckRuntimeUsageResponse(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_Query_checkRuntimeUsage(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Query", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "limit": - return ec.fieldContext_CheckRuntimeUsageResponse_limit(ctx, field) - case "usage": - return ec.fieldContext_CheckRuntimeUsageResponse_usage(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type CheckRuntimeUsageResponse", field.Name) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query_checkRuntimeUsage_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return - } - return fc, nil -} - func (ec *executionContext) _Query_checkMessages(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_checkMessages(ctx, field) if err != nil { @@ -4309,41 +4110,6 @@ func (ec *executionContext) _AvailableUpdate(ctx context.Context, sel ast.Select return out } -var checkRuntimeUsageResponseImplementors = []string{"CheckRuntimeUsageResponse"} - -func (ec *executionContext) _CheckRuntimeUsageResponse(ctx context.Context, sel ast.SelectionSet, obj *graph.CheckRuntimeUsageResponse) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, checkRuntimeUsageResponseImplementors) - out := graphql.NewFieldSet(fields) - var invalids uint32 - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("CheckRuntimeUsageResponse") - case "limit": - - out.Values[i] = ec._CheckRuntimeUsageResponse_limit(ctx, field, obj) - - if out.Values[i] == graphql.Null { - invalids++ - } - case "usage": - - out.Values[i] = ec._CheckRuntimeUsageResponse_usage(ctx, field, obj) - - if out.Values[i] == graphql.Null { - invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch() - if invalids > 0 { - return graphql.Null - } - return out -} - var configChangedResponseImplementors = []string{"ConfigChangedResponse"} func (ec *executionContext) _ConfigChangedResponse(ctx context.Context, sel ast.SelectionSet, obj *graph.ConfigChangedResponse) graphql.Marshaler { @@ -4585,26 +4351,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) } - out.Concurrently(i, func() graphql.Marshaler { - return rrm(innerCtx) - }) - case "checkRuntimeUsage": - field := field - - innerFunc := func(ctx context.Context) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Query_checkRuntimeUsage(ctx, field) - return res - } - - rrm := func(ctx context.Context) graphql.Marshaler { - return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) - } - out.Concurrently(i, func() graphql.Marshaler { return rrm(innerCtx) }) @@ -5620,13 +5366,6 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } -func (ec *executionContext) marshalOCheckRuntimeUsageResponse2ᚖgithubᚗcomᚋActiveStateᚋcliᚋinternalᚋgraphᚐCheckRuntimeUsageResponse(ctx context.Context, sel ast.SelectionSet, v *graph.CheckRuntimeUsageResponse) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return ec._CheckRuntimeUsageResponse(ctx, sel, v) -} - func (ec *executionContext) marshalOConfigChangedResponse2ᚖgithubᚗcomᚋActiveStateᚋcliᚋinternalᚋgraphᚐConfigChangedResponse(ctx context.Context, sel ast.SelectionSet, v *graph.ConfigChangedResponse) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/cmd/state-svc/main.go b/cmd/state-svc/main.go index 0234ad634b..e00aa575e0 100644 --- a/cmd/state-svc/main.go +++ b/cmd/state-svc/main.go @@ -195,12 +195,12 @@ func runForeground(cfg *config.Instance, an *anaSync.Client, auth *authenticatio ctx, cancel := context.WithCancel(context.Background()) defer cancel() - logFile := logging.FilePath() - logging.Debug("Logging to %q", logFile) + logFileName := logging.FileName() + logging.Debug("Logging to %q", logging.FilePathFor(logFileName)) stopTimer := logging.StartRotateLogTimer() defer stopTimer() - p := NewService(ctx, cfg, an, auth, logFile) + p := NewService(ctx, cfg, an, auth, logFileName) if argText != "" { argText = fmt.Sprintf(" (invoked by %q)", argText) diff --git a/cmd/state-svc/schema/schema.graphqls b/cmd/state-svc/schema/schema.graphqls index 1cf1f47a3a..719f36a4eb 100644 --- a/cmd/state-svc/schema/schema.graphqls +++ b/cmd/state-svc/schema/schema.graphqls @@ -31,11 +31,6 @@ type ReportRuntimeUsageResponse { received: Boolean! } -type CheckRuntimeUsageResponse { - limit: Int! - usage: Int! -} - enum MessageRepeatType { Disabled Constantly @@ -71,7 +66,6 @@ type Query { projects: [Project]! analyticsEvent(category: String!, action: String!, source: String!, label: String, dimensionsJson: String!): AnalyticsEventResponse reportRuntimeUsage(pid: Int!, exec: String!, source: String!, dimensionsJson: String!): ReportRuntimeUsageResponse - checkRuntimeUsage(organizationName: String!): CheckRuntimeUsageResponse checkMessages(command: String!, flags: [String!]!): [MessageInfo!]! configChanged(key: String!): ConfigChangedResponse fetchLogTail: String! diff --git a/cmd/state-svc/service.go b/cmd/state-svc/service.go index b290cd44a7..58c9a56bb1 100644 --- a/cmd/state-svc/service.go +++ b/cmd/state-svc/service.go @@ -12,6 +12,7 @@ import ( "github.com/ActiveState/cli/internal/config" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/ipc" + "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/svcctl" "github.com/ActiveState/cli/pkg/platform/authentication" @@ -55,12 +56,13 @@ func (s *service) Start() error { svcctl.HTTPAddrHandler(portText(s.server)), svcctl.LogFileHandler(s.logFile), svcctl.HeartbeatHandler(s.cfg, s.server.Resolver(), s.an), + svcctl.ExitCodeHandler(s.cfg, s.server.Resolver(), s.an), } s.ipcSrv = ipc.NewServer(s.ctx, spath, reqHandlers...) err = s.ipcSrv.Start() if err != nil { if errors.Is(err, ipc.ErrInUse) { - return errs.Wrap(err, "An existing server instance appears to be in use") + return locale.WrapInputError(err, "err_service_ipc_in_use", "An existing server instance appears to be in use") } return errs.Wrap(err, "Failed to start server") } diff --git a/cmd/state/autoupdate.go b/cmd/state/autoupdate.go index 8551c89f5e..d00406c164 100644 --- a/cmd/state/autoupdate.go +++ b/cmd/state/autoupdate.go @@ -73,24 +73,12 @@ func autoUpdate(svc *model.SvcModel, args []string, cfg *config.Instance, an ana err = up.InstallBlocking("") if err != nil { + if errs.Matches(err, &updater.ErrorInProgress{}) { + return false, nil // ignore + } if os.IsPermission(err) { - an.EventWithLabel(anaConst.CatUpdates, anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, &dimensions.Values{ - TargetVersion: ptr.To(avUpdate.Version), - Error: ptr.To("Could not update the state tool due to insufficient permissions."), - }) return false, locale.WrapInputError(err, locale.Tl("auto_update_permission_err", "", constants.DocumentationURL, errs.JoinMessage(err))) } - if errs.Matches(err, &updater.ErrorInProgress{}) { - an.EventWithLabel(anaConst.CatUpdates, anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, &dimensions.Values{ - TargetVersion: ptr.To(avUpdate.Version), - Error: ptr.To(anaConst.UpdateErrorInProgress), - }) - return false, nil - } - an.EventWithLabel(anaConst.CatUpdates, anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, &dimensions.Values{ - TargetVersion: ptr.To(avUpdate.Version), - Error: ptr.To(anaConst.UpdateErrorInstallFailed), - }) return false, locale.WrapError(err, locale.T("auto_update_failed")) } diff --git a/cmd/state/internal/cmdtree/activate.go b/cmd/state/internal/cmdtree/activate.go index d675e300b4..b87c43f6f3 100644 --- a/cmd/state/internal/cmdtree/activate.go +++ b/cmd/state/internal/cmdtree/activate.go @@ -87,6 +87,5 @@ func newActivateCommand(prime *primer.Values) *captain.Command { }, ) cmd.SetGroup(EnvironmentUsageGroup) - cmd.SetDoesNotSupportStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/auth.go b/cmd/state/internal/cmdtree/auth.go index 8bb549c1c4..8ae7a81ca8 100644 --- a/cmd/state/internal/cmdtree/auth.go +++ b/cmd/state/internal/cmdtree/auth.go @@ -54,7 +54,7 @@ func newAuthCommand(prime *primer.Values, globals *globalOptions) *captain.Comma params.NonInteractive = globals.NonInteractive return authRunner.Run(¶ms) }, - ).SetGroup(PlatformGroup) + ).SetGroup(PlatformGroup).SetSupportsStructuredOutput() } func newSignupCommand(prime *primer.Values) *captain.Command { @@ -70,7 +70,7 @@ func newSignupCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, args []string) error { return signupRunner.Run(¶ms) }, - ).SetDoesNotSupportStructuredOutput() + ) } func newLogoutCommand(prime *primer.Values) *captain.Command { @@ -85,5 +85,5 @@ func newLogoutCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, args []string) error { return logoutRunner.Run() }, - ).SetDoesNotSupportStructuredOutput() + ) } diff --git a/cmd/state/internal/cmdtree/branch.go b/cmd/state/internal/cmdtree/branch.go index e361c1b1ea..247494ccd9 100644 --- a/cmd/state/internal/cmdtree/branch.go +++ b/cmd/state/internal/cmdtree/branch.go @@ -20,7 +20,7 @@ func newBranchCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(_ *captain.Command, _ []string) error { return runner.Run() - }).SetGroup(PlatformGroup).SetUnstable(true) + }).SetGroup(PlatformGroup).SetSupportsStructuredOutput().SetUnstable(true) } func newBranchAddCommand(prime *primer.Values) *captain.Command { @@ -44,7 +44,7 @@ func newBranchAddCommand(prime *primer.Values) *captain.Command { }, func(_ *captain.Command, _ []string) error { return runner.Run(params) - }) + }).SetSupportsStructuredOutput() } func newBranchSwitchCommand(prime *primer.Values) *captain.Command { @@ -69,6 +69,7 @@ func newBranchSwitchCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params) }) + cmd.SetSupportsStructuredOutput() // We set this command to hidden for backwards compatibility as we cannot // alias `state switch` to `state branch switch` cmd.SetHidden(true) diff --git a/cmd/state/internal/cmdtree/bundles.go b/cmd/state/internal/cmdtree/bundles.go index d0932e7b37..c12e8f4e79 100644 --- a/cmd/state/internal/cmdtree/bundles.go +++ b/cmd/state/internal/cmdtree/bundles.go @@ -39,7 +39,7 @@ func newBundlesCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params, model.NamespaceBundle) }, - ).SetGroup(PackagesGroup).SetUnstable(true) + ).SetGroup(PackagesGroup).SetSupportsStructuredOutput().SetUnstable(true) } func newBundleInstallCommand(prime *primer.Values) *captain.Command { @@ -64,7 +64,7 @@ func newBundleInstallCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params, model.NamespaceBundle) }, - ) + ).SetSupportsStructuredOutput() } func newBundleUninstallCommand(prime *primer.Values) *captain.Command { @@ -89,7 +89,7 @@ func newBundleUninstallCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params, model.NamespaceBundle) }, - ) + ).SetSupportsStructuredOutput() } func newBundlesSearchCommand(prime *primer.Values) *captain.Command { @@ -125,5 +125,5 @@ func newBundlesSearchCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params, model.NamespaceBundle) }, - ) + ).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/checkout.go b/cmd/state/internal/cmdtree/checkout.go index f8f03613a5..bf7e7eb25b 100644 --- a/cmd/state/internal/cmdtree/checkout.go +++ b/cmd/state/internal/cmdtree/checkout.go @@ -53,5 +53,6 @@ func newCheckoutCommand(prime *primer.Values) *captain.Command { }, ) cmd.SetGroup(EnvironmentSetupGroup) + cmd.SetSupportsStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/clean.go b/cmd/state/internal/cmdtree/clean.go index 1a0e9bbe72..758f6bb4a1 100644 --- a/cmd/state/internal/cmdtree/clean.go +++ b/cmd/state/internal/cmdtree/clean.go @@ -19,7 +19,7 @@ func newCleanCommand(prime *primer.Values) *captain.Command { prime.Output().Print(ccmd.Help()) return nil }, - ).SetGroup(UtilsGroup) + ).SetGroup(UtilsGroup).SetSupportsStructuredOutput() } func newCleanUninstallCommand(prime *primer.Values, globals *globalOptions) *captain.Command { @@ -62,7 +62,7 @@ func newCleanUninstallCommand(prime *primer.Values, globals *globalOptions) *cap params.NonInteractive = globals.NonInteractive // distinct from --force return runner.Run(¶ms) }, - ).SetDoesNotSupportStructuredOutput() + ) } func newCleanCacheCommand(prime *primer.Values, globals *globalOptions) *captain.Command { @@ -86,7 +86,7 @@ func newCleanCacheCommand(prime *primer.Values, globals *globalOptions) *captain params.Force = globals.NonInteractive return runner.Run(¶ms) }, - ).SetDoesNotSupportStructuredOutput() + ) } func newCleanConfigCommand(prime *primer.Values) *captain.Command { @@ -109,5 +109,5 @@ func newCleanConfigCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, _ []string) error { return runner.Run(¶ms) }, - ).SetDoesNotSupportStructuredOutput() + ) } diff --git a/cmd/state/internal/cmdtree/cmdtree.go b/cmd/state/internal/cmdtree/cmdtree.go index 2a26337532..ec3cf4b5fd 100644 --- a/cmd/state/internal/cmdtree/cmdtree.go +++ b/cmd/state/internal/cmdtree/cmdtree.go @@ -204,7 +204,7 @@ func New(prime *primer.Values, args ...string) *CmdTree { refreshCmd, newSwitchCommand(prime), newTestCommand(prime), - newCommitCommand(prime), + //newCommitCommand(prime), // re-enable in DX-2307 ) return &CmdTree{ @@ -278,14 +278,6 @@ func newStateCommand(globals *globalOptions, prime *primer.Values) *captain.Comm Persist: true, Value: &globals.Output, }, - { - /* This option is only used for the vscode extension: It prevents the integrated terminal to close immediately after an error occurs, such that the user can read the message */ - Name: "confirm-exit-on-error", // Name and Shorthand should be kept in sync with cmd/state/output.go - Description: "prompts the user to press enter before exiting, when an error occurs", - Persist: true, - Hidden: true, // No need to add this to help messages - Value: &opts.ConfirmExit, - }, { Name: "non-interactive", // Name and Shorthand should be kept in sync with cmd/state/output.go Description: locale.T("flag_state_non_interactive_description"), @@ -321,6 +313,7 @@ func newStateCommand(globals *globalOptions, prime *primer.Values) *captain.Comm cmd.SetHasVariableArguments() cmd.OnExecStart(cmdCall.OnExecStart) cmd.OnExecStop(cmdCall.OnExecStop) + cmd.SetSupportsStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/commit.go b/cmd/state/internal/cmdtree/commit.go index 01ccce6b39..958fd0d6c2 100644 --- a/cmd/state/internal/cmdtree/commit.go +++ b/cmd/state/internal/cmdtree/commit.go @@ -23,6 +23,8 @@ func newCommitCommand(prime *primer.Values) *captain.Command { ) cmd.SetGroup(EnvironmentSetupGroup) + cmd.SetSupportsStructuredOutput() + cmd.SetUnstable(true) return cmd } diff --git a/cmd/state/internal/cmdtree/config.go b/cmd/state/internal/cmdtree/config.go index 8627adb07f..76d9daa495 100644 --- a/cmd/state/internal/cmdtree/config.go +++ b/cmd/state/internal/cmdtree/config.go @@ -21,7 +21,7 @@ func newConfigCommand(prime *primer.Values) *captain.Command { return err } return runner.Run(ccmd.Usage) - }).SetGroup(UtilsGroup) + }).SetGroup(UtilsGroup).SetSupportsStructuredOutput() } func newConfigGetCommand(prime *primer.Values) *captain.Command { @@ -43,7 +43,7 @@ func newConfigGetCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, args []string) error { runner := config.NewGet(prime) return runner.Run(params) - }) + }).SetSupportsStructuredOutput() } func newConfigSetCommand(prime *primer.Values) *captain.Command { @@ -71,5 +71,5 @@ func newConfigSetCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, args []string) error { runner := config.NewSet(prime) return runner.Run(params) - }) + }).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/cve.go b/cmd/state/internal/cmdtree/cve.go index c327f41667..0aa91b5a6e 100644 --- a/cmd/state/internal/cmdtree/cve.go +++ b/cmd/state/internal/cmdtree/cve.go @@ -24,6 +24,7 @@ func newCveCommand(prime *primer.Values) *captain.Command { ) cmd.SetGroup(PlatformGroup) cmd.SetAliases("cve") + cmd.SetSupportsStructuredOutput() cmd.SetUnstable(true) return cmd } @@ -50,7 +51,7 @@ func newReportCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return report.Run(¶ms) }, - ) + ).SetSupportsStructuredOutput() } func newOpenCommand(prime *primer.Values) *captain.Command { @@ -73,5 +74,5 @@ func newOpenCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return open.Run(params) }, - ).SetDoesNotSupportStructuredOutput() + ) } diff --git a/cmd/state/internal/cmdtree/deploy.go b/cmd/state/internal/cmdtree/deploy.go index 8a424b39ac..43efa22d67 100644 --- a/cmd/state/internal/cmdtree/deploy.go +++ b/cmd/state/internal/cmdtree/deploy.go @@ -54,7 +54,6 @@ func newDeployCommand(prime *primer.Values) *captain.Command { }) cmd.SetGroup(EnvironmentSetupGroup) cmd.SetHidden(true) - cmd.SetDoesNotSupportStructuredOutput() return cmd } @@ -85,7 +84,7 @@ func newDeployInstallCommand(prime *primer.Values) *captain.Command { }, func(cmd *captain.Command, args []string) error { return runner.Run(params) - }).SetDoesNotSupportStructuredOutput() + }) } func newDeployConfigureCommand(prime *primer.Values) *captain.Command { @@ -124,7 +123,7 @@ func newDeployConfigureCommand(prime *primer.Values) *captain.Command { }, func(cmd *captain.Command, args []string) error { return runner.Run(params) - }).SetDoesNotSupportStructuredOutput() + }) } func newDeploySymlinkCommand(prime *primer.Values) *captain.Command { @@ -159,7 +158,7 @@ func newDeploySymlinkCommand(prime *primer.Values) *captain.Command { }, func(cmd *captain.Command, args []string) error { return runner.Run(params) - }).SetDoesNotSupportStructuredOutput() + }) } func newDeployReportCommand(prime *primer.Values) *captain.Command { @@ -189,7 +188,7 @@ func newDeployReportCommand(prime *primer.Values) *captain.Command { }, func(cmd *captain.Command, args []string) error { return runner.Run(params) - }).SetDoesNotSupportStructuredOutput() + }) } func newDeployUninstallCommand(prime *primer.Values) *captain.Command { @@ -221,5 +220,5 @@ func newDeployUninstallCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(cmd *captain.Command, args []string) error { return runner.Run(params) - }).SetDoesNotSupportStructuredOutput() + }) } diff --git a/cmd/state/internal/cmdtree/events.go b/cmd/state/internal/cmdtree/events.go index 88634808a4..7ffae42333 100644 --- a/cmd/state/internal/cmdtree/events.go +++ b/cmd/state/internal/cmdtree/events.go @@ -19,7 +19,7 @@ func newEventsCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(cmd *captain.Command, args []string) error { return runner.Run() - }).SetGroup(AutomationGroup).SetUnstable(true) + }).SetGroup(AutomationGroup).SetSupportsStructuredOutput().SetUnstable(true) } func newEventsLogCommand(prime *primer.Values) *captain.Command { @@ -42,5 +42,5 @@ func newEventsLogCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(cmd *captain.Command, args []string) error { return runner.Run(¶ms) - }).SetDoesNotSupportStructuredOutput() + }) } diff --git a/cmd/state/internal/cmdtree/exec.go b/cmd/state/internal/cmdtree/exec.go index 5d237901c3..60a59b08c8 100644 --- a/cmd/state/internal/cmdtree/exec.go +++ b/cmd/state/internal/cmdtree/exec.go @@ -45,7 +45,6 @@ func newExecCommand(prime *primer.Values, args ...string) *captain.Command { cmd.SetGroup(EnvironmentUsageGroup) cmd.SetHasVariableArguments() - cmd.SetDoesNotSupportStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/export.go b/cmd/state/internal/cmdtree/export.go index bb66f88259..164b269356 100644 --- a/cmd/state/internal/cmdtree/export.go +++ b/cmd/state/internal/cmdtree/export.go @@ -24,7 +24,7 @@ func newExportCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(ccmd *captain.Command, args []string) error { return runner.Run(ccmd) - }).SetGroup(UtilsGroup) + }).SetGroup(UtilsGroup).SetSupportsStructuredOutput() } func newRecipeCommand(prime *primer.Values) *captain.Command { @@ -59,7 +59,7 @@ func newRecipeCommand(prime *primer.Values) *captain.Command { }, func(_ *captain.Command, _ []string) error { return recipe.Run(¶ms) - }).SetUnstable(true) + }).SetSupportsStructuredOutput().SetUnstable(true) } func newJWTCommand(prime *primer.Values) *captain.Command { @@ -76,7 +76,7 @@ func newJWTCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(ccmd *captain.Command, args []string) error { return jwt.Run(¶ms) - }) + }).SetSupportsStructuredOutput() } func newPrivateKeyCommand(prime *primer.Values) *captain.Command { @@ -93,7 +93,7 @@ func newPrivateKeyCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(ccmd *captain.Command, args []string) error { return privateKey.Run(¶ms) - }) + }).SetSupportsStructuredOutput() } func newAPIKeyCommand(prime *primer.Values) *captain.Command { @@ -117,7 +117,7 @@ func newAPIKeyCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, args []string) error { params.IsAuthed = prime.Auth().Authenticated return apikey.Run(params) - }) + }).SetSupportsStructuredOutput() } func newExportConfigCommand(prime *primer.Values) *captain.Command { @@ -142,7 +142,7 @@ func newExportConfigCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(ccmd *captain.Command, _ []string) error { return runner.Run(ccmd, ¶ms) - }).SetUnstable(true) + }).SetSupportsStructuredOutput().SetUnstable(true) } func newExportGithubActionCommand(prime *primer.Values) *captain.Command { @@ -158,7 +158,7 @@ func newExportGithubActionCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(ccmd *captain.Command, _ []string) error { return runner.Run(¶ms) - }).SetUnstable(true).SetDoesNotSupportStructuredOutput() + }).SetUnstable(true) } func newExportDocsCommand(prime *primer.Values) *captain.Command { @@ -177,7 +177,6 @@ func newExportDocsCommand(prime *primer.Values) *captain.Command { }) cmd.SetHidden(true) - cmd.SetDoesNotSupportStructuredOutput() return cmd } @@ -196,6 +195,7 @@ func newExportEnvCommand(prime *primer.Values) *captain.Command { return runner.Run() }) + cmd.SetSupportsStructuredOutput() cmd.SetUnstable(true) return cmd diff --git a/cmd/state/internal/cmdtree/fork.go b/cmd/state/internal/cmdtree/fork.go index 0bf35fea61..258d393a7f 100644 --- a/cmd/state/internal/cmdtree/fork.go +++ b/cmd/state/internal/cmdtree/fork.go @@ -43,5 +43,5 @@ func newForkCommand(prime *primer.Values) *captain.Command { }, func(cmd *captain.Command, args []string) error { return runner.Run(params) - }).SetGroup(VCSGroup) + }).SetGroup(VCSGroup).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/hello_example.go b/cmd/state/internal/cmdtree/hello_example.go index 9edd798d72..d15d1a6f6e 100644 --- a/cmd/state/internal/cmdtree/hello_example.go +++ b/cmd/state/internal/cmdtree/hello_example.go @@ -56,6 +56,8 @@ func newHelloCommand(prime *primer.Values) *captain.Command { // The group is used to group together commands in the --help output cmd.SetGroup(UtilsGroup) + // Commands should support structured (JSON) output whenever possible. + cmd.SetSupportsStructuredOutput() // Any new command should be marked unstable for the first release it goes out in. 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. diff --git a/cmd/state/internal/cmdtree/history.go b/cmd/state/internal/cmdtree/history.go index 6f19a70f44..b954b99e7a 100644 --- a/cmd/state/internal/cmdtree/history.go +++ b/cmd/state/internal/cmdtree/history.go @@ -21,5 +21,5 @@ func newHistoryCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, _ []string) error { return initRunner.Run(¶ms) }, - ).SetGroup(VCSGroup) + ).SetGroup(VCSGroup).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/init.go b/cmd/state/internal/cmdtree/init.go index 08d2f33a80..ac6e6df3d2 100644 --- a/cmd/state/internal/cmdtree/init.go +++ b/cmd/state/internal/cmdtree/init.go @@ -48,5 +48,5 @@ func newInitCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, _ []string) error { return initRunner.Run(¶ms) }, - ).SetGroup(EnvironmentSetupGroup) + ).SetGroup(EnvironmentSetupGroup).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/invite.go b/cmd/state/internal/cmdtree/invite.go index 93fbfe8b34..85235c6501 100644 --- a/cmd/state/internal/cmdtree/invite.go +++ b/cmd/state/internal/cmdtree/invite.go @@ -44,7 +44,6 @@ func newInviteCommand(prime *primer.Values) *captain.Command { cmd.SetGroup(PlatformGroup) cmd.SetUnstable(true) cmd.SetHasVariableArguments() - cmd.SetDoesNotSupportStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/languages.go b/cmd/state/internal/cmdtree/languages.go index 1c746c4c9a..93480fa2e2 100644 --- a/cmd/state/internal/cmdtree/languages.go +++ b/cmd/state/internal/cmdtree/languages.go @@ -20,7 +20,7 @@ func newLanguagesCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, _ []string) error { return runner.Run() }, - ).SetGroup(PlatformGroup).SetUnstable(true) + ).SetGroup(PlatformGroup).SetSupportsStructuredOutput().SetUnstable(true) } func newLanguageInstallCommand(prime *primer.Values) *captain.Command { @@ -45,5 +45,5 @@ func newLanguageInstallCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, _ []string) error { return runner.Run(¶ms) }, - ) + ).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/learn.go b/cmd/state/internal/cmdtree/learn.go index 1e09441d50..541413f81c 100644 --- a/cmd/state/internal/cmdtree/learn.go +++ b/cmd/state/internal/cmdtree/learn.go @@ -19,5 +19,5 @@ func newLearnCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(cmd *captain.Command, args []string) error { return learnRunner.Run() - }).SetGroup(UtilsGroup).SetDoesNotSupportStructuredOutput() + }).SetGroup(UtilsGroup) } diff --git a/cmd/state/internal/cmdtree/organizations.go b/cmd/state/internal/cmdtree/organizations.go index 5c52df8b37..4ef76984a0 100644 --- a/cmd/state/internal/cmdtree/organizations.go +++ b/cmd/state/internal/cmdtree/organizations.go @@ -26,6 +26,7 @@ func newOrganizationsCommand(prime *primer.Values) *captain.Command { cmd.SetGroup(PlatformGroup) cmd.SetAliases("orgs") + cmd.SetSupportsStructuredOutput() cmd.SetUnstable(true) return cmd diff --git a/cmd/state/internal/cmdtree/packages.go b/cmd/state/internal/cmdtree/packages.go index e3449a3e1f..f3c172a019 100644 --- a/cmd/state/internal/cmdtree/packages.go +++ b/cmd/state/internal/cmdtree/packages.go @@ -43,6 +43,7 @@ func newPackagesCommand(prime *primer.Values) *captain.Command { cmd.SetGroup(PackagesGroup) cmd.SetAliases("pkg", "package") + cmd.SetSupportsStructuredOutput() return cmd } @@ -69,7 +70,7 @@ func newInstallCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params, model.NamespacePackage) }, - ).SetGroup(PackagesGroup) + ).SetGroup(PackagesGroup).SetSupportsStructuredOutput() } func newUninstallCommand(prime *primer.Values) *captain.Command { @@ -94,7 +95,7 @@ func newUninstallCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params, model.NamespacePackage) }, - ).SetGroup(PackagesGroup) + ).SetGroup(PackagesGroup).SetSupportsStructuredOutput() } func newImportCommand(prime *primer.Values, globals *globalOptions) *captain.Command { @@ -120,7 +121,7 @@ func newImportCommand(prime *primer.Values, globals *globalOptions) *captain.Com params.NonInteractive = globals.NonInteractive return runner.Run(params) }, - ).SetGroup(PackagesGroup) + ).SetGroup(PackagesGroup).SetSupportsStructuredOutput() } func newSearchCommand(prime *primer.Values) *captain.Command { @@ -156,7 +157,7 @@ func newSearchCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params, model.NamespacePackage) }, - ).SetGroup(PackagesGroup).SetUnstable(true) + ).SetGroup(PackagesGroup).SetSupportsStructuredOutput().SetUnstable(true) } func newInfoCommand(prime *primer.Values) *captain.Command { @@ -187,5 +188,5 @@ func newInfoCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params, model.NamespacePackage) }, - ).SetGroup(PackagesGroup) + ).SetGroup(PackagesGroup).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/platforms.go b/cmd/state/internal/cmdtree/platforms.go index 640a1de6f9..0b149f4aad 100644 --- a/cmd/state/internal/cmdtree/platforms.go +++ b/cmd/state/internal/cmdtree/platforms.go @@ -20,7 +20,7 @@ func newPlatformsCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run() }, - ).SetGroup(PlatformGroup).SetUnstable(true) + ).SetGroup(PlatformGroup).SetSupportsStructuredOutput().SetUnstable(true) } func newPlatformsSearchCommand(prime *primer.Values) *captain.Command { @@ -36,7 +36,7 @@ func newPlatformsSearchCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run() }, - ) + ).SetSupportsStructuredOutput() } func newPlatformsAddCommand(prime *primer.Values) *captain.Command { @@ -67,7 +67,7 @@ func newPlatformsAddCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params) }, - ) + ).SetSupportsStructuredOutput() } func newPlatformsRemoveCommand(prime *primer.Values) *captain.Command { @@ -98,5 +98,5 @@ func newPlatformsRemoveCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params) }, - ) + ).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/prepare.go b/cmd/state/internal/cmdtree/prepare.go index 3269c4299c..43c2c4b5f0 100644 --- a/cmd/state/internal/cmdtree/prepare.go +++ b/cmd/state/internal/cmdtree/prepare.go @@ -23,7 +23,6 @@ func newPrepareCommand(prime *primer.Values) *captain.Command { ) cmd.SetHidden(true) - cmd.SetDoesNotSupportStructuredOutput() return cmd } @@ -44,7 +43,6 @@ func newPrepareCompletionsCommand(prime *primer.Values) *captain.Command { ) cmd.SetHidden(true) - cmd.SetDoesNotSupportStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/projects.go b/cmd/state/internal/cmdtree/projects.go index 8496e6f03c..1680559907 100644 --- a/cmd/state/internal/cmdtree/projects.go +++ b/cmd/state/internal/cmdtree/projects.go @@ -22,7 +22,7 @@ func newProjectsCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, args []string) error { return runner.Run(params) }, - ).SetGroup(ProjectUsageGroup) + ).SetGroup(ProjectUsageGroup).SetSupportsStructuredOutput() } func newRemoteProjectsCommand(prime *primer.Values) *captain.Command { @@ -39,7 +39,7 @@ func newRemoteProjectsCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, args []string) error { return runner.RunRemote(params) }, - ).SetGroup(ProjectUsageGroup) + ).SetGroup(ProjectUsageGroup).SetSupportsStructuredOutput() } func newProjectsEditCommand(prime *primer.Values) *captain.Command { @@ -84,7 +84,6 @@ func newProjectsEditCommand(prime *primer.Values) *captain.Command { ) cmd.SetGroup(ProjectUsageGroup) - cmd.SetDoesNotSupportStructuredOutput() cmd.SetUnstable(true) return cmd @@ -113,7 +112,6 @@ func newDeleteProjectsCommand(prime *primer.Values) *captain.Command { }, ) cmd.SetGroup(ProjectUsageGroup) - cmd.SetDoesNotSupportStructuredOutput() cmd.SetUnstable(true) return cmd @@ -148,7 +146,6 @@ func newMoveProjectsCommand(prime *primer.Values) *captain.Command { }, ) cmd.SetGroup(ProjectUsageGroup) - cmd.SetDoesNotSupportStructuredOutput() cmd.SetUnstable(true) return cmd diff --git a/cmd/state/internal/cmdtree/protocol.go b/cmd/state/internal/cmdtree/protocol.go index 46022bad4c..f968b1f28f 100644 --- a/cmd/state/internal/cmdtree/protocol.go +++ b/cmd/state/internal/cmdtree/protocol.go @@ -30,7 +30,6 @@ func newProtocolCommand(prime *primer.Values) *captain.Command { }, ) cmd.SetHidden(true) - cmd.SetDoesNotSupportStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/pull.go b/cmd/state/internal/cmdtree/pull.go index 3b687ea5c5..d673c00a2a 100644 --- a/cmd/state/internal/cmdtree/pull.go +++ b/cmd/state/internal/cmdtree/pull.go @@ -29,5 +29,5 @@ func newPullCommand(prime *primer.Values, globals *globalOptions) *captain.Comma func(cmd *captain.Command, args []string) error { params.Force = globals.NonInteractive return runner.Run(params) - }).SetGroup(VCSGroup) + }).SetGroup(VCSGroup).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/push.go b/cmd/state/internal/cmdtree/push.go index 1a1db09032..78a30bd844 100644 --- a/cmd/state/internal/cmdtree/push.go +++ b/cmd/state/internal/cmdtree/push.go @@ -32,5 +32,5 @@ func newPushCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, args []string) error { return pushRunner.Run(params) }, - ).SetGroup(VCSGroup) + ).SetGroup(VCSGroup).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/refresh.go b/cmd/state/internal/cmdtree/refresh.go index 6cbe4cc7d1..025cb66a55 100644 --- a/cmd/state/internal/cmdtree/refresh.go +++ b/cmd/state/internal/cmdtree/refresh.go @@ -33,6 +33,7 @@ func newRefreshCommand(prime *primer.Values) *captain.Command { }, ) cmd.SetGroup(EnvironmentUsageGroup) + cmd.SetSupportsStructuredOutput() cmd.SetUnstable(true) return cmd } diff --git a/cmd/state/internal/cmdtree/reset.go b/cmd/state/internal/cmdtree/reset.go index 688641c99a..44f550cc09 100644 --- a/cmd/state/internal/cmdtree/reset.go +++ b/cmd/state/internal/cmdtree/reset.go @@ -28,5 +28,5 @@ func newResetCommand(prime *primer.Values, globals *globalOptions) *captain.Comm params.Force = globals.NonInteractive return runner.Run(params) }, - ).SetGroup(VCSGroup) + ).SetGroup(VCSGroup).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/revert.go b/cmd/state/internal/cmdtree/revert.go index e22e19446f..896b0f75e8 100644 --- a/cmd/state/internal/cmdtree/revert.go +++ b/cmd/state/internal/cmdtree/revert.go @@ -35,5 +35,5 @@ func newRevertCommand(prime *primer.Values, globals *globalOptions) *captain.Com params.Force = globals.NonInteractive return runner.Run(params) }, - ).SetGroup(VCSGroup) + ).SetGroup(VCSGroup).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/run.go b/cmd/state/internal/cmdtree/run.go index 62adee00f5..8557296058 100644 --- a/cmd/state/internal/cmdtree/run.go +++ b/cmd/state/internal/cmdtree/run.go @@ -49,7 +49,6 @@ func newRunCommand(prime *primer.Values) *captain.Command { cmd.SetGroup(ProjectUsageGroup) cmd.SetDisableFlagParsing(true) cmd.SetHasVariableArguments() - cmd.SetDoesNotSupportStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/scripts.go b/cmd/state/internal/cmdtree/scripts.go index 98b877b782..fc6e0360e2 100644 --- a/cmd/state/internal/cmdtree/scripts.go +++ b/cmd/state/internal/cmdtree/scripts.go @@ -19,7 +19,7 @@ func newScriptsCommand(prime *primer.Values) *captain.Command { []*captain.Argument{}, func(ccmd *captain.Command, args []string) error { return runner.Run() - }).SetGroup(AutomationGroup) + }).SetGroup(AutomationGroup).SetSupportsStructuredOutput() } func newScriptsEditCommand(prime *primer.Values) *captain.Command { @@ -50,6 +50,6 @@ func newScriptsEditCommand(prime *primer.Values) *captain.Command { func(ccmd *captain.Command, args []string) error { return editRunner.Run(¶ms) }, - ).SetUnstable(true).SetDoesNotSupportStructuredOutput() + ).SetUnstable(true) } diff --git a/cmd/state/internal/cmdtree/secrets.go b/cmd/state/internal/cmdtree/secrets.go index f9004ac861..4446a839eb 100644 --- a/cmd/state/internal/cmdtree/secrets.go +++ b/cmd/state/internal/cmdtree/secrets.go @@ -39,6 +39,7 @@ func newSecretsCommand(secretsClient *secretsapi.Client, prime *primer.Values) * ccmd.SetGroup(PlatformGroup) ccmd.SetAliases("variables", "vars") + ccmd.SetSupportsStructuredOutput() ccmd.SetUnstable(true) return ccmd @@ -66,7 +67,7 @@ func newSecretsGetCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params) }, - ) + ).SetSupportsStructuredOutput() } func newSecretsSetCommand(prime *primer.Values) *captain.Command { @@ -97,7 +98,7 @@ func newSecretsSetCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params) }, - ).SetDoesNotSupportStructuredOutput() + ) } func newSecretsSyncCommand(secretsClient *secretsapi.Client, prime *primer.Values) *captain.Command { @@ -113,5 +114,5 @@ func newSecretsSyncCommand(secretsClient *secretsapi.Client, prime *primer.Value func(_ *captain.Command, _ []string) error { return runner.Run() }, - ).SetDoesNotSupportStructuredOutput() + ) } diff --git a/cmd/state/internal/cmdtree/shell.go b/cmd/state/internal/cmdtree/shell.go index b67c611d08..eebe87ccbd 100644 --- a/cmd/state/internal/cmdtree/shell.go +++ b/cmd/state/internal/cmdtree/shell.go @@ -42,7 +42,6 @@ func newShellCommand(prime *primer.Values) *captain.Command { }, ) cmd.SetGroup(EnvironmentUsageGroup) - cmd.SetDoesNotSupportStructuredOutput() cmd.SetAliases("prompt") return cmd } diff --git a/cmd/state/internal/cmdtree/show.go b/cmd/state/internal/cmdtree/show.go index 2b79b10871..709649096b 100644 --- a/cmd/state/internal/cmdtree/show.go +++ b/cmd/state/internal/cmdtree/show.go @@ -28,5 +28,5 @@ func newShowCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return runner.Run(params) }, - ).SetGroup(ProjectUsageGroup).SetUnstable(true) + ).SetGroup(ProjectUsageGroup).SetSupportsStructuredOutput().SetUnstable(true) } diff --git a/cmd/state/internal/cmdtree/switch.go b/cmd/state/internal/cmdtree/switch.go index be7b478b22..0f36062e38 100644 --- a/cmd/state/internal/cmdtree/switch.go +++ b/cmd/state/internal/cmdtree/switch.go @@ -31,5 +31,6 @@ func newSwitchCommand(prime *primer.Values) *captain.Command { }) cmd.SetGroup(EnvironmentSetupGroup) + cmd.SetSupportsStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/update.go b/cmd/state/internal/cmdtree/update.go index d12d579bf2..1e82a8b7cd 100644 --- a/cmd/state/internal/cmdtree/update.go +++ b/cmd/state/internal/cmdtree/update.go @@ -31,7 +31,6 @@ func newUpdateCommand(prime *primer.Values) *captain.Command { ) cmd.SetGroup(UtilsGroup) cmd.SetSkipChecks(true) - cmd.SetDoesNotSupportStructuredOutput() return cmd } @@ -58,6 +57,7 @@ func newUpdateLockCommand(prime *primer.Values, globals *globalOptions) *captain }, ) cmd.SetSkipChecks(true) + cmd.SetSupportsStructuredOutput() return cmd } @@ -78,6 +78,5 @@ func newUpdateUnlockCommand(prime *primer.Values, globals *globalOptions) *capta }, ) cmd.SetSkipChecks(true) - cmd.SetDoesNotSupportStructuredOutput() return cmd } diff --git a/cmd/state/internal/cmdtree/use.go b/cmd/state/internal/cmdtree/use.go index cf02eaa9e7..2971339b51 100644 --- a/cmd/state/internal/cmdtree/use.go +++ b/cmd/state/internal/cmdtree/use.go @@ -29,7 +29,7 @@ func newUseCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return use.NewUse(prime).Run(params) }, - ).SetGroup(EnvironmentUsageGroup) + ).SetGroup(EnvironmentUsageGroup).SetSupportsStructuredOutput() return cmd } @@ -47,7 +47,7 @@ func newUseResetCommand(prime *primer.Values, globals *globalOptions) *captain.C params.Force = globals.NonInteractive return use.NewReset(prime).Run(params) }, - ).SetDoesNotSupportStructuredOutput() + ) } func newUseShowCommand(prime *primer.Values) *captain.Command { @@ -61,6 +61,6 @@ func newUseShowCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, _ []string) error { return use.NewShow(prime).Run() }, - ) + ).SetSupportsStructuredOutput() return cmd } diff --git a/cmd/state/main.go b/cmd/state/main.go index 1d0dba8224..56850b62da 100644 --- a/cmd/state/main.go +++ b/cmd/state/main.go @@ -1,12 +1,9 @@ package main import ( - "bufio" "context" - "errors" "fmt" "os" - "os/exec" "runtime/debug" "strings" "time" @@ -32,10 +29,11 @@ import ( "github.com/ActiveState/cli/internal/prompt" _ "github.com/ActiveState/cli/internal/prompt" // Sets up survey defaults "github.com/ActiveState/cli/internal/rollbar" + "github.com/ActiveState/cli/internal/runbits/errors" + "github.com/ActiveState/cli/internal/runbits/legacy/projectmigration" "github.com/ActiveState/cli/internal/runbits/panics" "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/cli/internal/svcctl" - cmdletErrors "github.com/ActiveState/cli/pkg/cmdlets/errors" secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -103,21 +101,10 @@ func main() { // Run our main command logic, which is logic that defers to the error handling logic below err = run(os.Args, isInteractive, cfg, out) if err != nil { - exitCode, err = cmdletErrors.ParseUserFacing(err) + exitCode, err = errors.ParseUserFacing(err) if err != nil { out.Error(err) } - - // If a state tool error occurs in a VSCode integrated terminal, we want - // to pause and give time to the user to read the error message. - // But not, if we exit, because the last command in the activated sub-shell failed. - var eerr *exec.ExitError - isExitError := errors.As(err, &eerr) - if !isExitError && outFlags.ConfirmExit { - out.Print(locale.T("confirm_exit_on_error_prompt")) - br := bufio.NewReader(os.Stdin) - br.ReadLine() - } } } @@ -211,6 +198,13 @@ func run(args []string, isInteractive bool, cfg *config.Instance, out output.Out // Set up prompter prompter := prompt.New(isInteractive, an) + // This is an anti-pattern. DO NOT DO THIS! Normally we should be passing prompt and out as + // arguments everywhere it is needed. However, we need to support legacy projects with commitId in + // activestate.yaml, and whenever that commitId is needed, we need to prompt the user to migrate + // their project. This would result in a lot of boilerplate for a legacy feature, so we're + // working around it with package "globals". + projectmigration.Register(prompter, out) + // Set up conditional, which accesses a lot of primer data sshell := subshell.New(cfg) @@ -257,8 +251,10 @@ func run(args []string, isInteractive bool, cfg *config.Instance, out output.Out if childCmd != nil { cmdName = childCmd.JoinedSubCommandNames() + " " } - err = errs.AddTips(err, locale.Tl("err_tip_run_help", "Run → [ACTIONABLE]`state {{.V0}}--help`[/RESET] for general help", cmdName)) - cmdletErrors.ReportError(err, cmds.Command(), an) + if !out.Type().IsStructured() { + err = errs.AddTips(err, locale.Tl("err_tip_run_help", "Run → [ACTIONABLE]`state {{.V0}}--help`[/RESET] for general help", cmdName)) + } + errors.ReportError(err, cmds.Command(), an) } return err diff --git a/cmd/state/main_test.go b/cmd/state/main_test.go index d383b3bbae..77f44c8131 100644 --- a/cmd/state/main_test.go +++ b/cmd/state/main_test.go @@ -15,45 +15,44 @@ type MainTestSuite struct { func (suite *MainTestSuite) TestOutputer() { { - outputer, err := initOutput(outputFlags{"", false, false, false}, "", "") + outputer, err := initOutput(outputFlags{"", false, false}, "", "") suite.Require().NoError(err, errs.JoinMessage(err)) suite.Equal(output.PlainFormatName, outputer.Type(), "Returns Plain outputer") } { - outputer, err := initOutput(outputFlags{string(output.PlainFormatName), false, false, false}, "", "") + outputer, err := initOutput(outputFlags{string(output.PlainFormatName), false, false}, "", "") suite.Require().NoError(err) suite.Equal(output.PlainFormatName, outputer.Type(), "Returns Plain outputer") } { - outputer, err := initOutput(outputFlags{string(output.JSONFormatName), false, false, false}, "", "") + outputer, err := initOutput(outputFlags{string(output.JSONFormatName), false, false}, "", "") suite.Require().NoError(err) suite.Equal(output.JSONFormatName, outputer.Type(), "Returns JSON outputer") } { - outputer, err := initOutput(outputFlags{"", false, false, false}, string(output.JSONFormatName), "") + outputer, err := initOutput(outputFlags{"", false, false}, string(output.JSONFormatName), "") suite.Require().NoError(err) suite.Equal(output.JSONFormatName, outputer.Type(), "Returns JSON outputer") } { - outputer, err := initOutput(outputFlags{"", false, false, false}, string(output.EditorFormatName), "") + outputer, err := initOutput(outputFlags{"", false, false}, string(output.EditorFormatName), "") suite.Require().NoError(err) suite.Equal(output.EditorFormatName, outputer.Type(), "Returns JSON outputer") } } func (suite *MainTestSuite) TestParseOutputFlags() { - suite.Equal(outputFlags{"plain", false, false, false}, parseOutputFlags([]string{"state", "foo", "-o", "plain"})) - suite.Equal(outputFlags{"json", false, false, false}, parseOutputFlags([]string{"state", "foo", "--output", "json"})) - suite.Equal(outputFlags{"json", false, false, false}, parseOutputFlags([]string{"state", "foo", "-o", "json"})) - suite.Equal(outputFlags{"editor", false, false, false}, parseOutputFlags([]string{"state", "foo", "--output", "editor"})) - suite.Equal(outputFlags{"", true, false, false}, parseOutputFlags([]string{"state", "foo", "--mono"})) - suite.Equal(outputFlags{"", false, true, false}, parseOutputFlags([]string{"state", "foo", "--confirm-exit-on-error"})) - suite.Equal(outputFlags{"", false, false, true}, parseOutputFlags([]string{"state", "foo", "--non-interactive"})) - suite.Equal(outputFlags{"", false, false, true}, parseOutputFlags([]string{"state", "foo", "-n"})) + suite.Equal(outputFlags{"plain", false, false}, parseOutputFlags([]string{"state", "foo", "-o", "plain"})) + suite.Equal(outputFlags{"json", false, false}, parseOutputFlags([]string{"state", "foo", "--output", "json"})) + suite.Equal(outputFlags{"json", false, false}, parseOutputFlags([]string{"state", "foo", "-o", "json"})) + suite.Equal(outputFlags{"editor", false, false}, parseOutputFlags([]string{"state", "foo", "--output", "editor"})) + suite.Equal(outputFlags{"", true, false}, parseOutputFlags([]string{"state", "foo", "--mono"})) + suite.Equal(outputFlags{"", false, true}, parseOutputFlags([]string{"state", "foo", "--non-interactive"})) + suite.Equal(outputFlags{"", false, true}, parseOutputFlags([]string{"state", "foo", "-n"})) } func (suite *MainTestSuite) TestDisableColors() { diff --git a/cmd/state/output.go b/cmd/state/output.go index edce18f21d..74db4e896c 100644 --- a/cmd/state/output.go +++ b/cmd/state/output.go @@ -19,7 +19,6 @@ type outputFlags struct { // These should be kept in sync with cmd/state/internal/cmdtree (output flag) Output string `short:"o" long:"output"` Mono bool `long:"mono"` - ConfirmExit bool `long:"confirm-exit-on-error"` NonInteractive bool `short:"n" long:"non-interactive"` } diff --git a/go.mod b/go.mod index b14569794e..9a561d963e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ replace cloud.google.com/go => cloud.google.com/go v0.110.0 require ( github.com/99designs/gqlgen v0.17.19 github.com/ActiveState/go-ogle-analytics v0.0.0-20170510030904-9b3f14901527 - github.com/ActiveState/termtest v0.7.3-0.20230914171339-6e6462ec3e3f + github.com/ActiveState/termtest v0.7.3-0.20231006191111-13d903a6f2de github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 github.com/alecthomas/participle/v2 v2.0.0 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 @@ -133,7 +133,7 @@ require ( github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/sergi/go-diff v1.3.1 + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 github.com/src-d/gcfg v1.4.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect diff --git a/go.sum b/go.sum index 723da87d71..556af48072 100644 --- a/go.sum +++ b/go.sum @@ -345,8 +345,8 @@ github.com/ActiveState/graphql v0.0.0-20230719154233-6949037a6e48 h1:UCx/ObpVRgC github.com/ActiveState/graphql v0.0.0-20230719154233-6949037a6e48/go.mod h1:NhUbNQ8UpfnC6nZvZ8oThqYSCE/G8FQp9JUrK9jXJs0= github.com/ActiveState/pty v0.0.0-20230628221854-6fb90eb08a14 h1:RdhhSiwmgyUaaF2GBNrbqTwE5SM+MaVjwf91Ua+CK8c= github.com/ActiveState/pty v0.0.0-20230628221854-6fb90eb08a14/go.mod h1:5mM6vNRQwshCjlkOnVpwC//4ZpkiC6nmZr8lPOxJdXs= -github.com/ActiveState/termtest v0.7.3-0.20230914171339-6e6462ec3e3f h1:qvVEJlgKYo2NArClTlzSvn/xovhghZv8CJiV7My1TnI= -github.com/ActiveState/termtest v0.7.3-0.20230914171339-6e6462ec3e3f/go.mod h1:RyWp2NaaTrVAa+XjMHpKAqwBFWbL6wE12HQxiZNGAqU= +github.com/ActiveState/termtest v0.7.3-0.20231006191111-13d903a6f2de h1:KqSmWBIEQracF6ixlQ0eNeKN+wYxDHNJWbrShI6F8NE= +github.com/ActiveState/termtest v0.7.3-0.20231006191111-13d903a6f2de/go.mod h1:RyWp2NaaTrVAa+XjMHpKAqwBFWbL6wE12HQxiZNGAqU= github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= @@ -941,6 +941,8 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w= github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y= github.com/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE= diff --git a/installers/install.ps1 b/installers/install.ps1 index de163460dd..ca6efb24de 100644 --- a/installers/install.ps1 +++ b/installers/install.ps1 @@ -113,15 +113,54 @@ function error([string] $msg) Write-Host $msg -ForegroundColor Red } -if (!$script:VERSION) { - # Determine the latest version to fetch and parse info. - $jsonURL = "$script:BASEINFOURL/?channel=$script:CHANNEL&platform=windows&source=install" - $infoJson = ConvertFrom-Json -InputObject (download $jsonURL) - $version = $infoJson.Version - $checksum = $infoJson.Sha256 - $relUrl = $infoJson.Path +$version = $script:VERSION +if (!$version) { + # If the user did not specify a version, formulate a query to fetch the JSON info of the latest + # version, including where it is. + $jsonURL = "$script:BASEINFOURL/?channel=$script:CHANNEL&platform=windows&source=install" +} elseif (!($version | Select-String -Pattern "-SHA" -SimpleMatch)) { + # If the user specified a partial version (i.e. no SHA), formulate a query to fetch the JSON + # info of that version's latest SHA, including where it is. + $versionNoSHA = $version + $version = "" + $jsonURL = "$script:BASEINFOURL/?channel=$script:CHANNEL&platform=windows&source=install&target-version=$versionNoSHA" } else { - $relUrl = "$script:CHANNEL/$script:VERSION/windows-amd64/state-windows-amd64-$script:VERSION.zip" + # If the user specified a full version with SHA, formulate a query to fetch the JSON info of + # that version. + $versionNoSHA = $version -replace "-SHA.*", "" + $jsonURL = "$script:BASEINFOURL/?channel=$script:CHANNEL&platform=windows&source=install&target-version=$versionNoSHA" +} + +# Fetch version info. +try { + $infoJson = ConvertFrom-Json -InputObject (download $jsonURL) +} catch [System.Exception] { +} +if (!$infoJson) { + if (!$version) { + Write-Error "Unable to retrieve the latest version number" + } else { + Write-Error "Could not download a State Tool Installer for the given command line arguments" + } + Write-Error $_.Exception.Message + exit 1 +} + +# Extract checksum. +$checksum = $infoJson.Sha256 + +if (!$version) { + # If the user specified no version or a partial version we need to use the json URL to get the + # actual installer URL. + $version = $infoJson.Version + $relUrl = $infoJson.Path +} else { + # If the user specified a full version, construct the installer URL. + if ($version -ne $infoJson.Version) { + Write-Error "Unknown version: $version" + exit 1 + } + $relUrl = "$script:CHANNEL/$versionNoSHA/windows-amd64/state-windows-amd64-$version.zip" } # Fetch the requested or latest version. @@ -142,9 +181,9 @@ catch [System.Exception] exit 1 } -# Verify checksum if possible. +# Verify checksum. $hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash -if ($checksum -and $hash -ne $checksum) +if ($hash -ne $checksum) { Write-Warning "SHA256 sum did not match:" Write-Warning "Expected: $checksum" @@ -182,5 +221,5 @@ if (Test-Path env:ACTIVESTATE_SESSION_TOKEN) Remove-Item Env:\ACTIVESTATE_SESSION_TOKEN } if ( !$success ) { - exit 1 + exit 1 } diff --git a/installers/install.sh b/installers/install.sh index 7111624493..28aa820928 100755 --- a/installers/install.sh +++ b/installers/install.sh @@ -107,50 +107,75 @@ if [ -z "$TMPDIR" ]; then TMPDIR="/tmp" fi -TMPDIR="$TMPDIR/state-install-$RANDOM" -mkdir -p "$TMPDIR" +INSTALLERTMPDIR="$TMPDIR/state-install-$RANDOM" +mkdir -p "$INSTALLERTMPDIR" if [ -z "$VERSION" ]; then - # Determine the latest version to fetch. - STATEURL="$BASE_INFO_URL?channel=$CHANNEL&source=install&platform=$OS" - $FETCH $TMPDIR/info.json $STATEURL || exit 1 + # If the user did not specify a version, formulate a query to fetch the JSON info of the latest + # version, including where it is. + JSONURL="$BASE_INFO_URL?channel=$CHANNEL&source=install&platform=$OS" +elif [ -z "`echo $VERSION | grep -o '\-SHA'`" ]; then + # If the user specified a partial version (i.e. no SHA), formulate a query to fetch the JSON info + # of that version's latest SHA, including where it is. + VERSIONNOSHA="$VERSION" + VERSION="" + JSONURL="$BASE_INFO_URL?channel=$CHANNEL&source=install&platform=$OS&target-version=$VERSIONNOSHA" +else + # If the user specified a full version with SHA, formulate a query to fetch the JSON info of that + # version. + VERSIONNOSHA="`echo $VERSION | sed 's/-SHA.*$//'`" + JSONURL="$BASE_INFO_URL?channel=$CHANNEL&source=install&platform=$OS&target-version=$VERSIONNOSHA" +fi + +# Fetch version info. +$FETCH $INSTALLERTMPDIR/info.json $JSONURL || exit 1 +if [ ! -z "`grep -o Invalid $INSTALLERTMPDIR/info.json`" ]; then + error "Could not download a State Tool installer for the given command line arguments" + exit 1 +fi + +# Extract checksum. +SUM=`cat $INSTALLERTMPDIR/info.json | sed -ne 's/.*"sha256":[ \t]*"\([^"]*\)".*/\1/p'` - # Parse info. - VERSION=`cat $TMPDIR/info.json | sed -ne 's/.*"version":[ \t]*"\([^"]*\)".*/\1/p'` +if [ -z "$VERSION" ]; then + # If the user specified no version or a partial version we need to use the json URL to get the + # actual installer URL. + VERSION=`cat $INSTALLERTMPDIR/info.json | sed -ne 's/.*"version":[ \t]*"\([^"]*\)".*/\1/p'` if [ -z "$VERSION" ]; then error "Unable to retrieve the latest version number" exit 1 fi - SUM=`cat $TMPDIR/info.json | sed -ne 's/.*"sha256":[ \t]*"\([^"]*\)".*/\1/p'` - RELURL=`cat $TMPDIR/info.json | sed -ne 's/.*"path":[ \t]*"\([^"]*\)".*/\1/p'` - rm $TMPDIR/info.json - + RELURL=`cat $INSTALLERTMPDIR/info.json | sed -ne 's/.*"path":[ \t]*"\([^"]*\)".*/\1/p'` else - RELURL="$CHANNEL/$VERSION/$OS-amd64/state-$OS-amd64-$VERSION$DOWNLOADEXT" + # If the user specified a full version, construct the installer URL. + if [ "$VERSION" != "`cat $INSTALLERTMPDIR/info.json | sed -ne 's/.*"version":[ \t]*"\([^"]*\)".*/\1/p'`" ]; then + error "Unknown version: $VERSION" + exit 1 + fi + RELURL="$CHANNEL/$VERSIONNOSHA/$OS-amd64/state-$OS-amd64-$VERSION$DOWNLOADEXT" fi # Fetch the requested or latest version. progress "Preparing Installer for State Tool Package Manager version $VERSION" STATEURL="$BASE_FILE_URL/$RELURL" ARCHIVE="$OS-amd64$DOWNLOADEXT" -$FETCH $TMPDIR/$ARCHIVE $STATEURL +$FETCH $INSTALLERTMPDIR/$ARCHIVE $STATEURL # wget and curl differ on how to handle AWS' "Forbidden" result for unknown versions. # wget will exit with nonzero status. curl simply creates an XML file with the forbidden error. -# If curl was used, make sure the file downloaded is of type 'data', according to the UNIX `file` -# command. (The XML error will be reported as a 'text' type.) +# If curl was used, make sure the file downloaded is not an XML file (i.e. it does not start with "`[/RESET] to switch to a different branch. + Type [ACTIONABLE]'state branch switch '[/RESET] to switch to a different branch. err_fetch_project: other: "Could not fetch details for project: {{.V0}}" err_set_default_branch: @@ -1731,6 +1709,28 @@ package_info_request: err_push_outdated: other: | Your project has new changes available that need to be merged first. Please first run `[ACTIONABLE]state pull[/RESET]` to update your project. +err_tip_push_outdated: + other: Run '[ACTIONABLE]state pull[/RESET]'. +err_push_not_authenticated: + other: In order to update your project you need to be authenticated, please run '[ACTIONABLE]state auth[/RESET]' to authenticate. +err_push_no_project: + other: No project found, you may need to create one first. +err_push_headless: + other: Cannot push a headless project. To convert your project please visit {{.V0}} +push_push_tip_headless_init: + other: Run '[ACTIONABLE]state init[/RESET]' to create a project with the State Tool. +push_push_tip_headless_cwd: + other: Navigate to a directory with an activestate.yaml. +err_push_nocommit: + other: You have nothing to push, make some changes first with '[ACTIONABLE]state install[/RESET]'. +push_no_changes: + other: You have no local changes to push. To pull in remote changes run '[ACTIONABLE]state pull[/RESET]'. +err_push_create_nonunique: + other: The project name '[NOTICE]{{.V0}}[/RESET]' is already in use. +err_push_create_project_aborted: + other: Project creation aborted by user. +err_push_target_invalid_history: + other: The target's commit history does not match your local commit history. Are you pushing to the right project? err_pull_incompatible: other: | The remote project has an incompatible commit history. Target a different project with `[ACTIONABLE--set-project[/RESET]` or reset your local checkout with `[ACTIONABLE]state reset[/RESET]`. @@ -1745,8 +1745,8 @@ operation_success_local: other: | Your local project has been updated. Run [ACTIONABLE]state push[/RESET] to save changes to the platform. -solver_err: - other: "The Platform failed to resolve the dependencies for this build. {{.V0}}\n{{.V1}}" +buildplan_err: + other: "Could not plan build, platform responded with:\n{{.V0}}\n{{.V1}}" transient_solver_tip: other: You may want to retry this command if the failure was related to a resourcing or networking issue. warning_git_project_mismatch: @@ -1821,8 +1821,6 @@ package_ingredient_alternatives_nolang: No results found for search term "[NOTICE]{{.V0}}[/RESET]". This may be because you have not installed a language for your project. Install a language by running "[ACTIONABLE]state languages install [/RESET]". -progress_project: - other: " • Initializing Project" progress_search: other: " • Searching for [ACTIONABLE]{{.V0}}[/RESET] in the ActiveState Catalog" setup_runtime: @@ -2023,19 +2021,6 @@ pjfile_deprecation_msg: {{.V1}} To find out how to update these please read the deprecation information at: [ACTIONABLE]{{.V2}}[/RESET] -runtime_usage_limit_reached: - other: | - [WARNING]Heads up! You've reached your runtime limit for [/RESET][ACTIONABLE]{{.V0}}[/RESET][WARNING].[/RESET] - [WARNING]You can upgrade your plan or enroll in a free trial by visiting: [/RESET][ACTIONABLE]https://platform.activestate.com/upgrade/?org={{.V0}}[/RESET][WARNING].[/RESET] - - [WARNING]You are using [/RESET][ACTIONABLE]{{.V1}}[/RESET][WARNING] out of [/RESET][ACTIONABLE]{{.V2}}[/RESET][WARNING] available runtimes.[/RESET] - -runtime_limit_reached_title: - other: Runtime Limit Reached -runtime_limit_reached_msg: - other: Heads up! You've reached your runtime limit for {{.V0}}. -runtime_limit_reached_action: - other: Upgrade pjfile_deprecation_entry: other: " - '[ACTIONABLE]{{.V0}}[/RESET]' located at byte [ACTIONABLE]{{.V1}}[/RESET]" err_init_authenticated: @@ -2043,7 +2028,7 @@ err_init_authenticated: err_country_blocked: other: This service is not available in your region. err_commit_id_invalid: - other: "Invalid project commit ID: {{.V0}}" + other: "Expected commit ID to be UUID formatted, but got: {{.V0}}" commit_id_gitignore: other: | # Ignore {{.V0}}/{{.V1}} file; tracking this via VCS will cause users to have to resolve redundant conflicts @@ -2066,4 +2051,25 @@ err_edit_local_checkouts: err_edit_project_mapping: other: Could not update project mapping notice_runtime_disabled: - other: Skipping runtime setup because it was disabled by an environment variable \ No newline at end of file + other: Skipping runtime setup because it was disabled by an environment variable +err_local_commit_file: + other: | + Could not find or read the commit file for your project at '[ACTIONABLE]{{.V0}}[/RESET]'. + + You can fix this by either running '[ACTIONABLE]state pull[/RESET]' to update to the latest commit, or manually switch to a specific + commit by running '[ACTIONABLE]state switch [/RESET]'. + + To view your projects commits run '[ACTIONABLE]state history[/RESET]'. + + Please avoid deleting or editing this file directly. +projectmigration_confirm: + other: | + State Tool has introduced a new project format to provide enhanced functionality. Your project is currently using the old format and will remain read-only until you migrate to the new project format. + + [ERROR]WARNING:[/RESET] After you migrate, older versions of the State Tool will not be able to use your project. + + Would you like to perform the migration now? +err_searchingredient_toomany: + other: Too many ingredients match the query '[ACTIONABLE]{{.V0}}[/RESET]', please try to be more specific. +alternative_unknown_pkg_name: + other: unknown \ No newline at end of file diff --git a/internal/offinstall/storage_darwin.go b/internal/offinstall/storage_darwin.go deleted file mode 100644 index 893adf8a77..0000000000 --- a/internal/offinstall/storage_darwin.go +++ /dev/null @@ -1,17 +0,0 @@ -package offinstall - -import ( - "path/filepath" - - "github.com/ActiveState/cli/internal/errs" - "github.com/mitchellh/go-homedir" -) - -func DefaultInstallParentDir() (string, error) { - home, err := homedir.Dir() - if err != nil { - return "", errs.Wrap(err, "Could not get home directory") - } - - return filepath.Join(home, "Applications"), nil -} diff --git a/internal/offinstall/storage_linux.go b/internal/offinstall/storage_linux.go deleted file mode 100644 index 20d0f87d93..0000000000 --- a/internal/offinstall/storage_linux.go +++ /dev/null @@ -1,17 +0,0 @@ -package offinstall - -import ( - "path/filepath" - - "github.com/ActiveState/cli/internal/errs" - "github.com/mitchellh/go-homedir" -) - -func DefaultInstallParentDir() (string, error) { - home, err := homedir.Dir() - if err != nil { - return "", errs.Wrap(err, "Could not get home directory") - } - - return filepath.Join(home, ".local", "share", "applications"), nil -} diff --git a/internal/offinstall/storage_windows.go b/internal/offinstall/storage_windows.go deleted file mode 100644 index 54c4036337..0000000000 --- a/internal/offinstall/storage_windows.go +++ /dev/null @@ -1,11 +0,0 @@ -package offinstall - -import ( - "os" - "path/filepath" -) - -func DefaultInstallParentDir() (string, error) { - // There is no system install path for Windows - return filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local", "Programs"), nil -} diff --git a/internal/output/json.go b/internal/output/json.go index 5411d4a530..38f28c9b8d 100644 --- a/internal/output/json.go +++ b/internal/output/json.go @@ -65,7 +65,8 @@ func (f *JSON) Fprint(writer io.Writer, value interface{}) { } type StructuredError struct { - Error string `json:"error"` + Error string `json:"error"` + Tips []string `json:"tips,omitempty"` } // Error will marshal and print the given value to the error writer @@ -120,11 +121,11 @@ func toStructuredError(v interface{}) StructuredError { case StructuredError: return vv case error: - return StructuredError{locale.JoinedErrorMessage(vv)} + return StructuredError{Error: locale.JoinedErrorMessage(vv)} case string: - return StructuredError{vv} + return StructuredError{Error: vv} } message := fmt.Sprintf("Not a recognized error format: %v", v) multilog.Error(message) - return StructuredError{message} + return StructuredError{Error: message} } diff --git a/internal/output/mediator.go b/internal/output/mediator.go index 6b3cc2a680..c71acb111e 100644 --- a/internal/output/mediator.go +++ b/internal/output/mediator.go @@ -56,7 +56,7 @@ func mediatorValue(v interface{}, format Format) interface{} { if vt, ok := v.(StructuredMarshaller); ok { return vt.MarshalStructured(format) } - return StructuredError{locale.Tr("err_no_structured_output", string(format))} + return StructuredError{Error: locale.Tr("err_no_structured_output", string(format))} } if vt, ok := v.(Marshaller); ok { return vt.MarshalOutput(format) diff --git a/internal/output/mediator_test.go b/internal/output/mediator_test.go index 1b4adbe5f7..2648eb0117 100644 --- a/internal/output/mediator_test.go +++ b/internal/output/mediator_test.go @@ -67,7 +67,7 @@ func Test_mediatorValue(t *testing.T) { "unstructured", JSONFormatName, }, - StructuredError{locale.Tr("err_no_structured_output", string(JSONFormatName))}, + StructuredError{Error: locale.Tr("err_no_structured_output", string(JSONFormatName))}, }, } for _, tt := range tests { diff --git a/internal/runbits/activation/activation.go b/internal/runbits/activation/activation.go index d6d23740d8..865c7a9211 100644 --- a/internal/runbits/activation/activation.go +++ b/internal/runbits/activation/activation.go @@ -39,7 +39,7 @@ func ActivateAndWait( } } - ve, err := venv.GetEnv(false, true, projectDir, proj.Namespace().String()) + ve, err := venv.GetEnv(false, true, projectDir) if err != nil { return locale.WrapError(err, "error_could_not_activate_venv", "Could not retrieve environment information.") } diff --git a/pkg/cmdlets/auth/keypair.go b/internal/runbits/auth/keypair.go similarity index 100% rename from pkg/cmdlets/auth/keypair.go rename to internal/runbits/auth/keypair.go diff --git a/pkg/cmdlets/auth/login.go b/internal/runbits/auth/login.go similarity index 100% rename from pkg/cmdlets/auth/login.go rename to internal/runbits/auth/login.go diff --git a/pkg/cmdlets/auth/signup.go b/internal/runbits/auth/signup.go similarity index 100% rename from pkg/cmdlets/auth/signup.go rename to internal/runbits/auth/signup.go diff --git a/internal/runbits/buildscript/buildscript.go b/internal/runbits/buildscript/buildscript.go index 9414554e17..e5d8114cc1 100644 --- a/internal/runbits/buildscript/buildscript.go +++ b/internal/runbits/buildscript/buildscript.go @@ -57,11 +57,6 @@ func Sync(proj *project.Project, commitID *strfmt.UUID, out output.Outputer, aut return false, nil // nothing to do } logging.Debug("Merging changes") - expr, err = script.ToBuildExpression() - if err != nil { - return false, errs.Wrap(err, "Unable to translate local build script to build expression") - } - out.Notice(locale.Tl("buildscript_update", "Updating project to reflect build script changes...")) localCommitID, err := localcommit.Get(proj.Dir()) @@ -74,7 +69,7 @@ func Sync(proj *project.Project, commitID *strfmt.UUID, out output.Outputer, aut Owner: proj.Owner(), Project: proj.Name(), ParentCommit: localCommitID.String(), - Expression: expr, + Expression: script.Expr, }) if err != nil { return false, errs.Wrap(err, "Could not update project to reflect build script changes.") diff --git a/internal/runbits/buildscript/buildscript_test.go b/internal/runbits/buildscript/buildscript_test.go index 9ec51bbbf7..ceb19353b2 100644 --- a/internal/runbits/buildscript/buildscript_test.go +++ b/internal/runbits/buildscript/buildscript_test.go @@ -1,9 +1,13 @@ package buildscript import ( + "encoding/json" + "path/filepath" "testing" + "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/rtutils/ptr" + "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" "github.com/ActiveState/cli/pkg/platform/runtime/buildscript" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,11 +33,16 @@ in: runtime`)) require.NoError(t, err) - expr, err := script.ToBuildExpression() + // Make a copy of the original expression. + bytes, err := json.Marshal(script.Expr) + require.NoError(t, err) + expr, err := buildexpression.New(bytes) require.NoError(t, err) - (*script.Let.Assignments[0].Value.FuncCall.Arguments[0].Assignment.Value.List)[0].Str = ptr.To(`"77777"`) + // Modify the build script. + (*script.Expr.Let.Assignments[0].Value.Ap.Arguments[0].Assignment.Value.List)[0].Str = ptr.To(`77777`) + // Generate the difference between the modified script and the original expression. result, err := generateDiff(script, expr) require.NoError(t, err) assert.Equal(t, `let: @@ -57,3 +66,69 @@ in: in: runtime`, result) } + +// TestRealWorld tests a real-world case where: +// - There is a Platform Python project with an initial commit. +// - There is a local project that just checks it out. +// - The Platform project adds requests@2.30.0 (an older version). +// - The local project adds requests (latest version). +// - The local project pulls from the Platform project, resulting in conflicting times and version +// requirements for requests. +func TestRealWorld(t *testing.T) { + script1, err := buildscript.NewScript(fileutils.ReadFileUnsafe(filepath.Join("testdata", "buildscript1.yaml"))) + require.NoError(t, err) + script2, err := buildscript.NewScript(fileutils.ReadFileUnsafe(filepath.Join("testdata", "buildscript2.yaml"))) + require.NoError(t, err) + result, err := generateDiff(script1, script2.Expr) + require.NoError(t, err) + assert.Equal(t, `let: + runtime = state_tool_artifacts_v1( + build_flags = [ + ], + camel_flags = [ + ], + src = "$sources" + ) + sources = solve( +<<<<<<< local + at_time = "2023-10-16T22:20:29.000000Z", +======= + at_time = "2023-08-01T16:20:11.985000Z", +>>>>>>> remote + platforms = [ + "78977bc8-0f32-519d-80f3-9043f059398c", + "7c998ec2-7491-4e75-be4d-8885800ef5f2", + "96b7e6f2-bebf-564c-bc1c-f04482398f38" + ], + requirements = [ + { + name = "python", + namespace = "language", + version_requirements = [ + { + comparator = "eq", + version = "3.10.11" + } + ] + }, + { + name = "requests", +<<<<<<< local + namespace = "language/python" +======= + namespace = "language/python", + version_requirements = [ + { + comparator = "eq", + version = "2.30.0" + } + ] +>>>>>>> remote + } + ], + solver_version = null + ) + +in: + runtime`, result) +} diff --git a/internal/runbits/buildscript/testdata/buildscript1.yaml b/internal/runbits/buildscript/testdata/buildscript1.yaml new file mode 100644 index 0000000000..7ae010f237 --- /dev/null +++ b/internal/runbits/buildscript/testdata/buildscript1.yaml @@ -0,0 +1,36 @@ +let: + runtime = state_tool_artifacts_v1( + build_flags = [ + ], + camel_flags = [ + ], + src = "$sources" + ) + sources = solve( + at_time = "2023-10-16T22:20:29.000000Z", + platforms = [ + "78977bc8-0f32-519d-80f3-9043f059398c", + "7c998ec2-7491-4e75-be4d-8885800ef5f2", + "96b7e6f2-bebf-564c-bc1c-f04482398f38" + ], + requirements = [ + { + name = "python", + namespace = "language", + version_requirements = [ + { + comparator = "eq", + version = "3.10.11" + } + ] + }, + { + name = "requests", + namespace = "language/python" + } + ], + solver_version = null + ) + +in: + runtime \ No newline at end of file diff --git a/internal/runbits/buildscript/testdata/buildscript2.yaml b/internal/runbits/buildscript/testdata/buildscript2.yaml new file mode 100644 index 0000000000..0196b1d734 --- /dev/null +++ b/internal/runbits/buildscript/testdata/buildscript2.yaml @@ -0,0 +1,42 @@ +let: + runtime = state_tool_artifacts_v1( + build_flags = [ + ], + camel_flags = [ + ], + src = "$sources" + ) + sources = solve( + at_time = "2023-08-01T16:20:11.985000Z", + platforms = [ + "78977bc8-0f32-519d-80f3-9043f059398c", + "7c998ec2-7491-4e75-be4d-8885800ef5f2", + "96b7e6f2-bebf-564c-bc1c-f04482398f38" + ], + requirements = [ + { + name = "python", + namespace = "language", + version_requirements = [ + { + comparator = "eq", + version = "3.10.11" + } + ] + }, + { + name = "requests", + namespace = "language/python", + version_requirements = [ + { + comparator = "eq", + version = "2.30.0" + } + ] + } + ], + solver_version = null + ) + +in: + runtime \ No newline at end of file diff --git a/pkg/cmdlets/checker/checker.go b/internal/runbits/checker/checker.go similarity index 96% rename from pkg/cmdlets/checker/checker.go rename to internal/runbits/checker/checker.go index d3501a920e..c43b546318 100644 --- a/pkg/cmdlets/checker/checker.go +++ b/internal/runbits/checker/checker.go @@ -15,7 +15,7 @@ import ( "github.com/ActiveState/cli/internal/multilog" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/profile" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/updater" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" @@ -56,7 +56,7 @@ func CommitsBehind(p *project.Project) (int, error) { return 0, locale.NewError("err_latest_commit", "Latest commit ID is nil") } - commitID, err := localcommit.Get(p.Dir()) + commitID, err := commitmediator.Get(p) if err != nil { return 0, errs.Wrap(err, "Unable to get local commit") } diff --git a/pkg/cmdlets/checkout/checkout.go b/internal/runbits/checkout/checkout.go similarity index 79% rename from pkg/cmdlets/checkout/checkout.go rename to internal/runbits/checkout/checkout.go index a20788032c..5692e31ef3 100644 --- a/pkg/cmdlets/checkout/checkout.go +++ b/internal/runbits/checkout/checkout.go @@ -13,10 +13,8 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/language" - "github.com/ActiveState/cli/internal/multilog" "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/cmdlets/git" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/git" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" @@ -46,7 +44,9 @@ 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, noClone bool) (string, error) { +func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath string, noClone bool) (_ string, rerr error) { + defer r.rationalizeError(&rerr) + path, err := r.pathToUse(ns, targetPath) if err != nil { return "", errs.Wrap(err, "Could not get path to use") @@ -57,10 +57,11 @@ func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath return "", errs.Wrap(err, "Could not get absolute path") } - emptyDir, err := fileutils.IsEmptyDir(path) - if err != nil { - multilog.Error("Unable to check if directory is empty: %v", err) - } + // Re-enable in DX-2307. + //emptyDir, err := fileutils.IsEmptyDir(path) + //if err != nil { + // multilog.Error("Unable to check if directory is empty: %v", err) + //} // If project does not exist at path then we must checkout // the project and create the project file @@ -125,29 +126,31 @@ func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath configFile := filepath.Join(path, constants.ConfigFileName) if !fileutils.FileExists(configFile) { _, err = projectfile.Create(&projectfile.CreateParams{ - Owner: owner, - Project: pj.Name, // match case on the Platform - BranchName: branchName, - Directory: path, - Language: language.String(), - Cache: cachePath, + Owner: owner, + Project: pj.Name, // match case on the Platform + BranchName: branchName, + Directory: path, + Language: language.String(), + Cache: cachePath, + LegacyCommitID: commitID.String(), // remove in DX-2307 }) if err != nil { return "", errs.Wrap(err, "Could not create projectfile") } } - err = localcommit.Set(path, commitID.String()) - if err != nil { - return "", errs.Wrap(err, "Could not create local commit file") - } - if emptyDir || fileutils.DirExists(filepath.Join(path, ".git")) { - err = localcommit.AddToGitIgnore(path) - if err != nil { - r.Outputer.Notice(locale.Tr("notice_commit_id_gitignore", constants.ProjectConfigDirName, constants.CommitIdFileName)) - multilog.Error("Unable to add local commit file to .gitignore: %v", err) - } - } + // Re-enable in DX-2307. + //err = localcommit.Set(path, commitID.String()) + //if err != nil { + // return "", errs.Wrap(err, "Could not create local commit file") + //} + //if emptyDir || fileutils.DirExists(filepath.Join(path, ".git")) { + // err = localcommit.AddToGitIgnore(path) + // if err != nil { + // r.Outputer.Notice(locale.Tr("notice_commit_id_gitignore", constants.ProjectConfigDirName, constants.CommitIdFileName)) + // multilog.Error("Unable to add local commit file to .gitignore: %v", err) + // } + //} return path, nil } diff --git a/pkg/cmdlets/checkout/path.go b/internal/runbits/checkout/path.go similarity index 90% rename from pkg/cmdlets/checkout/path.go rename to internal/runbits/checkout/path.go index 8084feb02f..864693d499 100644 --- a/pkg/cmdlets/checkout/path.go +++ b/internal/runbits/checkout/path.go @@ -36,6 +36,11 @@ func (r *Checkout) pathToUse(namespace *project.Namespaced, preferredPath string return path, nil } +type ErrAlreadyCheckedOut struct { + error + Path string +} + func validatePath(ns *project.Namespaced, path string) error { if !fileutils.TargetExists(path) { return nil @@ -51,7 +56,7 @@ func validatePath(ns *project.Namespaced, path string) error { configFile := filepath.Join(path, constants.ConfigFileName) if fileutils.FileExists(configFile) { - return locale.NewInputError("err_already_checked_out", "", path) + return &ErrAlreadyCheckedOut{errs.New("already checked out at %s", path), path} } return nil diff --git a/internal/runbits/checkout/rationalize.go b/internal/runbits/checkout/rationalize.go new file mode 100644 index 0000000000..6d546b5a14 --- /dev/null +++ b/internal/runbits/checkout/rationalize.go @@ -0,0 +1,30 @@ +package checkout + +import ( + "errors" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/pkg/platform/model" +) + +func (c *Checkout) rationalizeError(err *error) { + var errAlreadyCheckedOut *ErrAlreadyCheckedOut + var errProjectNotFound *model.ErrProjectNotFound + + switch { + case err == nil: + return + case errors.As(*err, &errAlreadyCheckedOut): + *err = errs.WrapUserFacing( + *err, locale.Tr("err_already_checked_out", errAlreadyCheckedOut.Path), + errs.SetInput(), + ) + case errors.As(*err, &errProjectNotFound): + *err = errs.WrapUserFacing(*err, + locale.Tr("err_api_project_not_found", errProjectNotFound.Organization, errProjectNotFound.Project), + errs.SetIf(!c.auth.Authenticated(), errs.SetTips(locale.T("tip_private_project_auth"))), + errs.SetInput(), + ) + } +} diff --git a/pkg/cmdlets/commit/commit.go b/internal/runbits/commit/commit.go similarity index 100% rename from pkg/cmdlets/commit/commit.go rename to internal/runbits/commit/commit.go diff --git a/internal/runbits/commitmediator/commitmediator.go b/internal/runbits/commitmediator/commitmediator.go new file mode 100644 index 0000000000..37e5df525c --- /dev/null +++ b/internal/runbits/commitmediator/commitmediator.go @@ -0,0 +1,38 @@ +package commitmediator + +import ( + "github.com/go-openapi/strfmt" +) + +type projecter interface { + Dir() string + URL() string + Path() string + LegacyCommitID() string + LegacySetCommit(string) error // remove this in DX-2307 +} + +// Get returns the given project's commit ID in either the new format (commit file), or the old +// format (activestate.yaml). +// If you require the commit file, use localcommit.Get(). +func Get(proj projecter) (strfmt.UUID, error) { + // Re-enable the contents of this function in DX-2307 + //if commitID, err := localcommit.Get(proj.Dir()); err == nil { + // return commitID, nil + //} else if localcommit.IsFileDoesNotExistError(err) { + //if migrated, err := projectmigration.PromptAndMigrate(proj); err == nil && migrated { + // return localcommit.Get(proj.Dir()) + //} else if err != nil { + // return "", errs.Wrap(err, "Could not prompt and/or migrate project") + //} + return strfmt.UUID(proj.LegacyCommitID()), nil + //} else { + // return "", errs.Wrap(err, "Could not get local commit") + //} +} + +func Set(proj projecter, commitID string) error { + // Replace all calls to this function with localcommit.Set() in DX-2307. + // Also, consider changing localcommit.Set() to accept a projecter interface with Dir(). + return proj.LegacySetCommit(commitID) +} diff --git a/internal/runbits/errors/centralized.go b/internal/runbits/errors/centralized.go deleted file mode 100644 index 3f005a1fda..0000000000 --- a/internal/runbits/errors/centralized.go +++ /dev/null @@ -1,7 +0,0 @@ -package errors - -import "github.com/ActiveState/cli/internal/errs" - -type ErrNoProject struct { - *errs.WrapperError -} diff --git a/pkg/cmdlets/errors/errors.go b/internal/runbits/errors/errors.go similarity index 86% rename from pkg/cmdlets/errors/errors.go rename to internal/runbits/errors/errors.go index 41f5c67847..108eab2424 100644 --- a/pkg/cmdlets/errors/errors.go +++ b/internal/runbits/errors/errors.go @@ -6,6 +6,8 @@ import ( "os" "strings" + "github.com/thoas/go-funk" + "github.com/ActiveState/cli/internal/analytics" anaConst "github.com/ActiveState/cli/internal/analytics/constants" "github.com/ActiveState/cli/internal/analytics/dimensions" @@ -65,20 +67,14 @@ func (o *OutputError) MarshalOutput(f output.Format) interface{} { } // Concatenate error tips - errorTips := []string{} - err := o.error - for _, err := range errs.Unpack(err) { - if v, ok := err.(ErrorTips); ok { - errorTips = append(errorTips, v.ErrorTips()...) - } - } - errorTips = append(errorTips, locale.Tl("err_help_forum", "[NOTICE]Ask For Help →[/RESET] [ACTIONABLE]{{.V0}}[/RESET]", constants.ForumsURL)) + errorTips := getErrorTips(o.error) + errorTips = append(errorTips, locale.Tl("err_help_forum", "Ask For Help → [ACTIONABLE]{{.V0}}[/RESET]", constants.ForumsURL)) // Print tips enableTips := os.Getenv(constants.DisableErrorTipsEnvVarName) != "true" && f == output.PlainFormatName if enableTips { outLines = append(outLines, "") // separate error from "Need More Help?" header - outLines = append(outLines, output.Title(locale.Tl("err_more_help", "Need More Help?")).String()) + outLines = append(outLines, strings.TrimSpace(output.Title(locale.Tl("err_more_help", "Need More Help?")).String())) for _, tip := range errorTips { outLines = append(outLines, fmt.Sprintf(" [DISABLED]•[/RESET] %s", trimError(tip))) } @@ -86,8 +82,32 @@ func (o *OutputError) MarshalOutput(f output.Format) interface{} { return strings.Join(outLines, "\n") } +func getErrorTips(err error) []string { + errorTips := []string{} + for _, err := range errs.Unpack(err) { + v, ok := err.(ErrorTips) + if !ok { + continue + } + for _, tip := range v.ErrorTips() { + if funk.Contains(errorTips, tip) { + continue + } + errorTips = append(errorTips, tip) + } + } + return errorTips +} + func (o *OutputError) MarshalStructured(f output.Format) interface{} { - return output.StructuredError{locale.JoinedErrorMessage(o.error)} + var userFacingError errs.UserFacingError + var message string + if errors.As(o.error, &userFacingError) { + message = userFacingError.UserError() + } else { + message = locale.JoinedErrorMessage(o.error) + } + return output.StructuredError{message, getErrorTips(o.error)} } func trimError(msg string) string { diff --git a/pkg/cmdlets/git/git.go b/internal/runbits/git/git.go similarity index 98% rename from pkg/cmdlets/git/git.go rename to internal/runbits/git/git.go index f0e496685a..4ff0c248b7 100644 --- a/pkg/cmdlets/git/git.go +++ b/internal/runbits/git/git.go @@ -40,7 +40,7 @@ type Repo struct { // CloneProject will attempt to clone the associalted public git repository // for the project identified by / to the given directory func (r *Repo) CloneProject(owner, name, path string, out output.Outputer, an analytics.Dispatcher) error { - project, err := model.FetchProjectByName(owner, name) + project, err := model.LegacyFetchProjectByName(owner, name) if err != nil { return locale.WrapError(err, "err_git_fetch_project", "Could not fetch project details") } diff --git a/pkg/cmdlets/git/test/integration/git_test.go b/internal/runbits/git/test/integration/git_test.go similarity index 87% rename from pkg/cmdlets/git/test/integration/git_test.go rename to internal/runbits/git/test/integration/git_test.go index c8f2fb669c..07412744bc 100644 --- a/pkg/cmdlets/git/test/integration/git_test.go +++ b/internal/runbits/git/test/integration/git_test.go @@ -16,10 +16,9 @@ import ( "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/locale" + runbitsGit "github.com/ActiveState/cli/internal/runbits/git" "github.com/ActiveState/cli/internal/testhelpers/outputhelper" - gitlet "github.com/ActiveState/cli/pkg/cmdlets/git" "github.com/ActiveState/cli/pkg/project" - "github.com/ActiveState/cli/pkg/projectfile" ) type GitTestSuite struct { @@ -42,7 +41,7 @@ func (suite *GitTestSuite) BeforeTest(suiteName, testName string) { projectURL := fmt.Sprintf("https://%s/%s/%s", constants.PlatformURL, "test-owner", "test-project") - _, err = projectfile.TestOnlyCreateWithProjectURL(projectURL, suite.dir) + err = fileutils.WriteFile(filepath.Join(suite.dir, "activestate.yaml"), []byte("project: "+projectURL)) suite.NoError(err, "could not create a projectfile") err = fileutils.Touch(filepath.Join(suite.dir, "test-file")) @@ -80,7 +79,7 @@ func (suite *GitTestSuite) AfterTest(suiteName, testName string) { } func (suite *GitTestSuite) TestEnsureCorrectProject() { - err := gitlet.EnsureCorrectProject("test-owner", "test-project", filepath.Join(suite.dir, constants.ConfigFileName), "test-repo", outputhelper.NewCatcher(), blackhole.New()) + err := runbitsGit.EnsureCorrectProject("test-owner", "test-project", filepath.Join(suite.dir, constants.ConfigFileName), "test-repo", outputhelper.NewCatcher(), blackhole.New()) suite.NoError(err, "projectfile URL should contain owner and name") } @@ -89,7 +88,7 @@ func (suite *GitTestSuite) TestEnsureCorrectProject_Missmatch() { name := "bad-project" projectPath := filepath.Join(suite.dir, constants.ConfigFileName) actualCatcher := outputhelper.NewCatcher() - err := gitlet.EnsureCorrectProject(owner, name, projectPath, "test-repo", actualCatcher, blackhole.New()) + err := runbitsGit.EnsureCorrectProject(owner, name, projectPath, "test-repo", actualCatcher, blackhole.New()) suite.NoError(err) proj, err := project.Parse(projectPath) @@ -105,7 +104,7 @@ func (suite *GitTestSuite) TestEnsureCorrectProject_Missmatch() { func (suite *GitTestSuite) TestMoveFiles() { anotherDir := filepath.Join(suite.anotherDir, "anotherDir") - err := gitlet.MoveFiles(suite.dir, anotherDir) + err := runbitsGit.MoveFiles(suite.dir, anotherDir) suite.NoError(err, "should be able to move files wihout error") _, err = os.Stat(filepath.Join(anotherDir, constants.ConfigFileName)) @@ -123,7 +122,7 @@ func (suite *GitTestSuite) TestMoveFilesDirNoEmpty() { err = fileutils.Touch(filepath.Join(anotherDir, "file.txt")) suite.Require().NoError(err) - err = gitlet.MoveFiles(suite.dir, anotherDir) + err = runbitsGit.MoveFiles(suite.dir, anotherDir) expected := locale.WrapError(err, "err_git_verify_dir", "Could not verify destination directory") suite.EqualError(err, expected.Error()) } diff --git a/internal/runbits/legacy/projectmigration/projectmigration.go b/internal/runbits/legacy/projectmigration/projectmigration.go new file mode 100644 index 0000000000..cd701aec22 --- /dev/null +++ b/internal/runbits/legacy/projectmigration/projectmigration.go @@ -0,0 +1,93 @@ +package projectmigration + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/fileutils" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/multilog" + "github.com/ActiveState/cli/internal/output" + "github.com/ActiveState/cli/internal/prompt" + "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/pkg/projectfile" +) + +type projecter interface { + Dir() string + URL() string + Path() string + LegacyCommitID() string +} + +var prompter prompt.Prompter +var out output.Outputer +var declined bool + +// Register exists to avoid boilerplate in passing prompt and out to every caller of +// commitmediator.Get() for retrieving legacy commitId from activestate.yaml. +// This is an anti-pattern and is only used to make this legacy feature palatable. +func Register(prompter_ prompt.Prompter, out_ output.Outputer) { + prompter = prompter_ + out = out_ +} + +func PromptAndMigrate(proj projecter) (bool, error) { + if prompter == nil || out == nil { + return false, errs.New("projectmigration.Register() has not been called") + } + + if declined { + return false, nil + } + + if os.Getenv(constants.DisableProjectMigrationPrompt) == "true" { + return false, nil + } + + defaultChoice := false + if migrate, err := prompter.Confirm("", locale.T("projectmigration_confirm"), &defaultChoice); err == nil && !migrate { + if out.Config().Interactive { + out.Notice(locale.Tl("projectmigration_declined", "Migration declined for now")) + } + declined = true + return false, nil + } else if err != nil { + return false, errs.Wrap(err, "Could not confirm migration choice") + } + + if err := localcommit.Set(proj.Dir(), proj.LegacyCommitID()); err != nil { + return false, errs.Wrap(err, "Could not create local commit file") + } + + for dir := proj.Dir(); filepath.Dir(dir) != dir; dir = filepath.Dir(dir) { + if !fileutils.DirExists(filepath.Join(dir, ".git")) { + continue + } + err := localcommit.AddToGitIgnore(dir) + if err != nil { + if !errors.Is(err, fs.ErrPermission) { + multilog.Error("Unable to add local commit file to .gitignore: %v", err) + } + out.Notice(locale.T("notice_commit_id_gitignore")) + } + break + } + + pf := projectfile.NewProjectField() + if err := pf.LoadProject(proj.URL()); err != nil { + return false, errs.Wrap(err, "Could not load activestate.yaml") + } + pf.StripCommitID() + if err := pf.Save(proj.Path()); err != nil { + return false, errs.Wrap(err, "Could not save activestate.yaml") + } + + out.Notice(locale.Tl("projectmigration_success", "Your project was successfully migrated")) + + return true, nil +} diff --git a/internal/runbits/rationalize/types.go b/internal/runbits/rationalize/types.go new file mode 100644 index 0000000000..264794e7fa --- /dev/null +++ b/internal/runbits/rationalize/types.go @@ -0,0 +1,28 @@ +package rationalize + +import ( + "errors" + "fmt" +) + +// Inner is just an alias because otherwise external use of this struct would not be able to construct the error +// property, and we want to keep the boilerplate minimal. +type Inner error + +// ErrNoProject communicates that we were unable to find an activestate.yaml +var ErrNoProject = errors.New("no project") + +// ErrNotAuthenticated communicates that the user is not logged in +var ErrNotAuthenticated = errors.New("not authenticated") + +var ErrActionAborted = errors.New("aborted by user") + +type ErrAPI struct { + Wrapped error + Code int + Message string +} + +func (e *ErrAPI) Error() string { return fmt.Sprintf("API code %d: %s", e.Code, e.Message) } + +func (e *ErrAPI) Unwrap() error { return e.Wrapped } diff --git a/internal/runbits/refresh.go b/internal/runbits/refresh.go index b7d569b4a4..0dd7d9341c 100644 --- a/internal/runbits/refresh.go +++ b/internal/runbits/refresh.go @@ -5,7 +5,6 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/rtutils" - "github.com/ActiveState/cli/internal/runbits/buildscript" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime" @@ -25,10 +24,11 @@ func RefreshRuntime( trigger target.Trigger, svcm *model.SvcModel, ) (rerr error) { - _, err := buildscript.Sync(proj, &commitID, out, auth) - if err != nil { - return locale.WrapError(err, "err_update_build_script") - } + // Re-enable in DX-2307. + //_, err := buildscript.Sync(proj, &commitID, out, auth) + //if err != nil { + // return locale.WrapError(err, "err_update_build_script") + //} target := target.NewProjectTarget(proj, nil, trigger) isCached := true rt, err := runtime.New(target, an, svcm, auth) diff --git a/internal/runbits/requirements/rationalize.go b/internal/runbits/requirements/rationalize.go new file mode 100644 index 0000000000..b6babd8f84 --- /dev/null +++ b/internal/runbits/requirements/rationalize.go @@ -0,0 +1,59 @@ +package requirements + +import ( + "errors" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/runbits/rationalize" + "github.com/ActiveState/cli/pkg/localcommit" + bpModel "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" + "github.com/ActiveState/cli/pkg/platform/model" +) + +func (r *RequirementOperation) rationalizeError(err *error) { + var localCommitFileErr *localcommit.ErrLocalCommitFile + var tooManyMatchesErr *model.ErrTooManyMatches + var noMatchesErr *ErrNoMatches + var buildPlannerErr *bpModel.BuildPlannerError + + switch { + case err == nil: + return + + // Local commit file not found or malformed + case errors.As(*err, &localCommitFileErr): + *err = errs.WrapUserFacing(*err, + locale.Tr("err_local_commit_file", localCommitFileErr.File), + errs.SetInput()) + + // Too many matches + case errors.As(*err, &tooManyMatchesErr): + *err = errs.WrapUserFacing(*err, + locale.Tr("err_searchingredient_toomany", tooManyMatchesErr.Query), + errs.SetInput()) + + // No matches, and no alternate suggestions + case errors.As(*err, &noMatchesErr) && noMatchesErr.Alternatives == nil: + *err = errs.WrapUserFacing(*err, + locale.Tr("package_ingredient_alternatives_nosuggest", noMatchesErr.Query), + errs.SetInput()) + + // No matches, but have alternate suggestions + case errors.As(*err, &noMatchesErr) && noMatchesErr.Alternatives != nil: + *err = errs.WrapUserFacing(*err, + locale.Tr("package_ingredient_alternatives", noMatchesErr.Query, *noMatchesErr.Alternatives), + errs.SetInput()) + + // We communicate buildplanner errors verbatim as the intend is that these are curated by the buildplanner + case errors.As(*err, &buildPlannerErr): + *err = errs.WrapUserFacing(*err, + buildPlannerErr.LocalizedError(), + errs.SetIf(buildPlannerErr.InputError(), errs.SetInput())) + + case errors.Is(*err, rationalize.ErrNoProject): + *err = errs.WrapUserFacing(*err, + locale.Tr("err_no_project"), + errs.SetInput()) + } +} diff --git a/internal/runbits/requirements/requirements.go b/internal/runbits/requirements/requirements.go index 0e368828b2..5faf07f963 100644 --- a/internal/runbits/requirements/requirements.go +++ b/internal/runbits/requirements/requirements.go @@ -1,9 +1,7 @@ package requirements import ( - "errors" "fmt" - "os" "strconv" "strings" @@ -19,18 +17,16 @@ import ( "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/prompt" + "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/runbits" - "github.com/ActiveState/cli/internal/runbits/rtusage" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" + "github.com/ActiveState/cli/internal/runbits/rationalize" bpModel "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" medmodel "github.com/ActiveState/cli/pkg/platform/api/mediator/model" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" - "github.com/ActiveState/cli/pkg/platform/runtime/artifact" - "github.com/ActiveState/cli/pkg/platform/runtime/buildscript" "github.com/ActiveState/cli/pkg/platform/runtime/target" "github.com/ActiveState/cli/pkg/project" - "github.com/ActiveState/cli/pkg/projectfile" "github.com/thoas/go-funk" ) @@ -80,7 +76,16 @@ func NewRequirementOperation(prime primeable) *RequirementOperation { const latestVersion = "latest" -func (r *RequirementOperation) ExecuteRequirementOperation(requirementName, requirementVersion string, requirementBitWidth int, operation bpModel.Operation, nsType model.NamespaceType) (rerr error) { +type ErrNoMatches struct { + *locale.LocalizedError + Query string + Alternatives *string +} + +func (r *RequirementOperation) ExecuteRequirementOperation(requirementName, requirementVersion string, + requirementBitWidth int, operation bpModel.Operation, nsType model.NamespaceType) (rerr error) { + defer r.rationalizeError(&rerr) + var ns model.Namespace var langVersion string langName := "undetermined" @@ -95,32 +100,18 @@ func (r *RequirementOperation) ExecuteRequirementOperation(requirementName, requ }() var err error - pj := r.Project - if pj == nil { - pg = output.StartSpinner(out, locale.Tl("progress_project", "", requirementName), constants.TerminalAnimationInterval) - pj, err = initializeProject() - if err != nil { - return locale.WrapError(err, "err_package_get_project", "Could not get project from path") - } - pg.Stop(locale.T("progress_success")) - pg = nil // The defer above will redundantly call pg.Stop on success if we don't set this to nil - - defer func() { - if rerr != nil && !errors.Is(err, artifact.CamelRuntimeBuilding) { - if err := os.Remove(pj.Source().Path()); err != nil { - multilog.Error("could not remove temporary project file: %s", errs.JoinMessage(err)) - } - } - }() + if r.Project == nil { + return rationalize.ErrNoProject } - out.Notice(locale.Tl("operating_message", "", pj.NamespaceString(), pj.Dir())) + out.Notice(locale.Tl("operating_message", "", r.Project.NamespaceString(), r.Project.Dir())) switch nsType { case model.NamespacePackage, model.NamespaceBundle: - commitID, err := localcommit.Get(pj.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err := commitmediator.Get(r.Project) + if err != nil { return errs.Wrap(err, "Unable to get local commit") } + language, err := model.LanguageByCommit(commitID) if err == nil { langName = language.Name @@ -134,8 +125,6 @@ func (r *RequirementOperation) ExecuteRequirementOperation(requirementName, requ ns = model.NewNamespacePlatform() } - rtusage.PrintRuntimeUsage(r.SvcModel, out, pj.Owner()) - var validatePkg = operation == bpModel.OperationAdded && (ns.Type() == model.NamespacePackage || ns.Type() == model.NamespaceBundle) if !ns.IsValid() && (nsType == model.NamespacePackage || nsType == model.NamespaceBundle) { pg = output.StartSpinner(out, locale.Tl("progress_pkg_nolang", "", requirementName), constants.TerminalAnimationInterval) @@ -176,15 +165,22 @@ func (r *RequirementOperation) ExecuteRequirementOperation(requirementName, requ if err != nil { return locale.WrapError(err, "package_err_cannot_obtain_search_results") } + if len(packages) == 0 { suggestions, err := getSuggestions(ns, requirementName) if err != nil { multilog.Error("Failed to retrieve suggestions: %v", err) } + if len(suggestions) == 0 { - return locale.WrapInputError(err, "package_ingredient_alternatives_nosuggest", "", requirementName) + return &ErrNoMatches{ + locale.WrapInputError(err, "package_ingredient_alternatives_nosuggest", "", requirementName), + requirementName, nil} } - return locale.WrapInputError(err, "package_ingredient_alternatives", "", requirementName, strings.Join(suggestions, "\n")) + + return &ErrNoMatches{ + locale.WrapInputError(err, "package_ingredient_alternatives", "", requirementName, strings.Join(suggestions, "\n")), + requirementName, ptr.To(strings.Join(suggestions, "\n"))} } requirementName = normalized @@ -193,8 +189,8 @@ func (r *RequirementOperation) ExecuteRequirementOperation(requirementName, requ pg = nil } - parentCommitID, err := localcommit.Get(pj.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + parentCommitID, err := commitmediator.Get(r.Project) + if err != nil { return errs.Wrap(err, "Unable to get local commit") } hasParentCommit := parentCommitID != "" @@ -240,8 +236,8 @@ func (r *RequirementOperation) ExecuteRequirementOperation(requirementName, requ } params := model.StageCommitParams{ - Owner: pj.Owner(), - Project: pj.Name(), + Owner: r.Project.Owner(), + Project: r.Project.Name(), ParentCommit: string(parentCommitID), Description: commitMessage(operation, name, version, ns, requirementBitWidth), RequirementName: name, @@ -272,29 +268,31 @@ func (r *RequirementOperation) ExecuteRequirementOperation(requirementName, requ return errs.Wrap(err, "Unsupported namespace type: %s", ns.Type().String()) } - expr, err := bp.GetBuildExpression(pj.Owner(), pj.Name(), commitID.String()) - if err != nil { - return errs.Wrap(err, "Could not get remote build expr") - } + // Re-enable in DX-2307. + //expr, err := bp.GetBuildExpression(r.Project.Owner(), r.Project.Name(), commitID.String()) + //if err != nil { + // return errs.Wrap(err, "Could not get remote build expr") + //} - if err := localcommit.Set(pj.Dir(), commitID.String()); err != nil { + if err := commitmediator.Set(r.Project, commitID.String()); err != nil { return locale.WrapError(err, "err_package_update_commit_id") } // Note: a commit ID file needs to exist at this point. - err = buildscript.Update(pj, expr, r.Auth) - if err != nil { - return locale.WrapError(err, "err_update_build_script") - } + // Re-enable in DX-2307. + //err = buildscript.Update(r.Project, expr, r.Auth) + //if err != nil { + // return locale.WrapError(err, "err_update_build_script") + //} // refresh or install runtime - err = runbits.RefreshRuntime(r.Auth, r.Output, r.Analytics, pj, commitID, true, trigger, r.SvcModel) + err = runbits.RefreshRuntime(r.Auth, r.Output, r.Analytics, r.Project, commitID, true, trigger, r.SvcModel) if err != nil { return err } if !hasParentCommit { - out.Notice(locale.Tr("install_initial_success", pj.Source().Path())) + out.Notice(locale.Tr("install_initial_success", r.Project.Source().Path())) } // Print the result @@ -402,25 +400,6 @@ func getSuggestions(ns model.Namespace, name string) ([]string, error) { return suggestions, nil } -func initializeProject() (*project.Project, error) { - target, err := os.Getwd() - if err != nil { - return nil, locale.WrapError(err, "err_add_get_wd", "Could not get working directory for new project") - } - - createParams := &projectfile.CreateParams{ - ProjectURL: constants.DashboardCommitURL, - Directory: target, - } - - _, err = projectfile.Create(createParams) - if err != nil { - return nil, locale.WrapError(err, "err_add_create_projectfile", "Could not create new projectfile") - } - - return project.FromPath(target) -} - func commitMessage(op bpModel.Operation, name, version string, namespace model.Namespace, word int) string { switch namespace.Type() { case model.NamespaceLanguage: diff --git a/internal/runbits/rtusage/rtusage.go b/internal/runbits/rtusage/rtusage.go deleted file mode 100644 index 4292319548..0000000000 --- a/internal/runbits/rtusage/rtusage.go +++ /dev/null @@ -1,100 +0,0 @@ -package rtusage - -import ( - "context" - "os" - "strconv" - "time" - - "github.com/ActiveState/cli/internal/config" - "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/graph" - "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/multilog" - "github.com/ActiveState/cli/internal/notify" - "github.com/ActiveState/cli/internal/output" -) - -const CfgKeyLastNotify = "notify.rtusage.last" - -type dataHandler interface { - CheckRuntimeUsage(ctx context.Context, organizationName string) (*graph.CheckRuntimeUsageResponse, error) -} - -func PrintRuntimeUsage(data dataHandler, out output.Outputer, orgName string) { - if orgName == "" { - return - } - - logging.Debug("Checking to print runtime usage for %s", orgName) - - res, err := data.CheckRuntimeUsage(context.Background(), orgName) - if err != nil { - // Runtime usage is not enforced, so any errors should not interrupt the user either - multilog.Error("Could not check runtime usage: %v", errs.JoinMessage(err)) - return - } - - usage := res.Usage - if override := os.Getenv(constants.RuntimeUsageOverrideEnvVarName); override != "" { - logging.Debug("Overriding usage with %s", override) - usage, _ = strconv.Atoi(override) - } - - if usage > res.Limit { - out.Notice(locale.Tr("runtime_usage_limit_reached", orgName, strconv.Itoa(usage), strconv.Itoa(res.Limit))) - } -} - -func NotifyRuntimeUsage(cfg *config.Instance, data dataHandler, orgName string) { - if orgName == "" { - return - } - - res, err := data.CheckRuntimeUsage(context.Background(), orgName) - if err != nil { - multilog.Error("Soft limit: Failed to check runtime usage in heartbeat handler: %s", errs.JoinMessage(err)) - return - } - - usage := res.Usage - if override := os.Getenv(constants.RuntimeUsageOverrideEnvVarName); override != "" { - logging.Debug("Overriding usage with %s", override) - usage, _ = strconv.Atoi(override) - } - - if usage <= res.Limit { - return - } - - // Override silence time - silenceMs := time.Hour.Milliseconds() - if override := os.Getenv(constants.RuntimeUsageSilenceTimeOverrideEnvVarName); override != "" { - overrideInt, err := strconv.ParseInt(override, 10, 64) - if err != nil { - logging.Error("Failed to parse runtime usage silence time override: %v", err) - } else { - silenceMs = overrideInt - } - } - - // Don't notify if we already notified recently - if time.Now().Sub(cfg.GetTime(CfgKeyLastNotify)).Milliseconds() <= silenceMs { - return - } - - if err := cfg.Set(CfgKeyLastNotify, time.Now()); err != nil { - multilog.Error("Soft limit: Failed to set last notify time: %s", errs.JoinMessage(err)) - return - } - - err2 := notify.Send(locale.T("runtime_limit_reached_title"), - locale.Tr("runtime_limit_reached_msg", orgName), - locale.T("runtime_limit_reached_action"), - "state://platform/upgrade/?org="+orgName) // We have to use the state protocol because https:// is backgrounded by the OS - if err2 != nil { - multilog.Error("Soft limit: Failed to send notification: %s", errs.JoinMessage(err)) - } -} diff --git a/internal/runbits/runtime/rationalize.go b/internal/runbits/runtime/rationalize.go new file mode 100644 index 0000000000..e48ad8cb4d --- /dev/null +++ b/internal/runbits/runtime/rationalize.go @@ -0,0 +1,61 @@ +package runtime + +import ( + "errors" + "strings" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/multilog" + "github.com/ActiveState/cli/pkg/platform/authentication" + "github.com/ActiveState/cli/pkg/platform/model" + "github.com/ActiveState/cli/pkg/project" +) + +func rationalizeError(auth *authentication.Auth, proj *project.Project, rerr *error) { + var errNoMatchingPlatform *model.ErrNoMatchingPlatform + + isUpdateErr := errs.Matches(*rerr, &ErrUpdate{}) + switch { + case rerr == nil: + return + + case proj == nil: + multilog.Error("runtime:rationalizeError called with nil project, error: %s", errs.JoinMessage(*rerr)) + *rerr = errs.Pack(*rerr, errs.New("project is nil")) + + case proj.IsHeadless(): + *rerr = errs.NewUserFacing( + locale.Tl( + "err_runtime_headless", + "Cannot initialize runtime for a headless project. Please visit {{.V0}} to convert your project and try again.", + proj.URL(), + ), + errs.SetInput(), + ) + + // Could not find a platform that matches on the given branch, so suggest alternate branches if ones exist + case isUpdateErr && errors.As(*rerr, &errNoMatchingPlatform): + branches, err := model.BranchNamesForProjectFiltered(proj.Owner(), proj.Name(), proj.BranchName()) + if err == nil && len(branches) > 0 { + // Suggest alternate branches + *rerr = errs.NewUserFacing(locale.Tr( + "err_alternate_branches", + errNoMatchingPlatform.HostPlatform, errNoMatchingPlatform.HostArch, + proj.BranchName(), strings.Join(branches, "\n - "))) + } else { + *rerr = errs.NewUserFacing(locale.Tr( + "err_no_platform_data_remains", + errNoMatchingPlatform.HostPlatform, errNoMatchingPlatform.HostArch)) + } + + // If updating failed due to unidentified errors, and the user is not authenticated, add a tip suggesting that they authenticate as + // this may be a private project. + // Note since we cannot assert the actual error type we do not wrap this as user-facing, as we do not know what we're + // dealing with so the localized underlying errors are more appropriate. + case isUpdateErr && !auth.Authenticated(): + *rerr = errs.AddTips(*rerr, + locale.T("tip_private_project_auth"), + ) + } +} diff --git a/internal/runbits/runtime/runtime.go b/internal/runbits/runtime/runtime.go index 02c3e0785d..35c712d43e 100644 --- a/internal/runbits/runtime/runtime.go +++ b/internal/runbits/runtime/runtime.go @@ -1,10 +1,7 @@ package runtime import ( - "strings" - "github.com/ActiveState/cli/internal/analytics" - "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/rtutils" @@ -16,6 +13,10 @@ import ( "github.com/ActiveState/cli/pkg/project" ) +type ErrUpdate struct { + *locale.LocalizedError +} + // NewFromProject is a helper function that creates a new runtime or updates an existing one for // the given project. func NewFromProject( @@ -25,42 +26,25 @@ func NewFromProject( svcModel *model.SvcModel, out output.Outputer, auth *authentication.Auth) (_ *rt.Runtime, rerr error) { + defer rationalizeError(auth, proj, &rerr) rti, err := rt.New(target.NewProjectTarget(proj, nil, trigger), an, svcModel, auth) - switch { - case err == nil: - break + if err == nil { + return rti, nil + } - case rt.IsNeedsUpdateError(err): + if rt.IsNeedsCommitError(err) { + out.Notice(locale.T("notice_commit_build_script")) + } + + if rt.IsNeedsUpdateError(err) { pg := runbits.NewRuntimeProgressIndicator(out) defer rtutils.Closer(pg.Close, &rerr) - - if err = rti.Update(pg); err != nil { - if errs.Matches(err, &model.ErrOrderAuth{}) { - return nil, errs.AddTips( - locale.WrapInputError(err, "err_update_auth", "Could not update runtime"), - locale.T("tip_private_project_auth")) - } - if errs.Matches(err, &model.ErrNoMatchingPlatform{}) { - branches, err := model.BranchNamesForProjectFiltered(proj.Owner(), proj.Name(), proj.BranchName()) - if err == nil && len(branches) > 1 { - return nil, locale.NewInputError("err_alternate_branches", "", proj.BranchName(), strings.Join(branches, "\n - ")) - } - } - if !auth.Authenticated() { - return nil, errs.AddTips( - locale.WrapError(err, "err_new_runtime_auth", "Could not update runtime installation."), - locale.T("tip_private_project_auth")) - } - return nil, locale.WrapError(err, "err_update_runtime", "Could not update runtime installation.") + if err := rti.Update(pg); err != nil { + return nil, &ErrUpdate{locale.WrapError(err, "err_update_runtime", "Could not update runtime installation.")} } - - case rt.IsNeedsCommitError(err): - out.Notice(locale.T("notice_commit_build_script")) - - default: - return nil, locale.WrapError(err, "err_activate_runtime", "Could not initialize a runtime for this project.") + return rti, nil } - return rti, nil + return nil, locale.WrapError(err, "err_activate_runtime", "Could not initialize a runtime for this project.") } diff --git a/internal/runners/activate/activate.go b/internal/runners/activate/activate.go index f64e5c179a..ce2bd22be1 100644 --- a/internal/runners/activate/activate.go +++ b/internal/runners/activate/activate.go @@ -22,15 +22,14 @@ import ( "github.com/ActiveState/cli/internal/process" "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/runbits/activation" + "github.com/ActiveState/cli/internal/runbits/checker" + "github.com/ActiveState/cli/internal/runbits/checkout" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/runbits/findproject" - "github.com/ActiveState/cli/internal/runbits/rtusage" + "github.com/ActiveState/cli/internal/runbits/git" "github.com/ActiveState/cli/internal/runbits/runtime" "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/cli/internal/virtualenvironment" - "github.com/ActiveState/cli/pkg/cmdlets/checker" - "github.com/ActiveState/cli/pkg/cmdlets/checkout" - "github.com/ActiveState/cli/pkg/cmdlets/git" - "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime/target" @@ -81,7 +80,7 @@ func NewActivate(prime primeable) *Activate { } } -func (r *Activate) Run(params *ActivateParams) error { +func (r *Activate) Run(params *ActivateParams) (rerr error) { logging.Debug("Activate %v, %v", params.Namespace, params.PreferredPath) checker.RunUpdateNotifier(r.analytics, r.svcModel, r.out) @@ -106,8 +105,6 @@ func (r *Activate) Run(params *ActivateParams) error { } } - rtusage.PrintRuntimeUsage(r.svcModel, r.out, proj.Owner()) - alreadyActivated := process.IsActivated(r.config) if alreadyActivated { if !params.Default { @@ -141,18 +138,8 @@ func (r *Activate) Run(params *ActivateParams) error { } } - if proj != nil && params.Branch != "" { - if proj.IsHeadless() { - return locale.NewInputError( - "err_conflicting_branch_while_headless", - "Cannot activate branch [NOTICE]{{.V0}}[/RESET] while in a headless state. Please visit {{.V1}} to create your project.", - params.Branch, proj.URL(), - ) - } - - if params.Branch != proj.BranchName() { - return locale.NewInputError("err_conflicting_branch_while_checkedout", "", params.Branch, proj.BranchName()) - } + if proj != nil && params.Branch != "" && params.Branch != proj.BranchName() { + return locale.NewInputError("err_conflicting_branch_while_checkedout", "", params.Branch, proj.BranchName()) } // Have to call this once the project has been set @@ -198,8 +185,8 @@ func (r *Activate) Run(params *ActivateParams) error { } } - commitID, err := localcommit.Get(proj.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err := commitmediator.Get(proj) + if err != nil { return errs.Wrap(err, "Unable to get local commit") } if commitID == "" { @@ -211,11 +198,7 @@ func (r *Activate) Run(params *ActivateParams) error { return locale.WrapError(err, "err_activate_wait", "Could not activate runtime environment.") } - if proj.IsHeadless() { - r.out.Notice(locale.T("info_deactivated_by_commit")) - } else { - r.out.Notice(locale.T("info_deactivated", proj)) - } + r.out.Notice(locale.T("info_deactivated", proj)) return nil } diff --git a/internal/runners/auth/auth.go b/internal/runners/auth/auth.go index e27159944f..57efc2e415 100644 --- a/internal/runners/auth/auth.go +++ b/internal/runners/auth/auth.go @@ -1,15 +1,13 @@ package auth import ( - "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/keypairs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/prompt" - authlet "github.com/ActiveState/cli/pkg/cmdlets/auth" + "github.com/ActiveState/cli/internal/runbits/auth" "github.com/ActiveState/cli/pkg/platform/authentication" - "github.com/ActiveState/cli/pkg/platform/model" ) type Auth struct { @@ -72,37 +70,12 @@ func (a *Auth) Run(params *AuthParams) error { } username := a.Auth.WhoAmI() - organization, err := model.FetchOrgByURLName(username, a.Auth) - if err != nil { - return errs.Wrap(err, "Could not fetch organizations") - } - - tiers, err := model.FetchTiers() - if err != nil { - return errs.Wrap(err, "Could not fetch tiers") - } - - tier := organization.Tier - privateProjects := false - for _, t := range tiers { - if tier == t.Name && t.RequiresPayment { - privateProjects = true - break - } - } - a.Outputer.Print(output.Prepare( locale.T("logged_in_as", map[string]string{"Name": username}), &struct { - Username string `json:"username,omitempty"` - URLName string `json:"urlname,omitempty"` - Tier string `json:"tier,omitempty"` - PrivateProjects bool `json:"privateProjects"` + Username string `json:"username"` }{ username, - organization.URLname, - tier, - privateProjects, }, )) @@ -111,18 +84,18 @@ func (a *Auth) Run(params *AuthParams) error { func (a *Auth) authenticate(params *AuthParams) error { if params.Prompt || params.Username != "" { - return authlet.AuthenticateWithInput(params.Username, params.Password, params.Totp, params.NonInteractive, a.Cfg, a.Outputer, a.Prompter, a.Auth) + return auth.AuthenticateWithInput(params.Username, params.Password, params.Totp, params.NonInteractive, a.Cfg, a.Outputer, a.Prompter, a.Auth) } if params.Token != "" { - return authlet.AuthenticateWithToken(params.Token, a.Auth) + return auth.AuthenticateWithToken(params.Token, a.Auth) } if params.NonInteractive { return locale.NewInputError("err_auth_needinput") } - return authlet.AuthenticateWithBrowser(a.Outputer, a.Auth, a.Prompter) + return auth.AuthenticateWithBrowser(a.Outputer, a.Auth, a.Prompter) } func (a *Auth) verifyAuthentication() error { diff --git a/internal/runners/auth/signup.go b/internal/runners/auth/signup.go index 66183b4f8b..75ac4997b0 100644 --- a/internal/runners/auth/signup.go +++ b/internal/runners/auth/signup.go @@ -5,7 +5,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/prompt" - authlet "github.com/ActiveState/cli/pkg/cmdlets/auth" + "github.com/ActiveState/cli/internal/runbits/auth" "github.com/ActiveState/cli/pkg/platform/authentication" ) @@ -29,5 +29,5 @@ func (s *Signup) Run(params *SignupParams) error { return locale.NewInputError("err_auth_authenticated", "You are already authenticated as: {{.V0}}. You can log out by running `state auth logout`.", s.Auth.WhoAmI()) } - return authlet.SignupWithBrowser(s.Outputer, s.Auth, s.Prompter) + return auth.SignupWithBrowser(s.Outputer, s.Auth, s.Prompter) } diff --git a/internal/runners/branch/add.go b/internal/runners/branch/add.go index 6d77affb77..e3730936d0 100644 --- a/internal/runners/branch/add.go +++ b/internal/runners/branch/add.go @@ -5,7 +5,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" ) @@ -33,7 +33,7 @@ func (a *Add) Run(params AddParams) error { return locale.NewInputError("err_no_project") } - project, err := model.FetchProjectByName(a.project.Owner(), a.project.Name()) + project, err := model.LegacyFetchProjectByName(a.project.Owner(), a.project.Name()) if err != nil { return locale.WrapError(err, "err_fetch_project", a.project.Namespace().String()) } @@ -49,7 +49,7 @@ func (a *Add) Run(params AddParams) error { return locale.WrapError(err, "err_fetch_branch", "", localBranch) } - commitID, err := localcommit.Get(a.project.Dir()) + commitID, err := commitmediator.Get(a.project) if err != nil { return errs.Wrap(err, "Unable to get local commit") } diff --git a/internal/runners/branch/list.go b/internal/runners/branch/list.go index dcca2976b1..1022f2689a 100644 --- a/internal/runners/branch/list.go +++ b/internal/runners/branch/list.go @@ -37,7 +37,7 @@ func (l *List) Run() error { return locale.NewInputError("err_no_project") } - project, err := model.FetchProjectByName(l.project.Owner(), l.project.Name()) + project, err := model.LegacyFetchProjectByName(l.project.Owner(), l.project.Name()) if err != nil { return locale.WrapError(err, "err_fetch_project", "", l.project.Namespace().String()) } diff --git a/internal/runners/checkout/checkout.go b/internal/runners/checkout/checkout.go index 8c005cda27..d5ad5319f5 100644 --- a/internal/runners/checkout/checkout.go +++ b/internal/runners/checkout/checkout.go @@ -3,16 +3,16 @@ package checkout import ( "github.com/ActiveState/cli/internal/analytics" "github.com/ActiveState/cli/internal/config" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/internal/runbits/rtusage" + "github.com/ActiveState/cli/internal/runbits/checker" + "github.com/ActiveState/cli/internal/runbits/checkout" + "github.com/ActiveState/cli/internal/runbits/git" "github.com/ActiveState/cli/internal/runbits/runtime" "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/pkg/cmdlets/checker" - "github.com/ActiveState/cli/pkg/cmdlets/checkout" - "github.com/ActiveState/cli/pkg/cmdlets/git" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime/setup" @@ -60,7 +60,7 @@ func NewCheckout(prime primeable) *Checkout { } } -func (u *Checkout) Run(params *Params) error { +func (u *Checkout) Run(params *Params) (rerr error) { logging.Debug("Checkout %v", params.Namespace) checker.RunUpdateNotifier(u.analytics, u.svcModel, u.out) @@ -69,7 +69,7 @@ func (u *Checkout) Run(params *Params) error { var err error 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()) + return errs.Wrap(err, "Checkout failed") } proj, err := project.FromPath(projectDir) @@ -77,8 +77,6 @@ func (u *Checkout) Run(params *Params) error { return locale.WrapError(err, "err_project_frompath") } - rtusage.PrintRuntimeUsage(u.svcModel, u.out, proj.Owner()) - rti, err := runtime.NewFromProject(proj, target.TriggerCheckout, u.analytics, u.svcModel, u.out, u.auth) if err != nil { return locale.WrapError(err, "err_checkout_runtime_new", "Could not checkout this project.") diff --git a/internal/runners/cve/cve.go b/internal/runners/cve/cve.go index da56304200..9c58520fe8 100644 --- a/internal/runners/cve/cve.go +++ b/internal/runners/cve/cve.go @@ -7,7 +7,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" medmodel "github.com/ActiveState/cli/pkg/platform/api/mediator/model" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -66,7 +66,7 @@ func (c *Cve) Run() error { ) } - commitID, err := localcommit.Get(c.proj.Dir()) + commitID, err := commitmediator.Get(c.proj) if err != nil { return errs.Wrap(err, "Could not get local commit") } diff --git a/internal/runners/cve/report.go b/internal/runners/cve/report.go index 40ecea55bd..8315c71238 100644 --- a/internal/runners/cve/report.go +++ b/internal/runners/cve/report.go @@ -9,7 +9,7 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" medmodel "github.com/ActiveState/cli/pkg/platform/api/mediator/model" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -102,7 +102,7 @@ func (r *Report) fetchVulnerabilities(namespaceOverride project.Namespaced) (*me commitID = namespaceOverride.CommitID.String() } else { var err error - commitUUID, err := localcommit.Get(r.proj.Dir()) + commitUUID, err := commitmediator.Get(r.proj) if err != nil { return nil, errs.Wrap(err, "Unable to get local commit") } diff --git a/internal/runners/deploy/deploy.go b/internal/runners/deploy/deploy.go index 695d9a424b..4de02d08fe 100644 --- a/internal/runners/deploy/deploy.go +++ b/internal/runners/deploy/deploy.go @@ -23,7 +23,6 @@ import ( "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/rtutils" "github.com/ActiveState/cli/internal/runbits" - "github.com/ActiveState/cli/internal/runbits/rtusage" "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/cli/internal/subshell/sscommon" "github.com/ActiveState/cli/pkg/platform/authentication" @@ -96,8 +95,7 @@ func (d *Deploy) Run(params *Params) error { return locale.WrapError(err, "err_deploy_commitid", "Could not grab commit ID for project: {{.V0}}.", params.Namespace.String()) } - // Headless argument is simply false here as you cannot deploy a headless project - rtTarget := target.NewCustomTarget(params.Namespace.Owner, params.Namespace.Project, commitID, params.Path, target.TriggerDeploy, false) /* TODO: handle empty path */ + rtTarget := target.NewCustomTarget(params.Namespace.Owner, params.Namespace.Project, commitID, params.Path, target.TriggerDeploy) /* TODO: handle empty path */ logging.Debug("runSteps: %s", d.step.String()) @@ -156,8 +154,6 @@ func (d *Deploy) commitID(namespace project.Namespaced) (strfmt.UUID, error) { func (d *Deploy) install(rtTarget setup.Targeter) (rerr error) { d.output.Notice(output.Title(locale.T("deploy_install"))) - rtusage.PrintRuntimeUsage(d.svcModel, d.output, rtTarget.Owner()) - rti, err := runtime.New(rtTarget, d.analytics, d.svcModel, d.auth) if err == nil { d.output.Notice(locale.Tl("deploy_already_installed", "Already installed")) diff --git a/internal/runners/exec/exec.go b/internal/runners/exec/exec.go index 08ce26c6a2..601828940e 100644 --- a/internal/runners/exec/exec.go +++ b/internal/runners/exec/exec.go @@ -23,7 +23,6 @@ import ( "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/rtutils" "github.com/ActiveState/cli/internal/runbits" - "github.com/ActiveState/cli/internal/runbits/rtusage" "github.com/ActiveState/cli/internal/scriptfile" "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/cli/internal/virtualenvironment" @@ -96,13 +95,9 @@ func (s *Exec) Run(params *Params, args ...string) (rerr error) { projectDir = projectFromRuntimeDir(s.cfg, params.Path) proj, err = project.FromPath(projectDir) if err != nil { - logging.Warning("Could not get project dir from path: %s", errs.JoinMessage(err)) - // We do not know if the project is headless at this point so we default to true - // as there is no head - rtTarget = target.NewCustomTarget("", "", "", params.Path, trigger, true) - } else { - rtTarget = target.NewProjectTarget(proj, nil, trigger) + return locale.WrapInputError(err, "exec_no_project_at_path", "Could not find project file at {{.V0}}", projectDir) } + rtTarget = target.NewProjectTarget(proj, nil, trigger) projectNamespace = proj.NamespaceString() } else { proj = s.proj @@ -121,8 +116,6 @@ func (s *Exec) Run(params *Params, args ...string) (rerr error) { rtTarget = target.NewProjectTarget(proj, nil, trigger) } - rtusage.PrintRuntimeUsage(s.svcModel, s.out, rtTarget.Owner()) - s.out.Notice(locale.Tl("operating_message", "", projectNamespace, projectDir)) rt, err := runtime.New(rtTarget, s.analytics, s.svcModel, s.auth) @@ -142,7 +135,7 @@ func (s *Exec) Run(params *Params, args ...string) (rerr error) { } venv := virtualenvironment.New(rt) - env, err := venv.GetEnv(true, false, projectDir, projectNamespace) + env, err := venv.GetEnv(true, false, projectDir) if err != nil { return locale.WrapError(err, "err_exec_env", "Could not retrieve environment information for your runtime") } diff --git a/internal/runners/export/recipe.go b/internal/runners/export/recipe.go index 4f99d181fc..34c32411a3 100644 --- a/internal/runners/export/recipe.go +++ b/internal/runners/export/recipe.go @@ -8,7 +8,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" "github.com/ActiveState/cli/pkg/sysinfo" @@ -83,8 +83,8 @@ func fetchRecipe(proj *project.Project, commitID strfmt.UUID, platform string) ( if commitID == "" { var err error - commitID, err = localcommit.Get(proj.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err = commitmediator.Get(proj) + if err != nil { return "", errs.Wrap(err, "Unable to get local commit") } } diff --git a/internal/runners/hello/hello_example.go b/internal/runners/hello/hello_example.go index 5eab461cd9..bd78721b4b 100644 --- a/internal/runners/hello/hello_example.go +++ b/internal/runners/hello/hello_example.go @@ -8,13 +8,15 @@ package hello import ( + "errors" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/runbits" - "github.com/ActiveState/cli/internal/runbits/errors" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" + "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" ) @@ -58,26 +60,24 @@ func New(p primeable) *Hello { } } -// processError contains the scope in which errors are processed. This is -// useful for ensuring that errors are wrapped in a user-facing error and -// localized. -func processError(err *error) { - if err == nil { - return - } - +// rationalizeError is used to interpret the returned error and rationalize it for the end-user. +// This is so that end-users always get errors that clearly relate to what they were doing, with a good sense on what +// they can do to address it. +func rationalizeError(err *error) { switch { + case err == nil: + return case errs.Matches(*err, &runbits.NoNameProvidedError{}): // Errors that we are looking for should be wrapped in a user-facing error. // Ensure we wrap the top-level error returned from the runner and not // the unpacked error that we are inspecting. - *err = errs.WrapUserFacingError(*err, locale.Tl("hello_err_no_name", "Cannot say hello because no name was provided.")) - case errs.Matches(*err, &errors.ErrNoProject{}): + *err = errs.WrapUserFacing(*err, locale.Tl("hello_err_no_name", "Cannot say hello because no name was provided.")) + case errors.Is(*err, rationalize.ErrNoProject): // It's useful to offer users reasonable tips on recourses. - *err = errs.WrapUserFacingError( + *err = errs.WrapUserFacing( *err, locale.Tl("hello_err_no_project", "Cannot say hello because you are not in a project directory."), - errs.WithTips( + errs.SetTips( locale.Tl("hello_suggest_checkout", "Try using [ACTIONABLE]`state checkout`[/RESET] first."), ), ) @@ -86,12 +86,12 @@ func processError(err *error) { // Run contains the scope in which the hello runner logic is executed. func (h *Hello) Run(params *RunParams) (rerr error) { - defer processError(&rerr) + defer rationalizeError(&rerr) h.out.Print(locale.Tl("hello_notice", "This command is for example use only")) if h.project == nil { - return &errors.ErrNoProject{errs.New("Not in a project directory")} + return rationalize.ErrNoProject } // Reusable runner logic is contained within the runbits package. @@ -145,7 +145,7 @@ func currentCommitMessage(proj *project.Project) (string, error) { return "", errs.New("Cannot determine which project to use") } - commitId, err := localcommit.Get(proj.Dir()) + commitId, err := commitmediator.Get(proj) if err != nil { return "", errs.Wrap(err, "Cannot determine which commit to use") } diff --git a/internal/runners/history/history.go b/internal/runners/history/history.go index 0b971dab75..0ba655b9e0 100644 --- a/internal/runners/history/history.go +++ b/internal/runners/history/history.go @@ -5,8 +5,8 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/pkg/cmdlets/commit" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" @@ -39,22 +39,23 @@ func (h *History) Run(params *HistoryParams) error { } h.out.Notice(locale.Tl("operating_message", "", h.project.NamespaceString(), h.project.Dir())) - localCommitID, err := localcommit.Get(h.project.Dir()) + localCommitID, err := commitmediator.Get(h.project) if err != nil { return errs.Wrap(err, "Unable to get local commit") } - var latestRemoteID *strfmt.UUID - if !h.project.IsHeadless() { - remoteBranch, err := model.BranchForProjectNameByName(h.project.Owner(), h.project.Name(), h.project.BranchName()) - if err != nil { - return locale.WrapError(err, "err_history_remote_branch", "Could not get branch by local branch name") - } + if h.project.IsHeadless() { + return locale.NewInputError("err_history_headless", "Cannot get history for headless project. Please visit {{.V0}} to convert your project and try again.", h.project.URL()) + } - latestRemoteID, err = model.CommonParent(remoteBranch.CommitID, &localCommitID) - if err != nil { - return locale.WrapError(err, "err_history_common_parent", "Could not determine common parent commit") - } + remoteBranch, err := model.BranchForProjectNameByName(h.project.Owner(), h.project.Name(), h.project.BranchName()) + if err != nil { + return locale.WrapError(err, "err_history_remote_branch", "Could not get branch by local branch name") + } + + latestRemoteID, err := model.CommonParent(remoteBranch.CommitID, &localCommitID) + if err != nil { + return locale.WrapError(err, "err_history_common_parent", "Could not determine common parent commit") } commits, err := model.CommitHistoryFromID(localCommitID) diff --git a/internal/runners/initialize/init.go b/internal/runners/initialize/init.go index 44c206548c..9703958965 100644 --- a/internal/runners/initialize/init.go +++ b/internal/runners/initialize/init.go @@ -5,6 +5,8 @@ import ( "path/filepath" "strings" + "github.com/go-openapi/strfmt" + "github.com/ActiveState/cli/internal/analytics" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" @@ -17,7 +19,7 @@ import ( "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/runbits" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime/setup" @@ -69,8 +71,8 @@ func inferLanguage(config projectfile.ConfigGetter) (string, string, bool) { if err != nil { return "", "", false } - commitID, err := localcommit.Get(defaultProj.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err := commitmediator.Get(defaultProj) + if err != nil { multilog.Error("Unable to get local commit: %v", errs.JoinMessage(err)) return "", "", false } @@ -85,6 +87,7 @@ func inferLanguage(config projectfile.ConfigGetter) (string, string, bool) { } func (r *Initialize) Run(params *RunParams) (rerr error) { + defer rationalizeError(&rerr) logging.Debug("Init: %s/%s %v", params.Namespace.Owner, params.Namespace.Project, params.Private) if !r.auth.Authenticated() { @@ -153,10 +156,11 @@ func (r *Initialize) Run(params *RunParams) (rerr error) { } } - emptyDir, err := fileutils.IsEmptyDir(path) - if err != nil { - multilog.Error("Unable to check if directory is empty: %v", err) - } + // Re-enable in DX-2307. + //emptyDir, err := fileutils.IsEmptyDir(path) + //if err != nil { + // multilog.Error("Unable to check if directory is empty: %v", err) + //} // Match the case of the organization. // Otherwise the incorrect case will be written to the project file. @@ -216,38 +220,44 @@ func (r *Initialize) Run(params *RunParams) (rerr error) { return err } - commitID, err := model.CommitInitial(model.HostPlatform, lang.Requirement(), version) - if err != nil { - return locale.WrapError(err, "err_init_commit", "Could not create initial commit") - } + logging.Debug("Creating Platform project") - if err := localcommit.Set(proj.Dir(), commitID.String()); err != nil { - return errs.Wrap(err, "Unable to create local commit file") - } - if emptyDir || fileutils.DirExists(filepath.Join(path, ".git")) { - err := localcommit.AddToGitIgnore(path) - if err != nil { - r.out.Notice(locale.Tr("notice_commit_id_gitignore", constants.ProjectConfigDirName, constants.CommitIdFileName)) - multilog.Error("Unable to add local commit file to .gitignore: %v", err) - } + platformID, err := model.PlatformNameToPlatformID(model.HostPlatform) + if err != nil { + return errs.Wrap(err, "Unable to determine Platform ID from %s", model.HostPlatform) } - logging.Debug("Creating Platform project and pushing it") - - platformProject, err := model.CreateEmptyProject(namespace.Owner, namespace.Project, params.Private) + timestamp, err := model.FetchLatestTimeStamp() if err != nil { - return locale.WrapInputError(err, "err_init_create_project", "Failed to create a Platform project at {{.V0}}.", namespace.String()) + return errs.Wrap(err, "Unable to fetch latest timestamp") } - branch, err := model.DefaultBranchForProject(platformProject) // only one branch for newly created project + bp := model.NewBuildPlannerModel(r.auth) + commitID, err := bp.CreateProject(&model.CreateProjectParams{ + Owner: namespace.Owner, + Project: namespace.Project, + PlatformID: strfmt.UUID(platformID), + Language: lang.Requirement(), + Version: version, + Private: params.Private, + Timestamp: *timestamp, + Description: locale.T("commit_message_add_initial"), + }) if err != nil { - return locale.NewInputError("err_no_default_branch") + return locale.WrapError(err, "err_init_commit", "Could not create initial commit") } - err = model.UpdateProjectBranchCommitWithModel(platformProject, branch.Label, commitID) - if err != nil { - return locale.WrapError(err, "err_init_push", "Failed to push to the newly created Platform project at {{.V0}}", namespace.String()) + if err := commitmediator.Set(proj, commitID.String()); err != nil { + return errs.Wrap(err, "Unable to create local commit file") } + // Re-enable in DX-2307. + //if emptyDir || fileutils.DirExists(filepath.Join(path, ".git")) { + // err := localcommit.AddToGitIgnore(path) + // if err != nil { + // r.out.Notice(locale.Tr("notice_commit_id_gitignore", constants.ProjectConfigDirName, constants.CommitIdFileName)) + // multilog.Error("Unable to add local commit file to .gitignore: %v", err) + // } + //} err = runbits.RefreshRuntime(r.auth, r.out, r.analytics, proj, commitID, true, target.TriggerInit, r.svcModel) if err != nil { diff --git a/internal/runners/initialize/rationalize.go b/internal/runners/initialize/rationalize.go new file mode 100644 index 0000000000..606fc6f3f7 --- /dev/null +++ b/internal/runners/initialize/rationalize.go @@ -0,0 +1,29 @@ +package initialize + +import ( + "errors" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + bpModel "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" +) + +func rationalizeError(err *error) { + if err == nil { + return + } + + pcErr := &bpModel.ProjectCreatedError{} + if !errors.As(*err, &pcErr) { + return + } + switch pcErr.Type { + case bpModel.AlreadyExistsErrorType: + *err = errs.NewUserFacing(locale.Tl("err_create_project_exists", "That project already exists."), errs.SetInput()) + case bpModel.ForbiddenErrorType: + *err = errs.NewUserFacing( + locale.Tl("err_create_project_forbidden", "You do not have permission to create that project"), + errs.SetInput(), + errs.SetTips(locale.T("err_init_authenticated"))) + } +} diff --git a/internal/runners/languages/languages.go b/internal/runners/languages/languages.go index 5abd10fa96..1381ede185 100644 --- a/internal/runners/languages/languages.go +++ b/internal/runners/languages/languages.go @@ -6,7 +6,7 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" ) @@ -31,7 +31,7 @@ func (l *Languages) Run() error { return locale.NewInputError("err_no_project") } - commitID, err := localcommit.Get(l.project.Dir()) + commitID, err := commitmediator.Get(l.project) if err != nil { return errs.AddTips( locale.WrapError( diff --git a/internal/runners/packages/import.go b/internal/runners/packages/import.go index 943d39f105..f97e15f201 100644 --- a/internal/runners/packages/import.go +++ b/internal/runners/packages/import.go @@ -12,9 +12,8 @@ import ( "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/runbits" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/api" - gqlModel "github.com/ActiveState/cli/pkg/platform/api/graphql/model" "github.com/ActiveState/cli/pkg/platform/api/reqsimport" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -125,14 +124,6 @@ func (i *Import) Run(params *ImportRunParams) error { return errs.Wrap(err, "Could not import changeset") } - packageReqs := model.FilterCheckpointNamespace(reqs, model.NamespacePackage, model.NamespaceBundle) - if len(packageReqs) > 0 { - err = removeRequirements(i.Prompter, i.proj, params, packageReqs) - if err != nil { - return locale.WrapError(err, "err_cannot_remove_existing") - } - } - msg := locale.T("commit_reqstext_message") commitID, err := commitChangeset(i.proj, msg, changeset) if err != nil { @@ -142,26 +133,6 @@ func (i *Import) Run(params *ImportRunParams) error { return runbits.RefreshRuntime(i.auth, i.out, i.analytics, i.proj, commitID, true, target.TriggerImport, i.svcModel) } -func removeRequirements(conf Confirmer, project *project.Project, params *ImportRunParams, reqs []*gqlModel.Requirement) error { - if !params.NonInteractive { - msg := locale.T("confirm_remove_existing_prompt") - - defaultChoice := params.NonInteractive - confirmed, err := conf.Confirm(locale.T("confirm"), msg, &defaultChoice) - if err != nil { - return err - } - if !confirmed { - return locale.NewInputError("err_action_was_not_confirmed", "Cancelled Import.") - } - } - - removal := model.ChangesetFromRequirements(model.OperationRemoved, reqs) - msg := locale.T("commit_reqstext_remove_existing_message") - _, err := commitChangeset(project, msg, removal) - return err -} - func fetchImportChangeset(cp ChangesetProvider, file string, lang string) (model.Changeset, error) { data, err := ioutil.ReadFile(file) if err != nil { @@ -177,7 +148,7 @@ func fetchImportChangeset(cp ChangesetProvider, file string, lang string) (model } func commitChangeset(project *project.Project, msg string, changeset model.Changeset) (strfmt.UUID, error) { - localCommitID, err := localcommit.Get(project.Dir()) + localCommitID, err := commitmediator.Get(project) if err != nil { return "", errs.Wrap(err, "Unable to get local commit") } @@ -188,7 +159,7 @@ func commitChangeset(project *project.Project, msg string, changeset model.Chang locale.T("commit_failed_pull_tip")) } - if err := localcommit.Set(project.Dir(), commitID.String()); err != nil { + if err := commitmediator.Set(project, commitID.String()); err != nil { return "", locale.WrapError(err, "err_package_update_commit_id") } return commitID, nil diff --git a/internal/runners/packages/list.go b/internal/runners/packages/list.go index 115322aa91..f99ff25614 100644 --- a/internal/runners/packages/list.go +++ b/internal/runners/packages/list.go @@ -13,7 +13,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" ) @@ -109,8 +109,8 @@ func targetFromProjectFile(proj *project.Project) (*strfmt.UUID, error) { if proj == nil { return nil, locale.NewInputError("err_no_project") } - commit, err := localcommit.Get(proj.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commit, err := commitmediator.Get(proj) + if err != nil { return nil, errs.Wrap(err, "Unable to get local commit") } if commit == "" { diff --git a/internal/runners/packages/search.go b/internal/runners/packages/search.go index a7f243b791..d1b96698ba 100644 --- a/internal/runners/packages/search.go +++ b/internal/runners/packages/search.go @@ -9,7 +9,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" ) @@ -78,8 +78,8 @@ func targetedLanguage(languageOpt string, proj *project.Project) (string, error) ) } - commitID, err := localcommit.Get(proj.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err := commitmediator.Get(proj) + if err != nil { return "", errs.Wrap(err, "Unable to get local commit") } lang, err := model.LanguageByCommit(commitID) diff --git a/internal/runners/platforms/list.go b/internal/runners/platforms/list.go index 0de857b618..83311eb717 100644 --- a/internal/runners/platforms/list.go +++ b/internal/runners/platforms/list.go @@ -5,7 +5,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" "github.com/go-openapi/strfmt" @@ -33,8 +33,8 @@ func (l *List) Run() error { return locale.NewInputError("err_no_project") } - commitID, err := localcommit.Get(l.proj.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err := commitmediator.Get(l.proj) + if err != nil { return errs.Wrap(err, "Unable to get local commit") } diff --git a/internal/runners/prepare/prepare.go b/internal/runners/prepare/prepare.go index f1dd8402c9..f4050c4f74 100644 --- a/internal/runners/prepare/prepare.go +++ b/internal/runners/prepare/prepare.go @@ -20,8 +20,8 @@ import ( "github.com/ActiveState/cli/internal/osutils/autostart" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/platform/model" rt "github.com/ActiveState/cli/pkg/platform/runtime" "github.com/ActiveState/cli/pkg/platform/runtime/target" @@ -74,12 +74,12 @@ func (r *Prepare) resetExecutors() error { return errs.Wrap(err, "Could not get project from its directory") } - commitID, err := localcommit.Get(proj.Dir()) + commitID, err := commitmediator.Get(proj) if err != nil { return errs.Wrap(err, "Unable to get local commit") } - run, err := rt.New(target.NewCustomTarget(proj.Owner(), proj.Name(), commitID, defaultTargetDir, target.TriggerResetExec, proj.IsHeadless()), r.analytics, r.svcModel, nil) + run, err := rt.New(target.NewCustomTarget(proj.Owner(), proj.Name(), commitID, defaultTargetDir, target.TriggerResetExec), r.analytics, r.svcModel, nil) if err != nil { if rt.IsNeedsUpdateError(err) { return nil // project was never set up, so no executors to reset diff --git a/internal/runners/pull/pull.go b/internal/runners/pull/pull.go index 9be11843f9..ac1b769a94 100644 --- a/internal/runners/pull/pull.go +++ b/internal/runners/pull/pull.go @@ -1,7 +1,6 @@ package pull import ( - "errors" "path/filepath" "strings" @@ -10,14 +9,15 @@ import ( "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/runbits" buildscriptRunbits "github.com/ActiveState/cli/internal/runbits/buildscript" - "github.com/ActiveState/cli/internal/runbits/rtusage" - "github.com/ActiveState/cli/pkg/cmdlets/commit" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" + bpModel "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -78,19 +78,19 @@ func (o *pullOutput) MarshalStructured(format output.Format) interface{} { return o } -func (p *Pull) Run(params *PullParams) error { +func (p *Pull) Run(params *PullParams) (rerr error) { + defer rationalizeError(&rerr) + if p.project == nil { return locale.NewInputError("err_no_project") } p.out.Notice(locale.Tl("operating_message", "", p.project.NamespaceString(), p.project.Dir())) - rtusage.PrintRuntimeUsage(p.svcModel, p.out, p.project.Owner()) - - if p.project.IsHeadless() && params.SetProject == "" { + if p.project.IsHeadless() { return locale.NewInputError("err_pull_headless", "You must first create a project. Please visit {{.V0}} to create your project.", p.project.URL()) } - if !p.project.IsHeadless() && p.project.BranchName() == "" { + if p.project.BranchName() == "" { return locale.NewError("err_pull_branch", "Your [NOTICE]activestate.yaml[/RESET] project field does not contain a branch. Please ensure you are using the latest version of the State Tool by running [ACTIONABLE]`state update`[/RESET] and then trying again.") } @@ -101,8 +101,8 @@ func (p *Pull) Run(params *PullParams) error { } var localCommit *strfmt.UUID - localCommitID, err := localcommit.Get(p.project.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + localCommitID, err := commitmediator.Get(p.project) + if err != nil { return errs.Wrap(err, "Unable to get local commit") } if localCommitID != "" { @@ -128,21 +128,31 @@ func (p *Pull) Run(params *PullParams) error { resultingCommit := remoteCommit // resultingCommit is the commit we want to update the local project file with if localCommit != nil { - strategies, err := model.MergeCommit(*remoteCommit, *localCommit) - if err != nil { - if errors.Is(err, model.ErrMergeFastForward) { - // No merge necessary - resultingCommit = localCommit - } else if !errors.Is(err, model.ErrMergeCommitInHistory) { - return locale.WrapError(err, "err_mergecommit", "Could not detect if merge is necessary.") - } + // Attempt to fast-forward merge. This will succeed if the commits are + // compatible, meaning that we can simply update the local commit ID to + // the remoteCommit ID. The commitID returned from MergeCommit with this + // strategy should just be the remote commit ID. + // If this call fails then we will try a recursive merge. + bp := model.NewBuildPlannerModel(p.auth) + params := &model.MergeCommitParams{ + Owner: remoteProject.Owner, + Project: remoteProject.Project, + TargetRef: localCommit.String(), + OtherRef: remoteCommit.String(), + Strategy: bpModel.MergeCommitStrategyFastForward, } - if err == nil && strategies != nil { - c, err := p.performMerge(strategies, *remoteCommit, *localCommit, remoteProject, p.project.BranchName()) + + resultCommit, mergeErr := bp.MergeCommit(params) + if mergeErr != nil { + logging.Debug("Merge with fast-forward failed with error: %s, trying recursive overwrite", mergeErr.Error()) + c, err := p.performMerge(*remoteCommit, *localCommit, remoteProject, p.project.BranchName()) if err != nil { return errs.Wrap(err, "performing merge commit failed") } resultingCommit = &c + } else { + logging.Debug("Fast-forward merge succeeded, setting commit ID to %s", resultCommit.String()) + resultingCommit = &resultCommit } } @@ -153,13 +163,13 @@ func (p *Pull) Run(params *PullParams) error { } } - commitID, err := localcommit.Get(p.project.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err := commitmediator.Get(p.project) + if err != nil { return errs.Wrap(err, "Unable to get local commit") } if commitID != *resultingCommit { - err := localcommit.Set(p.project.Dir(), resultingCommit.String()) + err := commitmediator.Set(p.project, resultingCommit.String()) if err != nil { return errs.Wrap(err, "Unable to set local commit") } @@ -183,24 +193,28 @@ func (p *Pull) Run(params *PullParams) error { return nil } -func (p *Pull) performMerge(strategies *mono_models.MergeStrategies, remoteCommit strfmt.UUID, localCommit strfmt.UUID, namespace *project.Namespaced, branchName string) (strfmt.UUID, error) { - err := p.mergeBuildScript(strategies, remoteCommit) - if err != nil { - return "", errs.Wrap(err, "Could not merge local build script with remote changes") - } +func (p *Pull) performMerge(remoteCommit, localCommit strfmt.UUID, namespace *project.Namespaced, branchName string) (strfmt.UUID, error) { + // Re-enable in DX-2307. + //err := p.mergeBuildScript(strategies, remoteCommit) + //if err != nil { + // return "", errs.Wrap(err, "Could not merge local build script with remote changes") + //} p.out.Notice(output.Title(locale.Tl("pull_diverged", "Merging history"))) p.out.Notice(locale.Tr( "pull_diverged_message", - namespace.String(), branchName, localCommit.String(), remoteCommit.String())) + namespace.String(), branchName, localCommit.String(), remoteCommit.String()), + ) - commitID, err := localcommit.Get(p.project.Dir()) - if err != nil { - return "", errs.Wrap(err, "Unable to get local commit") + bp := model.NewBuildPlannerModel(p.auth) + params := &model.MergeCommitParams{ + Owner: namespace.Owner, + Project: namespace.Project, + TargetRef: localCommit.String(), + OtherRef: remoteCommit.String(), + Strategy: bpModel.MergeCommitStrategyRecursiveOverwriteOnConflict, } - - commitMessage := locale.Tr("pull_merge_commit", remoteCommit.String(), commitID.String()) - resultCommit, err := model.CommitChangeset(remoteCommit, commitMessage, strategies.OverwriteChanges) + resultCommit, err := bp.MergeCommit(params) if err != nil { return "", locale.WrapError(err, "err_pull_merge_commit", "Could not create merge commit.") } @@ -212,7 +226,8 @@ func (p *Pull) performMerge(strategies *mono_models.MergeStrategies, remoteCommi changes, _ := commit.FormatChanges(cmit) p.out.Notice(locale.Tl( "pull_diverged_changes", - "The following changes will be merged:\n{{.V0}}\n", strings.Join(changes, "\n"))) + "The following changes will be merged:\n{{.V0}}\n", strings.Join(changes, "\n")), + ) return resultCommit, nil } @@ -227,10 +242,7 @@ func (p *Pull) mergeBuildScript(strategies *mono_models.MergeStrategies, remoteC } // Get the local and remote build expressions to merge. - exprA, err := script.ToBuildExpression() - if err != nil { - return errs.Wrap(err, "Unable to transform local buildscript into buildexpression") - } + exprA := script.Expr bp := model.NewBuildPlannerModel(p.auth) exprB, err := bp.GetBuildExpression(p.project.Owner(), p.project.Name(), remoteCommit.String()) if err != nil { diff --git a/internal/runners/pull/rationalize.go b/internal/runners/pull/rationalize.go new file mode 100644 index 0000000000..4690016f48 --- /dev/null +++ b/internal/runners/pull/rationalize.go @@ -0,0 +1,31 @@ +package pull + +import ( + "errors" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" +) + +func rationalizeError(err *error) { + if err == nil { + return + } + + var mergeCommitErr *model.MergedCommitError + + switch { + case errors.As(*err, &mergeCommitErr): + switch mergeCommitErr.Type { + // Custom target does not have a compatible history + case model.NoCommonBaseFoundType: + *err = errs.WrapUserFacing(*err, + locale.Tl("err_pull_no_common_base", + "Could not merge, no common base found between local and remote commits", + ), + errs.SetInput(), + ) + } + } +} diff --git a/internal/runners/push/push.go b/internal/runners/push/push.go index 1420f1b9f9..73bc4f1f15 100644 --- a/internal/runners/push/push.go +++ b/internal/runners/push/push.go @@ -11,7 +11,10 @@ import ( "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/prompt" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/rtutils/ptr" + "github.com/ActiveState/cli/internal/runbits/commitmediator" + "github.com/ActiveState/cli/internal/runbits/rationalize" + bpModel "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -55,20 +58,37 @@ type intention uint16 const ( pushCustomNamespace intention = 0x0001 // User is pushing to a custom remote, ignoring the namespace in the current yaml pushFromNoPermission = 0x0002 // User made modifications to someone elses project, and it now trying to push them - pushFromHeadless = 0x0004 // User is operating in headless mode and is now trying to push // The rest is supplemental intendCreateProject = 0x0008 ) -func (r *Push) Run(params PushParams) error { +var ( + errNoChanges = errors.New("no changes") + errNoCommit = errors.New("no commit") +) + +type errProjectNameInUse struct { + error + Namespace *project.Namespaced +} + +type errHeadless struct { + error + ProjectURL string +} + +func (r *Push) Run(params PushParams) (rerr error) { + defer rationalizeError(&rerr) + if err := r.verifyInput(); err != nil { return errs.Wrap(err, "verifyInput failed") } r.out.Notice(locale.Tl("operating_message", "", r.project.NamespaceString(), r.project.Dir())) - commitID, err := localcommit.Get(r.project.Dir()) // The commit we want to push + commitID, err := commitmediator.Get(r.project) // The commit we want to push if err != nil { + // Note: should not get here, as verifyInput() ensures there is a local commit return errs.Wrap(err, "Unable to get local commit") } @@ -78,7 +98,7 @@ func (r *Push) Run(params PushParams) error { var err error targetNamespace, err = r.namespaceFromProject() if err != nil { - return locale.WrapError(err, "err_valid_namespace", "Could not get a valid namespace, is your activestate.yaml malformed?") + return errs.Wrap(err, "Could not get a valid namespace, is your activestate.yaml malformed?") } } @@ -86,22 +106,23 @@ func (r *Push) Run(params PushParams) error { logging.Debug("%s can write to %s: %v", r.auth.WhoAmI(), targetNamespace.Owner, r.auth.CanWrite(targetNamespace.Owner)) } + if r.project.IsHeadless() { + return &errHeadless{err, r.project.URL()} + } + // Capture the primary intend of the user var intend intention switch { - case r.project.IsHeadless(): - intend = pushFromHeadless | intendCreateProject - case targetNamespace.IsValid() && !r.auth.CanWrite(targetNamespace.Owner): + case targetNamespace.IsValid() && !r.auth.CanWrite(r.project.Owner()): intend = pushFromNoPermission | intendCreateProject case params.Namespace.IsValid(): intend = pushCustomNamespace // Could still lead to creating a project, but that's not explicitly the intend } // Ask to create a copy if the user does not have org permissions - if intend&pushFromNoPermission > 0 { + if intend&pushFromNoPermission > 0 && !params.Namespace.IsValid() { var err error - var createCopy bool - createCopy, err = r.prompt.Confirm("", locale.T("push_prompt_not_authorized"), &createCopy) + createCopy, err := r.prompt.Confirm("", locale.T("push_prompt_not_authorized"), ptr.To(true)) if err != nil || !createCopy { return err } @@ -111,112 +132,113 @@ func (r *Push) Run(params PushParams) error { // - No namespace could be detect so far // - We want to create a copy of the current namespace, and no custom namespace was provided if !targetNamespace.IsValid() || (intend&pushFromNoPermission > 0 && !params.Namespace.IsValid()) { - var err error - if intend&pushFromHeadless > 0 { - r.out.Notice(locale.T("push_first_new_project")) - } targetNamespace, err = r.promptNamespace() if err != nil { - return locale.WrapError(err, "err_prompt_namespace", "Could not prompt for namespace") + return errs.Wrap(err, "Could not prompt for namespace") } } // Get the project remotely if it already exists var targetPjm *mono_models.Project - targetPjm, err = model.FetchProjectByName(targetNamespace.Owner, targetNamespace.Project) + targetPjm, err = model.LegacyFetchProjectByName(targetNamespace.Owner, targetNamespace.Project) if err != nil { if !errs.Matches(err, &model.ErrProjectNotFound{}) { - return locale.WrapError(err, "err_push_try_project", "Failed to check for existence of project.") + return errs.Wrap(err, "Failed to check for existence of project") } } + bp := model.NewBuildPlannerModel(r.auth) + var branch *mono_models.Branch // the branch to write to as.yaml if it changed + // Create remote project var projectCreated bool if intend&intendCreateProject > 0 || targetPjm == nil { if targetPjm != nil { - return locale.NewInputError( - "err_push_create_nonunique", - "The project [NOTICE]{{.V0}}[/RESET] is already in use.", targetNamespace.String()) + return &errProjectNameInUse{errs.New("project name in use"), targetNamespace} } // If the user didn't necessarily intend to create the project we should ask them for confirmation if intend&intendCreateProject == 0 { - createProject := true - createProject, err = r.prompt.Confirm( + createProject, err := r.prompt.Confirm( locale.Tl("create_project", "Create Project"), locale.Tl("push_confirm_create_project", "You are about to create the project [NOTICE]{{.V0}}[/RESET], continue?", targetNamespace.String()), - &createProject) + ptr.To(true)) if err != nil { return errs.Wrap(err, "Confirmation failed") } if !createProject { - return locale.WrapInputError(err, "push_create_project_aborted", "Project creation aborted by user") + return rationalize.ErrActionAborted } } r.out.Notice(locale.Tl("push_creating_project", "Creating project [NOTICE]{{.V1}}[/RESET] under [NOTICE]{{.V0}}[/RESET] on the ActiveState Platform", targetNamespace.Owner, targetNamespace.Project)) - targetPjm, err = model.CreateEmptyProject(targetNamespace.Owner, targetNamespace.Project, r.project.Private()) + + // Create a new project with the current project's buildexpression. + expr, err := bp.GetBuildExpression(r.project.Owner(), r.project.Name(), commitID.String()) if err != nil { - return locale.WrapError(err, "push_project_create_empty_err", "Failed to create a project {{.V0}}.", r.project.Namespace().String()) + return errs.Wrap(err, "Could not get buildexpression") + } + commitID, err = bp.CreateProject(&model.CreateProjectParams{ + Owner: targetNamespace.Owner, + Project: targetNamespace.Project, + Private: r.project.Private(), + Description: locale.T("commit_message_add_initial"), + Expr: expr, + }) + if err != nil { + return locale.WrapError(err, "err_push_create_project", "Could not create new project") } - projectCreated = true - } - - // Now we get to the actual push logic - r.out.Notice(locale.Tl("push_to_project", "Pushing to project [NOTICE]{{.V1}}[/RESET] under [NOTICE]{{.V0}}[/RESET].", targetNamespace.Owner, targetNamespace.Project)) + // Update the project's commitID with the create project or push result. + if err := commitmediator.Set(r.project, commitID.String()); err != nil { + return errs.Wrap(err, "Unable to create local commit file") + } - // Detect the target branch - var branch *mono_models.Branch - if projectCreated || r.project.BranchName() == "" { - // https://www.pivotaltracker.com/story/show/176806415 - // If we have created an empty project the only existing branch will be the default one - branch, err = model.DefaultBranchForProject(targetPjm) + // Fetch the newly created project's default branch (for updating activestate.yaml with). + targetPjm, err = model.LegacyFetchProjectByName(targetNamespace.Owner, targetNamespace.Project) if err != nil { - return locale.NewInputError("err_no_default_branch") + return errs.Wrap(err, "Failed to fetch newly created project") } - } else { - branch, err = model.BranchForProjectByName(targetPjm, r.project.BranchName()) + branch, err = model.DefaultBranchForProject(targetPjm) if err != nil { - return locale.WrapError(err, "err_fetch_branch", "", r.project.BranchName()) + return errs.Wrap(err, "Project has no default branch") } - } - // Check if branch is already up to date - if branch.CommitID != nil && branch.CommitID.String() == commitID.String() { - r.out.Notice(locale.T("push_no_changes")) - return nil - } + projectCreated = true - // Check whether there is a conflict - if branch.CommitID != nil { - mergeStrategy, err := model.MergeCommit(*branch.CommitID, commitID) - if err != nil { - if errors.Is(err, model.ErrMergeCommitInHistory) { - r.out.Notice(locale.T("push_no_changes")) - return nil + } else { + + // Now we get to the actual push logic + r.out.Notice(locale.Tl("push_to_project", "Pushing to project [NOTICE]{{.V1}}[/RESET] under [NOTICE]{{.V0}}[/RESET].", targetNamespace.Owner, targetNamespace.Project)) + + // Detect the target branch + if r.project.BranchName() == "" { + branch, err = model.DefaultBranchForProject(targetPjm) + if err != nil { + return errs.Wrap(err, "Project has no default branch") } - if !errors.Is(err, model.ErrMergeFastForward) { - if params.Namespace.IsValid() { - return locale.WrapError(err, "err_mergecommit_customtarget", "The targets commit history does not match your local commit history.") - } - return locale.WrapError(err, "err_mergecommit", "Could not detect if merge is necessary.") + } else { + branch, err = model.BranchForProjectByName(targetPjm, r.project.BranchName()) + if err != nil { + return errs.Wrap(err, "Could not get branch %s", r.project.BranchName()) } } - if mergeStrategy != nil { - return errs.AddTips( - locale.NewInputError("err_push_outdated"), - locale.Tl("err_tip_push_outdated", "Run `[ACTIONABLE]state pull[/RESET]`")) + + // Check if branch is already up to date + if branch.CommitID != nil && branch.CommitID.String() == commitID.String() { + return errNoChanges } - } - // Update the project at the given commit id. - err = model.UpdateProjectBranchCommitWithModel(targetPjm, branch.Label, commitID) - if err != nil { - if errs.Matches(err, &model.ErrUpdateBranchAuth{}) { - return locale.WrapInputError(err, "push_project_branch_no_permission", "You do not have permission to push to {{.V0}}.", targetNamespace.String()) - } else { - return locale.WrapError(err, "push_project_branch_commit_err", "Failed to update new project {{.V0}} to current commitID.", targetNamespace.String()) + // Perform the (fast-forward) push. + _, err = bp.MergeCommit(&model.MergeCommitParams{ + Owner: targetNamespace.Owner, + Project: targetNamespace.Project, + TargetRef: branch.Label, // using branch name will fast-forward + OtherRef: commitID.String(), + Strategy: bpModel.MergeCommitStrategyFastForward, + }) + if err != nil { + return errs.Wrap(err, "Could not push") } } @@ -247,24 +269,20 @@ func (r *Push) Run(params PushParams) error { func (r *Push) verifyInput() error { if !r.auth.Authenticated() { - return locale.NewInputError("err_push_not_authenticated", "In order to update your project you need to be authenticated, please run '[ACTIONABLE]state auth[/RESET]' to authenticate.") + return rationalize.ErrNotAuthenticated } // Check if as.yaml exists if r.project == nil { - return errs.AddTips(locale.NewInputError( - "err_push_headless", - "You must first create a project."), - locale.Tl("push_headless_push_tip_state_init", "Run [ACTIONABLE]state init[/RESET] to create a project with the State Tool."), - ) + return rationalize.ErrNoProject } - commitID, err := localcommit.Get(r.project.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err := commitmediator.Get(r.project) + if err != nil { return errs.Wrap(err, "Unable to get local commit") } if commitID == "" { - return locale.NewInputError("err_push_nocommit", "You have nothing to push, make some changes first with [ACTIONABLE]state install[/RESET].") + return errNoCommit } return nil @@ -297,7 +315,7 @@ func (r *Push) promptNamespace() (*project.Namespaced, error) { } var name string - commitID, err := localcommit.Get(r.project.Dir()) + commitID, err := commitmediator.Get(r.project) if err != nil { return nil, errs.Wrap(err, "Unable to get local commit") } @@ -317,12 +335,12 @@ func (r *Push) promptNamespace() (*project.Namespaced, error) { func fetchLanguage(commitID strfmt.UUID) (*language.Supported, string, error) { lang, err := model.FetchLanguageForCommit(commitID) if err != nil { - return nil, "", errs.Wrap(err, "Failed to retrieve language information for headless commit.") + return nil, "", errs.Wrap(err, "Failed to retrieve language information for headless commit") } l, err := language.MakeByNameAndVersion(lang.Name, lang.Version) if err != nil { - return nil, "", errs.Wrap(err, "Failed to convert commit language to supported language.") + return nil, "", errs.Wrap(err, "Failed to convert commit language to supported language") } ls := language.Supported{Language: l} diff --git a/internal/runners/push/rationalize.go b/internal/runners/push/rationalize.go new file mode 100644 index 0000000000..881f381121 --- /dev/null +++ b/internal/runners/push/rationalize.go @@ -0,0 +1,98 @@ +package push + +import ( + "errors" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/runbits/rationalize" + bpModel "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" +) + +func rationalizeError(err *error) { + if err == nil { + return + } + + var projectNameInUseErr *errProjectNameInUse + + var headlessErr *errHeadless + + var mergeCommitErr *bpModel.MergedCommitError + + switch { + + // Not authenticated + case errors.Is(*err, rationalize.ErrNotAuthenticated): + *err = errs.WrapUserFacing(*err, + locale.T("err_push_not_authenticated"), + errs.SetInput()) + + // No activestate.yaml + case errors.Is(*err, rationalize.ErrNoProject): + *err = errs.WrapUserFacing(*err, + locale.T("err_push_no_project"), + errs.SetInput(), + errs.SetTips( + locale.T("push_push_tip_headless_init"), + locale.T("push_push_tip_headless_cwd"), + )) + + case errors.As(*err, &headlessErr): + *err = errs.WrapUserFacing(*err, + locale.Tr("err_push_headless", headlessErr.ProjectURL), + errs.SetInput(), + ) + + // No commits made yet + case errors.Is(*err, errNoCommit): + *err = errs.WrapUserFacing(*err, + locale.T("err_push_nocommit"), + errs.SetInput(), + ) + + // No changes made + case errors.Is(*err, errNoChanges): + *err = errs.WrapUserFacing(*err, + locale.T("push_no_changes"), + errs.SetInput(), + ) + + // Project name is already in use + case errors.As(*err, &projectNameInUseErr): + *err = errs.WrapUserFacing(*err, + locale.Tr("err_push_create_nonunique", projectNameInUseErr.Namespace.String()), + errs.SetInput(), + ) + + // Project creation aborted + case errors.Is(*err, rationalize.ErrActionAborted): + *err = errs.WrapUserFacing(*err, + locale.T("err_push_create_project_aborted"), + errs.SetInput()) + + case errors.As(*err, &mergeCommitErr): + switch mergeCommitErr.Type { + // Need to pull first + case bpModel.FastForwardErrorType: + *err = errs.WrapUserFacing(*err, + locale.T("err_push_outdated"), + errs.SetInput(), + errs.SetTips(locale.T("err_tip_push_outdated"))) + + // Custom target does not have a compatible history + case bpModel.NoCommonBaseFoundType: + *err = errs.WrapUserFacing(*err, + locale.T("err_push_target_invalid_history"), + errs.SetInput()) + + // No changes made + case bpModel.NoChangeSinceLastCommitErrorType: + *err = errs.WrapUserFacing(*err, + locale.T("push_no_changes"), + errs.SetInput(), + ) + + } + } +} diff --git a/internal/runners/refresh/refresh.go b/internal/runners/refresh/refresh.go index 28c94dcdee..05361977cb 100644 --- a/internal/runners/refresh/refresh.go +++ b/internal/runners/refresh/refresh.go @@ -10,7 +10,6 @@ import ( "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/runbits/findproject" - "github.com/ActiveState/cli/internal/runbits/rtusage" "github.com/ActiveState/cli/internal/runbits/runtime" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -64,8 +63,6 @@ func (r *Refresh) Run(params *Params) error { return locale.WrapError(err, "err_refresh_cannot_load_project", "Cannot load project to update runtime for") } - rtusage.PrintRuntimeUsage(r.svcModel, r.out, proj.Owner()) - rti, err := runtime.NewFromProject(proj, target.TriggerRefresh, r.analytics, r.svcModel, r.out, r.auth) if err != nil { return locale.WrapInputError(err, "err_refresh_runtime_new", "Could not update runtime for this project.") diff --git a/internal/runners/reset/reset.go b/internal/runners/reset/reset.go index f64ac2cd6d..53427a68d4 100644 --- a/internal/runners/reset/reset.go +++ b/internal/runners/reset/reset.go @@ -8,7 +8,7 @@ import ( "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/runbits" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime/target" @@ -63,7 +63,7 @@ func (r *Reset) Run(params *Params) error { if err != nil { return locale.WrapError(err, "err_reset_latest_commit", "Could not get latest commit ID") } - localCommitID, err := localcommit.Get(r.project.Dir()) + localCommitID, err := commitmediator.Get(r.project) if err != nil { return errs.Wrap(err, "Unable to get local commit") } @@ -76,7 +76,7 @@ func (r *Reset) Run(params *Params) error { return locale.NewInputError("Invalid commit ID") } commitID = strfmt.UUID(params.CommitID) - localCommitID, err := localcommit.Get(r.project.Dir()) + localCommitID, err := commitmediator.Get(r.project) if err != nil { return errs.Wrap(err, "Unable to get local commit") } @@ -100,7 +100,7 @@ func (r *Reset) Run(params *Params) error { return locale.NewInputError("err_reset_aborted", "Reset aborted by user") } - err = localcommit.Set(r.project.Dir(), commitID.String()) + err = commitmediator.Set(r.project, commitID.String()) if err != nil { return errs.Wrap(err, "Unable to set local commit") } diff --git a/internal/runners/revert/revert.go b/internal/runners/revert/revert.go index f2e345a29a..996fa440c9 100644 --- a/internal/runners/revert/revert.go +++ b/internal/runners/revert/revert.go @@ -8,10 +8,9 @@ import ( "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/runbits" - "github.com/ActiveState/cli/pkg/cmdlets/commit" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" gqlmodel "github.com/ActiveState/cli/pkg/platform/api/graphql/model" - "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime/target" @@ -61,43 +60,37 @@ func (r *Revert) Run(params *Params) error { if !strfmt.IsUUID(params.CommitID) { return locale.NewInputError("err_invalid_commit_id", "Invalid commit ID") } - latestCommit, err := localcommit.Get(r.project.Dir()) + latestCommit, err := commitmediator.Get(r.project) if err != nil { return errs.Wrap(err, "Unable to get local commit") } + if params.CommitID == latestCommit.String() && params.To { return locale.NewInputError("err_revert_to_current_commit", "The commit to revert to cannot be the latest commit") } r.out.Notice(locale.Tl("operating_message", "", r.project.NamespaceString(), r.project.Dir())) - commitID := strfmt.UUID(params.CommitID) - var targetCommit *mono_models.Commit // the commit to revert the contents of, or the commit to revert to - var fromCommit, toCommit strfmt.UUID - if !params.To { - priorCommits, err := model.CommitHistoryPaged(commitID, 0, 2) - if err != nil { - return errs.AddTips( - locale.WrapError(err, "err_revert_get_commit", "", params.CommitID), - locale.T("tip_private_project_auth"), - ) - } - if priorCommits.TotalCommits < 2 { - return locale.NewInputError("err_revert_no_history", "Cannot revert commit {{.V0}}: no prior history", params.CommitID) - } - targetCommit = priorCommits.Commits[0] - fromCommit = commitID - toCommit = priorCommits.Commits[1].CommitID // parent commit - } else { - var err error - targetCommit, err = model.GetCommitWithinCommitHistory(latestCommit, commitID) - if err != nil { - return errs.AddTips( - locale.WrapError(err, "err_revert_get_commit", "", params.CommitID), - locale.T("tip_private_project_auth"), - ) - } - fromCommit = latestCommit - toCommit = targetCommit.CommitID + bp := model.NewBuildPlannerModel(r.auth) + targetCommitID := params.CommitID // the commit to revert the contents of, or the commit to revert to + revertParams := revertParams{ + organization: r.project.Owner(), + project: r.project.Name(), + parentCommitID: latestCommit.String(), + revertCommitID: params.CommitID, + } + revertFunc := r.revertCommit + preposition := "" + if params.To { + revertFunc = r.revertToCommit + preposition = " to" // need leading whitespace + } + + targetCommit, err := model.GetCommitWithinCommitHistory(latestCommit, strfmt.UUID(targetCommitID)) + if err != nil { + return errs.AddTips( + locale.WrapError(err, "err_revert_get_commit", "", params.CommitID), + locale.T("tip_private_project_auth"), + ) } var orgs []gqlmodel.Organization @@ -108,10 +101,7 @@ func (r *Revert) Run(params *Params) error { return locale.WrapError(err, "err_revert_get_organizations", "Could not get organizations for current user") } } - preposition := "" - if params.To { - preposition = " to" // need leading whitespace - } + if !r.out.Type().IsStructured() { r.out.Print(locale.Tl("revert_info", "You are about to revert{{.V0}} the following commit:", preposition)) commit.PrintCommit(r.out, targetCommit, orgs) @@ -126,7 +116,7 @@ func (r *Revert) Run(params *Params) error { return locale.NewInputError("err_revert_aborted", "Revert aborted by user") } - revertCommit, err := model.RevertCommitWithinHistory(fromCommit, toCommit, latestCommit) + revertCommit, err := revertFunc(revertParams, bp) if err != nil { return errs.AddTips( locale.WrapError(err, "err_revert_commit", "", preposition, params.CommitID), @@ -134,14 +124,14 @@ func (r *Revert) Run(params *Params) error { locale.T("tip_private_project_auth")) } - err = runbits.RefreshRuntime(r.auth, r.out, r.analytics, r.project, revertCommit.CommitID, true, target.TriggerRevert, r.svcModel) + err = commitmediator.Set(r.project, revertCommit.String()) if err != nil { - return locale.WrapError(err, "err_refresh_runtime") + return errs.Wrap(err, "Unable to set local commit") } - err = localcommit.Set(r.project.Dir(), revertCommit.CommitID.String()) + err = runbits.RefreshRuntime(r.auth, r.out, r.analytics, r.project, revertCommit, true, target.TriggerRevert, r.svcModel) if err != nil { - return errs.Wrap(err, "Unable to set local commit") + return locale.WrapError(err, "err_refresh_runtime") } r.out.Print(output.Prepare( @@ -149,18 +139,49 @@ func (r *Revert) Run(params *Params) error { &struct { CurrentCommitID string `json:"current_commit_id"` }{ - revertCommit.CommitID.String(), + revertCommit.String(), }, )) r.out.Notice(locale.T("operation_success_local")) return nil } -func containsCommitID(history []*mono_models.Commit, commitID strfmt.UUID) bool { - for _, c := range history { - if c.CommitID == commitID { - return true - } +type revertFunc func(params revertParams, bp *model.BuildPlanner) (strfmt.UUID, error) + +type revertParams struct { + organization string + project string + parentCommitID string + revertCommitID string +} + +func (r *Revert) revertCommit(params revertParams, bp *model.BuildPlanner) (strfmt.UUID, error) { + newCommitID, err := bp.RevertCommit(params.organization, params.project, params.parentCommitID, params.revertCommitID) + if err != nil { + return "", errs.Wrap(err, "Could not revert commit") + } + + return newCommitID, nil +} + +func (r *Revert) revertToCommit(params revertParams, bp *model.BuildPlanner) (strfmt.UUID, error) { + buildExpression, err := bp.GetBuildExpression(params.organization, params.project, params.revertCommitID) + if err != nil { + return "", errs.Wrap(err, "Could not get build expression") + } + + stageCommitParams := model.StageCommitParams{ + Owner: params.organization, + Project: params.project, + ParentCommit: params.parentCommitID, + Description: locale.Tl("revert_commit_description", "Revert to commit {{.V0}}", params.revertCommitID), + Expression: buildExpression, } - return false + + newCommitID, err := bp.StageCommit(stageCommitParams) + if err != nil { + return "", errs.Wrap(err, "Could not stage commit") + } + + return newCommitID, nil } diff --git a/internal/runners/run/run.go b/internal/runners/run/run.go index 875c31f1bd..dc62091646 100644 --- a/internal/runners/run/run.go +++ b/internal/runners/run/run.go @@ -10,10 +10,9 @@ import ( "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/internal/runbits/rtusage" + "github.com/ActiveState/cli/internal/runbits/checker" "github.com/ActiveState/cli/internal/scriptrun" "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/pkg/cmdlets/checker" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" @@ -61,7 +60,6 @@ func (r *Run) Run(name string, args []string) error { return locale.NewInputError("err_no_project") } - rtusage.PrintRuntimeUsage(r.svcModel, r.out, r.proj.Owner()) checker.RunUpdateNotifier(r.analytics, r.svcModel, r.out) r.out.Notice(locale.Tl("operating_message", "", r.proj.NamespaceString(), r.proj.Dir())) diff --git a/internal/runners/secrets/set.go b/internal/runners/secrets/set.go index 015e41c09c..551143ca18 100644 --- a/internal/runners/secrets/set.go +++ b/internal/runners/secrets/set.go @@ -60,7 +60,7 @@ func (s *Set) Run(params SetRunParams) error { return err } - remoteProject, err := model.FetchProjectByName(org.URLname, s.proj.Name()) + remoteProject, err := model.LegacyFetchProjectByName(org.URLname, s.proj.Name()) if err != nil { return err } diff --git a/internal/runners/shell/shell.go b/internal/runners/shell/shell.go index 22cf15d058..15c0d8f540 100644 --- a/internal/runners/shell/shell.go +++ b/internal/runners/shell/shell.go @@ -1,11 +1,8 @@ package shell import ( - "os" - "github.com/ActiveState/cli/internal/analytics" "github.com/ActiveState/cli/internal/config" - "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" @@ -14,12 +11,11 @@ import ( "github.com/ActiveState/cli/internal/process" "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/runbits/activation" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/runbits/findproject" - "github.com/ActiveState/cli/internal/runbits/rtusage" "github.com/ActiveState/cli/internal/runbits/runtime" "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/cli/internal/virtualenvironment" - "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime/setup" @@ -76,9 +72,7 @@ func (u *Shell) Run(params *Params) error { return locale.WrapError(err, "err_shell_cannot_load_project") } - rtusage.PrintRuntimeUsage(u.svcModel, u.out, proj.Owner()) - - commitID, err := localcommit.Get(proj.Dir()) + commitID, err := commitmediator.Get(proj) if err != nil { return errs.Wrap(err, "Unable to get local commit") } @@ -93,9 +87,7 @@ func (u *Shell) Run(params *Params) error { } if process.IsActivated(u.config) { - activatedProjectNamespace := os.Getenv(constants.ActivatedStateNamespaceEnvVarName) - activatedProjectDir := os.Getenv(constants.ActivatedStateEnvVarName) - return locale.NewInputError("err_shell_already_active", "", activatedProjectNamespace, activatedProjectDir) + return locale.NewInputError("err_shell_already_active", "", proj.NamespaceString(), proj.Dir()) } u.out.Notice(locale.Tl("shell_project_statement", "", @@ -110,11 +102,7 @@ func (u *Shell) Run(params *Params) error { return locale.WrapError(err, "err_shell_wait", "Could not start runtime shell/prompt.") } - if proj.IsHeadless() { - u.out.Notice(locale.T("info_deactivated_by_commit")) - } else { - u.out.Notice(locale.T("info_deactivated", proj)) - } + u.out.Notice(locale.T("info_deactivated", proj)) return nil } diff --git a/internal/runners/show/show.go b/internal/runners/show/show.go index 33bbe54919..faeda19abd 100644 --- a/internal/runners/show/show.go +++ b/internal/runners/show/show.go @@ -14,8 +14,8 @@ import ( "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/secrets" - "github.com/ActiveState/cli/pkg/localcommit" "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" @@ -193,7 +193,7 @@ func (s *Show) Run(params Params) error { return locale.WrapError(err, "err_show_scripts", "Could not parse scripts") } - commitID, err = localcommit.Get(s.project.Dir()) + commitID, err = commitmediator.Get(s.project) if err != nil { return errs.Wrap(err, "Unable to get local commit") } @@ -209,7 +209,7 @@ func (s *Show) Run(params Params) error { projectTarget = target.NewProjectTarget(s.project, nil, "").Dir() } - remoteProject, err := model.FetchProjectByName(owner, projectName) + remoteProject, err := model.LegacyFetchProjectByName(owner, projectName) if err != nil && errs.Matches(err, &model.ErrProjectNotFound{}) { return locale.WrapError(err, "err_show_project_not_found", "Please run `state push` to synchronize this project with the ActiveState Platform.") } else if err != nil { @@ -385,7 +385,7 @@ func commitsData(owner, project, branchName string, commitID strfmt.UUID, localP if err != nil { return "", locale.WrapError(err, "err_show_commits_behind", "Could not determine number of commits behind latest") } - localCommitID, err := localcommit.Get(localProject.Dir()) + localCommitID, err := commitmediator.Get(localProject) if err != nil { return "", errs.Wrap(err, "Unable to get local commit") } diff --git a/internal/runners/state/state.go b/internal/runners/state/state.go index 6e40f7d4d6..1b6ddc3849 100644 --- a/internal/runners/state/state.go +++ b/internal/runners/state/state.go @@ -11,7 +11,7 @@ import ( "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/profile" - "github.com/ActiveState/cli/pkg/cmdlets/checker" + "github.com/ActiveState/cli/internal/runbits/checker" "github.com/ActiveState/cli/pkg/platform/model" ) diff --git a/internal/runners/swtch/switch.go b/internal/runners/swtch/switch.go index ab6e40b176..b194786fc0 100644 --- a/internal/runners/swtch/switch.go +++ b/internal/runners/swtch/switch.go @@ -8,8 +8,7 @@ import ( "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/runbits" - "github.com/ActiveState/cli/internal/runbits/rtusage" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -86,9 +85,7 @@ func (s *Switch) Run(params SwitchParams) error { } s.out.Notice(locale.Tl("operating_message", "", s.project.NamespaceString(), s.project.Dir())) - rtusage.PrintRuntimeUsage(s.svcModel, s.out, s.project.Owner()) - - project, err := model.FetchProjectByName(s.project.Owner(), s.project.Name()) + project, err := model.LegacyFetchProjectByName(s.project.Owner(), s.project.Name()) if err != nil { return locale.WrapError(err, "err_fetch_project", "", s.project.Namespace().String()) } @@ -113,7 +110,7 @@ func (s *Switch) Run(params SwitchParams) error { return locale.NewInputError("err_identifier_branch_not_on_branch", "Commit does not belong to history for branch [ACTIONABLE]{{.V0}}[/RESET]", s.project.BranchName()) } - err = localcommit.Set(s.project.Dir(), identifier.CommitID().String()) + err = commitmediator.Set(s.project, identifier.CommitID().String()) if err != nil { return errs.Wrap(err, "Unable to set local commit") } diff --git a/internal/runners/update/update.go b/internal/runners/update/update.go index 12e1164dc9..599827e140 100644 --- a/internal/runners/update/update.go +++ b/internal/runners/update/update.go @@ -55,7 +55,8 @@ func New(prime primeable) *Update { func (u *Update) Run(params *Params) error { // Check for available update - upd, err := u.svc.CheckUpdate(context.Background(), params.Channel, "") + channel := fetchChannel(params.Channel, false) + upd, err := u.svc.CheckUpdate(context.Background(), channel, "") if err != nil { return errs.AddTips(locale.WrapError( err, "err_update_fetch", @@ -69,7 +70,7 @@ func (u *Update) Run(params *Params) error { if !update.ShouldInstall() { logging.Debug("No update found") u.out.Print(output.Prepare( - locale.Tr("update_none_found", params.Channel), + locale.Tr("update_none_found", channel), &struct{}{}, )) return nil @@ -79,10 +80,10 @@ func (u *Update) Run(params *Params) error { // Handle switching channels var installPath string - if params.Channel != "" && params.Channel != constants.BranchName { - installPath, err = installation.InstallPathForBranch(params.Channel) + if channel != constants.BranchName { + installPath, err = installation.InstallPathForBranch(channel) if err != nil { - return locale.WrapError(err, "err_update_install_path", "Could not get installation path for branch {{.V0}}", params.Channel) + return locale.WrapError(err, "err_update_install_path", "Could not get installation path for branch {{.V0}}", channel) } } @@ -100,7 +101,7 @@ func (u *Update) Run(params *Params) error { } message := "" - if params.Channel != constants.BranchName { + if channel != constants.BranchName { message = locale.Tl("update_switch_channel", "[NOTICE]Please start a new shell for the update to take effect.[/RESET]") } u.out.Print(output.Prepare( diff --git a/internal/runners/use/use.go b/internal/runners/use/use.go index 19b5307270..c752af05dc 100644 --- a/internal/runners/use/use.go +++ b/internal/runners/use/use.go @@ -12,14 +12,13 @@ import ( "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/prompt" + "github.com/ActiveState/cli/internal/runbits/checker" + "github.com/ActiveState/cli/internal/runbits/checkout" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/runbits/findproject" - "github.com/ActiveState/cli/internal/runbits/rtusage" + "github.com/ActiveState/cli/internal/runbits/git" "github.com/ActiveState/cli/internal/runbits/runtime" "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/pkg/cmdlets/checker" - "github.com/ActiveState/cli/pkg/cmdlets/checkout" - "github.com/ActiveState/cli/pkg/cmdlets/git" - "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime/setup" @@ -78,9 +77,7 @@ func (u *Use) Run(params *Params) error { return locale.WrapInputError(err, "err_use_cannot_find_local_project", "Local project cannot be found.") } - rtusage.PrintRuntimeUsage(u.svcModel, u.out, proj.Owner()) - - commitID, err := localcommit.Get(proj.Dir()) + commitID, err := commitmediator.Get(proj) if err != nil { return errs.Wrap(err, "Unable to get local commit") } diff --git a/internal/scriptrun/scriptrun.go b/internal/scriptrun/scriptrun.go index 9894a51c3e..70b90c4e24 100644 --- a/internal/scriptrun/scriptrun.go +++ b/internal/scriptrun/scriptrun.go @@ -85,7 +85,7 @@ func (s *ScriptRun) PrepareVirtualEnv() (rerr error) { venv := virtualenvironment.New(rt) projDir := filepath.Dir(s.project.Source().Path()) - env, err := venv.GetEnv(true, true, projDir, s.project.Namespace().String()) + env, err := venv.GetEnv(true, true, projDir) if err != nil { return errs.Wrap(err, "Could not get venv environment") } @@ -95,7 +95,7 @@ func (s *ScriptRun) PrepareVirtualEnv() (rerr error) { } // search the "clean" path first (PATHS that are set by venv) - env, err = venv.GetEnv(false, true, "", "") + env, err = venv.GetEnv(false, true, "") if err != nil { return errs.Wrap(err, "Could not get venv environment") } diff --git a/internal/secrets/secrets.go b/internal/secrets/secrets.go index f67f074eb7..6c9f3593ab 100644 --- a/internal/secrets/secrets.go +++ b/internal/secrets/secrets.go @@ -108,7 +108,7 @@ func LoadKeypairFromConfigDir(cfg keypairs.Configurable) (keypairs.Keypair, erro // DefsByProject fetches the secret definitions for the current user relevant to the given project func DefsByProject(secretsClient *secretsapi.Client, owner string, projectName string) ([]*secretsModels.SecretDefinition, error) { - pjm, err := model.FetchProjectByName(owner, projectName) + pjm, err := model.LegacyFetchProjectByName(owner, projectName) if err != nil { return nil, err } @@ -120,7 +120,7 @@ func DefsByProject(secretsClient *secretsapi.Client, owner string, projectName s func ByProject(secretsClient *secretsapi.Client, owner string, projectName string, auth *authentication.Auth) ([]*secretsModels.UserSecret, error) { result := []*secretsModels.UserSecret{} - pjm, err := model.FetchProjectByName(owner, projectName) + pjm, err := model.LegacyFetchProjectByName(owner, projectName) if err != nil { return result, err } diff --git a/internal/subshell/sscommon/rcfile.go b/internal/subshell/sscommon/rcfile.go index bf670d6533..0a7f5ac828 100644 --- a/internal/subshell/sscommon/rcfile.go +++ b/internal/subshell/sscommon/rcfile.go @@ -73,12 +73,9 @@ func WriteRcFile(rcTemplateName string, path string, data RcIdentification, env } rcData := map[string]interface{}{ - "Start": data.Start, - "Stop": data.Stop, - "Env": env, - "ActivatedEnv": constants.ActivatedStateEnvVarName, - "ConfigFile": constants.ConfigFileName, - "ActivatedNamespaceEnv": constants.ActivatedStateNamespaceEnvVarName, + "Start": data.Start, + "Stop": data.Stop, + "Env": env, } if err := CleanRcFile(path, data); err != nil { @@ -306,15 +303,6 @@ func SetupProjectRcFile(prj *project.Project, templateName, ext string, env map[ isConsole := ext == ".bat" // yeah this is a dirty cheat, should find something more deterministic - var activatedMessage string - if !prj.IsHeadless() { - activatedMessage = locale.Tl("project_activated", - "[SUCCESS]✔ Project \"{{.V0}}\" Has Been Activated[/RESET]", prj.Namespace().String()) - } else { - activatedMessage = locale.Tl("headless_project_activated", - "[SUCCESS]✔ Virtual Environment Activated[/RESET]") - } - actualEnv := map[string]string{} for k, v := range env { if strings.Contains(v, "\n") { @@ -325,14 +313,15 @@ func SetupProjectRcFile(prj *project.Project, templateName, ext string, env map[ } rcData := map[string]interface{}{ - "Owner": prj.Owner(), - "Name": prj.Name(), - "Env": actualEnv, - "WD": wd, - "UserScripts": userScripts, - "Scripts": scripts, - "ExecName": constants.CommandName, - "ActivatedMessage": colorize.ColorizedOrStrip(activatedMessage, isConsole), + "Owner": prj.Owner(), + "Name": prj.Name(), + "Env": actualEnv, + "WD": wd, + "UserScripts": userScripts, + "Scripts": scripts, + "ExecName": constants.CommandName, + "ActivatedMessage": colorize.ColorizedOrStrip(locale.Tl("project_activated", + "[SUCCESS]✔ Project \"{{.V0}}\" Has Been Activated[/RESET]", prj.Namespace().String()), isConsole), } currExec := osutils.Executable() diff --git a/internal/subshell/sscommon/rcfile_test.go b/internal/subshell/sscommon/rcfile_test.go index 19162f7e7b..bd01eb3833 100644 --- a/internal/subshell/sscommon/rcfile_test.go +++ b/internal/subshell/sscommon/rcfile_test.go @@ -3,7 +3,6 @@ package sscommon import ( "fmt" "reflect" - "runtime" "strings" "testing" @@ -44,25 +43,10 @@ func TestWriteRcFile(t *testing.T) { path string env map[string]string } - - fish := fmt.Sprintf( - `set -xg PATH "foo:$PATH" -if test ! -z "$%s"; test -f "$%s/%s" - echo "State Tool is operating on project $%s, located at $%s" -end`, - constants.ActivatedStateEnvVarName, - constants.ActivatedStateEnvVarName, - constants.ConfigFileName, - constants.ActivatedStateNamespaceEnvVarName, - constants.ActivatedStateEnvVarName) - if runtime.GOOS == "windows" { - fish = strings.ReplaceAll(fish, "\n", "\r\n") - } - tests := []struct { name string args args - want error + want error wantContents string }{ { @@ -75,7 +59,7 @@ end`, }, }, nil, - fakeContents("", fish, ""), + fakeContents("", `set -xg PATH "foo:$PATH"`, ""), }, { "Write RC update", @@ -87,7 +71,7 @@ end`, }, }, nil, - fakeContents(strings.Join([]string{"before", "after"}, fileutils.LineEnd), fish, ""), + fakeContents(strings.Join([]string{"before", "after"}, fileutils.LineEnd), `set -xg PATH "foo:$PATH"`, ""), }, } for _, tt := range tests { diff --git a/internal/svcctl/comm.go b/internal/svcctl/comm.go index 7ada81cdcf..f02938d153 100644 --- a/internal/svcctl/comm.go +++ b/internal/svcctl/comm.go @@ -18,17 +18,16 @@ import ( "github.com/ActiveState/cli/internal/multilog" "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/runbits/panics" - "github.com/ActiveState/cli/internal/runbits/rtusage" "github.com/ActiveState/cli/internal/svcctl/svcmsg" "github.com/ActiveState/cli/pkg/platform/runtime/executors/execmeta" "github.com/ActiveState/cli/pkg/platform/runtime/target" - "github.com/ActiveState/cli/pkg/project" ) var ( KeyHTTPAddr = "http-addr" KeyLogFile = "log-file" KeyHeartbeat = "heart<" + KeyExitCode = "exitcode<" ) type Requester interface { @@ -74,11 +73,11 @@ func (c *Comm) GetLogFileName(ctx context.Context) (string, error) { type Resolver interface { ReportRuntimeUsage(ctx context.Context, pid int, exec, source string, dimensionsJSON string) (*graph.ReportRuntimeUsageResponse, error) - CheckRuntimeUsage(ctx context.Context, organizationName string) (*graph.CheckRuntimeUsageResponse, error) } type AnalyticsReporter interface { EventWithSource(category, action, source string, dims ...*dimensions.Values) + EventWithSourceAndLabel(category, action, source, label string, dims ...*dimensions.Values) } func HeartbeatHandler(cfg *config.Instance, resolver Resolver, analyticsReporter AnalyticsReporter) ipc.RequestHandler { @@ -126,17 +125,6 @@ func HeartbeatHandler(cfg *config.Instance, resolver Resolver, analyticsReporter return } - // Soft limit notification - logging.Debug("Checking runtime usage for %s", metaData.Namespace) - if metaData.Namespace != "" { - ns, err := project.ParseNamespace(metaData.Namespace) - if err != nil { - multilog.Error("Soft limit: Could not parse namespace in heartbeat handler: %s", err) - } else { - rtusage.NotifyRuntimeUsage(cfg, resolver, ns.Owner) - } - } - logging.Debug("Firing runtime usage events for %s", metaData.Namespace) analyticsReporter.EventWithSource(constants.CatRuntimeUsage, constants.ActRuntimeAttempt, constants.SrcExecutor, dims) _, err = resolver.ReportRuntimeUsage(context.Background(), pidNum, hb.ExecPath, constants.SrcExecutor, dimsJSON) @@ -150,6 +138,24 @@ func HeartbeatHandler(cfg *config.Instance, resolver Resolver, analyticsReporter } } -func (c *Comm) SendHeartbeat(ctx context.Context, pid string) (string, error) { - return c.req.Request(ctx, KeyHeartbeat+pid) +func ExitCodeHandler(cfg *config.Instance, resolver Resolver, analyticsReporter AnalyticsReporter) ipc.RequestHandler { + return func(input string) (string, bool) { + defer func() { panics.HandlePanics(recover(), debug.Stack()) }() + + if !strings.HasPrefix(input, KeyExitCode) { + return "", false + } + + logging.Debug("Exit Code: Received exit code through ipc") + + data := input[len(KeyExitCode):] + exitCode := svcmsg.NewExitCodeFromSvcMsg(data) + + logging.Debug("Firing exit code event for %s", exitCode.ExecPath) + analyticsReporter.EventWithSourceAndLabel(constants.CatDebug, constants.ActExecutorExit, constants.SrcExecutor, exitCode.ExitCode, &dimensions.Values{ + Command: ptr.To(exitCode.ExecPath), + }) + + return "ok", true + } } diff --git a/internal/svcctl/svcmsg/exitcode.go b/internal/svcctl/svcmsg/exitcode.go new file mode 100644 index 0000000000..8c0e03fb1a --- /dev/null +++ b/internal/svcctl/svcmsg/exitcode.go @@ -0,0 +1,42 @@ +// Package svcmsg models the Exit Code data that the executor must communicate +// to the service. +// +// IMPORTANT: This package should have minimal dependencies as it will be +// imported by cmd/state-exec. The resulting compiled executable must remain as +// small as possible. +package svcmsg + +import ( + "fmt" + "strings" +) + +type ExitCode struct { + ExecPath string + ExitCode string +} + +func NewExitCodeFromSvcMsg(data string) *ExitCode { + var execPath, exitCode string + + ss := strings.SplitN(data, "<", 2) + if len(ss) > 0 { + execPath = ss[0] + } + if len(ss) > 1 { + exitCode = ss[1] + } + + return NewExitCode(execPath, exitCode) +} + +func NewExitCode(execPath, exitCode string) *ExitCode { + return &ExitCode{ + ExecPath: execPath, + ExitCode: exitCode, + } +} + +func (e *ExitCode) SvcMsg() string { + return fmt.Sprintf("exitcode<%s<%s", e.ExecPath, e.ExitCode) +} diff --git a/internal/svcctl/svcmsg/svcmsg.go b/internal/svcctl/svcmsg/svcmsg.go new file mode 100644 index 0000000000..24ac38dc4f --- /dev/null +++ b/internal/svcctl/svcmsg/svcmsg.go @@ -0,0 +1,5 @@ +package svcmsg + +type Messager interface { + SvcMsg() string +} diff --git a/internal/testhelpers/e2e/session.go b/internal/testhelpers/e2e/session.go index a7ac73c423..93d22c0723 100644 --- a/internal/testhelpers/e2e/session.go +++ b/internal/testhelpers/e2e/session.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/termtest" "github.com/go-openapi/strfmt" "github.com/google/uuid" @@ -32,6 +33,8 @@ import ( "github.com/ActiveState/cli/internal/osutils/stacktrace" "github.com/ActiveState/cli/internal/rtutils/singlethread" "github.com/ActiveState/cli/internal/strutils" + "github.com/ActiveState/cli/internal/subshell/bash" + "github.com/ActiveState/cli/internal/subshell/sscommon" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" "github.com/ActiveState/cli/pkg/platform/api" "github.com/ActiveState/cli/pkg/platform/api/mono" @@ -40,6 +43,7 @@ import ( "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" + "github.com/ActiveState/cli/pkg/projectfile" // remove in DX-2307 ) // Session represents an end-to-end testing session during which several console process can be spawned and tested @@ -66,7 +70,9 @@ var ( PersistentPassword string PersistentToken string - defaultTimeout = 40 * time.Second + defaultTimeout = 40 * time.Second + RuntimeSourcingTimeout = 3 * time.Minute + RuntimeSourcingTimeoutOpt = termtest.OptExpectTimeout(3 * time.Minute) ) func init() { @@ -175,6 +181,7 @@ func new(t *testing.T, retainDirs, updatePath bool, extraEnv ...string) *Session constants.ProjectEnvVarName + "=", constants.E2ETestEnvVarName + "=true", constants.DisableUpdates + "=true", + constants.DisableProjectMigrationPrompt + "=true", constants.OptinUnstableEnvVarName + "=true", constants.ServiceSockDir + "=" + dirs.SockRoot, constants.HomeEnvVarName + "=" + dirs.HomeDir, @@ -183,12 +190,35 @@ func new(t *testing.T, retainDirs, updatePath bool, extraEnv ...string) *Session if updatePath { // add bin path + // Remove release state tool installation from PATH in tests + // This is a workaround as our test sessions are not compeltely + // sandboxed. This should be addressed in: https://activestatef.atlassian.net/browse/DX-2285 oldPath, _ := os.LookupEnv("PATH") + installPath, err := installation.InstallPathForBranch("release") + require.NoError(t, err) + + binPath := filepath.Join(installPath, "bin") + oldPath = strings.Replace(oldPath, binPath+string(os.PathListSeparator), "", -1) newPath := fmt.Sprintf( "PATH=%s%s%s", dirs.Bin, string(os.PathListSeparator), oldPath, ) env = append(env, newPath) + t.Setenv("PATH", newPath) + + cfg, err := config.New() + require.NoError(t, err) + + // In order to ensure that the release state tool does not appear on the PATH + // when a new subshell is started we remove the installation entries from the + // rc file. This is added back later in the session's Close method. + // Again, this is a workaround to be addressed in: https://activestatef.atlassian.net/browse/DX-2285 + if runtime.GOOS != "windows" { + s := bash.SubShell{} + err = s.CleanUserEnv(cfg, sscommon.InstallID, false) + require.NoError(t, err) + } + t.Setenv(constants.HomeEnvVarName, dirs.HomeDir) } // add session environment variables @@ -202,6 +232,14 @@ func new(t *testing.T, retainDirs, updatePath bool, extraEnv ...string) *Session session.SvcExe = session.copyExeToBinDir(svcExe) session.ExecutorExe = session.copyExeToBinDir(execExe) + // Set up environment for test runs. This is separate + // from the environment for the session itself. + // Setting environment variables here allows helper + // functions access to them. + // This is a workaround as our test sessions are not compeltely + // sandboxed. This should be addressed in: https://activestatef.atlassian.net/browse/DX-2285 + t.Setenv(constants.HomeEnvVarName, dirs.HomeDir) + err = fileutils.Touch(filepath.Join(dirs.Base, installation.InstallDirMarker)) require.NoError(session.t, err) @@ -261,9 +299,12 @@ func (s *Session) SpawnCmdWithOpts(exe string, optSetters ...SpawnOptSetter) *Sp termtest.OptRows(30), // Needs to be able to accommodate most JSON output ) - // Work around issue where multiline values sometimes have the wrong line endings - // See for example TestBranch_List - // https://activestatef.atlassian.net/browse/DX-2169 + // TTYs output newlines in two steps: '\r' (CR) to move the caret to the beginning of the line, + // and '\n' (LF) to move the caret one line down. Terminal emulators do the same thing, so the + // raw terminal output will contain "\r\n". Since our multi-line expectation messages often use + // '\n', normalize line endings to that for convenience, regardless of platform ('\n' for Linux + // and macOS, "\r\n" for Windows). + // More info: https://superuser.com/a/1774370 spawnOpts.TermtestOpts = append(spawnOpts.TermtestOpts, termtest.OptNormalizedLineEnds(true), ) @@ -340,7 +381,11 @@ func (s *Session) PrepareActiveStateYAML(contents string) { } func (s *Session) PrepareCommitIdFile(commitID string) { - require.NoError(s.t, fileutils.WriteFile(filepath.Join(s.Dirs.Work, constants.ProjectConfigDirName, constants.CommitIdFileName), []byte(commitID))) + // Replace the contents of this function with the line below in DX-2307. + //require.NoError(s.t, fileutils.WriteFile(filepath.Join(s.Dirs.Work, constants.ProjectConfigDirName, constants.CommitIdFileName), []byte(commitID))) + pjfile, err := projectfile.Parse(filepath.Join(s.Dirs.Work, constants.ConfigFileName)) + require.NoError(s.t, err) + require.NoError(s.t, pjfile.LegacySetCommit(commitID)) } // PrepareProject creates a very simple activestate.yaml file for the given org/project and, if a @@ -575,9 +620,22 @@ func (s *Session) Close() error { } } - // Trap "flisten in use" errors to help debug DX-2090. - if contents := s.SvcLog(); strings.Contains(contents, "flisten in use") { - s.t.Fatal(s.DebugMessage("Found 'flisten in use' error in state-svc log file")) + // Add back the release state tool installation to the bash RC file. + // This was done on session creation to ensure that the release state tool + // does not appear on the PATH when a new subshell is started. This is a + // workaround to be addressed in: https://activestatef.atlassian.net/browse/DX-2285 + if runtime.GOOS != "windows" { + installPath, err := installation.InstallPathForBranch("release") + if err != nil { + s.t.Errorf("Could not get install path: %v", errs.JoinMessage(err)) + } + binDir := filepath.Join(installPath, "bin") + + ss := bash.SubShell{} + err = ss.WriteUserEnv(cfg, map[string]string{"PATH": binDir}, sscommon.InstallID, false) + if err != nil { + s.t.Errorf("Could not clean user env: %v", errs.JoinMessage(err)) + } } return nil @@ -694,6 +752,33 @@ func (s *Session) DetectLogErrors() { } } +func (s *Session) SetupRCFile() { + if runtime.GOOS == "windows" { + return + } + + cfg, err := config.New() + require.NoError(s.t, err) + + s.SetupRCFileCustom(subshell.New(cfg)) +} + +func (s *Session) SetupRCFileCustom(subshell subshell.SubShell) { + if runtime.GOOS == "windows" { + return + } + + rcFile, err := subshell.RcFile() + require.NoError(s.t, err) + + if fileutils.TargetExists(filepath.Join(s.Dirs.HomeDir, filepath.Base(rcFile))) { + err = fileutils.CopyFile(rcFile, filepath.Join(s.Dirs.HomeDir, filepath.Base(rcFile))) + } else { + err = fileutils.Touch(rcFile) + } + require.NoError(s.t, err) +} + func RunningOnCI() bool { return condition.OnCI() } diff --git a/internal/testhelpers/tagsuite/tagsuite.go b/internal/testhelpers/tagsuite/tagsuite.go index d412bd62f7..2fb0fe4783 100644 --- a/internal/testhelpers/tagsuite/tagsuite.go +++ b/internal/testhelpers/tagsuite/tagsuite.go @@ -38,6 +38,7 @@ const ( Init = "init" InstallScripts = "install-scripts" Installer = "installer" + Install = "install" Invite = "invite" RemoteInstaller = "remote-installer" Interrupt = "interrupt" @@ -69,10 +70,8 @@ const ( Update = "update" Use = "use" Commit = "commit" - VSCode = "vscode" Performance = "performance" Service = "service" - SoftLimit = "softlimit" Executor = "executor" Deprecation = "deprecation" Compatibility = "compatibility" diff --git a/internal/updater/checker.go b/internal/updater/checker.go index 481efe9f2d..a2b2b7a17f 100644 --- a/internal/updater/checker.go +++ b/internal/updater/checker.go @@ -2,6 +2,7 @@ package updater import ( "encoding/json" + "net" "net/url" "os" "runtime" @@ -115,6 +116,10 @@ func (u *Checker) getUpdateInfo(desiredChannel, desiredVersion string) (*Availab label = anaConst.UpdateLabelFailed msg = anaConst.UpdateErrorFetch err = errs.Wrap(err, "Could not fetch update info from %s", infoURL) + if e, ok := err.(net.Error); ok && e.Timeout() { + logging.Debug("Silencing network timeout error: %v", err) + err = errs.Silence(err) + } } u.an.EventWithLabel( diff --git a/internal/updater/fetcher.go b/internal/updater/fetcher.go index c7fca8e2ce..b748a584c2 100644 --- a/internal/updater/fetcher.go +++ b/internal/updater/fetcher.go @@ -3,37 +3,45 @@ package updater import ( "crypto/sha256" "fmt" + "io/ioutil" "github.com/ActiveState/cli/internal/analytics" anaConst "github.com/ActiveState/cli/internal/analytics/constants" "github.com/ActiveState/cli/internal/analytics/dimensions" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/fileutils" - "github.com/ActiveState/cli/internal/httpreq" "github.com/ActiveState/cli/internal/logging" + "github.com/ActiveState/cli/internal/retryhttp" "github.com/ActiveState/cli/internal/rtutils/ptr" ) const CfgUpdateTag = "update_tag" type Fetcher struct { - httpreq *httpreq.Client - an analytics.Dispatcher + retryhttp *retryhttp.Client + an analytics.Dispatcher } func NewFetcher(an analytics.Dispatcher) *Fetcher { - return &Fetcher{httpreq.New(), an} + return &Fetcher{retryhttp.DefaultClient, an} } func (f *Fetcher) Fetch(update *UpdateInstaller, targetDir string) error { logging.Debug("Fetching update: %s", update.url) - b, _, err := f.httpreq.Get(update.url) + resp, err := f.retryhttp.Get(update.url) if err != nil { msg := fmt.Sprintf("Fetch %s failed", update.url) f.analyticsEvent(update.AvailableUpdate.Version, msg) return errs.Wrap(err, msg) } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + msg := "Could not read response body" + f.analyticsEvent(update.AvailableUpdate.Version, msg) + return errs.Wrap(err, msg) + } + if err := verifySha(b, update.AvailableUpdate.Sha256); err != nil { msg := "Could not verify sha256" f.analyticsEvent(update.AvailableUpdate.Version, msg) diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 61e4e68aa4..4e97ba79b3 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -132,7 +132,7 @@ func (u *UpdateInstaller) DownloadAndUnpack() (string, error) { tmpDir, err := os.MkdirTemp("", "state-update") if err != nil { msg := anaConst.UpdateErrorTempDir - u.analyticsEvent(anaConst.ActUpdateDownload, anaConst.UpdateLabelFailed, u.AvailableUpdate.Version, msg) + u.analyticsEvent(anaConst.ActUpdateDownload, anaConst.UpdateLabelFailed, msg) return "", errs.Wrap(err, msg) } @@ -149,13 +149,13 @@ func (u *UpdateInstaller) prepareInstall(installTargetPath string, args []string if err != nil { return "", nil, err } - u.analyticsEvent(anaConst.ActUpdateDownload, "success", u.AvailableUpdate.Version, "") + u.analyticsEvent(anaConst.ActUpdateDownload, anaConst.UpdateLabelSuccess, "") installerPath := filepath.Join(sourcePath, InstallerName) logging.Debug("Using installer: %s", installerPath) if !fileutils.FileExists(installerPath) { msg := anaConst.UpdateErrorNoInstaller - u.analyticsEvent(anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, u.AvailableUpdate.Version, msg) + u.analyticsEvent(anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, msg) return "", nil, errs.Wrap(err, msg) } @@ -163,7 +163,7 @@ func (u *UpdateInstaller) prepareInstall(installTargetPath string, args []string installTargetPath, err = installation.InstallPathFromExecPath() if err != nil { msg := anaConst.UpdateErrorInstallPath - u.analyticsEvent(anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, u.AvailableUpdate.Version, msg) + u.analyticsEvent(anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, msg) return "", nil, errs.Wrap(err, msg) } } @@ -173,9 +173,24 @@ func (u *UpdateInstaller) prepareInstall(installTargetPath string, args []string return installerPath, args, nil } -func (u *UpdateInstaller) InstallBlocking(installTargetPath string, args ...string) error { +func (u *UpdateInstaller) InstallBlocking(installTargetPath string, args ...string) (rerr error) { logging.Debug("InstallBlocking path: %s, args: %v", installTargetPath, args) + // Report any failure to analytics. + defer func() { + if rerr == nil { + return + } + switch { + case os.IsPermission(rerr): + u.analyticsEvent(anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, "Could not update the state tool due to insufficient permissions.") + case errs.Matches(rerr, &ErrorInProgress{}): + u.analyticsEvent(anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, anaConst.UpdateErrorInProgress) + default: + u.analyticsEvent(anaConst.ActUpdateInstall, anaConst.UpdateLabelFailed, anaConst.UpdateErrorInstallFailed) + } + }() + err := checkAdmin() if errors.Is(err, errPrivilegeMistmatch) { return locale.NewInputError("err_update_privilege_mismatch") @@ -216,6 +231,8 @@ func (u *UpdateInstaller) InstallBlocking(installTargetPath string, args ...stri return errs.Wrap(err, "Could not run installer") } + u.analyticsEvent(anaConst.ActUpdateInstall, anaConst.UpdateLabelSuccess, "") + return nil } @@ -259,14 +276,15 @@ func (u *UpdateInstaller) InstallWithProgress(installTargetPath string, progress return proc, nil } -func (u *UpdateInstaller) analyticsEvent(action, label, version, msg string) { - dims := &dimensions.Values{ - TargetVersion: ptr.To(version), +func (u *UpdateInstaller) analyticsEvent(action, label, msg string) { + dims := &dimensions.Values{} + if u.AvailableUpdate != nil { + dims.TargetVersion = ptr.To(u.AvailableUpdate.Version) } if msg != "" { dims.Error = ptr.To(msg) } - u.an.EventWithLabel(anaConst.CatUpdates, anaConst.ActUpdateDownload, label, dims) + u.an.EventWithLabel(anaConst.CatUpdates, action, label, dims) } diff --git a/internal/virtualenvironment/virtualenvironment.go b/internal/virtualenvironment/virtualenvironment.go index 055bc890fc..e6aca20fe3 100644 --- a/internal/virtualenvironment/virtualenvironment.go +++ b/internal/virtualenvironment/virtualenvironment.go @@ -29,7 +29,7 @@ func New(runtime *runtime.Runtime) *VirtualEnvironment { } // GetEnv returns a map of the cumulative environment variables for all active virtual environments -func (v *VirtualEnvironment) GetEnv(inherit bool, useExecutors bool, projectDir, namespace string) (map[string]string, error) { +func (v *VirtualEnvironment) GetEnv(inherit bool, useExecutors bool, projectDir string) (map[string]string, error) { envMap := make(map[string]string) // Source runtime environment information @@ -44,7 +44,6 @@ func (v *VirtualEnvironment) GetEnv(inherit bool, useExecutors bool, projectDir, if projectDir != "" { envMap[constants.ActivatedStateEnvVarName] = projectDir envMap[constants.ActivatedStateIDEnvVarName] = v.activationID - envMap[constants.ActivatedStateNamespaceEnvVarName] = namespace // Get project from explicitly defined configuration file configFile := filepath.Join(projectDir, constants.ConfigFileName) diff --git a/pkg/cmdlets/README.md b/pkg/cmdlets/README.md deleted file mode 100644 index 314a6c7637..0000000000 --- a/pkg/cmdlets/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Commandlets (cmdlets) are packages that contain logic used by our commands that do not belong in any other package. In -most cases this logic is contained in the command itself, but in some cases logic needs to be shared between commands, -this is where cmdlets come in. \ No newline at end of file diff --git a/pkg/localcommit/localcommit.go b/pkg/localcommit/localcommit.go index 06121f6f61..8a8f62deda 100644 --- a/pkg/localcommit/localcommit.go +++ b/pkg/localcommit/localcommit.go @@ -2,20 +2,33 @@ package localcommit import ( "bytes" + "errors" "fmt" "path/filepath" "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/locale" "github.com/go-openapi/strfmt" ) -type FileDoesNotExistError struct{ *locale.LocalizedError } +type ErrLocalCommitFile struct { + *locale.LocalizedError // for backwards compatibility with runners that don't implement rationalizers + errorMsg string + IsDoesNotExist bool + File string +} + +func (e *ErrLocalCommitFile) Error() string { + return e.errorMsg +} func IsFileDoesNotExistError(err error) bool { - return errs.Matches(err, &FileDoesNotExistError{}) + var errLocalCommit *ErrLocalCommitFile + if errors.As(err, &errLocalCommit) { + return errLocalCommit.IsDoesNotExist + } + return false } func getCommitFile(projectDir string) string { @@ -26,18 +39,26 @@ func Get(projectDir string) (strfmt.UUID, error) { configDir := filepath.Join(projectDir, constants.ProjectConfigDirName) commitFile := getCommitFile(projectDir) if !fileutils.DirExists(configDir) || !fileutils.TargetExists(commitFile) { - return "", &FileDoesNotExistError{locale.NewError("err_commit_file_does_not_exist", - "Your project runtime's commit ID file '{{.V0}}' does not exist", commitFile)} + return "", &ErrLocalCommitFile{ + locale.NewError("err_local_commit_file", commitFile), + "local commit file does not exist", + true, commitFile} } b, err := fileutils.ReadFile(commitFile) if err != nil { - return "", locale.WrapError(err, "err_get_commit_file", "Could not read your project runtime's commit ID file") + return "", &ErrLocalCommitFile{ + locale.NewError("err_local_commit_file", commitFile), + "local commit could not be read", + false, commitFile} } commitID := string(b) if !strfmt.IsUUID(commitID) { - return "", locale.NewError("err_commit_id_invalid", commitID) + return "", &ErrLocalCommitFile{ + locale.NewError("err_local_commit_file", commitFile), + "local commit is not uuid formatted", + false, commitFile} } return strfmt.UUID(commitID), nil diff --git a/pkg/platform/api/api.go b/pkg/platform/api/api.go index c478bb66a7..f0605cce83 100644 --- a/pkg/platform/api/api.go +++ b/pkg/platform/api/api.go @@ -8,6 +8,7 @@ import ( "reflect" "strings" + "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/alecthomas/template" "github.com/ActiveState/cli/pkg/sysinfo" @@ -144,3 +145,18 @@ func ErrorMessageFromPayload(err error) string { } return codeVal.String() } + +func ErrorFromPayload(err error) error { + return ErrorFromPayloadTyped(err) +} + +func ErrorFromPayloadTyped(err error) *rationalize.ErrAPI { + if err == nil { + return nil + } + return &rationalize.ErrAPI{ + err, + ErrorCodeFromPayload(err), + ErrorMessageFromPayload(err), + } +} diff --git a/pkg/platform/api/buildplanner/model/buildplan.go b/pkg/platform/api/buildplanner/model/buildplan.go index ef65076a09..726974d81f 100644 --- a/pkg/platform/api/buildplanner/model/buildplan.go +++ b/pkg/platform/api/buildplanner/model/buildplan.go @@ -13,6 +13,8 @@ import ( type Operation int +type MergeStrategy string + const ( OperationAdded Operation = iota OperationRemoved @@ -36,7 +38,7 @@ const ( // Tag types TagSource = "src" - TagDependency = "dep" + TagDependency = "deps" TagBuilder = "builder" TagOrphan = "orphans" @@ -44,6 +46,7 @@ const ( BuildLogRecipeID = "RECIPE_ID" BuildRequestID = "BUILD_REQUEST_ID" + // Version Comparators ComparatorEQ string = "eq" ComparatorGT = "gt" ComparatorGTE = "gte" @@ -51,15 +54,27 @@ const ( ComparatorLTE = "lte" ComparatorNE = "ne" + // Version Requirement keys VersionRequirementComparatorKey = "comparator" VersionRequirementVersionKey = "version" + // MIME types XArtifactMimeType = "application/x.artifact" XActiveStateArtifactMimeType = "application/x-activestate-artifacts" XCamelInstallerMimeType = "application/x-camel-installer" XGozipInstallerMimeType = "application/x-gozip-installer" XActiveStateBuilderMimeType = "application/x-activestate-builder" + // RevertCommit strategies + RevertCommitStrategyForce = "Force" + RevertCommitStrategyDefault = "Default" + + // MergeCommit strategies + MergeCommitStrategyRecursive MergeStrategy = "Recursive" + MergeCommitStrategyRecursiveOverwriteOnConflict MergeStrategy = "RecursiveOverwriteOnConflict" + MergeCommitStrategyRecursiveKeepOnConflict MergeStrategy = "RecursiveKeepOnConflict" + MergeCommitStrategyFastForward MergeStrategy = "FastForward" + // Error types ErrorType = "Error" NotFoundErrorType = "NotFound" @@ -71,6 +86,11 @@ const ( ForbiddenErrorType = "Forbidden" RemediableSolveErrorType = "RemediableSolveError" PlanningErrorType = "PlanningError" + MergeConflictType = "MergeConflict" + FastForwardErrorType = "FastForwardError" + NoCommonBaseFoundType = "NoCommonBaseFound" + ValidationErrorType = "ValidationError" + MergeConflictErrorType = "MergeConflict" ) func IsStateToolArtifact(mimeType string) bool { @@ -151,7 +171,7 @@ func (e *BuildPlannerError) Error() string { } var err error - err = locale.NewError("solver_err", "", croppedMessage, errorLines) + err = locale.NewError("buildplan_err", "", croppedMessage, errorLines) if e.IsTransient { err = errs.AddTips(err, locale.Tr("transient_solver_tip")) } @@ -268,7 +288,11 @@ func IsErrorResponse(errorType string) bool { errorType == ForbiddenErrorType || errorType == RemediableSolveErrorType || errorType == PlanningErrorType || - errorType == NotFoundErrorType + errorType == NotFoundErrorType || + errorType == MergeConflictType || + errorType == FastForwardErrorType || + errorType == NoCommonBaseFoundType || + errorType == ValidationErrorType } func ProcessCommitError(commit *Commit, fallbackMessage string) error { @@ -333,12 +357,41 @@ func ProcessProjectError(project *Project, fallbackMessage string) error { return errs.New(fallbackMessage) } +type ProjectCreatedError struct { + Type string + Message string +} + +func (p *ProjectCreatedError) Error() string { return p.Message } + +func ProcessProjectCreatedError(pcErr *projectCreated, fallbackMessage string) error { + if pcErr.Type != "" { + // These will be handled individually per type as user-facing errors in DX-2300. + return &ProjectCreatedError{pcErr.Type, pcErr.Message} + } + return errs.New(fallbackMessage) +} + type BuildExpression struct { Type string `json:"__typename"` Commit *Commit `json:"commit"` *Error } +type MergedCommitError struct { + Type string + Message string +} + +func (m *MergedCommitError) Error() string { return m.Message } + +func ProcessMergedCommitError(mcErr *mergedCommit, fallbackMessage string) error { + if mcErr.Type != "" { + return &MergedCommitError{mcErr.Type, mcErr.Message} + } + return errs.New(fallbackMessage) +} + // PushCommitResult is the result of a push commit mutation. // It contains the resulting commit from the operation and any errors. // The resulting commit is pushed to the platform automatically. @@ -355,6 +408,51 @@ type StageCommitResult struct { Commit *Commit `json:"stageCommit"` } +type projectCreated struct { + Type string `json:"__typename"` + Commit *Commit `json:"commit"` + *Error + *NotFoundError + *ParseError + *ForbiddenError +} + +type CreateProjectResult struct { + ProjectCreated *projectCreated `json:"createProject"` +} + +type revertedCommit struct { + Type string `json:"__typename"` + Commit *Commit `json:"commit"` + CommonAncestor strfmt.UUID `json:"commonAncestorID"` + ConflictPaths []string `json:"conflictPaths"` + *Error +} + +type RevertCommitResult struct { + RevertedCommit *revertedCommit `json:"revertCommit"` +} + +type mergedCommit struct { + Type string `json:"__typename"` + Commit *Commit `json:"commit"` + *Error + *MergeConflictError + *MergeError + *NotFoundError + *ParseError + *ForbiddenError + *HeadOnBranchMovedError + *NoChangeSinceLastCommitError +} + +// MergeCommitResult is the result of a merge commit mutation. +// The resulting commit is only pushed to the platform automatically if the target ref was a named +// branch and the merge strategy was FastForward. +type MergeCommitResult struct { + MergedCommit *mergedCommit `json:"mergeCommit"` +} + // Error contains an error message. type Error struct { Message string `json:"message"` @@ -557,6 +655,20 @@ type NoChangeSinceLastCommitError struct { NoChangeCommitID strfmt.UUID `json:"commitId"` } +// MergeConflictError represents an error that occurred because of a merge conflict. +type MergeConflictError struct { + CommonAncestorID strfmt.UUID `json:"commonAncestorId"` + ConflictPaths []string `json:"conflictPaths"` +} + +// MergeError represents two different errors in the BuildPlanner's graphQL +// schema with the same fields. Those errors being: FastForwardError and +// NoCommonBaseFound. Inspect the Type field to determine which error it is. +type MergeError struct { + TargetVCSRef strfmt.UUID `json:"targetVcsRef"` + OtherVCSRef strfmt.UUID `json:"otherVcsRef"` +} + // 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/createproject.go b/pkg/platform/api/buildplanner/request/createproject.go new file mode 100644 index 0000000000..81551b7d8b --- /dev/null +++ b/pkg/platform/api/buildplanner/request/createproject.go @@ -0,0 +1,57 @@ +package request + +import "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" + +func CreateProject(owner, project string, private bool, expr *buildexpression.BuildExpression, description string) *createProject { + return &createProject{map[string]interface{}{ + "organization": owner, + "project": project, + "private": private, + "expr": expr, + "description": description, + }} +} + +type createProject struct { + vars map[string]interface{} +} + +func (c *createProject) Query() string { + return ` +mutation ($organization: String!, $project: String!, $private: Boolean!, $expr: BuildExpr!, $description: String!) { + createProject(input:{organization:$organization, project:$project, private:$private, expr:$expr, description:$description}) { + ... on ProjectCreated { + __typename + commit { + __typename + commitId + } + } + ... on AlreadyExists { + __typename + message + } + ... on NotFound { + __typename + message + } + ... on ParseError { + __typename + message + path + } + ... on ValidationError { + __typename + message + } + ... on Forbidden { + __typename + message + } + } +}` +} + +func (c *createProject) Vars() map[string]interface{} { + return c.vars +} diff --git a/pkg/platform/api/buildplanner/request/mergecommit.go b/pkg/platform/api/buildplanner/request/mergecommit.go new file mode 100644 index 0000000000..d4ce1ace25 --- /dev/null +++ b/pkg/platform/api/buildplanner/request/mergecommit.go @@ -0,0 +1,76 @@ +package request + +import "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" + +func MergeCommit(owner, project, targetRef, otherRef string, strategy model.MergeStrategy) *mergeCommit { + return &mergeCommit{map[string]interface{}{ + "organization": owner, + "project": project, + "targetRef": targetRef, + "otherRef": otherRef, + "strategy": strategy, + }} +} + +type mergeCommit struct { + vars map[string]interface{} +} + +func (b *mergeCommit) Query() string { + return ` +mutation ($organization: String!, $project: String!, $targetRef: String!, $otherRef: String!, $strategy: MergeStrategy) { + mergeCommit(input:{organization:$organization, project:$project, targetVcsRef:$targetRef, otherVcsRef:$otherRef, strategy:$strategy}) { + ... on MergedCommit { + commit { + __typename + commitId + } + } + ... on MergeConflict { + __typename + message + } + ... on FastForwardError { + __typename + message + } + ... on NoCommonBaseFound { + __typename + message + } + ... on NotFound { + __typename + message + mayNeedAuthentication + } + ... on ParseError { + __typename + message + } + ... on ValidationError { + __typename + message + } + ... on Forbidden { + __typename + message + } + ... on HeadOnBranchMoved { + __typename + message + commitId + branchId + } + ... on NoChangeSinceLastCommit { + __typename + message + commitId + } + } +} +` +} + +func (b *mergeCommit) Vars() map[string]interface{} { + return b.vars +} diff --git a/pkg/platform/api/buildplanner/request/revertcommit.go b/pkg/platform/api/buildplanner/request/revertcommit.go new file mode 100644 index 0000000000..70c0829f7a --- /dev/null +++ b/pkg/platform/api/buildplanner/request/revertcommit.go @@ -0,0 +1,79 @@ +package request + +import "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" + +func RevertCommit(organization, project, targetVcsRef, commitID string) *revertCommit { + return &revertCommit{map[string]interface{}{ + "organization": organization, + "project": project, + "targetVcsRef": targetVcsRef, + "commitId": commitID, + // Currently, we use the force strategy for all revert commits. + // This is because we don't have a way to show the user the conflicts + // and let them resolve them yet. + // https://activestatef.atlassian.net/browse/AR-80?focusedCommentId=46998 + "strategy": model.RevertCommitStrategyForce, + }} +} + +type revertCommit struct { + vars map[string]interface{} +} + +func (r *revertCommit) Query() string { + return ` +mutation ($organization: String!, $project: String!, $commitId: String!, $targetVcsRef: String!, $strategy: RevertStrategy) { + revertCommit( + input: {organization: $organization, project: $project, commitId: $commitId, targetVcsRef: $targetVcsRef, strategy: $strategy} + ) { + ... on RevertedCommit { + __typename + commit { + __typename + commitId + } + } + ... on RevertConflict { + __typename + message + commitId + targetCommitId + conflictPaths + } + ... on CommitHasNoParent { + __typename + message + } + ... on NotFound { + __typename + message + mayNeedAuthentication + } + ... on ParseError { + __typename + message + path + } + ... on ValidationError { + __typename + message + } + ... on Forbidden { + __typename + message + } + ... on HeadOnBranchMoved { + __typename + message + } + ... on NoChangeSinceLastCommit { + message + commitId + } + } +}` +} + +func (r *revertCommit) Vars() map[string]interface{} { + return r.vars +} diff --git a/pkg/platform/api/graphql/request/usage.go b/pkg/platform/api/graphql/request/usage.go deleted file mode 100644 index 8b5f9a0d42..0000000000 --- a/pkg/platform/api/graphql/request/usage.go +++ /dev/null @@ -1,40 +0,0 @@ -package request - -import ( - "time" - - "github.com/go-openapi/strfmt" - - "github.com/ActiveState/cli/pkg/platform/api/graphql/model" -) - -// RuntimeUsage reports rtusage for the last 8 days, ensuring we reach into the previous week regardless of timezone. -// The use-case of this function is to get the current rtusage, but in order to do this we have to request the last 78 -// days rather than the current time, because the week field does not have timezone data, and so requesting the current -// date will result in timezone miss-match issues. -// It is then up to the consuming code to use the most recent rtusage week (ie. the first element in the slice) -func RuntimeUsage(organizationID strfmt.UUID) *usage { - return &usage{map[string]interface{}{ - "organization_id": organizationID, - "week": model.Date{time.Now().Add(-(8 * 24 * time.Hour))}, - }} -} - -type usage struct { - vars map[string]interface{} -} - -func (p *usage) Query() string { - return ` - query ($organization_id: uuid!, $week: date) { - organizations_runtime_usage(limit: 1, order_by: [{week_of: desc}], where: {_and: [{organization_id: {_eq: $organization_id}}, {week_of: {_gte: $week}}]}) { - limit_runtimes - active_runtimes - } - } - ` -} - -func (p *usage) Vars() map[string]interface{} { - return p.vars -} diff --git a/pkg/platform/api/graphql/request/usage_test.go b/pkg/platform/api/graphql/request/usage_test.go deleted file mode 100644 index 76cb67ae37..0000000000 --- a/pkg/platform/api/graphql/request/usage_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package request - -import ( - "testing" - - "github.com/go-openapi/strfmt" - "github.com/stretchr/testify/require" - - "github.com/ActiveState/cli/pkg/platform/api/graphql" - "github.com/ActiveState/cli/pkg/platform/api/graphql/model" - "github.com/ActiveState/cli/pkg/platform/authentication" -) - -func TestUsage(t *testing.T) { - // This is not a test meant to run on CI, but rather we can use to debug things locally. - // To run this comment out the next line and comment out the `if condition.InUnitTest() {` section inside api/settings.go - t.Skip("For development use only") - - tests := []struct { - name string - organizationID strfmt.UUID - }{ - { - "Basic", - strfmt.UUID("5587a9d9-a564-4d49-89fd-4dd04d51f13e"), // ActiveState-labs - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - auth := authentication.LegacyGet() - require.NoError(t, auth.Authenticate()) - - client := graphql.New() - client.SetTokenProvider(auth) - - resp := model.RuntimeUsageResponse{} - err := client.Run(RuntimeUsage(tt.organizationID), &resp) - - usage := resp.Usage[0] - - require.NoError(t, err) - require.NotEmpty(t, resp.Usage) - require.NotEmpty(t, usage.ActiveDynamicRuntimes) - - }) - } -} diff --git a/pkg/platform/api/svc/request/checkrtusage.go b/pkg/platform/api/svc/request/checkrtusage.go deleted file mode 100644 index 39afd606e4..0000000000 --- a/pkg/platform/api/svc/request/checkrtusage.go +++ /dev/null @@ -1,26 +0,0 @@ -package request - -type CheckRuntimeUsage struct { - organizationName string -} - -func NewCheckRuntimeUsage(organizationName string) *CheckRuntimeUsage { - return &CheckRuntimeUsage{ - organizationName: organizationName, - } -} - -func (e *CheckRuntimeUsage) Query() string { - return `query($organizationName: String!) { - checkRuntimeUsage(organizationName: $organizationName) { - limit - usage - } - }` -} - -func (e *CheckRuntimeUsage) Vars() map[string]interface{} { - return map[string]interface{}{ - "organizationName": e.organizationName, - } -} diff --git a/pkg/platform/model/buildplanner.go b/pkg/platform/model/buildplanner.go index c1ef78b35f..392eb90a76 100644 --- a/pkg/platform/model/buildplanner.go +++ b/pkg/platform/model/buildplanner.go @@ -113,30 +113,6 @@ func (bp *BuildPlanner) FetchBuildResult(commitID strfmt.UUID, owner, project st // response with emtpy targets that we should remove removeEmptyTargets(build) - // Extract the available platforms from the build plan - var bpPlatforms []strfmt.UUID - for _, t := range build.Terminals { - if t.Tag == bpModel.TagOrphan { - continue - } - bpPlatforms = append(bpPlatforms, strfmt.UUID(strings.TrimPrefix(t.Tag, "platform:"))) - } - - // Get the platform ID for the current platform - platformID, err := FilterCurrentPlatform(HostPlatform, bpPlatforms) - if err != nil { - return nil, locale.WrapError(err, "err_filter_current_platform") - } - - // Filter the build terminals to only include the current platform - var filteredTerminals []*bpModel.NamedTarget - for _, t := range build.Terminals { - if platformID.String() == strings.TrimPrefix(t.Tag, "platform:") { - filteredTerminals = append(filteredTerminals, t) - } - } - build.Terminals = filteredTerminals - buildEngine := Alternative for _, s := range build.Sources { if s.Namespace == "builder" && s.Name == "camel" { @@ -349,6 +325,135 @@ func (bp *BuildPlanner) GetBuildExpression(owner, project, commitID string) (*bu return expression, nil } +// CreateProjectParams contains information for the project to create. +// When creating a project from scratch, the PlatformID, Language, Version, and Timestamp fields +// are used to create a buildexpression to use. +// When creating a project based off of another one, the Expr field is used (PlatformID, Language, +// Version, and Timestamp are ignored). +type CreateProjectParams struct { + Owner string + Project string + PlatformID strfmt.UUID + Language string + Version string + Private bool + Timestamp strfmt.DateTime + Description string + Expr *buildexpression.BuildExpression +} + +func (bp *BuildPlanner) CreateProject(params *CreateProjectParams) (strfmt.UUID, error) { + logging.Debug("CreateProject, owner: %s, project: %s, language: %s, version: %s", params.Owner, params.Project, params.Language, params.Version) + + expr := params.Expr + if expr == nil { + // Construct an initial buildexpression for the new project. + var err error + expr, err = buildexpression.NewEmpty() + if err != nil { + return "", errs.Wrap(err, "Unable to create initial buildexpression") + } + + // Add the platform. + expr.UpdatePlatform(model.OperationAdded, params.PlatformID) + + // Create a requirement for the given language and version. + versionRequirements, err := VersionStringToRequirements(params.Version) + if err != nil { + return "", errs.Wrap(err, "Unable to read version") + } + expr.UpdateRequirement(model.OperationAdded, bpModel.Requirement{ + Name: params.Language, + Namespace: "language", // TODO: make this a constant DX-1738 + VersionRequirement: versionRequirements, + }) + + // Add the timestamp. + expr.UpdateTimestamp(params.Timestamp) + } + + // Create the project. + request := request.CreateProject(params.Owner, params.Project, params.Private, expr, params.Description) + resp := &bpModel.CreateProjectResult{} + err := bp.client.Run(request, resp) + if err != nil { + return "", processBuildPlannerError(err, "Failed to create project") + } + + if resp.ProjectCreated == nil { + return "", errs.New("ProjectCreated is nil") + } + + if bpModel.IsErrorResponse(resp.ProjectCreated.Type) { + return "", bpModel.ProcessProjectCreatedError(resp.ProjectCreated, "Could not create project") + } + + if resp.ProjectCreated.Commit == nil { + return "", errs.New("ProjectCreated.Commit is nil") + } + + return resp.ProjectCreated.Commit.CommitID, nil +} + +func (bp *BuildPlanner) RevertCommit(organization, project, parentCommitID, commitID string) (strfmt.UUID, error) { + logging.Debug("RevertCommit, organization: %s, project: %s, commitID: %s", organization, project, commitID) + resp := &bpModel.RevertCommitResult{} + err := bp.client.Run(request.RevertCommit(organization, project, parentCommitID, commitID), resp) + if err != nil { + return "", processBuildPlannerError(err, "Failed to revert commit") + } + + if resp.RevertedCommit == nil { + return "", errs.New("Commit is nil") + } + + if bpModel.IsErrorResponse(resp.RevertedCommit.Type) { + return "", bpModel.ProcessCommitError(resp.RevertedCommit.Commit, "Could not revert commit") + } + + if resp.RevertedCommit.Commit.CommitID == "" { + return "", errs.New("Commit does not contain commitID") + } + + return resp.RevertedCommit.Commit.CommitID, nil +} + +type MergeCommitParams struct { + Owner string + Project string + TargetRef string // the commit ID or branch name to merge into + OtherRef string // the commit ID or branch name to merge from + Strategy model.MergeStrategy +} + +func (bp *BuildPlanner) MergeCommit(params *MergeCommitParams) (strfmt.UUID, error) { + logging.Debug("MergeCommit, owner: %s, project: %s", params.Owner, params.Project) + request := request.MergeCommit(params.Owner, params.Project, params.TargetRef, params.OtherRef, params.Strategy) + resp := &bpModel.MergeCommitResult{} + err := bp.client.Run(request, resp) + if err != nil { + return "", processBuildPlannerError(err, "Failed to merge commit") + } + + if resp.MergedCommit == nil { + return "", errs.New("MergedCommit is nil") + } + + if bpModel.IsErrorResponse(resp.MergedCommit.Type) { + return "", bpModel.ProcessMergedCommitError(resp.MergedCommit, "Could not merge commit") + } + + if resp.MergedCommit.Commit == nil { + return "", errs.New("Merge commit's commit is nil'") + } + + if bpModel.IsErrorResponse(resp.MergedCommit.Commit.Type) { + return "", bpModel.ProcessCommitError(resp.MergedCommit.Commit, "Could not process error response from merge commit") + } + + return resp.MergedCommit.Commit.CommitID, nil +} + // processBuildPlannerError will check for special error types that should be // handled differently. If no special error type is found, the fallback message // will be used. diff --git a/pkg/platform/model/buildrequest.go b/pkg/platform/model/buildrequest.go index dcb5570d7d..6c0b071539 100644 --- a/pkg/platform/model/buildrequest.go +++ b/pkg/platform/model/buildrequest.go @@ -14,7 +14,7 @@ func RequestBuild(auth *authentication.Auth, recipeID, commitID strfmt.UUID, own var platProj *mono_models.Project if owner != "" && project != "" { var err error - platProj, err = FetchProjectByName(owner, project) + platProj, err = LegacyFetchProjectByName(owner, project) if err != nil { return headchef.Error, nil, locale.WrapError(err, "build_request_get_project_err", "Could not find project {{.V0}}/{{.V1}} on ActiveState Platform.", owner, project) } diff --git a/pkg/platform/model/inventory.go b/pkg/platform/model/inventory.go index dc6c877ddc..56182a5601 100644 --- a/pkg/platform/model/inventory.go +++ b/pkg/platform/model/inventory.go @@ -16,7 +16,14 @@ import ( "github.com/ActiveState/cli/pkg/platform/authentication" ) -type ErrNoMatchingPlatform struct{ *locale.LocalizedError } +type ErrNoMatchingPlatform struct { + HostPlatform string + HostArch string +} + +func (e ErrNoMatchingPlatform) Error() string { + return "no matching platform" +} // IngredientAndVersion is a sane version of whatever the hell it is go-swagger thinks it's doing type IngredientAndVersion struct { @@ -93,6 +100,11 @@ func FetchAuthors(ingredID, ingredVersionID *strfmt.UUID) (Authors, error) { return results.Payload.Authors, nil } +type ErrTooManyMatches struct { + *locale.LocalizedError + Query string +} + func searchIngredientsNamespace(ns Namespace, name string, includeVersions bool, exactOnly bool) ([]*IngredientAndVersion, error) { limit := int64(100) offset := int64(0) @@ -116,7 +128,7 @@ func searchIngredientsNamespace(ns Namespace, name string, includeVersions bool, for offset == 0 || len(entries) == int(limit) { if offset > (limit * 10) { // at most we will get 10 pages of ingredients (that's ONE THOUSAND ingredients) // Guard against queries that match TOO MANY ingredients - return nil, locale.NewError("err_searchingredient_toomany", "Query matched too many ingredients. Please use a more specific query.") + return nil, &ErrTooManyMatches{locale.NewInputError("err_searchingredient_toomany", "", name), name} } params.SetOffset(&offset) @@ -248,9 +260,7 @@ func filterPlatformIDs(hostPlatform, hostArch string, platformIDs []strfmt.UUID) return fallback, nil } - return nil, &ErrNoMatchingPlatform{locale.NewInputError( - "err_no_platform_data_remains", "", hostPlatform, hostArch, - )} + return nil, &ErrNoMatchingPlatform{hostPlatform, hostArch} } func FetchPlatformByUID(uid strfmt.UUID) (*Platform, error) { diff --git a/pkg/platform/model/projects.go b/pkg/platform/model/projects.go index d437b72733..e3cc24da17 100644 --- a/pkg/platform/model/projects.go +++ b/pkg/platform/model/projects.go @@ -2,7 +2,6 @@ package model import ( "fmt" - "strings" "github.com/go-openapi/strfmt" @@ -23,7 +22,28 @@ import ( type ErrProjectNameConflict struct{ *locale.LocalizedError } -type ErrProjectNotFound struct{ *locale.LocalizedError } +type ErrProjectNotFound struct { + Organization string + Project string +} + +func (e *ErrProjectNotFound) Error() string { + return fmt.Sprintf("project not found: %s/%s", e.Organization, e.Project) +} + +// LegacyFetchProjectByName is intended for legacy code which still relies on localised errors, do NOT use it for new code. +func LegacyFetchProjectByName(orgName string, projectName string) (*mono_models.Project, error) { + project, err := FetchProjectByName(orgName, projectName) + if err == nil || !errs.Matches(err, &ErrProjectNotFound{}) { + return project, err + } + if !authentication.LegacyGet().Authenticated() { + return nil, errs.AddTips( + locale.NewInputError("err_api_project_not_found", "", orgName, projectName), + locale.T("tip_private_project_auth")) + } + return nil, errs.Pack(err, locale.NewInputError("err_api_project_not_found", "", orgName, projectName)) +} // FetchProjectByName fetches a project for an organization. func FetchProjectByName(orgName string, projectName string) (*mono_models.Project, error) { @@ -39,12 +59,7 @@ func FetchProjectByName(orgName string, projectName string) (*mono_models.Projec } if len(response.Projects) == 0 { - if !authentication.LegacyGet().Authenticated() { - return nil, errs.AddTips( - locale.NewInputError("err_api_project_not_found_unauthenticated", "", orgName, projectName), - locale.T("tip_private_project_auth")) - } - return nil, &ErrProjectNotFound{locale.NewInputError("err_api_project_not_found", "", projectName, orgName)} + return nil, &ErrProjectNotFound{orgName, projectName} } return response.Projects[0].ToMonoProject() @@ -56,7 +71,15 @@ func FetchOrganizationProjects(orgName string) ([]*mono_models.Project, error) { projParams.SetOrganizationName(orgName) orgProjects, err := authentication.Client().Projects.ListProjects(projParams, authentication.ClientAuth()) if err != nil { - return nil, processProjectErrorResponse(err) + switch statusCode := api.ErrorCode(err); statusCode { + case 401: + return nil, locale.WrapInputError(err, "err_api_not_authenticated") + case 404: + // NOT a project not found error; we didn't ask for a specific project. + return nil, locale.WrapInputError(err, "err_api_org_not_found") + default: + return nil, locale.WrapError(err, "err_api_unknown", "Unexpected API error") + } } return orgProjects.Payload, nil } @@ -76,7 +99,7 @@ func LanguageByCommit(commitID strfmt.UUID) (Language, error) { // DefaultBranchForProjectName retrieves the default branch for the given project owner/name. func DefaultBranchForProjectName(owner, name string) (*mono_models.Branch, error) { - proj, err := FetchProjectByName(owner, name) + proj, err := LegacyFetchProjectByName(owner, name) if err != nil { return nil, err } @@ -85,7 +108,7 @@ func DefaultBranchForProjectName(owner, name string) (*mono_models.Branch, error } func BranchesForProject(owner, name string) ([]*mono_models.Branch, error) { - proj, err := FetchProjectByName(owner, name) + proj, err := LegacyFetchProjectByName(owner, name) if err != nil { return nil, err } @@ -93,7 +116,7 @@ func BranchesForProject(owner, name string) ([]*mono_models.Branch, error) { } func BranchNamesForProjectFiltered(owner, name string, excludes ...string) ([]string, error) { - proj, err := FetchProjectByName(owner, name) + proj, err := LegacyFetchProjectByName(owner, name) if err != nil { return nil, err } @@ -121,7 +144,7 @@ func DefaultBranchForProject(pj *mono_models.Project) (*mono_models.Branch, erro // BranchForProjectNameByName retrieves the named branch for the given project // org/name func BranchForProjectNameByName(owner, name, branch string) (*mono_models.Branch, error) { - proj, err := FetchProjectByName(owner, name) + proj, err := LegacyFetchProjectByName(owner, name) if err != nil { return nil, err } @@ -167,7 +190,7 @@ func CreateEmptyProject(owner, name string, private bool) (*mono_models.Project, func CreateCopy(sourceOwner, sourceName, targetOwner, targetName string, makePrivate bool) (*mono_models.Project, error) { // Retrieve the source project that we'll be forking - sourceProject, err := FetchProjectByName(sourceOwner, sourceName) + sourceProject, err := LegacyFetchProjectByName(sourceOwner, sourceName) if err != nil { return nil, locale.WrapInputError(err, "err_fork_fetchProject", "Could not find the source project: {{.V0}}/{{.V1}}", sourceOwner, sourceName) } @@ -240,23 +263,6 @@ func ProjectURL(owner, name, commitID string) string { return url } -// CommitURL creates a valid platform commit URL for the given commit -func CommitURL(commitID string) string { - return fmt.Sprintf("%s/%s", strings.TrimSuffix(constants.DashboardCommitURL, "/"), commitID) -} - -func processProjectErrorResponse(err error, params ...string) error { - switch statusCode := api.ErrorCode(err); statusCode { - case 401: - return locale.WrapInputError(err, "err_api_not_authenticated") - case 404: - p := append([]string{""}, params...) - return &ErrProjectNotFound{locale.WrapInputError(err, "err_api_project_not_found", p...)} - default: - return locale.WrapError(err, "err_api_unknown", "Unexpected API error") - } -} - func AddBranch(projectID strfmt.UUID, label string) (strfmt.UUID, error) { var branchID strfmt.UUID addParams := projects.NewAddBranchParams() diff --git a/pkg/platform/model/recipe.go b/pkg/platform/model/recipe.go index dacf1360cc..008adc4f96 100644 --- a/pkg/platform/model/recipe.go +++ b/pkg/platform/model/recipe.go @@ -32,7 +32,7 @@ type SolverError struct { } func (e *SolverError) Error() string { - return "solver_error" + return "buildplan_error" } func (e *SolverError) Unwrap() error { diff --git a/pkg/platform/model/svc.go b/pkg/platform/model/svc.go index 4a86d8b5f5..40d206614d 100644 --- a/pkg/platform/model/svc.go +++ b/pkg/platform/model/svc.go @@ -119,18 +119,6 @@ func (m *SvcModel) ReportRuntimeUsage(ctx context.Context, pid int, exec, source return nil } -func (m *SvcModel) CheckRuntimeUsage(ctx context.Context, organizationName string) (*graph.CheckRuntimeUsageResponse, error) { - defer profile.Measure("svc:CheckRuntimeUsage", time.Now()) - - r := request.NewCheckRuntimeUsage(organizationName) - u := graph.CheckRuntimeUsageResponseOuter{} - if err := m.request(ctx, r, &u); err != nil { - return nil, errs.Wrap(err, "Error sending check runtime usage event via state-svc") - } - - return &u.Usage, nil -} - func (m *SvcModel) CheckMessages(ctx context.Context, command string, flags []string) ([]*graph.MessageInfo, error) { logging.Debug("Checking for messages") defer profile.Measure("svc:CheckMessages", time.Now()) diff --git a/pkg/platform/model/vcs.go b/pkg/platform/model/vcs.go index 1556b33b57..5ebb5f8e74 100644 --- a/pkg/platform/model/vcs.go +++ b/pkg/platform/model/vcs.go @@ -33,7 +33,7 @@ var ( ErrMergeCommitInHistory = errs.New("Can't merge commit thats already in target commits history") ) -type ErrOrderAuth struct{ *locale.LocalizedError } +var ErrOrderForbidden = errs.New("no permission to retrieve order") type ErrUpdateBranchAuth struct{ *locale.LocalizedError } @@ -228,7 +228,7 @@ func FilterSupportedIngredients(supported []model.SupportedLanguage, ingredients // BranchCommitID returns the latest commit id by owner and project names. It // is possible for a nil commit id to be returned without failure. func BranchCommitID(ownerName, projectName, branchName string) (*strfmt.UUID, error) { - proj, err := FetchProjectByName(ownerName, projectName) + proj, err := LegacyFetchProjectByName(ownerName, projectName) if err != nil { return nil, err } @@ -449,7 +449,7 @@ func AddChangeset(parentCommitID strfmt.UUID, commitMessage string, changeset Ch } func UpdateBranchForProject(pj ProjectInfo, commitID strfmt.UUID) error { - pjm, err := FetchProjectByName(pj.Owner(), pj.Name()) + pjm, err := LegacyFetchProjectByName(pj.Owner(), pj.Name()) if err != nil { return errs.Wrap(err, "Could not fetch project") } @@ -517,7 +517,7 @@ func DeleteBranch(branchID strfmt.UUID) error { // UpdateProjectBranchCommitByName updates the vcs branch for a project given by its namespace with a new commitID func UpdateProjectBranchCommit(pj ProjectInfo, commitID strfmt.UUID) error { - pjm, err := FetchProjectByName(pj.Owner(), pj.Name()) + pjm, err := LegacyFetchProjectByName(pj.Owner(), pj.Name()) if err != nil { return errs.Wrap(err, "Could not fetch project") } @@ -737,7 +737,7 @@ func FetchOrderFromCommit(commitID strfmt.UUID) (*mono_models.Order, error) { if err != nil { code := api.ErrorCode(err) if code == 401 || code == 403 { - return nil, &ErrOrderAuth{locale.NewInputError("err_order_auth", "Fetch order failed with authentication error")} + return nil, errs.Pack(err, ErrOrderForbidden) } return nil, errors.New(api.ErrorMessageFromPayload(err)) } diff --git a/pkg/platform/runtime/buildexpression/buildexpression.go b/pkg/platform/runtime/buildexpression/buildexpression.go index d759e99819..9aebc10e84 100644 --- a/pkg/platform/runtime/buildexpression/buildexpression.go +++ b/pkg/platform/runtime/buildexpression/buildexpression.go @@ -84,7 +84,7 @@ type In struct { Name *string } -// NewBuildExpression creates a BuildExpression from a JSON byte array. +// New creates a BuildExpression from a JSON byte array. // The JSON must be a valid BuildExpression in the following format: // // { @@ -171,6 +171,33 @@ func New(data []byte) (*BuildExpression, error) { return expr, nil } +// NewEmpty creates a minimal, empty buildexpression. +func NewEmpty() (*BuildExpression, error) { + // At this time, there is no way to ask the Platform for an empty buildexpression, so build one + // manually. + expr, err := New([]byte(` + { + "let": { + "runtime": { + "solve_legacy": { + "at_time": "", + "build_flags": [], + "camel_flags": [], + "platforms": [], + "requirements": [], + "solver_version": null + } + }, + "in": "$runtime" + } + } + `)) + if err != nil { + return nil, errs.Wrap(err, "Unable to create initial buildexpression") + } + return expr, nil +} + func newLet(path []string, m map[string]interface{}) (*Let, error) { path = append(path, ctxLet) defer func() { @@ -302,6 +329,10 @@ func newValue(path []string, valueInterface interface{}) (*Value, error) { case float64: value.Float = ptr.To(v) + case nil: + // An empty value is interpreted as JSON null. + value.Null = &Null{} + default: logging.Debug("Unknown type: %T at path %s", v, strings.Join(path, ".")) // An empty value is interpreted as JSON null. @@ -592,7 +623,7 @@ func (e *BuildExpression) getSolveNode() (*Ap, error) { continue } - if a.Assignment.Name == "" || a.Assignment.Name != "runtime" { + if a.Assignment.Name == "" { continue } @@ -625,7 +656,7 @@ func recurseLets(let *Let) (*Ap, error) { continue } - if a.Name == "" || a.Name != "runtime" { + if a.Name == "" { continue } diff --git a/pkg/platform/runtime/buildexpression/buildexpression_test.go b/pkg/platform/runtime/buildexpression/buildexpression_test.go index 489f9650d8..08d965e1ef 100644 --- a/pkg/platform/runtime/buildexpression/buildexpression_test.go +++ b/pkg/platform/runtime/buildexpression/buildexpression_test.go @@ -63,6 +63,13 @@ func TestNew(t *testing.T) { }, wantErr: false, }, + { + name: "alternate", + args: args{ + filename: "buildexpression-alternate.json", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -145,6 +152,29 @@ func TestBuildExpression_Requirements(t *testing.T) { }, wantErr: false, }, + { + name: "alternate", + args: args{ + filename: "buildexpression-alternate.json", + }, + want: []model.Requirement{ + { + Name: "Path-Tiny", + Namespace: "language/perl", + }, + { + Name: "perl", + Namespace: "language", + VersionRequirement: []model.VersionRequirement{ + map[string]string{ + "comparator": string(model.ComparatorEQ), + "version": "5.36.1", + }, + }, + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -372,6 +402,38 @@ func TestBuildExpression_Update(t *testing.T) { }, wantErr: false, }, + { + name: "add-alternate", + args: args{ + requirement: model.Requirement{ + Name: "JSON", + Namespace: "language/perl", + }, + operation: model.OperationAdded, + filename: "buildexpression-alternate.json", + }, + want: []model.Requirement{ + { + Name: "Path-Tiny", + Namespace: "language/perl", + }, + { + Name: "perl", + Namespace: "language", + VersionRequirement: []model.VersionRequirement{ + map[string]string{ + "comparator": string(model.ComparatorEQ), + "version": "5.36.1", + }, + }, + }, + { + Name: "JSON", + Namespace: "language/perl", + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/platform/runtime/buildexpression/merge/merge.go b/pkg/platform/runtime/buildexpression/merge/merge.go index 9a71742937..176f402bb2 100644 --- a/pkg/platform/runtime/buildexpression/merge/merge.go +++ b/pkg/platform/runtime/buildexpression/merge/merge.go @@ -3,7 +3,6 @@ package merge import ( "encoding/json" "reflect" - "sort" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/logging" @@ -71,7 +70,7 @@ func isAutoMergePossible(exprA *buildexpression.BuildExpression, exprB *buildexp logging.Debug("Checking for possibility of auto-merging build expressions") logging.Debug("JsonA: %v", jsonA) logging.Debug("JsonB: %v", jsonB) - return reflect.DeepEqual(jsonA, jsonB) // TODO: replace with DX-1939 solution + return reflect.DeepEqual(jsonA, jsonB) } // getComparableJson returns a comparable JSON map[string]interface{} structure for the given build @@ -98,8 +97,6 @@ func getComparableJson(expr *buildexpression.BuildExpression) (map[string]interf return nil, errs.New("'let' key is not a JSON object") } deleteKey(&letMap, "requirements") - // TODO: the following shouldn't be needed after DX-1939. - sortLists(&letMap) deleteKey(&letMap, "at_time") return m, nil @@ -121,21 +118,3 @@ func deleteKey(m *map[string]interface{}, key string) bool { } return false } - -// sortLists recursively iterates over the given JSON map looking for string lists, and sorts them. -// This is needed because isAutoMergePossible() does a reflect.DeepEqual(), but build expression -// list order does not matter. -// This will not be necessary after DX-1939 is implemented. -func sortLists(m *map[string]interface{}) { - for _, v := range *m { - if list, ok := v.([]interface{}); ok { - sort.SliceStable(list, func(i, j int) bool { - s1, ok1 := list[i].(string) - s2, ok2 := list[j].(string) - return ok1 && ok2 && s1 < s2 - }) - } else if m2, ok := v.(map[string]interface{}); ok { - sortLists(&m2) - } - } -} diff --git a/pkg/platform/runtime/buildexpression/merge/merge_test.go b/pkg/platform/runtime/buildexpression/merge/merge_test.go index 69233a21a5..bbf3d142e9 100644 --- a/pkg/platform/runtime/buildexpression/merge/merge_test.go +++ b/pkg/platform/runtime/buildexpression/merge/merge_test.go @@ -80,35 +80,6 @@ in: mergedScript, err := buildscript.NewScriptFromBuildExpression(mergedExpr) require.NoError(t, err) - // TODO: delete this block after DX-1939. Sorting requirements is needed until we have - // buildexpression hashes for comparing equality. - assert.Equal(t, - `let: - runtime = solve( - platforms = [ - "12345", - "67890" - ], - requirements = [ - { - name = "JSON", - namespace = "language/perl" - }, - { - name = "perl", - namespace = "language" - }, - { - name = "DateTime", - namespace = "language/perl" - } - ] - ) - -in: - runtime`, mergedScript.String()) - return - assert.Equal(t, `let: runtime = solve( @@ -168,14 +139,12 @@ in: exprA, err := buildexpression.New(bytes) require.NoError(t, err) - // Note the intentional swap of platform order. Buildexpression list order does not matter. - // isAutoMergePossible() should still return true, and the original platforms will be used. scriptB, err := buildscript.NewScript([]byte( `let: runtime = solve( platforms = [ - "67890", - "12345" + "12345", + "67890" ], requirements = [ { @@ -211,31 +180,6 @@ in: mergedScript, err := buildscript.NewScriptFromBuildExpression(mergedExpr) require.NoError(t, err) - // TODO: delete this block after DX-1939. Sorting requirements is needed until we have - // buildexpression hashes for comparing equality. - assert.Equal(t, - `let: - runtime = solve( - platforms = [ - "12345", - "67890" - ], - requirements = [ - { - name = "DateTime", - namespace = "language/perl" - }, - { - name = "perl", - namespace = "language" - } - ] - ) - -in: - runtime`, mergedScript.String()) - return - assert.Equal(t, `let: runtime = solve( @@ -321,15 +265,3 @@ func TestDeleteKey(t *testing.T) { _, exists := m["foo"].(map[string]interface{})["quux"] assert.False(t, exists, "did not delete quux") } - -func TestSortLists(t *testing.T) { - m := map[string]interface{}{ - "one": []interface{}{"foo", "bar", "baz"}, - "two": map[string]interface{}{ - "three": []interface{}{"foobar", "barfoo", "barbaz"}, - }, - } - sortLists(&m) - assert.Equal(t, []interface{}{"bar", "baz", "foo"}, m["one"]) - assert.Equal(t, []interface{}{"barbaz", "barfoo", "foobar"}, m["two"].(map[string]interface{})["three"]) -} diff --git a/pkg/platform/runtime/buildexpression/testdata/buildexpression-alternate.json b/pkg/platform/runtime/buildexpression/testdata/buildexpression-alternate.json new file mode 100644 index 0000000000..0b68613a4d --- /dev/null +++ b/pkg/platform/runtime/buildexpression/testdata/buildexpression-alternate.json @@ -0,0 +1,37 @@ +{ + "let": { + "sources": { + "solve": { + "at_time": "2023-10-03T22:15:45.727000Z", + "platforms": [ + "0fa42e8c-ac7b-5dd7-9407-8aa15f9b993a", + "46a5b48f-226a-4696-9746-ba4d50d661c2" + ], + "requirements": [ + { + "name": "Path-Tiny", + "namespace": "language/perl" + }, + { + "name": "perl", + "namespace": "language", + "version_requirements": [ + { + "comparator": "eq", + "version": "5.36.1" + } + ] + } + ], + "solver_version": null + } + }, + "runtime": { + "state_tool_artifacts_v1": { + "src": "$sources", + "build_flags": [] + } + }, + "in": "$runtime" + } +} \ No newline at end of file diff --git a/pkg/platform/runtime/buildplan/buildplan.go b/pkg/platform/runtime/buildplan/buildplan.go index f967923b48..6fe0be1084 100644 --- a/pkg/platform/runtime/buildplan/buildplan.go +++ b/pkg/platform/runtime/buildplan/buildplan.go @@ -1,19 +1,106 @@ package buildplan import ( + "strings" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/pkg/platform/api/buildplanner/model" + platformModel "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/platform/runtime/artifact" "github.com/go-openapi/strfmt" ) -// NewMapFromBuildPlan creates an artifact map from a build plan. It creates a +type ArtifactListing struct { + build *model.Build + runtimeClosure artifact.Map + buildtimeClosure artifact.Map + artifactIDs []artifact.ArtifactID +} + +func NewArtifactListing(build *model.Build, buildtimeClosure bool) (*ArtifactListing, error) { + al := &ArtifactListing{build: build} + if buildtimeClosure { + buildtimeClosure, err := newMapFromBuildPlan(al.build, true) + if err != nil { + return nil, errs.Wrap(err, "Could not create buildtime closure") + } + al.buildtimeClosure = buildtimeClosure + } else { + runtimeClosure, err := newMapFromBuildPlan(al.build, false) + if err != nil { + return nil, errs.Wrap(err, "Could not create runtime closure") + } + al.runtimeClosure = runtimeClosure + } + + return al, nil +} + +func (al *ArtifactListing) RuntimeClosure() (artifact.Map, error) { + if al.runtimeClosure != nil { + return al.runtimeClosure, nil + } + + runtimeClosure, err := newMapFromBuildPlan(al.build, false) + if err != nil { + return nil, errs.Wrap(err, "Could not create runtime closure") + } + al.runtimeClosure = runtimeClosure + + return runtimeClosure, nil +} + +func (al *ArtifactListing) BuildtimeClosure() (artifact.Map, error) { + if al.buildtimeClosure != nil { + return al.buildtimeClosure, nil + } + + buildtimeClosure, err := newMapFromBuildPlan(al.build, true) + if err != nil { + return nil, errs.Wrap(err, "Could not create buildtime closure") + } + al.buildtimeClosure = buildtimeClosure + + return buildtimeClosure, nil +} + +func (al *ArtifactListing) ArtifactIDs(buildtimeClosure bool) ([]artifact.ArtifactID, error) { + if al.artifactIDs != nil { + return al.artifactIDs, nil + } + + var artifactMap artifact.Map + var err error + if buildtimeClosure { + artifactMap, err = al.BuildtimeClosure() + if err != nil { + return nil, errs.Wrap(err, "Could not calculate buildtime closure") + } + } else { + artifactMap, err = al.RuntimeClosure() + if err != nil { + return nil, errs.Wrap(err, "Could not calculate runtime closure") + } + } + + for _, artifact := range artifactMap { + al.artifactIDs = append(al.artifactIDs, artifact.ArtifactID) + } + + return al.artifactIDs, nil +} + +// newMapFromBuildPlan creates an artifact map from a build plan. It creates a // lookup table and calls the recursive function buildMap to build up the // artifact map by traversing the build plan from the terminal targets through // all of the runtime dependencies for each of the artifacts in the DAG. -func NewMapFromBuildPlan(build *model.Build) (artifact.Map, error) { + +// Setting calculateBuildtimeClosure as true calculates the artifact map with the buildtime +// dependencies. This is different from the runtime dependency calculation as it +// includes ALL of the input artifacts of the step that generated each artifact. +func newMapFromBuildPlan(build *model.Build, calculateBuildtimeClosure bool) (artifact.Map, error) { res := make(artifact.Map) lookup := make(map[strfmt.UUID]interface{}) @@ -28,8 +115,13 @@ func NewMapFromBuildPlan(build *model.Build) (artifact.Map, error) { lookup[source.NodeID] = source } + filtered, err := filterPlatformTerminals(build) + if err != nil { + return nil, errs.Wrap(err, "Could not filter terminals") + } + var terminalTargetIDs []strfmt.UUID - for _, terminal := range build.Terminals { + for _, terminal := range filtered { // If there is an artifact for this terminal and its mime type is not a state tool artifact // then we need to recurse back through the DAG until we find nodeIDs that are state tool // artifacts. These are the terminal targets. @@ -38,9 +130,13 @@ func NewMapFromBuildPlan(build *model.Build) (artifact.Map, error) { } } + buildMap := buildRuntimeClosureMap + if calculateBuildtimeClosure { + buildMap = buildBuildtimeClosureMap + } + for _, id := range terminalTargetIDs { - err := buildMap(id, lookup, res) - if err != nil { + if err := buildMap(id, lookup, res); err != nil { return nil, errs.Wrap(err, "Could not build map for terminal %s", id) } } @@ -48,6 +144,36 @@ func NewMapFromBuildPlan(build *model.Build) (artifact.Map, error) { return res, nil } +// filterPlatformTerminals filters the build terminal nodes to only include +// terminals that are for the current host platform. +func filterPlatformTerminals(build *model.Build) ([]*model.NamedTarget, error) { + // Extract the available platforms from the build plan + // We are only interested in terminals with the platform tag + var bpPlatforms []strfmt.UUID + for _, t := range build.Terminals { + if !strings.Contains(t.Tag, "platform:") { + continue + } + bpPlatforms = append(bpPlatforms, strfmt.UUID(strings.TrimPrefix(t.Tag, "platform:"))) + } + + // Get the platform ID for the current host platform + platformID, err := platformModel.FilterCurrentPlatform(platformModel.HostPlatform, bpPlatforms) + if err != nil { + return nil, locale.WrapError(err, "err_filter_current_platform") + } + + // Filter the build terminals to only include the current platform + var filteredTerminals []*model.NamedTarget + for _, t := range build.Terminals { + if platformID.String() == strings.TrimPrefix(t.Tag, "platform:") { + filteredTerminals = append(filteredTerminals, t) + } + } + + return filteredTerminals, nil +} + // buildTerminals recursively builds up a list of terminal targets. It expects an ID that // resolves to an artifact. If the artifact's mime type is that of a state tool artifact it // adds it to the terminal listing. Otherwise it looks up the step that generated the artifact @@ -82,7 +208,7 @@ func buildTerminals(nodeID strfmt.UUID, lookup map[strfmt.UUID]interface{}, resu } } -// buildMap recursively builds the artifact map from the lookup table. It expects an ID that +// buildRuntimeClosureMap recursively builds the artifact map from the lookup table. It expects an ID that // represents an artifact. With that ID it retrieves the artifact from the lookup table and // recursively calls itself with each of the artifacts dependencies. Finally, once all of the // dependencies have been processed, it adds the artifact to the result map. @@ -91,7 +217,7 @@ func buildTerminals(nodeID strfmt.UUID, lookup map[strfmt.UUID]interface{}, resu // iterate through the artifact's dependencies, we also have to build up the dependencies of // each of those dependencies. Once we have a complete list of dependencies for the artifact, // we can continue to build up the results map. -func buildMap(baseID strfmt.UUID, lookup map[strfmt.UUID]interface{}, result artifact.Map) error { +func buildRuntimeClosureMap(baseID strfmt.UUID, lookup map[strfmt.UUID]interface{}, result artifact.Map) error { target := lookup[baseID] currentArtifact, ok := target.(*model.Artifact) if !ok { @@ -110,8 +236,7 @@ func buildMap(baseID strfmt.UUID, lookup map[strfmt.UUID]interface{}, result art deps[id] = struct{}{} } - err = buildMap(depID, lookup, result) - if err != nil { + if err := buildRuntimeClosureMap(depID, lookup, result); err != nil { return errs.Wrap(err, "Could not build map for runtime dependency %s", currentArtifact.NodeID) } } @@ -254,8 +379,8 @@ func RecursiveDependenciesFor(a artifact.ArtifactID, artifacts artifact.Map) []a // NewMapFromBuildPlan creates an artifact map from a build plan // where the key is the artifact name rather than the artifact ID. -func NewNamedMapFromBuildPlan(build *model.Build) (artifact.NamedMap, error) { - am, err := NewMapFromBuildPlan(build) +func NewNamedMapFromBuildPlan(build *model.Build, buildtimeClosure bool) (artifact.NamedMap, error) { + am, err := newMapFromBuildPlan(build, buildtimeClosure) if err != nil { return nil, errs.Wrap(err, "Could not create artifact map") } @@ -268,150 +393,132 @@ func NewNamedMapFromBuildPlan(build *model.Build) (artifact.NamedMap, error) { return res, nil } -// BuildtimeArtifacts iterates through all artifacts in a given build and -// adds the artifact's dependencies to a map. This is different from the -// runtime dependency calculation as it includes ALL of the input artifacts of the -// step that generated each artifact. The includeBuilders argument determines whether -// or not to include builder artifacts in the final result. -func BuildtimeArtifacts(build *model.Build, includeBuilders bool) (artifact.Map, error) { - result := make(artifact.Map) - lookup := make(map[strfmt.UUID]interface{}) - - for _, artifact := range build.Artifacts { - lookup[artifact.NodeID] = artifact +// buildBuildtimeClosureMap recursively builds the artifact map from the lookup table. +// If the current artifact is not already contained in the results map it first +// builds the artifacts build-time dependencies and then adds the artifact to the +// results map. +func buildBuildtimeClosureMap(baseID strfmt.UUID, lookup map[strfmt.UUID]interface{}, result artifact.Map) error { + if _, ok := result[baseID]; ok { + // We have already processed this artifact, skipping + return nil } - for _, step := range build.Steps { - lookup[step.StepID] = step + + target := lookup[baseID] + currentArtifact, ok := target.(*model.Artifact) + if !ok { + return errs.New("Incorrect target type for id %s, expected Artifact", baseID) } - for _, source := range build.Sources { - lookup[source.NodeID] = source + + deps := make(map[strfmt.UUID]struct{}) + buildTimeDeps, err := buildBuildClosureDependencies(baseID, lookup, deps, result) + if err != nil { + return errs.Wrap(err, "Could not build buildtime dependencies for artifact %s", baseID) } - for _, a := range build.Artifacts { - if !includeBuilders && a.MimeType == model.XActiveStateBuilderMimeType { + var uniqueDeps []strfmt.UUID + for id := range buildTimeDeps { + if _, ok := deps[id]; !ok { continue } + uniqueDeps = append(uniqueDeps, id) + } - _, ok := result[strfmt.UUID(a.NodeID)] - // We are only interested in artifacts that have not already been added - // to the result and that have been submitted to be built. - if !ok && a.Status != model.ArtifactNotSubmitted { - // deps here refer to the dependencies of the artifact itself. - // This includes the direct dependencies, which we get through - // the RuntimeDependencies field, as well as the inputs of the - // step that generated the artifact. - deps := make(map[strfmt.UUID]struct{}) - for _, depID := range a.RuntimeDependencies { - artifact, ok := lookup[depID].(*model.Artifact) - if ok { - if !includeBuilders && artifact.MimeType == model.XActiveStateBuilderMimeType { - continue - } - } - - // Add our current dependency to the map of dependencies - // and then recursively add all of its dependencies. - deps[depID] = struct{}{} - recursiveDeps, err := generateBuildtimeDependencies(depID, includeBuilders, lookup, deps) - if err != nil { - return nil, errs.Wrap(err, "Could not resolve runtime dependencies for artifact: %s", depID) - } - - // This is a list of all of the dependencies of the current - // artifact, including the dependencies of its dependencies. - for id := range recursiveDeps { - deps[id] = struct{}{} - } - } - - // We need to convert the map of dependencies to a list of - // dependencies. - var uniqueDeps []strfmt.UUID - for depID := range deps { - uniqueDeps = append(uniqueDeps, depID) - } - - // We need to get the source information for the artifact. - // This is done by looking at the step that generated the - // artifact and then looking at the source that was used - // in that step. - info, err := getSourceInfo(a.GeneratedBy, lookup) - if err != nil { - return nil, errs.Wrap(err, "Could not resolve source information") - } + info, err := getSourceInfo(currentArtifact.GeneratedBy, lookup) + if err != nil { + return errs.Wrap(err, "Could not resolve source information") + } - result[strfmt.UUID(a.NodeID)] = artifact.Artifact{ - ArtifactID: strfmt.UUID(a.NodeID), - Name: info.Name, - Namespace: info.Namespace, - Version: &info.Version, - RequestedByOrder: false, - GeneratedBy: a.GeneratedBy, - Dependencies: uniqueDeps, - } - } + result[strfmt.UUID(currentArtifact.NodeID)] = artifact.Artifact{ + ArtifactID: strfmt.UUID(currentArtifact.NodeID), + Name: info.Name, + Namespace: info.Namespace, + Version: &info.Version, + RequestedByOrder: true, + GeneratedBy: currentArtifact.GeneratedBy, + Dependencies: uniqueDeps, + URL: currentArtifact.URL, } - return result, nil + return nil } -// generateBuildtimeDependencies recursively iterates through an artifacts dependencies -// looking to the step that generated the artifact and then to ALL of the artifacts that -// are inputs to that step. This will lead to the result containing more than what is in -// the runtime closure. The includeBuilders argument is used to determine if we should -// include artifacts that are builders. -func generateBuildtimeDependencies(artifactID strfmt.UUID, includeBuilders bool, lookup map[strfmt.UUID]interface{}, result map[strfmt.UUID]struct{}) (map[strfmt.UUID]struct{}, error) { - artifact, ok := lookup[artifactID].(*model.Artifact) - if !ok { - _, sourceOK := lookup[artifactID].(*model.Source) - if sourceOK { - // Dependency is a source, skipping - return nil, nil - } - return nil, errs.New("Incorrect target type for id %s, expected Artifact or Source", artifactID) +// buildBuildClosureDependencies is a recursive function that builds up a map +// of build-time dependencies for a given artifact if it is not already present +// in the results map. It first iterates through the runtime dependencies of the +// artifact recursively adding all of the dependencies to the results map. +// Then it iterates through the inputs of the step that generated the +// artifact and recursively adds all of those dependencies as well. +func buildBuildClosureDependencies(artifactID strfmt.UUID, lookup map[strfmt.UUID]interface{}, deps map[strfmt.UUID]struct{}, result artifact.Map) (map[strfmt.UUID]struct{}, error) { + if _, ok := result[artifactID]; ok { + // We have already processed this artifact, skipping + return nil, nil } - if !includeBuilders && artifact.MimeType == model.XActiveStateBuilderMimeType { - return nil, nil + currentArtifact, ok := lookup[artifactID].(*model.Artifact) + if !ok { + return nil, errs.New("Incorrect target type for id %s, expected Artifact", artifactID) } // We iterate through the direct dependencies of the artifact // and recursively add all of the dependencies of those artifacts map. - for _, depID := range artifact.RuntimeDependencies { - result[depID] = struct{}{} - _, err := generateBuildtimeDependencies(depID, includeBuilders, lookup, result) + // This is the same as the runtime closure calculation. + for _, depID := range currentArtifact.RuntimeDependencies { + deps[depID] = struct{}{} + artifactDeps := make(map[strfmt.UUID]struct{}) + _, err := buildBuildClosureDependencies(depID, lookup, artifactDeps, result) if err != nil { - return nil, errs.Wrap(err, "Could not build map for runtime dependencies of artifact %s", artifact.NodeID) + return nil, errs.Wrap(err, "Could not build map for runtime dependencies of artifact %s", currentArtifact.NodeID) } } - step, ok := lookup[artifact.GeneratedBy].(*model.Step) + // Here we iterate through the inputs of the step that generated the + // artifact, specifically the inputs that are tagged as dependencies. + // We recursively add all of the dependencies of the step intputs to + // the result map. This is the buildtime closure calculation. + step, ok := lookup[currentArtifact.GeneratedBy].(*model.Step) if !ok { - _, ok := lookup[artifact.GeneratedBy].(*model.Source) - if !ok { - return nil, errs.New("Incorrect target type for id %s, expected Step or Source", artifact.GeneratedBy) - } - - // Artifact was not generated by a step, skipping + // Artifact was not generated by a step, skipping because these + // artifacts do not need to be built. return nil, nil } // We iterate through the inputs of the step that generated the - // artifact and recursively add all of the dependencies of those - // artifacts, skipping over the builder artifacts as those resolve - // directly to sources. This is because they are not built and therefore - // not generated by a step. + // artifact, specifically the inputs that are tagged as dependencies and + // build a build-time closure for each. for _, input := range step.Inputs { - if input.Tag == model.TagBuilder { + if input.Tag != model.TagDependency { continue } - for _, id := range input.NodeIDs { - _, err := generateBuildtimeDependencies(id, includeBuilders, lookup, result) + + for _, inputID := range input.NodeIDs { + deps[inputID] = struct{}{} + _, err := buildBuildClosureDependencies(inputID, lookup, deps, result) if err != nil { - return nil, errs.Wrap(err, "Could not build map for step dependencies of artifact %s", artifact.NodeID) + return nil, errs.Wrap(err, "Could not build map for step dependencies of artifact %s", currentArtifact.NodeID) } } } - return result, nil + var uniqueDeps []strfmt.UUID + for id := range deps { + uniqueDeps = append(uniqueDeps, id) + } + + info, err := getSourceInfo(currentArtifact.GeneratedBy, lookup) + if err != nil { + return nil, errs.Wrap(err, "Could not resolve source information") + } + + result[strfmt.UUID(currentArtifact.NodeID)] = artifact.Artifact{ + ArtifactID: strfmt.UUID(currentArtifact.NodeID), + Name: info.Name, + Namespace: info.Namespace, + Version: &info.Version, + RequestedByOrder: true, + GeneratedBy: currentArtifact.GeneratedBy, + Dependencies: uniqueDeps, + URL: currentArtifact.URL, + } + + return deps, nil } diff --git a/pkg/platform/runtime/buildplan/changeset.go b/pkg/platform/runtime/buildplan/changeset.go index 9a5614e856..48e8619554 100644 --- a/pkg/platform/runtime/buildplan/changeset.go +++ b/pkg/platform/runtime/buildplan/changeset.go @@ -6,13 +6,13 @@ import ( "github.com/ActiveState/cli/pkg/platform/runtime/artifact" ) -func NewArtifactChangesetByBuildPlan(oldBuildPlan *model.Build, build *model.Build, requestedOnly bool) (artifact.ArtifactChangeset, error) { - old, err := NewNamedMapFromBuildPlan(oldBuildPlan) +func NewArtifactChangesetByBuildPlan(oldBuildPlan *model.Build, build *model.Build, requestedOnly, buildtimeClosure bool) (artifact.ArtifactChangeset, error) { + old, err := NewNamedMapFromBuildPlan(oldBuildPlan, buildtimeClosure) if err != nil { return artifact.ArtifactChangeset{}, errs.Wrap(err, "failed to build map from old build plan") } - new, err := NewNamedMapFromBuildPlan(build) + new, err := NewNamedMapFromBuildPlan(build, buildtimeClosure) if err != nil { return artifact.ArtifactChangeset{}, errs.Wrap(err, "failed to build map from new build plan") } @@ -22,8 +22,8 @@ func NewArtifactChangesetByBuildPlan(oldBuildPlan *model.Build, build *model.Bui return cs, nil } -func NewBaseArtifactChangesetByBuildPlan(build *model.Build, requestedOnly bool) (artifact.ArtifactChangeset, error) { - new, err := NewNamedMapFromBuildPlan(build) +func NewBaseArtifactChangesetByBuildPlan(build *model.Build, requestedOnly, buildtimeClosure bool) (artifact.ArtifactChangeset, error) { + new, err := NewNamedMapFromBuildPlan(build, buildtimeClosure) if err != nil { return artifact.ArtifactChangeset{}, errs.Wrap(err, "failed to build map from new build plan") } diff --git a/pkg/platform/runtime/buildscript/buildexpression.go b/pkg/platform/runtime/buildscript/buildexpression.go deleted file mode 100644 index 4a4864b293..0000000000 --- a/pkg/platform/runtime/buildscript/buildexpression.go +++ /dev/null @@ -1,228 +0,0 @@ -package buildscript - -import ( - "encoding/json" - "sort" - "strings" - - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/multilog" - "github.com/ActiveState/cli/internal/rtutils/ptr" - "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" -) - -const SolveFunction = "solve" -const SolveLegacyFunction = "solve_legacy" -const MergeFunction = "merge" - -func NewScriptFromBuildExpression(expr *buildexpression.BuildExpression) (*Script, error) { - data, err := json.Marshal(expr) - if err != nil { - return nil, errs.Wrap(err, "Unable to marshal buildexpression to JSON") - } - - m := make(map[string]interface{}) - err = json.Unmarshal(data, &m) - if err != nil { // this really should not happen - return nil, errs.Wrap(err, "Could not unmarshal buildexpression") - } - - letValue, ok := m["let"] - if !ok { - return nil, errs.New("Build expression has no 'let' key") - } - letMap, ok := letValue.(map[string]interface{}) - if !ok { - return nil, errs.New("'let' key is not a JSON object") - } - inValue, ok := letMap["in"] - if !ok { - return nil, errs.New("Build expression's 'let' object has no 'in' key") - } - delete(letMap, "in") // prevent duplication of "in" field when writing the build script - - let, err := newLet(letMap) - if err != nil { - return nil, errs.Wrap(err, "Could not parse 'let' key") - } - - in, err := newIn(inValue) - if err != nil { - return nil, errs.Wrap(err, "Could not parse 'in' key's value: %v", inValue) - } - - return &Script{let, in}, nil -} - -func newLet(m map[string]interface{}) (*Let, error) { - assignments, err := newAssignments(m) - if err != nil { - return nil, errs.Wrap(err, "Could not parse 'let' key") - } - return &Let{Assignments: *assignments}, nil -} - -func isFunction(name string) bool { - return name == SolveFunction || name == SolveLegacyFunction || name == MergeFunction -} - -func newValue(valueInterface interface{}, preferIdent bool) (*Value, error) { - value := &Value{} - - switch v := valueInterface.(type) { - case map[string]interface{}: - // Examine keys first to see if this is a function call. - for key := range v { - if isFunction(key) { - f, err := newFuncCall(v) - if err != nil { - return nil, errs.Wrap(err, "Could not parse '%s' function's value: %v", key, v) - } - value.FuncCall = f - } - } - - if value.FuncCall == nil { - // It's not a function call, but an object. - object, err := newAssignments(v) - if err != nil { - return nil, errs.Wrap(err, "Could not parse object: %v", v) - } - value.Object = object - } - - case []interface{}: - values := []*Value{} - for _, item := range v { - value, err := newValue(item, false) - if err != nil { - return nil, errs.Wrap(err, "Could not parse list: %v", v) - } - values = append(values, value) - } - value.List = &values - - case string: - if preferIdent { - value.Ident = &v - } else { - b, err := json.Marshal(v) - if err != nil { - return nil, errs.Wrap(err, "Could not marshal string '%s'", v) - } - value.Str = ptr.To(string(b)) - } - - case float64: - value.Number = ptr.To(v) - - default: - // An empty value is interpreted as JSON null. - value.Null = &Null{} - } - - return value, nil -} - -func newFuncCall(m map[string]interface{}) (*FuncCall, error) { - // Look in the given object for the function's name and argument object or list. - var name string - var argsInterface interface{} - for key, value := range m { - if isFunction(key) { - name = key - argsInterface = value - break - } - } - - args := []*Value{} - - switch v := argsInterface.(type) { - case map[string]interface{}: - for key, valueInterface := range v { - value, err := newValue(valueInterface, name == MergeFunction) - if err != nil { - return nil, errs.Wrap(err, "Could not parse '%s' function's argument '%s': %v", name, key, valueInterface) - } - args = append(args, &Value{Assignment: &Assignment{Key: key, Value: value}}) - } - sort.SliceStable(args, func(i, j int) bool { return args[i].Assignment.Key < args[j].Assignment.Key }) - - case []interface{}: - for _, item := range v { - value, err := newValue(item, false) - if err != nil { - return nil, errs.Wrap(err, "Could not parse '%s' function's argument list item: %v", name, item) - } - args = append(args, value) - } - - default: - return nil, errs.New("Function '%s' expected to be object or list", name) - } - - return &FuncCall{Name: name, Arguments: args}, nil -} - -func newAssignments(m map[string]interface{}) (*[]*Assignment, error) { - assignments := []*Assignment{} - for key, valueInterface := range m { - value, err := newValue(valueInterface, false) - if err != nil { - return nil, errs.Wrap(err, "Could not parse '%s' key's value: %v", key, valueInterface) - } - assignments = append(assignments, &Assignment{Key: key, Value: value}) - } - sort.SliceStable(assignments, func(i, j int) bool { return assignments[i].Key < assignments[j].Key }) - return &assignments, nil -} - -func newIn(inValue interface{}) (*In, error) { - in := &In{} - - switch v := inValue.(type) { - case map[string]interface{}: - f, err := newFuncCall(v) - if err != nil { - return nil, errs.Wrap(err, "'in' object is not a function call") - } - in.FuncCall = f - - case string: - in.Name = ptr.To(strings.TrimPrefix(v, "$")) - - default: - return nil, errs.New("'in' value expected to be a function call or string") - } - - return in, nil -} - -func (s *Script) EqualsBuildExpressionBytes(exprBytes []byte) bool { - expr, err := buildexpression.New(exprBytes) - if err != nil { - multilog.Error("Unable to create buildexpression from incoming JSON: %v", err) - return false - } - return s.EqualsBuildExpression(expr) -} - -func (s *Script) EqualsBuildExpression(expr *buildexpression.BuildExpression) bool { - myJson, err := json.Marshal(s) - if err != nil { - multilog.Error("Unable to marshal this buildscript to JSON: %v", err) - return false - } - otherScript, err := NewScriptFromBuildExpression(expr) - if err != nil { - multilog.Error("Unable to transform buildexpression to buildscript: %v", err) - return false - } - otherJson, err := json.Marshal(otherScript) - if err != nil { - multilog.Error("Unable to marshal other buildscript to JSON: %v", err) - return false - } - return string(myJson) == string(otherJson) -} diff --git a/pkg/platform/runtime/buildscript/buildscript.go b/pkg/platform/runtime/buildscript/buildscript.go index e26355f112..de4b765fcd 100644 --- a/pkg/platform/runtime/buildscript/buildscript.go +++ b/pkg/platform/runtime/buildscript/buildscript.go @@ -2,18 +2,27 @@ package buildscript import ( "bytes" + "encoding/json" "fmt" "strconv" "strings" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/multilog" + "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" "github.com/alecthomas/participle/v2" ) +// Script's tagged fields will be initially filled in by Participle. +// Expr will be constructed later and is this script's buildexpression. We keep a copy of the build +// expression here with any changes that have been applied before either writing it to disk or +// submitting it to the build planner. It's easier to operate on build expressions directly than to +// modify or manually populate the Participle-produced fields and re-generate a build expression. type Script struct { - Let *Let `parser:"'let' ':' @@"` - In *In `parser:"'in' ':' @@"` + Let *Let `parser:"'let' ':' @@"` + In *In `parser:"'in' ':' @@"` + Expr *buildexpression.BuildExpression } type Let struct { @@ -62,9 +71,24 @@ func NewScript(data []byte) (*Script, error) { return nil, errs.Wrap(err, "Could not parse build script") } + // Construct the equivalent buildexpression. + bytes, err := json.Marshal(script) + if err != nil { + return nil, errs.Wrap(err, "Could not marshal build script to build expression") + } + expr, err := buildexpression.New(bytes) + if err != nil { + return nil, errs.Wrap(err, "Could not construct build expression") + } + script.Expr = expr + return script, nil } +func NewScriptFromBuildExpression(expr *buildexpression.BuildExpression) (*Script, error) { + return &Script{Expr: expr}, nil +} + func indent(s string) string { return fmt.Sprintf("\t%s", strings.ReplaceAll(s, "\n", "\n\t")) } @@ -72,34 +96,35 @@ func indent(s string) string { func (s *Script) String() string { buf := strings.Builder{} buf.WriteString("let:\n") - for _, assignment := range s.Let.Assignments { - buf.WriteString(indent(assignment.String())) + for _, assignment := range s.Expr.Let.Assignments { + buf.WriteString(indent(assignmentString(assignment))) + buf.WriteString("\n") } - buf.WriteString("\n\n") + buf.WriteString("\n") buf.WriteString("in:\n") switch { - case s.In.FuncCall != nil: - buf.WriteString(indent(s.In.FuncCall.String())) - case s.In.Name != nil: - buf.WriteString(indent(*s.In.Name)) + case s.Expr.Let.In.FuncCall != nil: + buf.WriteString(indent(apString(s.Expr.Let.In.FuncCall))) + case s.Expr.Let.In.Name != nil: + buf.WriteString(indent(*s.Expr.Let.In.Name)) } return buf.String() } -func (a *Assignment) String() string { - return fmt.Sprintf("%s = %s", a.Key, a.Value.String()) +func assignmentString(a *buildexpression.Var) string { + return fmt.Sprintf("%s = %s", a.Name, valueString(a.Value)) } -func (v *Value) String() string { +func valueString(v *buildexpression.Value) string { switch { - case v.FuncCall != nil: - return v.FuncCall.String() + case v.Ap != nil: + return apString(v.Ap) case v.List != nil: buf := bytes.Buffer{} buf.WriteString("[\n") for i, item := range *v.List { - buf.WriteString(indent(item.String())) + buf.WriteString(indent(valueString(item))) if i+1 < len(*v.List) { buf.WriteString(",") } @@ -109,22 +134,22 @@ func (v *Value) String() string { return buf.String() case v.Str != nil: - return *v.Str + return strconv.Quote(*v.Str) - case v.Number != nil: - return strconv.FormatFloat(*v.Number, 'G', -1, 64) // 64-bit float with minimum digits on display + case v.Float != nil: + return strconv.FormatFloat(*v.Float, 'G', -1, 64) // 64-bit float with minimum digits on display case v.Null != nil: return "null" case v.Assignment != nil: - return v.Assignment.String() + return assignmentString(v.Assignment) case v.Object != nil: buf := bytes.Buffer{} buf.WriteString("{\n") for i, pair := range *v.Object { - buf.WriteString(indent(pair.String())) + buf.WriteString(indent(assignmentString(pair))) if i+1 < len(*v.Object) { buf.WriteString(",") } @@ -140,11 +165,11 @@ func (v *Value) String() string { return fmt.Sprintf("[\n]") // participle does not create v.List if it's empty } -func (f *FuncCall) String() string { +func apString(f *buildexpression.Ap) string { buf := bytes.Buffer{} buf.WriteString(fmt.Sprintf("%s(\n", f.Name)) for i, argument := range f.Arguments { - buf.WriteString(indent(argument.String())) + buf.WriteString(indent(valueString(argument))) if i+1 < len(f.Arguments) { buf.WriteString(",") } @@ -153,3 +178,26 @@ func (f *FuncCall) String() string { buf.WriteString(")") return buf.String() } + +func (s *Script) EqualsBuildExpressionBytes(exprBytes []byte) bool { + expr, err := buildexpression.New(exprBytes) + if err != nil { + multilog.Error("Unable to create buildexpression from incoming JSON: %v", err) + return false + } + return s.EqualsBuildExpression(expr) +} + +func (s *Script) EqualsBuildExpression(expr *buildexpression.BuildExpression) bool { + myJson, err := json.Marshal(s.Expr) + if err != nil { + multilog.Error("Unable to marshal this buildscript to JSON: %v", err) + return false + } + otherJson, err := json.Marshal(expr) + if err != nil { + multilog.Error("Unable to marshal other buildscript to JSON: %v", err) + return false + } + return string(myJson) == string(otherJson) +} diff --git a/pkg/platform/runtime/buildscript/buildscript_test.go b/pkg/platform/runtime/buildscript/buildscript_test.go index 26920ef376..b705392934 100644 --- a/pkg/platform/runtime/buildscript/buildscript_test.go +++ b/pkg/platform/runtime/buildscript/buildscript_test.go @@ -12,6 +12,18 @@ import ( "github.com/stretchr/testify/require" ) +// toBuildExpression converts given script constructed by Participle into a buildexpression. +// This function should not be used to convert an arbitrary script to buildexpression. +// NewScript*() populates the expr field with the equivalent build expression. +// This function exists solely for testing that functionality. +func toBuildExpression(script *Script) (*buildexpression.BuildExpression, error) { + bytes, err := json.Marshal(script) + if err != nil { + return nil, err + } + return buildexpression.New(bytes) +} + func TestBasic(t *testing.T) { script, err := NewScript([]byte( `let: @@ -39,6 +51,9 @@ in: `)) require.NoError(t, err) + expr, err := toBuildExpression(script) + require.NoError(t, err) + assert.Equal(t, &Script{ &Let{ []*Assignment{ @@ -73,6 +88,7 @@ in: }, }, &In{Name: ptr.To("runtime")}, + expr, }, script) } @@ -97,13 +113,16 @@ func TestComplex(t *testing.T) { ) in: - merge( + merge( win_installer(win_runtime), tar_installer(linux_runtime) ) `)) require.NoError(t, err) + expr, err := toBuildExpression(script) + require.NoError(t, err) + assert.Equal(t, &Script{ &Let{ []*Assignment{ @@ -145,6 +164,7 @@ in: {FuncCall: &FuncCall{"win_installer", []*Value{{Ident: ptr.To("win_runtime")}}}}, {FuncCall: &FuncCall{"tar_installer", []*Value{{Ident: ptr.To("linux_runtime")}}}}, }}}, + expr, }, script) } @@ -177,6 +197,9 @@ func TestExample(t *testing.T) { script, err := NewScript([]byte(example)) require.NoError(t, err) + expr, err := toBuildExpression(script) + require.NoError(t, err) + assert.Equal(t, &Script{ &Let{ []*Assignment{ @@ -217,6 +240,7 @@ func TestExample(t *testing.T) { }, }, &In{Name: ptr.To("runtime")}, + expr, }, script) } @@ -224,8 +248,8 @@ func TestString(t *testing.T) { script, err := NewScript([]byte( `let: runtime = solve( - requirements=[{name="language/python"}], - platforms=["12345", "67890"] + platforms=["12345", "67890"], + requirements=[{name="language/python"}] ) in: runtime @@ -235,14 +259,14 @@ in: assert.Equal(t, `let: runtime = solve( + platforms = [ + "12345", + "67890" + ], requirements = [ { name = "language/python" } - ], - platforms = [ - "12345", - "67890" ] ) @@ -362,15 +386,12 @@ func TestBuildExpression(t *testing.T) { script, err := NewScriptFromBuildExpression(expr) require.NoError(t, err) require.NotNil(t, script) - //newExpr, err := script.ToBuildExpression() - //require.NoError(t, err) + newExpr := script.Expr exprBytes, err := json.Marshal(expr) require.NoError(t, err) - //newExprBytes, err := json.Marshal(newExpr) - //require.NoError(t, err) - // TODO: re-enable this test in DX-1939. Buildexpression equality is implicitly tested - // elsewhere, so temporarily disabling this explicit test is okay. - //assert.Equal(t, string(exprBytes), string(newExprBytes)) + newExprBytes, err := json.Marshal(newExpr) + require.NoError(t, err) + assert.Equal(t, string(exprBytes), string(newExprBytes)) // Verify comparisons between buildscripts and buildexpressions is accurate. assert.True(t, script.EqualsBuildExpression(expr)) @@ -379,12 +400,12 @@ func TestBuildExpression(t *testing.T) { // Verify null JSON value is handled correctly. var null *string nullHandled := false - for _, assignment := range script.Let.Assignments { - if assignment.Key == "runtime" { - args := assignment.Value.FuncCall.Arguments + for _, assignment := range script.Expr.Let.Assignments { + if assignment.Name == "runtime" { + args := assignment.Value.Ap.Arguments require.NotNil(t, args) for _, arg := range args { - if arg.Assignment != nil && arg.Assignment.Key == "solver_version" { + if arg.Assignment != nil && arg.Assignment.Name == "solver_version" { assert.Equal(t, null, arg.Assignment.Value.Str) nullHandled = true } @@ -393,48 +414,3 @@ func TestBuildExpression(t *testing.T) { } assert.True(t, nullHandled, "JSON null not encountered") } - -func TestJsonListEquality(t *testing.T) { - // When comparing buildscripts to buildexpressions, the former is converted to the latter - // via JSON marshaling. Since buildexpression list order does not matter (in addition to - // key-value order not mattering), test for list equality. - // This should not be necessary after DX-1939. - - // Test that ["foo", "bar"] == ["bar", "foo"]. - v1 := &Value{List: &[]*Value{ - {Str: ptr.To(`"foo"`)}, - {Str: ptr.To(`"bar"`)}, - }} - v2 := &Value{List: &[]*Value{ - {Str: ptr.To(`"bar"`)}, - {Str: ptr.To(`"foo"`)}, - }} - - b1, err := json.Marshal(v1) - require.NoError(t, err) - b2, err := json.Marshal(v2) - require.NoError(t, err) - - assert.Equal(t, string(b2), string(b1)) - - // Test that [{"name": "foo"}, {"name": "bar"}] == [{"name": "bar"}, {"name": "foo"}]. - v1 = &Value{List: &[]*Value{ - {Object: &[]*Assignment{ - {"name", &Value{Str: ptr.To(`"foo"`)}}, - {"name", &Value{Str: ptr.To(`"bar"`)}}, - }}, - }} - v2 = &Value{List: &[]*Value{ - {Object: &[]*Assignment{ - {"name", &Value{Str: ptr.To(`"foo"`)}}, - {"name", &Value{Str: ptr.To(`"bar"`)}}, - }}, - }} - - b1, err = json.Marshal(v1) - require.NoError(t, err) - b2, err = json.Marshal(v2) - require.NoError(t, err) - - assert.Equal(t, string(b2), string(b1)) -} diff --git a/pkg/platform/runtime/buildscript/json.go b/pkg/platform/runtime/buildscript/json.go index 0f22a8a451..c82100113c 100644 --- a/pkg/platform/runtime/buildscript/json.go +++ b/pkg/platform/runtime/buildscript/json.go @@ -4,21 +4,12 @@ import ( "encoding/json" "errors" "fmt" - "sort" "strings" - - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" ) -func (s *Script) ToBuildExpression() (*buildexpression.BuildExpression, error) { - data, err := json.Marshal(s) - if err != nil { - return nil, errs.Wrap(err, "Unable to marshal buildscript into JSON") - } - return buildexpression.New(data) -} - +// MarshalJSON marshals the Participle-produced Script into an equivalent buildexpression. +// Users of buildscripts do not need to do this manually; the Expr field contains the +// equivalent buildexpression. func (s *Script) MarshalJSON() ([]byte, error) { m := make(map[string]interface{}) let := make(map[string]interface{}) @@ -41,17 +32,7 @@ func (v *Value) MarshalJSON() ([]byte, error) { case v.FuncCall != nil: return json.Marshal(v.FuncCall) case v.List != nil: - // Buildexpression list order does not matter, so sorting is necessary for - // comparisons. Go's JSON marshaling is deterministic, so utilize that. - // This should not be necessary when DX-1939 is implemented. - list := make([]*Value, len(*v.List)) - copy(list, *v.List) - sort.SliceStable(list, func(i, j int) bool { - b1, err1 := json.Marshal(list[i]) - b2, err2 := json.Marshal(list[j]) - return err1 == nil && err2 == nil && string(b1) < string(b2) - }) - return json.Marshal(list) + return json.Marshal(v.List) case v.Str != nil: return json.Marshal(strings.Trim(*v.Str, `"`)) case v.Number != nil: @@ -79,6 +60,8 @@ func (f *FuncCall) MarshalJSON() ([]byte, error) { switch { case argument.Assignment != nil: args[argument.Assignment.Key] = argument.Assignment.Value + case argument.FuncCall != nil: + args[argument.FuncCall.Name] = argument.FuncCall.Arguments default: return nil, errors.New(fmt.Sprintf("Cannot marshal %v (arg %v)", f, argument)) } diff --git a/pkg/platform/runtime/executors/executors.go b/pkg/platform/runtime/executors/executors.go index 0cf1dedc56..3c5c88dd80 100644 --- a/pkg/platform/runtime/executors/executors.go +++ b/pkg/platform/runtime/executors/executors.go @@ -1,13 +1,14 @@ package executors import ( - "github.com/ActiveState/cli/pkg/project" "io/ioutil" "os" "path/filepath" rt "runtime" "strings" + "github.com/ActiveState/cli/pkg/project" + "github.com/ActiveState/cli/internal/exeutils" "github.com/ActiveState/cli/internal/installation" "github.com/ActiveState/cli/internal/osutils" @@ -31,7 +32,6 @@ type Targeter interface { Name() string Owner() string Dir() string - Headless() bool } type Executors struct { @@ -74,7 +74,6 @@ func (es *Executors) Apply(sockPath string, targeter Targeter, env map[string]st CommitUUID: targeter.CommitUUID().String(), Namespace: ns.String(), Dir: targeter.Dir(), - Headless: targeter.Headless(), } m := execmeta.New(sockPath, osutils.EnvMapToSlice(env), t, executors) if err := m.WriteToDisk(es.executorPath); err != nil { diff --git a/pkg/platform/runtime/executors/executors_test.go b/pkg/platform/runtime/executors/executors_test.go index c4ca4b0a3a..cf13178cff 100644 --- a/pkg/platform/runtime/executors/executors_test.go +++ b/pkg/platform/runtime/executors/executors_test.go @@ -25,7 +25,7 @@ func TestExecutor(t *testing.T) { err = fileutils.WriteFile(dummyExecSrc, dummyExecData) require.NoError(t, err, errs.JoinMessage(err)) - target := target.NewCustomTarget("owner", "project", "1234abcd-1234-abcd-1234-abcd1234abcd", "dummy/path", target.NewExecTrigger("test"), false) + target := target.NewCustomTarget("owner", "project", "1234abcd-1234-abcd-1234-abcd1234abcd", "dummy/path", target.NewExecTrigger("test")) execDir := filepath.Join(tmpDir, "exec") execInit := New(execDir) execInit.altExecSrcPath = dummyExecSrc diff --git a/pkg/platform/runtime/runtime.go b/pkg/platform/runtime/runtime.go index 7e36db21e4..e3441f5b72 100644 --- a/pkg/platform/runtime/runtime.go +++ b/pkg/platform/runtime/runtime.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "strconv" "strings" "golang.org/x/net/context" @@ -90,7 +89,6 @@ func New(target setup.Targeter, an analytics.Dispatcher, svcm *model.SvcModel, a recordAttempt(an, target) an.Event(anaConsts.CatRuntimeDebug, anaConsts.ActRuntimeStart, &dimensions.Values{ Trigger: ptr.To(target.Trigger().String()), - Headless: ptr.To(strconv.FormatBool(target.Headless())), CommitID: ptr.To(target.CommitUUID().String()), ProjectNameSpace: ptr.To(project.NewNamespace(target.Owner(), target.Name(), target.CommitUUID().String()).String()), InstanceID: ptr.To(instanceid.ID()), @@ -118,6 +116,8 @@ func (r *Runtime) validateCache() error { return nil } + return nil // remove in DX-2307 to re-enable buildscripts below + // Check if local build script has changes that should be committed. script, err := buildscript.NewScriptFromProject(r.target, r.auth) if err != nil { @@ -321,7 +321,6 @@ func usageDims(target setup.Targeter) *dimensions.Values { return &dimensions.Values{ Trigger: ptr.To(target.Trigger().String()), CommitID: ptr.To(target.CommitUUID().String()), - Headless: ptr.To(strconv.FormatBool(target.Headless())), ProjectNameSpace: ptr.To(project.NewNamespace(target.Owner(), target.Name(), target.CommitUUID().String()).String()), InstanceID: ptr.To(instanceid.ID()), } diff --git a/pkg/platform/runtime/setup/implementations/alternative/resolver.go b/pkg/platform/runtime/setup/implementations/alternative/resolver.go new file mode 100644 index 0000000000..28abf13492 --- /dev/null +++ b/pkg/platform/runtime/setup/implementations/alternative/resolver.go @@ -0,0 +1,21 @@ +package alternative + +import ( + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/pkg/platform/runtime/artifact" +) + +type Resolver struct { + artifactsForNameResolving artifact.Map +} + +func NewResolver(artifactsForNameResolving artifact.Map) *Resolver { + return &Resolver{artifactsForNameResolving: artifactsForNameResolving} +} + +func (r *Resolver) ResolveArtifactName(id artifact.ArtifactID) string { + if artf, ok := r.artifactsForNameResolving[id]; ok { + return artf.Name + } + return locale.T("alternative_unknown_pkg_name") +} diff --git a/pkg/platform/runtime/setup/implementations/alternative/runtime.go b/pkg/platform/runtime/setup/implementations/alternative/runtime.go index 452c0fe80a..f6a0930c9e 100644 --- a/pkg/platform/runtime/setup/implementations/alternative/runtime.go +++ b/pkg/platform/runtime/setup/implementations/alternative/runtime.go @@ -18,12 +18,11 @@ import ( ) type Setup struct { - artifactsForNameResolving artifact.Map - store *store.Store + store *store.Store } -func NewSetup(store *store.Store, artifactsForNameResolving artifact.Map) *Setup { - return &Setup{store: store, artifactsForNameResolving: artifactsForNameResolving} +func NewSetup(store *store.Store) *Setup { + return &Setup{store: store} } func (s *Setup) DeleteOutdatedArtifacts(changeset artifact.ArtifactChangeset, storedArtifacted, alreadyInstalled store.StoredArtifactMap) error { @@ -140,10 +139,7 @@ func artifactsContainFile(file string, artifactCache map[artifact.ArtifactID]sto } func (s *Setup) ResolveArtifactName(a artifact.ArtifactID) string { - if artf, ok := s.artifactsForNameResolving[a]; ok { - return artf.Name - } - return locale.Tl("alternative_unknown_pkg_name", "unknown") + return locale.T("alternative_unknown_pkg_name") } func (s *Setup) DownloadsFromBuild(build model.Build, artifacts map[strfmt.UUID]artifact.Artifact) (download []artifact.ArtifactDownload, err error) { diff --git a/pkg/platform/runtime/setup/implementations/camel/resolver.go b/pkg/platform/runtime/setup/implementations/camel/resolver.go new file mode 100644 index 0000000000..1075f1db3a --- /dev/null +++ b/pkg/platform/runtime/setup/implementations/camel/resolver.go @@ -0,0 +1,16 @@ +package camel + +import ( + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/pkg/platform/runtime/artifact" +) + +type Resolver struct{} + +func NewResolver() *Resolver { + return &Resolver{} +} + +func (r *Resolver) ResolveArtifactName(_ artifact.ArtifactID) string { + return locale.Tl("camel_bundle_name", "legacy bundle") +} diff --git a/pkg/platform/runtime/setup/setup.go b/pkg/platform/runtime/setup/setup.go index 5dd8a87023..5a473c2a3d 100644 --- a/pkg/platform/runtime/setup/setup.go +++ b/pkg/platform/runtime/setup/setup.go @@ -36,7 +36,6 @@ import ( "github.com/ActiveState/cli/pkg/platform/runtime/artifact" "github.com/ActiveState/cli/pkg/platform/runtime/artifactcache" "github.com/ActiveState/cli/pkg/platform/runtime/buildplan" - "github.com/ActiveState/cli/pkg/platform/runtime/buildscript" "github.com/ActiveState/cli/pkg/platform/runtime/envdef" "github.com/ActiveState/cli/pkg/platform/runtime/executors" "github.com/ActiveState/cli/pkg/platform/runtime/setup/buildlog" @@ -115,7 +114,6 @@ type Targeter interface { Name() string Owner() string Dir() string - Headless() bool Trigger() target.Trigger ProjectDir() string @@ -137,7 +135,6 @@ type Setup struct { type Setuper interface { // DeleteOutdatedArtifacts deletes outdated artifact as best as it can DeleteOutdatedArtifacts(artifact.ArtifactChangeset, store.StoredArtifactMap, store.StoredArtifactMap) error - ResolveArtifactName(artifact.ArtifactID) string DownloadsFromBuild(build bpModel.Build, artifacts map[strfmt.UUID]artifact.Artifact) (download []artifact.ArtifactDownload, err error) } @@ -148,6 +145,10 @@ type ArtifactSetuper interface { Unarchiver() unarchiver.Unarchiver } +type ArtifactResolver interface { + ResolveArtifactName(artifact.ArtifactID) string +} + type artifactInstaller func(artifact.ArtifactID, string, ArtifactSetuper) error type artifactUninstaller func() error @@ -162,6 +163,25 @@ func New(target Targeter, eventHandler events.Handler, auth *authentication.Auth // Update installs the runtime locally (or updates it if it's already partially installed) func (s *Setup) Update() (rerr error) { + defer func() { + // Panics are serious, and reproducing them in the runtime package is HARD. To help with this we dump + // the build plan when a panic occurs so we have something more to go on. + if r := recover(); r != nil { + buildplan, err := s.store.BuildPlanRaw() + if err != nil { + logging.Error("Could not get raw buildplan: %s", err) + } + env, err := s.store.EnvDef() + if err != nil { + logging.Error("Could not get envdef: %s", err) + } + // We do a standard error log first here, as rollbar reports will pick up the most recent log lines. + // We can't put the buildplan in the multilog message as it'd be way too big a message for rollbar. + logging.Error("Panic during runtime update: %s, build plan:\n%s\n\nEnvDef:\n%#v", r, buildplan, env) + multilog.Critical("Panic during runtime update: %s", r) + panic(r) // We're just logging the panic while we have context, we're not meant to handle it here + } + }() defer func() { var ev events.Eventer = events.Success{} if rerr != nil { @@ -380,42 +400,55 @@ func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(installFunc artifactInstal return nil, nil, errs.Wrap(err, "Could not handle SolveSuccess event") } + // If the build is not ready or if we are installing the buildtime closure + // then we need to include the buildtime closure in the changed artifacts + // and the progress reporting. + includeBuildtimeClosure := strings.EqualFold(os.Getenv(constants.InstallBuildDependencies), "true") || !buildResult.BuildReady + // Compute and handle the change summary - // runtimeAndBuildtimeArtifacts records all artifacts that will need to be built in order to obtain the runtime. - // Disabled due to DX-2033. - // Please use this var when we come back to this in the future as we need to make a clear distinction between this - // and runtime-only artifacts. - // var runtimeAndBuildtimeArtifacts artifact.Map - var runtimeArtifacts artifact.Map // Artifacts required for the runtime to function - if buildResult.Build != nil { - runtimeArtifacts, err = buildplan.NewMapFromBuildPlan(buildResult.Build) + var requestedArtifacts artifact.Map // Artifacts required for the runtime to function + artifactListing, err := buildplan.NewArtifactListing(buildResult.Build, includeBuildtimeClosure) + if err != nil { + return nil, nil, errs.Wrap(err, "Failed to create artifact listing") + } + + // If we are installing build dependencies, then the requested artifacts + // will include the buildtime closure. Otherwise, we only need the runtime + // closure. + if strings.EqualFold(os.Getenv(constants.InstallBuildDependencies), "true") { + logging.Debug("Installing build dependencies") + requestedArtifacts, err = artifactListing.BuildtimeClosure() + if err != nil { + return nil, nil, errs.Wrap(err, "Failed to compute buildtime closure") + } + } else { + requestedArtifacts, err = artifactListing.RuntimeClosure() if err != nil { return nil, nil, errs.Wrap(err, "Failed to create artifact map from build plan") } } - setup, err := s.selectSetupImplementation(buildResult.BuildEngine, runtimeArtifacts) + resolver, err := selectArtifactResolver(buildResult, artifactListing) + if err != nil { + return nil, nil, errs.Wrap(err, "Failed to select artifact resolver") + } + + setup, err := s.selectSetupImplementation(buildResult.BuildEngine) if err != nil { return nil, nil, errs.Wrap(err, "Failed to select setup implementation") } - downloadablePrebuiltResults, err := setup.DownloadsFromBuild(*buildResult.Build, runtimeArtifacts) + downloadablePrebuiltResults, err := setup.DownloadsFromBuild(*buildResult.Build, requestedArtifacts) if err != nil { if errors.Is(err, artifact.CamelRuntimeBuilding) { - localeID := "build_status_in_progress" - messageURL := apimodel.ProjectURL(s.target.Owner(), s.target.Name(), s.target.CommitUUID().String()) - if s.target.Owner() == "" && s.target.Name() == "" { - localeID = "build_status_in_progress_headless" - messageURL = apimodel.CommitURL(s.target.CommitUUID().String()) - } - return nil, nil, locale.WrapInputError(err, localeID, "", messageURL) + return nil, nil, locale.WrapInputError(err, "build_status_in_progress", "", apimodel.ProjectURL(s.target.Owner(), s.target.Name(), s.target.CommitUUID().String())) } return nil, nil, errs.Wrap(err, "could not extract artifacts that are ready to download.") } // buildResult doesn't have namespace info and will happily report internal only artifacts downloadablePrebuiltResults = funk.Filter(downloadablePrebuiltResults, func(ad artifact.ArtifactDownload) bool { - ar, ok := runtimeArtifacts[ad.ArtifactID] + ar, ok := requestedArtifacts[ad.ArtifactID] if !ok { return true } @@ -432,7 +465,7 @@ func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(installFunc artifactInstal s.analytics.Event(anaConsts.CatRuntimeDebug, anaConsts.ActRuntimeBuild, dimensions) } - changedArtifacts, err := buildplan.NewBaseArtifactChangesetByBuildPlan(buildResult.Build, false) + changedArtifacts, err := buildplan.NewBaseArtifactChangesetByBuildPlan(buildResult.Build, false, includeBuildtimeClosure) if err != nil { return nil, nil, errs.Wrap(err, "Could not compute base artifact changeset") } @@ -443,7 +476,7 @@ func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(installFunc artifactInstal } if oldBuildPlan != nil { - changedArtifacts, err = buildplan.NewArtifactChangesetByBuildPlan(oldBuildPlan, buildResult.Build, false) + changedArtifacts, err = buildplan.NewArtifactChangesetByBuildPlan(oldBuildPlan, buildResult.Build, false, includeBuildtimeClosure) if err != nil { return nil, nil, errs.Wrap(err, "Could not compute artifact changeset") } @@ -457,12 +490,12 @@ func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(installFunc artifactInstal alreadyInstalled := reusableArtifacts(buildResult.Build.Artifacts, storedArtifacts) // Report resolved artifacts - artifactIDs := []artifact.ArtifactID{} - for _, a := range runtimeArtifacts { - artifactIDs = append(artifactIDs, a.ArtifactID) + artifactIDs, err := artifactListing.ArtifactIDs(includeBuildtimeClosure) + if err != nil { + return nil, nil, errs.Wrap(err, "Could not get artifact IDs from build plan") } - artifactNames := artifact.ResolveArtifactNames(setup.ResolveArtifactName, artifactIDs) + artifactNames := artifact.ResolveArtifactNames(resolver.ResolveArtifactName, artifactIDs) artifactNamesList := []string{} for _, n := range artifactNames { artifactNamesList = append(artifactNamesList, n) @@ -481,23 +514,19 @@ func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(installFunc artifactInstal ) artifactsToInstall := []artifact.ArtifactID{} - // buildtimeArtifacts := runtimeArtifacts + var artifactsToBuild artifact.Map if buildResult.BuildReady { - // If the build is already done we can just look at the downloadable artifacts as they will be a fully accurate - // prediction of what we will be installing. for _, a := range downloadablePrebuiltResults { if _, alreadyInstalled := alreadyInstalled[a.ArtifactID]; !alreadyInstalled { artifactsToInstall = append(artifactsToInstall, a.ArtifactID) } } + artifactsToBuild, err = artifactListing.BuildtimeClosure() } else { - // If the build is not yet complete then we have to speculate as to the artifacts that will be installed. - // The actual number of installable artifacts may be lower than what we have here, we can only do a best effort. - for _, a := range runtimeArtifacts { - if _, alreadyInstalled := alreadyInstalled[a.ArtifactID]; !alreadyInstalled { - artifactsToInstall = append(artifactsToInstall, a.ArtifactID) - } - } + artifactsToBuild, err = artifactListing.RuntimeClosure() + } + if err != nil { + return nil, nil, errs.Wrap(err, "Failed to compute artifacts to build") } // The log file we want to use for builds @@ -514,7 +543,7 @@ func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(installFunc artifactInstal ArtifactNames: artifactNames, LogFilePath: logFilePath, ArtifactsToBuild: func() []artifact.ArtifactID { - return artifact.ArtifactIDsFromBuildPlanMap(runtimeArtifacts) // This does not account for cached builds + return artifact.ArtifactIDsFromBuildPlanMap(artifactsToBuild) // This does not account for cached builds }(), // Yes these have the same value; this is intentional. // Separating these out just allows us to be more explicit and intentional in our event handling logic. @@ -534,7 +563,7 @@ func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(installFunc artifactInstal s.analytics.Event(anaConsts.CatRuntimeDebug, anaConsts.ActRuntimeDownload, dimensions) } - err = s.installArtifactsFromBuild(buildResult, runtimeArtifacts, artifact.ArtifactIDsToMap(artifactsToInstall), downloadablePrebuiltResults, setup, installFunc, logFilePath) + err = s.installArtifactsFromBuild(buildResult, requestedArtifacts, artifact.ArtifactIDsToMap(artifactsToInstall), downloadablePrebuiltResults, setup, resolver, installFunc, logFilePath) if err != nil { return nil, nil, err } @@ -552,9 +581,10 @@ func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(installFunc artifactInstal } if s.target.ProjectDir() != "" { - if err := buildscript.Update(s.target, buildResult.BuildExpression, s.auth); err != nil { - return nil, nil, errs.Wrap(err, "Could not save build script.") - } + // Re-enable in DX-2307 + //if err := buildscript.Update(s.target, buildResult.BuildExpression, s.auth); err != nil { + // return nil, nil, errs.Wrap(err, "Could not save build script.") + //} } return buildResult.OrderedArtifacts(), uninstallArtifacts, nil @@ -579,7 +609,7 @@ func aggregateErrors() (chan<- error, <-chan error) { return bgErrs, aggErr } -func (s *Setup) installArtifactsFromBuild(buildResult *model.BuildResult, artifacts artifact.Map, artifactsToInstall map[artifact.ArtifactID]struct{}, downloads []artifact.ArtifactDownload, setup Setuper, installFunc artifactInstaller, logFilePath string) error { +func (s *Setup) installArtifactsFromBuild(buildResult *model.BuildResult, artifacts artifact.Map, artifactsToInstall map[artifact.ArtifactID]struct{}, downloads []artifact.ArtifactDownload, setup Setuper, resolver ArtifactResolver, installFunc artifactInstaller, logFilePath string) error { // Artifacts are installed in two stages // - The first stage runs concurrently in MaxConcurrency worker threads (download, unpacking, relocation) // - The second stage moves all files into its final destination is running in a single thread (using the mainthread library) to avoid file conflicts @@ -589,16 +619,16 @@ func (s *Setup) installArtifactsFromBuild(buildResult *model.BuildResult, artifa if err := s.handleEvent(events.BuildSkipped{}); err != nil { return errs.Wrap(err, "Could not handle BuildSkipped event") } - err = s.installFromBuildResult(buildResult, artifacts, artifactsToInstall, downloads, setup, installFunc) + err = s.installFromBuildResult(buildResult, artifacts, artifactsToInstall, downloads, setup, resolver, installFunc) } else { - err = s.installFromBuildLog(buildResult, artifacts, artifactsToInstall, setup, installFunc, logFilePath) + err = s.installFromBuildLog(buildResult, artifacts, artifactsToInstall, setup, resolver, installFunc, logFilePath) } return err } // setupArtifactSubmitFunction returns a function that sets up an artifact and can be submitted to a workerpool -func (s *Setup) setupArtifactSubmitFunction(a artifact.ArtifactDownload, ar *artifact.Artifact, expectedArtifactInstalls map[artifact.ArtifactID]struct{}, buildResult *model.BuildResult, setup Setuper, installFunc artifactInstaller, errors chan<- error) func() { +func (s *Setup) setupArtifactSubmitFunction(a artifact.ArtifactDownload, ar *artifact.Artifact, expectedArtifactInstalls map[artifact.ArtifactID]struct{}, buildResult *model.BuildResult, setup Setuper, resolver ArtifactResolver, installFunc artifactInstaller, errors chan<- error) func() { return func() { // If artifact has no valid download, just count it as completed and return if strings.Contains(ar.URL, "as-builds/noop") || @@ -625,21 +655,21 @@ func (s *Setup) setupArtifactSubmitFunction(a artifact.ArtifactDownload, ar *art unarchiver := as.Unarchiver() archivePath, err := s.obtainArtifact(a, unarchiver.Ext()) if err != nil { - name := setup.ResolveArtifactName(a.ArtifactID) + name := resolver.ResolveArtifactName(a.ArtifactID) errors <- locale.WrapError(err, "artifact_download_failed", "", name, a.ArtifactID.String()) return } err = installFunc(a.ArtifactID, archivePath, as) if err != nil { - name := setup.ResolveArtifactName(a.ArtifactID) + name := resolver.ResolveArtifactName(a.ArtifactID) errors <- locale.WrapError(err, "artifact_setup_failed", "", name, a.ArtifactID.String()) return } } } -func (s *Setup) installFromBuildResult(buildResult *model.BuildResult, artifacts artifact.Map, artifactsToInstall map[artifact.ArtifactID]struct{}, downloads []artifact.ArtifactDownload, setup Setuper, installFunc artifactInstaller) error { +func (s *Setup) installFromBuildResult(buildResult *model.BuildResult, artifacts artifact.Map, artifactsToInstall map[artifact.ArtifactID]struct{}, downloads []artifact.ArtifactDownload, setup Setuper, resolver ArtifactResolver, installFunc artifactInstaller) error { logging.Debug("Installing artifacts from build result") errs, aggregatedErr := aggregateErrors() mainthread.Run(func() { @@ -653,7 +683,7 @@ func (s *Setup) installFromBuildResult(buildResult *model.BuildResult, artifacts if arv, ok := artifacts[a.ArtifactID]; ok { ar = &arv } - wp.Submit(s.setupArtifactSubmitFunction(a, ar, map[artifact.ArtifactID]struct{}{}, buildResult, setup, installFunc, errs)) + wp.Submit(s.setupArtifactSubmitFunction(a, ar, map[artifact.ArtifactID]struct{}{}, buildResult, setup, resolver, installFunc, errs)) } wp.StopWait() @@ -662,7 +692,7 @@ func (s *Setup) installFromBuildResult(buildResult *model.BuildResult, artifacts return <-aggregatedErr } -func (s *Setup) installFromBuildLog(buildResult *model.BuildResult, artifacts artifact.Map, artifactsToInstall map[artifact.ArtifactID]struct{}, setup Setuper, installFunc artifactInstaller, logFilePath string) error { +func (s *Setup) installFromBuildLog(buildResult *model.BuildResult, artifacts artifact.Map, artifactsToInstall map[artifact.ArtifactID]struct{}, setup Setuper, resolver ArtifactResolver, installFunc artifactInstaller, logFilePath string) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -703,7 +733,7 @@ func (s *Setup) installFromBuildLog(buildResult *model.BuildResult, artifacts ar logging.Debug("Unmonitored artifact buildlog event discarded: %s", a.ArtifactID) continue } - wp.Submit(s.setupArtifactSubmitFunction(a, ar, artifactsToInstall, buildResult, setup, installFunc, errs)) + wp.Submit(s.setupArtifactSubmitFunction(a, ar, artifactsToInstall, buildResult, setup, resolver, installFunc, errs)) } }() @@ -852,10 +882,10 @@ func (s *Setup) unpackArtifact(ua unarchiver.Unarchiver, tarballPath string, tar return numUnpackedFiles, ua.Unarchive(proxy, i, targetDir) } -func (s *Setup) selectSetupImplementation(buildEngine model.BuildEngine, artifacts artifact.Map) (Setuper, error) { +func (s *Setup) selectSetupImplementation(buildEngine model.BuildEngine) (Setuper, error) { switch buildEngine { case model.Alternative: - return alternative.NewSetup(s.store, artifacts), nil + return alternative.NewSetup(s.store), nil case model.Camel: return camel.NewSetup(s.store), nil default: @@ -863,6 +893,28 @@ func (s *Setup) selectSetupImplementation(buildEngine model.BuildEngine, artifac } } +func selectArtifactResolver(buildResult *model.BuildResult, artifactListing *buildplan.ArtifactListing) (ArtifactResolver, error) { + var artifacts artifact.Map + var err error + if buildResult.BuildReady || strings.EqualFold(os.Getenv(constants.InstallBuildDependencies), "true") { + artifacts, err = artifactListing.BuildtimeClosure() + } else { + artifacts, err = artifactListing.RuntimeClosure() + } + if err != nil { + return nil, errs.Wrap(err, "Failed to create artifact map from build plan") + } + + switch buildResult.BuildEngine { + case model.Alternative: + return alternative.NewResolver(artifacts), nil + case model.Camel: + return camel.NewResolver(), nil + default: + return nil, errs.New("Unknown build engine: %s", buildResult.BuildEngine) + } +} + func (s *Setup) selectArtifactSetupImplementation(buildEngine model.BuildEngine, a artifact.ArtifactID) (ArtifactSetuper, error) { switch buildEngine { case model.Alternative: diff --git a/pkg/platform/runtime/store/store.go b/pkg/platform/runtime/store/store.go index 5bdfd905b5..448e28970d 100644 --- a/pkg/platform/runtime/store/store.go +++ b/pkg/platform/runtime/store/store.go @@ -239,6 +239,14 @@ func (s *Store) updateEnviron(orderedArtifacts []artifact.ArtifactID, artifacts } } + if rtGlobal == nil { + // Returning nil will end up causing a nil-pointer-exception panic in setup.Update(). + // There is additional logging of the buildplan there that may help diagnose why this is happening. + logging.Error("There were artifacts returned, but none of them ended up being stored/installed.") + logging.Error("Artifacts returned: %v", orderedArtifacts) + logging.Error("Artifacts stored: %v", artifacts) + } + return rtGlobal, nil } @@ -247,12 +255,21 @@ func (s *Store) InstallPath() string { return s.installPath } -func (s *Store) BuildPlan() (*bpModel.Build, error) { +func (s *Store) BuildPlanRaw() ([]byte, error) { data, err := fileutils.ReadFile(s.buildPlanFile()) if err != nil { return nil, errs.Wrap(err, "Could not read build plan file.") } + return data, nil +} + +func (s *Store) BuildPlan() (*bpModel.Build, error) { + data, err := s.BuildPlanRaw() + if err != nil { + return nil, errs.Wrap(err, "Could not get build plan file.") + } + var buildPlan bpModel.Build err = json.Unmarshal(data, &buildPlan) if err != nil { diff --git a/pkg/platform/runtime/target/target.go b/pkg/platform/runtime/target/target.go index 49e19564c5..de5ee4d73b 100644 --- a/pkg/platform/runtime/target/target.go +++ b/pkg/platform/runtime/target/target.go @@ -11,7 +11,7 @@ import ( "github.com/ActiveState/cli/internal/installation/storage" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/multilog" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/project" "github.com/go-openapi/strfmt" ) @@ -88,8 +88,8 @@ func (p *ProjectTarget) CommitUUID() strfmt.UUID { if p.customCommit != nil { return *p.customCommit } - commitID, err := localcommit.Get(p.Project.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err := commitmediator.Get(p.Project) + if err != nil { multilog.Error("Unable to get local commit: %v", errs.JoinMessage(err)) return "" } @@ -103,10 +103,6 @@ func (p *ProjectTarget) Trigger() Trigger { return p.trigger } -func (p *ProjectTarget) Headless() bool { - return p.Project.IsHeadless() -} - func (p *ProjectTarget) ReadOnly() bool { return false } @@ -136,17 +132,16 @@ type CustomTarget struct { commitUUID strfmt.UUID dir string trigger Trigger - headless bool } -func NewCustomTarget(owner string, name string, commitUUID strfmt.UUID, dir string, trigger Trigger, headless bool) *CustomTarget { +func NewCustomTarget(owner string, name string, commitUUID strfmt.UUID, dir string, trigger Trigger) *CustomTarget { cleanDir, err := fileutils.ResolveUniquePath(dir) if err != nil { multilog.Error("Could not resolve unique path for dir: %s, error: %s", dir, err.Error()) } else { dir = cleanDir } - return &CustomTarget{owner, name, commitUUID, dir, trigger, headless} + return &CustomTarget{owner, name, commitUUID, dir, trigger} } func (c *CustomTarget) Owner() string { @@ -172,10 +167,6 @@ func (c *CustomTarget) Trigger() Trigger { return c.trigger } -func (c *CustomTarget) Headless() bool { - return c.headless -} - func (c *CustomTarget) ReadOnly() bool { return c.commitUUID == "" } @@ -238,10 +229,6 @@ func (i *OfflineTarget) Trigger() Trigger { return i.trigger } -func (i *OfflineTarget) Headless() bool { - return false -} - func (i *OfflineTarget) ReadOnly() bool { return false } diff --git a/pkg/project/expander.go b/pkg/project/expander.go index e7fd89955b..b3dcca3f16 100644 --- a/pkg/project/expander.go +++ b/pkg/project/expander.go @@ -12,9 +12,9 @@ import ( "github.com/ActiveState/cli/internal/language" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/osutils" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/rxutils" "github.com/ActiveState/cli/internal/scriptfile" - "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/projectfile" ) @@ -238,7 +238,7 @@ func ProjectExpander(_ string, name string, _ string, isFunction bool, ctx *Expa case "url": return project.URL(), nil case "commit": - commitID, err := localcommit.Get(project.Dir()) + commitID, err := commitmediator.Get(project) if err != nil { return "", errs.Wrap(err, "Unable to get local commit") } diff --git a/pkg/project/namespace.go b/pkg/project/namespace.go index 0b11f818e5..668e82b1f0 100644 --- a/pkg/project/namespace.go +++ b/pkg/project/namespace.go @@ -8,7 +8,7 @@ import ( "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/multilog" - "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/pkg/projectfile" "github.com/go-openapi/strfmt" ) @@ -153,8 +153,8 @@ func NameSpaceForConfig(configFile string) *Namespaced { Project: prj.Name(), } - commitID, err := localcommit.Get(prj.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { + commitID, err := commitmediator.Get(prj) + if err != nil { multilog.Error("Unable to get local commit: %v", errs.JoinMessage(err)) } if commitID != "" { diff --git a/pkg/project/namespace_test.go b/pkg/project/namespace_test.go index 8b1f6e3b54..c622ea7190 100644 --- a/pkg/project/namespace_test.go +++ b/pkg/project/namespace_test.go @@ -41,13 +41,6 @@ func TestParseProjectNoOwner(t *testing.T) { assert.Equal(t, parsed.Project, "project") assert.Empty(t, parsed.CommitID) assert.True(t, parsed.AllowOmitOwner) - - parsed, err = ParseProjectNoOwner("project#a10-b11c12-d13e14-f15") - assert.NoError(t, err, "should be able to parse project part of namspace") - assert.Empty(t, parsed.Owner) - assert.Equal(t, parsed.Project, "project") - assert.Equal(t, *parsed.CommitID, strfmt.UUID("a10-b11c12-d13e14-f15")) - assert.True(t, parsed.AllowOmitOwner) } func TestParseNamespaceOrConfigfile(t *testing.T) { diff --git a/pkg/project/project.go b/pkg/project/project.go index 7fa85cf02a..448ffb6206 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -19,7 +19,6 @@ import ( "github.com/ActiveState/cli/internal/multilog" "github.com/ActiveState/cli/internal/osutils" "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/pkg/localcommit" secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/projectfile" @@ -215,6 +214,17 @@ func (p *Project) ProjectDir() string { return p.Dir() } +// LegacyCommitID is for use by commitmediator.Get() ONLY. +func (p *Project) LegacyCommitID() string { + return p.projectfile.LegacyCommitID() +} + +// LegacySetCommit is for use by commitmediator.Set() ONLY. +// Remove in DX-2307. +func (p *Project) LegacySetCommit(commitID string) error { + return p.projectfile.LegacySetCommit(commitID) +} + func (p *Project) IsHeadless() bool { match := projectfile.CommitURLRe.FindStringSubmatch(p.URL()) return len(match) > 1 @@ -242,11 +252,7 @@ func (p *Project) Cache() string { return p.projectfile.Cache } // Namespace returns project namespace func (p *Project) Namespace() *Namespaced { - commitID, err := localcommit.Get(p.Dir()) - if err != nil && !localcommit.IsFileDoesNotExistError(err) { - multilog.Error("Unable to get local commit: %v", errs.JoinMessage(err)) - } - return &Namespaced{p.projectfile.Owner(), p.projectfile.Name(), &commitID, false} + return &Namespaced{Owner: p.projectfile.Owner(), Project: p.projectfile.Name()} } // NamespaceString is a convenience function to make interfaces simpler diff --git a/pkg/project/project_test.go b/pkg/project/project_test.go index b8c3ae4b63..57bd0a392d 100644 --- a/pkg/project/project_test.go +++ b/pkg/project/project_test.go @@ -11,8 +11,8 @@ import ( "github.com/ActiveState/cli/internal/environment" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/language" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/project" "github.com/ActiveState/cli/pkg/projectfile" @@ -61,9 +61,11 @@ func (suite *ProjectTestSuite) TestGetSafe() { } func (suite *ProjectTestSuite) TestProject() { - suite.Equal("https://platform.activestate.com/ActiveState/project?branch=main", suite.project.URL(), "Values should match") + projectLine := "https://platform.activestate.com/ActiveState/project?branch=main" + projectLine += "&commitID=00010001-0001-0001-0001-000100010001" // remove in DX-2307 + suite.Equal(projectLine, suite.project.URL(), "Values should match") suite.Equal("project", suite.project.Name(), "Values should match") - commitID, err := localcommit.Get(suite.project.Dir()) + commitID, err := commitmediator.Get(suite.project) suite.NoError(err) suite.Equal("00010001-0001-0001-0001-000100010001", commitID.String(), "Values should match") suite.Equal("ActiveState", suite.project.Owner(), "Values should match") diff --git a/pkg/project/secrets.go b/pkg/project/secrets.go index acdd45c9b6..1a8a81b549 100644 --- a/pkg/project/secrets.go +++ b/pkg/project/secrets.go @@ -117,7 +117,7 @@ func (e *SecretExpander) Project() (*mono_models.Project, error) { } var err error if e.remoteProject == nil { - e.remoteProject, err = model.FetchProjectByName(e.project.Owner(), e.project.Name()) + e.remoteProject, err = model.LegacyFetchProjectByName(e.project.Owner(), e.project.Name()) if err != nil { return nil, err } diff --git a/pkg/project/testdata/activestate.yaml b/pkg/project/testdata/activestate.yaml index 41f1f8d8ec..80908d57b2 100644 --- a/pkg/project/testdata/activestate.yaml +++ b/pkg/project/testdata/activestate.yaml @@ -1,4 +1,4 @@ -project: https://platform.activestate.com/ActiveState/project?branch=main +project: https://platform.activestate.com/ActiveState/project?branch=main&commitID=00010001-0001-0001-0001-000100010001 environments: "something" lock: "master@1.0.0-SHA123" namespace: "my/name/space" diff --git a/pkg/projectfile/projectfield.go b/pkg/projectfile/projectfield.go index 471e63549c..fbf3da6c5c 100644 --- a/pkg/projectfile/projectfield.go +++ b/pkg/projectfile/projectfield.go @@ -29,9 +29,6 @@ func (p *projectField) LoadProject(rawProjectValue string) error { } p.url = u - // Strip legacy commitID parameter. - p.unsetQuery("commitID") - return nil } @@ -47,6 +44,15 @@ func (p *projectField) SetBranch(branch string) { p.setQuery("branch", branch) } +func (p *projectField) StripCommitID() { + p.unsetQuery("commitID") // legacy +} + +// Remove this in DX-2307. +func (p *projectField) LegacySetCommit(commitID string) { + p.setQuery("commitID", commitID) +} + func (p *projectField) setPath(path string) { p.url.Path = path p.url.RawPath = p.url.EscapedPath() diff --git a/pkg/projectfile/projectfile.go b/pkg/projectfile/projectfile.go index e9bba2450d..664c7a72be 100644 --- a/pkg/projectfile/projectfile.go +++ b/pkg/projectfile/projectfile.go @@ -32,7 +32,6 @@ import ( "github.com/ActiveState/cli/internal/rtutils" "github.com/ActiveState/cli/internal/sliceutils" "github.com/ActiveState/cli/internal/strutils" - "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/sysinfo" "github.com/go-openapi/strfmt" "github.com/google/uuid" @@ -465,21 +464,6 @@ func (p *Project) Init() error { } p.parsedURL = parsedURL - if p.parsedURL.LegacyCommitID != "" { - // Migrate from commitID in activestate.yaml to .activestate/commit file. - // Writing to disk during Parse() feels wrong though. - if err := localcommit.Set(filepath.Dir(p.Path()), p.parsedURL.LegacyCommitID); err != nil { - return errs.Wrap(err, "Could not create local commit file") - } - pf := NewProjectField() - if err := pf.LoadProject(p.Project); err != nil { - return errs.Wrap(err, "Could not load activestate.yaml") - } - if err := pf.Save(p.path); err != nil { - return errs.Wrap(err, "Could not save activestate.yaml") - } - } - // Ensure branch name is set if p.parsedURL.Owner != "" && p.parsedURL.BranchName == "" { logging.Debug("Appending default branch as none is set") @@ -549,6 +533,11 @@ func detectDeprecations(dat []byte, configFilepath string) error { } } +// URL returns the project namespace's string URL from activestate.yaml. +func (p *Project) URL() string { + return p.Project +} + // Owner returns the project namespace's organization func (p *Project) Owner() string { return p.parsedURL.Owner @@ -569,6 +558,35 @@ func (p *Project) Path() string { return p.path } +// LegacyCommitID is for use by commitmediator.Get() ONLY. +// It returns a pre-migrated project's commit ID from activestate.yaml. +func (p *Project) LegacyCommitID() string { + return p.parsedURL.LegacyCommitID +} + +// LegacySetCommit is for use by commitmediator.Set() ONLY. +// It changes the legacy commit ID in activestate.yaml. +// Remove this in DX-2307. +func (p *Project) LegacySetCommit(commitID string) error { + pf := NewProjectField() + if err := pf.LoadProject(p.Project); err != nil { + return errs.Wrap(err, "Could not load activestate.yaml") + } + pf.LegacySetCommit(commitID) + if err := pf.Save(p.path); err != nil { + return errs.Wrap(err, "Could not save activestate.yaml") + } + + p.parsedURL.LegacyCommitID = commitID + p.Project = pf.String() + return nil +} + +// Remove this function in DX-2307. +func (p *Project) Dir() string { + return filepath.Dir(p.path) +} + // SetPath sets the path of the project file and should generally only be used by tests func (p *Project) SetPath(path string) { p.path = path @@ -904,24 +922,17 @@ func FromExactPath(path string) (*Project, error) { // CreateParams are parameters that we create a custom activestate.yaml file from type CreateParams struct { - Owner string - Project string - BranchName string - Directory string - Content string - Language string - Private bool - path string - ProjectURL string - Cache string -} - -// TestOnlyCreateWithProjectURL a new activestate.yaml with default content -func TestOnlyCreateWithProjectURL(projectURL, path string) (*Project, error) { - return createCustom(&CreateParams{ - ProjectURL: projectURL, - Directory: path, - }, language.Python3) + Owner string + Project string + BranchName string + Directory string + Content string + Language string + Private bool + path string + ProjectURL string + Cache string + LegacyCommitID string // remove in DX-2307 } // Create will create a new activestate.yaml with a projectURL for the given details @@ -952,6 +963,11 @@ func createCustom(params *CreateParams, lang language.Language) (*Project, error q.Set("branch", params.BranchName) } + // Remove this block in DX-2307. + if params.LegacyCommitID != "" { + q.Set("commitID", params.LegacyCommitID) + } + u.RawQuery = q.Encode() params.ProjectURL = u.String() } @@ -976,8 +992,9 @@ func createCustom(params *CreateParams, lang language.Language) (*Project, error shell = "batch" } + languageDisabled := os.Getenv(constants.DisableLanguageTemplates) == "true" content := params.Content - if content == "" && lang != language.Unset && lang != language.Unknown { + if !languageDisabled && content == "" && lang != language.Unset && lang != language.Unknown { tplName := "activestate.yaml." + strings.TrimRight(lang.String(), "23") + ".tpl" template, err := assets.ReadFileBytes(tplName) if err != nil { diff --git a/pkg/projectfile/projectfile_test.go b/pkg/projectfile/projectfile_test.go index 64f80d2a88..6e1657145f 100644 --- a/pkg/projectfile/projectfile_test.go +++ b/pkg/projectfile/projectfile_test.go @@ -12,7 +12,7 @@ import ( "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/fileutils" + "github.com/ActiveState/cli/internal/language" "github.com/ActiveState/cli/internal/locale" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -314,17 +314,17 @@ func TestNewProjectfile(t *testing.T) { assert.NoError(t, err, "Should be no error when getting a temp directory") os.Chdir(dir) - pjFile, err := TestOnlyCreateWithProjectURL("https://platform.activestate.com/xowner/xproject", dir) + pjFile, err := testOnlyCreateWithProjectURL("https://platform.activestate.com/xowner/xproject", dir) assert.NoError(t, err, "There should be no error when loading from a path") assert.Equal(t, "activationMessage", pjFile.Scripts[0].Name) - _, err = TestOnlyCreateWithProjectURL("https://platform.activestate.com/xowner/xproject", "") + _, err = testOnlyCreateWithProjectURL("https://platform.activestate.com/xowner/xproject", "") assert.Error(t, err, "We don't accept blank paths") setCwd(t, "") dir, err = os.Getwd() assert.NoError(t, err, "Should be no error when getting the CWD") - _, err = TestOnlyCreateWithProjectURL("https://platform.activestate.com/xowner/xproject", dir) + _, err = testOnlyCreateWithProjectURL("https://platform.activestate.com/xowner/xproject", dir) assert.Error(t, err, "Cannot create new project if existing as.yaml ...exists") } @@ -469,24 +469,10 @@ languages: } } -func TestMigrateCommitFromASY(t *testing.T) { - tempDir := fileutils.TempDirUnsafe() - defer os.RemoveAll(tempDir) - - commitID := "7BA74758-8665-4D3F-921C-757CD271A0C1" - asy := filepath.Join(tempDir, constants.ConfigFileName) - err := fileutils.WriteFile(asy, []byte("project: https://platform.activestate.com/Owner/Name?branch=main&commitID="+commitID)) - require.NoError(t, err) - - proj, err := Parse(asy) - require.NoError(t, err) - assert.Equal(t, "Owner", proj.Owner()) - assert.Equal(t, "Name", proj.Name()) - assert.Equal(t, "main", proj.BranchName()) - - commitIdFile := filepath.Join(tempDir, constants.ProjectConfigDirName, constants.CommitIdFileName) - require.FileExists(t, commitIdFile) - assert.Equal(t, commitID, string(fileutils.ReadFileUnsafe(commitIdFile))) - - assert.NotContains(t, string(fileutils.ReadFileUnsafe(asy)), commitID) +// testOnlyCreateWithProjectURL a new activestate.yaml with default content +func testOnlyCreateWithProjectURL(projectURL, path string) (*Project, error) { + return createCustom(&CreateParams{ + ProjectURL: projectURL, + Directory: path, + }, language.Python3) } diff --git a/scripts/ci/update-generator/main.go b/scripts/ci/update-generator/main.go index 66293ca5cf..dfd2b51925 100644 --- a/scripts/ci/update-generator/main.go +++ b/scripts/ci/update-generator/main.go @@ -66,9 +66,9 @@ func archiveMeta() (archiveMethod archiver.Archiver, ext string) { return archiver.NewTarGz(), ".tar.gz" } -func createUpdate(outputPath, channel, version, platform, target string) error { +func createUpdate(outputPath, channel, version, versionNumber, platform, target string) error { relChannelPath := filepath.Join(channel, platform) - relVersionedPath := filepath.Join(channel, version, platform) + relVersionedPath := filepath.Join(channel, versionNumber, platform) _ = os.MkdirAll(filepath.Join(outputPath, relChannelPath), 0o755) _ = os.MkdirAll(filepath.Join(outputPath, relVersionedPath), 0o755) @@ -129,12 +129,13 @@ func createInstaller(buildPath, outputPath, channel, platform string) error { func run() error { var ( - binDir = defaultBuildDir - inDir = defaultInputDir - outDir = defaultOutputDir - platform = fetchPlatform() - branch = constants.BranchName - version = constants.Version + binDir = defaultBuildDir + inDir = defaultInputDir + outDir = defaultOutputDir + platform = fetchPlatform() + branch = constants.BranchName + version = constants.Version + versionNumber = constants.VersionNumber ) flag.StringVar(&outDir, "o", outDir, "Override directory to output archive to.") @@ -150,7 +151,7 @@ func run() error { return err } - if err := createUpdate(outDir, branch, version, platform, inDir); err != nil { + if err := createUpdate(outDir, branch, version, versionNumber, platform, inDir); err != nil { return err } diff --git a/scripts/ci/update-generator/main_test.go b/scripts/ci/update-generator/main_test.go index 1e5e0ccf5e..317e59855b 100644 --- a/scripts/ci/update-generator/main_test.go +++ b/scripts/ci/update-generator/main_test.go @@ -32,12 +32,12 @@ func TestCreateUpdate(t *testing.T) { require.NoError(t, err) } - err = createUpdate(dir, "channel", "version", "platform", dir) + err = createUpdate(dir, "channel", "version-SHA", "version", "platform", dir) require.NoError(t, err) _, ext := archiveMeta() assert.FileExists(t, filepath.Join(dir, "channel", "platform", "info.json"), "Should create update bits") assert.FileExists(t, filepath.Join(dir, "channel", "version", "platform", "info.json"), "Should create update bits") - assert.FileExists(t, filepath.Join(dir, "channel", "version", "platform", "state-platform-version"+ext), "Should create update bits") + assert.FileExists(t, filepath.Join(dir, "channel", "version", "platform", "state-platform-version-SHA"+ext), "Should create update bits") } diff --git a/scripts/ci/update-version-list/main.go b/scripts/ci/update-version-list/main.go new file mode 100644 index 0000000000..681ba4b108 --- /dev/null +++ b/scripts/ci/update-version-list/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/thoas/go-funk" + + "github.com/ActiveState/cli/internal/condition" + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/fileutils" + "github.com/ActiveState/cli/internal/httputil" + "github.com/ActiveState/cli/internal/updater" +) + +// Where the master version file lives on S3. +const S3PrefixURL = "https://state-tool.s3.amazonaws.com/" +const S3Bucket = "update/state/" +const VersionsJson = "versions.json" + +// Valid channels to update the master version file with. +var ValidChannels = []string{constants.BetaBranch, constants.ReleaseBranch} + +func init() { + if !condition.OnCI() { + // Allow testing with artifacts produced by `state run generate-test-update` + ValidChannels = append(ValidChannels, "test-channel") + } +} + +func main() { + if len(os.Args) != 2 { + log.Fatalf("Usage: %s ", os.Args[0]) + } + + // Fetch the current master list from S3. + versions := []updater.AvailableUpdate{} + fmt.Printf("Fetching master %s file from S3\n", VersionsJson) + bytes, err := httputil.Get(S3PrefixURL + S3Bucket + VersionsJson) + if err != nil { + log.Fatalf("Failed to fetch file: %s", err.Error()) + } + err = json.Unmarshal(bytes, &versions) + if err != nil { + log.Fatalf("Failed to decode JSON: %s", err.Error()) + } + + // Find info.json files to add to the master list and add them. + updated := false + buildDir := os.Args[1] + fmt.Printf("Searching for info.json files in %s\n", buildDir) + files, err := fileutils.ListDir(buildDir, false) + if err != nil { + log.Fatalf("Failed to search %s: %s", buildDir, err.Error()) + } + for _, file := range files { + if file.Name() != "info.json" { + continue + } + channel := strings.Split(file.RelativePath(), string(filepath.Separator))[0] + if !funk.Contains(ValidChannels, channel) { + continue + } + fmt.Printf("Found %s\n", file.RelativePath()) + bytes, err := fileutils.ReadFile(file.Path()) + if err != nil { + log.Fatalf("Unable to read file: %s", err.Error()) + } + info := updater.AvailableUpdate{} + err = json.Unmarshal(bytes, &info) + if err != nil { + log.Fatalf("Unable to decode JSON: %s", err.Error()) + } + info.Path = S3PrefixURL + S3Bucket + info.Path // convert relative path to full URL + versions = append(versions, info) + updated = true + } + + if !updated { + fmt.Println("No updates found.") + return + } + + // Write the updated list to disk. The s3-deployer script should pick it up and upload it. + localVersionsJson := filepath.Join(buildDir, VersionsJson) + fmt.Printf("Writing updated %s locally to %s\n", VersionsJson, localVersionsJson) + bytes, err = json.Marshal(versions) + if err != nil { + log.Fatalf("Failed to encode JSON: %s", err.Error()) + } + err = fileutils.WriteFile(localVersionsJson, bytes) + if err != nil { + log.Fatalf("Failed to write file: %s", err.Error()) + } +} diff --git a/test/integration/activate_int_test.go b/test/integration/activate_int_test.go index 992f9685b2..0426cee0b9 100644 --- a/test/integration/activate_int_test.go +++ b/test/integration/activate_int_test.go @@ -178,12 +178,12 @@ func (suite *ActivateIntegrationTestSuite) TestActivatePythonByHostOnly() { projectName := "Python-LinuxWorks" cp := ts.SpawnWithOpts( e2e.OptArgs("activate", "cli-integration-tests/"+projectName, "--path="+ts.Dirs.Work), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) if runtime.GOOS == "linux" { cp.Expect("Creating a Virtual Environment") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput(termtest.OptExpectTimeout(40 * time.Second)) cp.SendLine("exit") cp.ExpectExitCode(0) @@ -196,6 +196,10 @@ func (suite *ActivateIntegrationTestSuite) TestActivatePythonByHostOnly() { cp.Expect("Your current platform") cp.Expect("does not appear to be configured") cp.ExpectNotExitCode(0) + + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } } } @@ -222,11 +226,11 @@ func (suite *ActivateIntegrationTestSuite) activatePython(version string, extraE cp := ts.SpawnWithOpts( e2e.OptArgs("activate", namespace), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), e2e.OptAppendEnv(extraEnv...), ) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) // ensure that shell is functional cp.ExpectInput() @@ -274,9 +278,9 @@ func (suite *ActivateIntegrationTestSuite) activatePython(version string, extraE cp = ts.SpawnCmdWithOpts( executor, e2e.OptArgs("-c", "import sys; print(sys.copyright);"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("ActiveState Software Inc.") + cp.Expect("ActiveState Software Inc.", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) } @@ -292,10 +296,10 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_PythonPath() { cp := ts.SpawnWithOpts( e2e.OptArgs("activate", namespace), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) // ensure that shell is functional cp.ExpectInput() @@ -351,7 +355,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_SpaceInCacheDir() { e2e.OptArgs("activate", "ActiveState-CLI/Python3"), ) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.SendLine("python3 --version") cp.Expect("Python 3.") @@ -372,14 +376,12 @@ func (suite *ActivateIntegrationTestSuite) TestActivatePerl() { cp := ts.SpawnWithOpts( e2e.OptArgs("activate", "ActiveState-CLI/Perl"), - e2e.OptAppendEnv( - "ACTIVESTATE_CLI_DISABLE_RUNTIME=false", - ), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Downloading", termtest.OptExpectTimeout(40*time.Second)) cp.Expect("Installing", termtest.OptExpectTimeout(140*time.Second)) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) suite.assertCompletedStatusBarReport(cp.Output()) @@ -421,6 +423,7 @@ version: %s `, constants.BranchName, constants.Version)) ts.PrepareActiveStateYAML(content) + ts.PrepareCommitIdFile("59404293-e5a9-4fd0-8843-77cd4761b5b5") // Pull to ensure we have an up to date config file cp := ts.Spawn("pull") @@ -451,7 +454,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_NamespaceWins() { suite.Require().NoError(err) // Create the project file at the root of the temp dir - ts.PrepareProject("ActiveState-CLI/Python3", "") + ts.PrepareProject("ActiveState-CLI/Python3", "59404293-e5a9-4fd0-8843-77cd4761b5b5") // Pull to ensure we have an up to date config file cp := ts.Spawn("pull") @@ -462,6 +465,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_NamespaceWins() { c2 := ts.SpawnWithOpts( e2e.OptArgs("activate", "ActiveState-CLI/Python2"), // activate a different namespace e2e.OptWD(targetPath), + e2e.OptAppendEnv(constants.DisableLanguageTemplates+"=true"), ) c2.Expect("ActiveState-CLI/Python2") c2.Expect("Activated") @@ -507,11 +511,11 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_FromCache() { cp := ts.SpawnWithOpts( e2e.OptArgs("activate", "ActiveState-CLI/small-python", "--path", ts.Dirs.Work), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Downloading") cp.Expect("Installing") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) suite.assertCompletedStatusBarReport(cp.Output()) cp.SendLine("exit") @@ -520,10 +524,10 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_FromCache() { // next activation is cached cp = ts.SpawnWithOpts( e2e.OptArgs("activate", "ActiveState-CLI/small-python", "--path", ts.Dirs.Work), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.ExpectInput() + cp.ExpectInput(e2e.RuntimeSourcingTimeoutOpt) cp.SendLine("exit") cp.ExpectExitCode(0) suite.NotContains(cp.Output(), "Downloading") @@ -545,11 +549,9 @@ func (suite *ActivateIntegrationTestSuite) TestActivateCommitURL() { contents := fmt.Sprintf("project: https://platform.activestate.com/commit/%s\n", commitID) ts.PrepareActiveStateYAML(contents) - // Ensure we have the most up to date version of the project before activating cp := ts.Spawn("activate") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) - cp.SendLine("exit") - cp.ExpectExitCode(0) + cp.Expect("Cannot initialize runtime for a headless project", e2e.RuntimeSourcingTimeoutOpt) + cp.ExpectExitCode(1) } func (suite *ActivateIntegrationTestSuite) TestActivate_AlreadyActive() { @@ -661,10 +663,10 @@ func (suite *ActivateIntegrationTestSuite) TestActivateArtifactsCached() { cp := ts.SpawnWithOpts( e2e.OptArgs("activate", namespace), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.SendLine("exit") cp.ExpectExitCode(0) @@ -690,13 +692,13 @@ func (suite *ActivateIntegrationTestSuite) TestActivateArtifactsCached() { cp = ts.SpawnWithOpts( e2e.OptArgs("activate", namespace), e2e.OptAppendEnv( - "ACTIVESTATE_CLI_DISABLE_RUNTIME=false", + constants.DisableRuntime+"=false", "VERBOSE=true", // Necessary to assert "Fetched cached artifact" ), ) cp.Expect("Fetched cached artifact") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.SendLine("exit") cp.ExpectExitCode(0) } diff --git a/test/integration/analytics_int_test.go b/test/integration/analytics_int_test.go index 6b24829581..e0807e5dae 100644 --- a/test/integration/analytics_int_test.go +++ b/test/integration/analytics_int_test.go @@ -77,7 +77,7 @@ func (suite *AnalyticsIntegrationTestSuite) TestHeartbeats() { } cp.Expect("Creating a Virtual Environment") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput(termtest.OptExpectTimeout(120 * time.Second)) time.Sleep(time.Second) // Ensure state-svc has time to report events @@ -148,6 +148,8 @@ func (suite *AnalyticsIntegrationTestSuite) TestHeartbeats() { }) suite.Require().Equal(1, countEvents(executorEvents, anaConst.CatRuntimeUsage, anaConst.ActRuntimeAttempt, anaConst.SrcExecutor), ts.DebugMessage("Should have a runtime attempt, events:\n"+suite.summarizeEvents(executorEvents))) + suite.Require().Equal(1, countEvents(eventsAfterExecutor, anaConst.CatDebug, anaConst.ActExecutorExit, anaConst.SrcExecutor), + ts.DebugMessage("Should have an executor exit event, events:\n"+suite.summarizeEvents(executorEvents))) // It's possible due to the timing of the heartbeats and the fact that they are async that we have gotten either // one or two by this point. Technically more is possible, just very unlikely. @@ -218,8 +220,7 @@ func (suite *AnalyticsIntegrationTestSuite) TestExecEvents() { commitID := "efcc851f-1451-4d0a-9dcb-074ac3f35f0a" // We want to do a clean test without an activate event, so we have to manually seed the yaml - url := fmt.Sprintf("https://platform.activestate.com/%s?branch=main&commitID=%s", namespace, commitID) - suite.Require().NoError(fileutils.WriteFile(filepath.Join(ts.Dirs.Work, "activestate.yaml"), []byte("project: "+url))) + ts.PrepareProject(namespace, commitID) heartbeatInterval := 1000 // in milliseconds sleepTime := time.Duration(heartbeatInterval) * time.Millisecond @@ -238,7 +239,7 @@ func (suite *AnalyticsIntegrationTestSuite) TestExecEvents() { e2e.OptAppendEnv(env...), ) - cp.Expect("DONE") + cp.Expect("DONE", e2e.RuntimeSourcingTimeoutOpt) time.Sleep(sleepTime) @@ -525,7 +526,7 @@ func (suite *AnalyticsIntegrationTestSuite) TestAttempts() { ) cp.Expect("Creating a Virtual Environment") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput(termtest.OptExpectTimeout(120 * time.Second)) cp.SendLine("python3 --version") @@ -568,7 +569,7 @@ func (suite *AnalyticsIntegrationTestSuite) TestHeapEvents() { ) cp.Expect("Creating a Virtual Environment") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput(termtest.OptExpectTimeout(120 * time.Second)) time.Sleep(time.Second) // Ensure state-svc has time to report events diff --git a/test/integration/auth_int_test.go b/test/integration/auth_int_test.go index 06c957efe3..fb875fc8a7 100644 --- a/test/integration/auth_int_test.go +++ b/test/integration/auth_int_test.go @@ -87,20 +87,13 @@ func (suite *AuthIntegrationTestSuite) ensureLogout(ts *e2e.Session) { } type userJSON struct { - Username string `json:"username,omitempty"` - URLName string `json:"urlname,omitempty"` - Tier string `json:"tier,omitempty"` - PrivateProjects bool `json:"privateProjects"` + Username string `json:"username,omitempty"` } func (suite *AuthIntegrationTestSuite) authOutput(method string) { - user := userJSON{ - Username: e2e.PersistentUsername, - URLName: e2e.PersistentUsername, - Tier: "free", - PrivateProjects: false, - } - data, err := json.Marshal(user) + data, err := json.Marshal(userJSON{ + Username: e2e.PersistentUsername, + }) suite.Require().NoError(err) ts := e2e.New(suite.T(), false) @@ -109,7 +102,7 @@ func (suite *AuthIntegrationTestSuite) authOutput(method string) { expected := string(data) ts.LoginAsPersistentUser() cp := ts.Spawn(tagsuite.Auth, "--output", method) - cp.Expect("false}") + cp.Expect(`"}`) cp.ExpectExitCode(0) suite.Contains(cp.Output(), fmt.Sprintf("%s", string(expected))) } diff --git a/test/integration/branch_int_test.go b/test/integration/branch_int_test.go index 1b74f0f56d..9d1364bc2c 100644 --- a/test/integration/branch_int_test.go +++ b/test/integration/branch_int_test.go @@ -23,7 +23,7 @@ func (suite *BranchIntegrationTestSuite) TestBranch_List() { ts.PrepareProject("ActiveState-CLI/Branches", "") - cp := ts.SpawnWithOpts(e2e.OptArgs("branch"), e2e.OptTermTest(termtest.OptVerboseLogging())) + cp := ts.SpawnWithOpts(e2e.OptArgs("branch"), e2e.OptTermTest(termtest.OptVerboseLogger())) // Sometimes there's a space before the line break, unsure exactly why, but hence the regex cp.ExpectRe(`main \(Current\)\s?\n ├─ firstbranch\s?\n │ └─ firstbranchchild\s?\n │ └─ childoffirstbranchchild\s?\n ├─ secondbranch\s?\n └─ thirdbranch`, termtest.OptExpectTimeout(5*time.Second)) cp.ExpectExitCode(0) diff --git a/test/integration/bundle_int_test.go b/test/integration/bundle_int_test.go index 88f4a40d96..c449cb4f6a 100644 --- a/test/integration/bundle_int_test.go +++ b/test/integration/bundle_int_test.go @@ -8,6 +8,7 @@ import ( "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" ) @@ -80,7 +81,7 @@ func (suite *BundleIntegrationTestSuite) TestBundle_project_invalid() { defer ts.Close() cp := ts.Spawn("bundles", "--namespace", "junk/junk") - cp.Expect("The requested project junk/junk could not be found") + cp.Expect("The requested project junk does not exist under junk") cp.ExpectExitCode(1) } @@ -165,7 +166,7 @@ func (suite *BundleIntegrationTestSuite) TestBundle_searchWithBadLang() { cp.ExpectExitCode(1) } -func (suite *BundleIntegrationTestSuite) TestBundle_headless_operation() { +func (suite *BundleIntegrationTestSuite) TestBundle_detached_operation() { suite.OnlyRunForTags(tagsuite.Bundle) if runtime.GOOS == "darwin" { suite.T().Skip("Skipping mac for now as the builds are still too unreliable") @@ -222,18 +223,25 @@ func (suite *BundleIntegrationTestSuite) TestJSON() { AssertValidJSON(suite.T(), cp) cp = ts.SpawnWithOpts( - e2e.OptArgs("bundles", "install", "Testing", "--output", "json"), + e2e.OptArgs("checkout", "ActiveState-CLI/Bundles", "."), e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), ) - cp.Expect(`"name":"Testing"`) + cp.Expect("Checked out project") + cp.ExpectExitCode(0) + + cp = ts.SpawnWithOpts( + e2e.OptArgs("bundles", "install", "Testing", "--output", "json"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), + ) + cp.Expect(`"name":"Testing"`, e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) AssertValidJSON(suite.T(), cp) cp = ts.SpawnWithOpts( e2e.OptArgs("bundles", "uninstall", "Testing", "-o", "editor"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect(`"name":"Testing"`) + cp.Expect(`"name":"Testing"`, e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) AssertValidJSON(suite.T(), cp) } diff --git a/test/integration/checkout_int_test.go b/test/integration/checkout_int_test.go index 9c35f2b52f..f8f823b578 100644 --- a/test/integration/checkout_int_test.go +++ b/test/integration/checkout_int_test.go @@ -7,9 +7,7 @@ import ( "runtime" "strings" "testing" - "time" - "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" "github.com/ActiveState/cli/internal/constants" @@ -35,19 +33,20 @@ func (suite *CheckoutIntegrationTestSuite) TestCheckout() { // Checkout and verify. cp := ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/Python-3.9", "."), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Checked out project", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Checked out project", e2e.RuntimeSourcingTimeoutOpt) suite.Require().True(fileutils.DirExists(ts.Dirs.Work), "state checkout should have created "+ts.Dirs.Work) suite.Require().True(fileutils.FileExists(filepath.Join(ts.Dirs.Work, constants.ConfigFileName)), "ActiveState-CLI/Python3 was not checked out properly") // Verify .activestate/commit and .gitignore were created. - projectConfigDir := filepath.Join(ts.Dirs.Work, constants.ProjectConfigDirName) - suite.Require().True(fileutils.DirExists(projectConfigDir), "state checkout should have created "+projectConfigDir) - suite.Assert().True(fileutils.FileExists(filepath.Join(projectConfigDir, constants.CommitIdFileName)), "commit file not created") - gitignoreFile := filepath.Join(ts.Dirs.Work, ".gitignore") - suite.Assert().True(fileutils.FileExists(gitignoreFile), "fresh checkout did not create .gitignore") - suite.Assert().Contains(string(fileutils.ReadFileUnsafe(gitignoreFile)), fmt.Sprintf("%s/%s", constants.ProjectConfigDirName, constants.CommitIdFileName), "commit file not added to .gitignore") + // Re-enable the following lines in DX-2307. + //projectConfigDir := filepath.Join(ts.Dirs.Work, constants.ProjectConfigDirName) + //suite.Require().True(fileutils.DirExists(projectConfigDir), "state checkout should have created "+projectConfigDir) + //suite.Assert().True(fileutils.FileExists(filepath.Join(projectConfigDir, constants.CommitIdFileName)), "commit file not created") + //gitignoreFile := filepath.Join(ts.Dirs.Work, ".gitignore") + //suite.Assert().True(fileutils.FileExists(gitignoreFile), "fresh checkout did not create .gitignore") + //suite.Assert().Contains(string(fileutils.ReadFileUnsafe(gitignoreFile)), fmt.Sprintf("%s/%s", constants.ProjectConfigDirName, constants.CommitIdFileName), "commit file not added to .gitignore") // Verify runtime was installed correctly and works. targetDir := target.ProjectDirToTargetDir(ts.Dirs.Work, ts.Dirs.Cache) @@ -68,12 +67,12 @@ func (suite *CheckoutIntegrationTestSuite) TestCheckout() { cp = ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/Python-3.9", "."), e2e.OptAppendEnv( - "ACTIVESTATE_CLI_DISABLE_RUNTIME=false", + constants.DisableRuntime+"=false", "VERBOSE=true", // Necessary to assert "Fetched cached artifact" ), ) - cp.Expect("Fetched cached artifact", termtest.OptExpectTimeout(120*time.Second)) // Comes from log, which is why we're using VERBOSE=true - cp.Expect("Checked out project", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Fetched cached artifact", e2e.RuntimeSourcingTimeoutOpt) // Comes from log, which is why we're using VERBOSE=true + cp.Expect("Checked out project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) }) } @@ -93,16 +92,20 @@ func (suite *CheckoutIntegrationTestSuite) TestCheckoutNonEmptyDir() { // Checkout and verify. cp := ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/Python3", tmpdir), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=true"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("already a project checked out at") cp.ExpectExitCode(1) + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } + // remove file suite.Require().NoError(os.Remove(filepath.Join(tmpdir, constants.ConfigFileName))) cp = ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/Python3", tmpdir), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=true"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Checked out project") cp.ExpectExitCode(0) @@ -151,9 +154,10 @@ func (suite *CheckoutIntegrationTestSuite) TestCheckoutWithFlags() { cp.ExpectExitCode(0) suite.Require().True(fileutils.DirExists(python3Dir), "state checkout should have created "+python3Dir) - commitIdFile := filepath.Join(python3Dir, constants.ProjectConfigDirName, constants.CommitIdFileName) - suite.Require().True(fileutils.FileExists(commitIdFile), "ActiveState-CLI/Python3 was not checked out properly") - suite.Assert().Equal(string(fileutils.ReadFileUnsafe(commitIdFile)), "6d9280e7-75eb-401a-9e71-0d99759fbad3", "did not check out specific commit ID") + // Re-enable the following lines in DX-2307. + //commitIdFile := filepath.Join(python3Dir, constants.ProjectConfigDirName, constants.CommitIdFileName) + //suite.Require().True(fileutils.FileExists(commitIdFile), "ActiveState-CLI/Python3 was not checked out properly") + //suite.Assert().Equal(string(fileutils.ReadFileUnsafe(commitIdFile)), "6d9280e7-75eb-401a-9e71-0d99759fbad3", "did not check out specific commit ID") // Test --branch mismatch in non-checked-out project. branchPath := filepath.Join(ts.Dirs.Base, "branch") @@ -176,9 +180,9 @@ func (suite *CheckoutIntegrationTestSuite) TestCheckoutCustomRTPath() { // Checkout and verify. cp := ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/Python3", fmt.Sprintf("--runtime-path=%s", customRTPath)), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Checked out project", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Checked out project", e2e.RuntimeSourcingTimeoutOpt) pythonExe := filepath.Join(setup.ExecDir(customRTPath), "python3"+exeutils.Extension) suite.Require().True(fileutils.DirExists(customRTPath)) @@ -192,7 +196,7 @@ func (suite *CheckoutIntegrationTestSuite) TestCheckoutCustomRTPath() { // Verify that state exec works with custom cache. cp = ts.SpawnWithOpts( e2e.OptArgs("exec", "python3", "--", "-c", "import sys;print(sys.executable)"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), e2e.OptWD(filepath.Join(ts.Dirs.Work, "Python3")), ) if runtime.GOOS == "windows" { @@ -200,7 +204,23 @@ func (suite *CheckoutIntegrationTestSuite) TestCheckoutCustomRTPath() { suite.Require().NoError(err) customRTPath = strings.ToLower(customRTPath) } - cp.Expect(customRTPath) + cp.Expect(customRTPath, e2e.RuntimeSourcingTimeoutOpt) +} + +func (suite *CheckoutIntegrationTestSuite) TestCheckoutNotFound() { + suite.OnlyRunForTags(tagsuite.Checkout) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + cp := ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/Bogus-Project-That-Doesnt-Exist")) + cp.Expect("does not exist under") // error + cp.Expect("If this is a private project") // tip + cp.ExpectExitCode(1) + + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } } func (suite *CheckoutIntegrationTestSuite) TestCheckoutAlreadyCheckedOut() { @@ -226,6 +246,12 @@ func (suite *CheckoutIntegrationTestSuite) TestJSON() { cp := ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/small-python", "-o", "json")) cp.ExpectExitCode(0) AssertValidJSON(suite.T(), cp) + + cp = ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/Bogus-Project-That-Doesnt-Exist", "-o", "json")) + cp.Expect("does not exist") // error + cp.Expect(`"tips":["If this is a private project`) // tip + cp.ExpectNotExitCode(0) + AssertValidJSON(suite.T(), cp) } func (suite *CheckoutIntegrationTestSuite) TestCheckoutCaseInsensitive() { @@ -244,6 +270,28 @@ func (suite *CheckoutIntegrationTestSuite) TestCheckoutCaseInsensitive() { suite.Assert().NotContains(string(bytes), "ACTIVESTATE-CLI/SMALL-PYTHON", "kept incorrect namespace case") } +func (suite *CheckoutIntegrationTestSuite) TestCheckoutBuildtimeClosure() { + suite.OnlyRunForTags(tagsuite.Checkout) + + if runtime.GOOS != "linux" { + suite.T().Skip("Skipping buildtime closure test on non-linux platform") + } + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + cp := ts.SpawnWithOpts( + e2e.OptArgs("checkout", "ActiveState-CLI/small-python#5a1e49e5-8ceb-4a09-b605-ed334474855b"), + e2e.OptAppendEnv(constants.InstallBuildDependencies+"=true"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), + ) + // Expect the number of build deps to be 27 which is more than the number of runtime deps. + // Also expect libxcrypt which should not be in the runtime closure. + cp.Expect("27") + cp.Expect("libxcrypt") + cp.ExpectExitCode(0) +} + func TestCheckoutIntegrationTestSuite(t *testing.T) { suite.Run(t, new(CheckoutIntegrationTestSuite)) } diff --git a/test/integration/commit_int_test.go b/test/integration/commit_int_test.go index 8595b8bf6a..57bf60d0fe 100644 --- a/test/integration/commit_int_test.go +++ b/test/integration/commit_int_test.go @@ -4,7 +4,6 @@ import ( "bytes" "path/filepath" "testing" - "time" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/fileutils" @@ -12,7 +11,6 @@ import ( "github.com/ActiveState/cli/internal/testhelpers/tagsuite" "github.com/ActiveState/cli/pkg/platform/runtime/buildscript" "github.com/ActiveState/cli/pkg/project" - "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" ) @@ -22,6 +20,7 @@ type CommitIntegrationTestSuite struct { func (suite *CommitIntegrationTestSuite) TestCommitManualBuildScriptMod() { suite.OnlyRunForTags(tagsuite.Commit) + suite.T().Skip("Temporarily disable buildscripts until DX-2307") // remove in DX-2307 ts := e2e.New(suite.T(), false) defer ts.Close() @@ -33,9 +32,9 @@ func (suite *CommitIntegrationTestSuite) TestCommitManualBuildScriptMod() { "ActiveState-CLI/Commit-Test-A#7a1b416e-c17f-4d4a-9e27-cbad9e8f5655", ".", ), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Checked out", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Checked out", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) proj, err := project.FromPath(ts.Dirs.Work) diff --git a/test/integration/deploy_int_test.go b/test/integration/deploy_int_test.go index 5e7ad02d14..ab83c944af 100644 --- a/test/integration/deploy_int_test.go +++ b/test/integration/deploy_int_test.go @@ -7,9 +7,7 @@ import ( "path/filepath" "runtime" "testing" - "time" - "github.com/ActiveState/termtest" "github.com/google/uuid" "github.com/stretchr/testify/suite" @@ -39,7 +37,7 @@ func (suite *DeployIntegrationTestSuite) deploy(ts *e2e.Session, prj string, tar case "windows": cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", prj, "--path", targetPath), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) case "darwin": // On MacOS the command is the same as Linux, however some binaries @@ -47,22 +45,22 @@ func (suite *DeployIntegrationTestSuite) deploy(ts *e2e.Session, prj string, tar cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", prj, "--path", targetPath, "--force"), e2e.OptAppendEnv("SHELL=bash"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) default: cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", prj, "--path", targetPath), e2e.OptAppendEnv("SHELL=bash"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) } - cp.Expect("Installing", termtest.OptExpectTimeout(40*time.Second)) - cp.Expect("Configuring", termtest.OptExpectTimeout(40*time.Second)) + cp.Expect("Installing", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Configuring") if runtime.GOOS != "windows" { - cp.Expect("Symlinking", termtest.OptExpectTimeout(30*time.Second)) + cp.Expect("Symlinking") } - cp.Expect("Deployment Information", termtest.OptExpectTimeout(60*time.Second)) + cp.Expect("Deployment Information") cp.Expect(targetID) // expect bin dir if runtime.GOOS == "windows" { cp.Expect("log out") @@ -100,13 +98,14 @@ func (suite *DeployIntegrationTestSuite) TestDeployPerl() { "cmd.exe", e2e.OptArgs("/k", filepath.Join(targetPath, "bin", "shell.bat")), e2e.OptAppendEnv("PATHEXT=.COM;.EXE;.BAT;.LNK", "SHELL="), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) } else { cp = ts.SpawnCmdWithOpts( "/bin/bash", e2e.OptAppendEnv("PROMPT_COMMAND="), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false")) + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), + ) cp.SendLine(fmt.Sprintf("source %s\n", filepath.Join(targetPath, "bin", "shell.sh"))) } @@ -158,7 +157,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployPython() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.SetupRCFile(ts) + ts.SetupRCFile() suite.T().Setenv("ACTIVESTATE_HOME", ts.Dirs.HomeDir) targetID, err := uuid.NewUUID() @@ -176,13 +175,14 @@ func (suite *DeployIntegrationTestSuite) TestDeployPython() { "cmd.exe", e2e.OptArgs("/k", filepath.Join(targetPath, "bin", "shell.bat")), e2e.OptAppendEnv("PATHEXT=.COM;.EXE;.BAT;.LNK", "SHELL="), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) } else { cp = ts.SpawnCmdWithOpts( "/bin/bash", e2e.OptAppendEnv("PROMPT_COMMAND="), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false")) + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), + ) cp.SendLine(fmt.Sprintf("source %s\n", filepath.Join(targetPath, "bin", "shell.sh"))) } @@ -244,12 +244,12 @@ func (suite *DeployIntegrationTestSuite) TestDeployInstall() { func (suite *DeployIntegrationTestSuite) InstallAndAssert(ts *e2e.Session, targetPath string) { cp := ts.SpawnWithOpts( e2e.OptArgs("deploy", "install", "ActiveState-CLI/Python3", "--path", targetPath), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Installing Runtime") - cp.Expect("Installing", termtest.OptExpectTimeout(120*time.Second)) - cp.Expect("Installation completed", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Installing", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Installation completed", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) } @@ -261,7 +261,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployConfigure() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.SetupRCFile(ts) + ts.SetupRCFile() suite.T().Setenv("ACTIVESTATE_HOME", ts.Dirs.HomeDir) targetID, err := uuid.NewUUID() @@ -272,7 +272,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployConfigure() { // Install step is required cp := ts.SpawnWithOpts( e2e.OptArgs("deploy", "configure", "ActiveState-CLI/Python3", "--path", targetPath), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("need to run the install step") cp.ExpectExitCode(1) @@ -282,25 +282,25 @@ func (suite *DeployIntegrationTestSuite) TestDeployConfigure() { cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", "configure", "ActiveState-CLI/Python3", "--path", targetPath), e2e.OptAppendEnv("SHELL=bash"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) } else { cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", "configure", "ActiveState-CLI/Python3", "--path", targetPath), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) } - cp.Expect("Configuring shell", termtest.OptExpectTimeout(60*time.Second)) + cp.Expect("Configuring shell", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) suite.AssertConfig(ts, targetID.String()) if runtime.GOOS == "windows" { cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", "configure", "ActiveState-CLI/Python3", "--path", targetPath, "--user"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Configuring shell", termtest.OptExpectTimeout(60*time.Second)) + cp.Expect("Configuring shell", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) out, err := exec.Command("reg", "query", `HKCU\Environment`, "/v", "Path").Output() @@ -309,22 +309,6 @@ func (suite *DeployIntegrationTestSuite) TestDeployConfigure() { } } -func (suite *DeployIntegrationTestSuite) SetupRCFile(ts *e2e.Session) { - if runtime.GOOS == "windows" { - return - } - - cfg, err := config.New() - suite.Require().NoError(err) - - subshell := subshell.New(cfg) - rcFile, err := subshell.RcFile() - suite.Require().NoError(err) - - err = fileutils.CopyFile(rcFile, filepath.Join(ts.Dirs.HomeDir, filepath.Base(rcFile))) - suite.Require().NoError(err) -} - func (suite *DeployIntegrationTestSuite) AssertConfig(ts *e2e.Session, targetID string) { if runtime.GOOS != "windows" { // Test config file @@ -364,7 +348,7 @@ func (suite *DeployIntegrationTestSuite) TestDeploySymlink() { // Install step is required cp := ts.SpawnWithOpts( e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("need to run the install step") cp.ExpectExitCode(1) @@ -373,12 +357,12 @@ func (suite *DeployIntegrationTestSuite) TestDeploySymlink() { if runtime.GOOS != "darwin" { cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) } else { cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath, "--force"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) } @@ -405,7 +389,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployReport() { // Install step is required cp := ts.SpawnWithOpts( e2e.OptArgs("deploy", "report", "ActiveState-CLI/Python3", "--path", targetPath), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("need to run the install step") cp.ExpectExitCode(1) @@ -413,7 +397,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployReport() { cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", "report", "ActiveState-CLI/Python3", "--path", targetPath), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Deployment Information") cp.Expect(targetID.String()) // expect bin dir @@ -444,7 +428,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployTwice() { cp := ts.SpawnWithOpts( e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath), e2e.OptAppendEnv(fmt.Sprintf("PATH=%s", pathDir)), // Avoid conflicts - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.ExpectExitCode(0) @@ -457,7 +441,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployTwice() { cpx := ts.SpawnWithOpts( e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath), e2e.OptAppendEnv(fmt.Sprintf("PATH=%s", pathDir)), // Avoid conflicts - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cpx.ExpectExitCode(0) } @@ -483,7 +467,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployUninstall() { // Uninstall deployed runtime. cp := ts.SpawnWithOpts( e2e.OptArgs("deploy", "uninstall", "--path", filepath.Join(ts.Dirs.Work, "target")), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Uninstall Deployed Runtime") cp.Expect("Successful") @@ -494,7 +478,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployUninstall() { // Trying to uninstall again should fail cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", "uninstall", "--path", filepath.Join(ts.Dirs.Work, "target")), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("no deployed runtime") cp.ExpectExitCode(1) @@ -503,7 +487,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployUninstall() { // Trying to uninstall in a non-deployment directory should fail. cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", "uninstall"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("no deployed runtime") cp.ExpectExitCode(1) @@ -512,7 +496,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployUninstall() { // Trying to uninstall in a non-deployment directory should not delete that directory. cp = ts.SpawnWithOpts( e2e.OptArgs("deploy", "uninstall", "--path", ts.Dirs.Work), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("no deployed runtime") cp.ExpectExitCode(1) diff --git a/test/integration/edit_int_test.go b/test/integration/edit_int_test.go index 683ad7ad6d..37357360c6 100644 --- a/test/integration/edit_int_test.go +++ b/test/integration/edit_int_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/testhelpers/e2e" @@ -79,7 +80,7 @@ func (suite *EditIntegrationTestSuite) TestEdit_NonInteractive() { } ts, env := suite.setup() defer ts.Close() - extraEnv := e2e.OptAppendEnv("ACTIVESTATE_NONINTERACTIVE=true") + extraEnv := e2e.OptAppendEnv(constants.NonInteractiveEnvVarName + "=true") cp := ts.SpawnWithOpts(e2e.OptArgs("scripts", "edit", "test-script"), env, extraEnv) cp.Expect("Watching file changes") diff --git a/test/integration/exec_int_test.go b/test/integration/exec_int_test.go index 23c422ae4f..9f5861661e 100644 --- a/test/integration/exec_int_test.go +++ b/test/integration/exec_int_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" @@ -181,15 +182,15 @@ func (suite *ExecIntegrationTestSuite) TestExecWithPath() { cp = ts.SpawnWithOpts( e2e.OptArgs("exec", "--path", pythonDir, "which", "python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Operating on project ActiveState-CLI/Python-3.9") + cp.Expect("Operating on project ActiveState-CLI/Python-3.9", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectRe(regexp.MustCompile("cache/[0-9A-Fa-f]+/usr/bin/python3").String()) cp.ExpectExitCode(0) cp = ts.SpawnWithOpts( e2e.OptArgs("exec", "echo", "python3", "--path", pythonDir, "--", "--path", "doesNotExist", "--", "extra"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("python3 --path doesNotExist -- extra") cp.ExpectExitCode(0) diff --git a/test/integration/executor_int_test.go b/test/integration/executor_int_test.go index 8bb6aa9a09..f65577d71a 100644 --- a/test/integration/executor_int_test.go +++ b/test/integration/executor_int_test.go @@ -4,9 +4,7 @@ import ( "os" "path/filepath" "testing" - "time" - "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" "github.com/ActiveState/cli/internal/constants" @@ -37,9 +35,9 @@ func (suite *ExecutorIntegrationTestSuite) TestExecutorForwards() { cp = ts.SpawnWithOpts( e2e.OptArgs("shell", "ActiveState-CLI/Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput() cp.SendLine("python3 -c \"import sys; print(sys.copyright)\"") @@ -64,9 +62,9 @@ func (suite *ExecutorIntegrationTestSuite) TestExecutorExitCode() { cp = ts.SpawnWithOpts( e2e.OptArgs("shell", "ActiveState-CLI/Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput() cp.SendLine("python3 -c \"exit(42)\"") diff --git a/test/integration/export_int_test.go b/test/integration/export_int_test.go index 853fa2726a..ab69a38bd2 100644 --- a/test/integration/export_int_test.go +++ b/test/integration/export_int_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" "github.com/ActiveState/termtest" @@ -87,9 +88,9 @@ func (suite *ExportIntegrationTestSuite) TestExport_Env() { ts.PrepareProject("ActiveState-CLI/Export", "5397f645-da8a-4591-b106-9d7fa99545fe") cp := ts.SpawnWithOpts( e2e.OptArgs("export", "env"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect(`PATH: `) + cp.Expect(`PATH: `, e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) suite.Assert().NotContains(cp.Output(), "ACTIVESTATE_ACTIVATED") @@ -107,15 +108,15 @@ func (suite *ExportIntegrationTestSuite) TestJSON() { cp = ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/small-python", "."), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.ExpectExitCode(0, termtest.OptExpectTimeout(120*time.Second)) + cp.ExpectExitCode(0, e2e.RuntimeSourcingTimeoutOpt) cp = ts.SpawnWithOpts( e2e.OptArgs("export", "env", "-o", "json"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.ExpectExitCode(0) + cp.ExpectExitCode(0, e2e.RuntimeSourcingTimeoutOpt) AssertValidJSON(suite.T(), cp) ts.LoginAsPersistentUser() diff --git a/test/integration/history_int_test.go b/test/integration/history_int_test.go index dcf1d96a79..a6f9d15bc9 100644 --- a/test/integration/history_int_test.go +++ b/test/integration/history_int_test.go @@ -6,6 +6,7 @@ import ( "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" + "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" ) @@ -38,7 +39,9 @@ func (suite *HistoryIntegrationTestSuite) TestHistory_History() { cp.Expect("• requests (2.26.0 → 2.7.0)") cp.Expect("• autopip (1.6.0 → Auto)") cp.Expect("+ autopip 1.6.0") + cp.SetLogger(termtest.VerboseLogger) cp.Expect("- convertdate") + cp.SetLogger(termtest.VoidLogger) cp.Expect(`+ Platform`) suite.Assert().NotContains(cp.Output(), "StructuredChanges") cp.ExpectExitCode(0) diff --git a/test/integration/import_int_test.go b/test/integration/import_int_test.go index df3ab98706..3951b0d494 100644 --- a/test/integration/import_int_test.go +++ b/test/integration/import_int_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "io/ioutil" "path/filepath" "runtime" @@ -16,7 +17,7 @@ type ImportIntegrationTestSuite struct { tagsuite.Suite } -func (suite *ImportIntegrationTestSuite) TestImport_headless() { +func (suite *ImportIntegrationTestSuite) TestImport_detached() { suite.OnlyRunForTags(tagsuite.Import) if runtime.GOOS == "darwin" { suite.T().Skip("Skipping mac for now as the builds are still too unreliable") @@ -49,6 +50,44 @@ func (suite *ImportIntegrationTestSuite) TestImport_headless() { cp.ExpectExitCode(0) } +func (suite *ImportIntegrationTestSuite) TestImport() { + suite.OnlyRunForTags(tagsuite.Import) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + username, _ := ts.CreateNewUser() + namespace := fmt.Sprintf("%s/%s", username, "Python3") + + cp := ts.Spawn("init", "--language", "python", namespace, ts.Dirs.Work) + cp.Expect("successfully initialized") + cp.ExpectExitCode(0) + + reqsFilePath := filepath.Join(cp.WorkDirectory(), reqsFileName) + + suite.Run("invalid requirements.txt", func() { + ts.SetT(suite.T()) + ts.PrepareFile(reqsFilePath, badReqsData) + + cp := ts.Spawn("import", "requirements.txt") + cp.ExpectNotExitCode(0) + }) + + suite.Run("valid requirements.txt", func() { + ts.SetT(suite.T()) + ts.PrepareFile(reqsFilePath, reqsData) + + cp := ts.Spawn("import", "requirements.txt") + cp.ExpectExitCode(0) + + cp = ts.Spawn("push") + cp.ExpectExitCode(0) + + cp = ts.Spawn("import", "requirements.txt") + cp.Expect("already exists") + cp.ExpectNotExitCode(0) + }) +} + func TestImportIntegrationTestSuite(t *testing.T) { suite.Run(t, new(ImportIntegrationTestSuite)) } diff --git a/test/integration/init_int_test.go b/test/integration/init_int_test.go index 474396279a..bf5e8fa5eb 100644 --- a/test/integration/init_int_test.go +++ b/test/integration/init_int_test.go @@ -115,9 +115,9 @@ func (suite *InitIntegrationTestSuite) TestInit_InferLanguageFromUse() { cp = ts.SpawnWithOpts( e2e.OptArgs("use", "Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) pname := strutils.UUID() diff --git a/test/integration/install_int_test.go b/test/integration/install_int_test.go new file mode 100644 index 0000000000..7f78ff5846 --- /dev/null +++ b/test/integration/install_int_test.go @@ -0,0 +1,94 @@ +package integration + +import ( + "strings" + "testing" + + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/testhelpers/e2e" + "github.com/ActiveState/cli/internal/testhelpers/tagsuite" + "github.com/stretchr/testify/suite" +) + +type InstallIntegrationTestSuite struct { + tagsuite.Suite +} + +func (suite *InstallIntegrationTestSuite) TestInstall() { + suite.OnlyRunForTags(tagsuite.Install, tagsuite.Critical) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") + cp := ts.SpawnWithOpts(e2e.OptArgs("install", "trender")) + cp.Expect("Package added") + cp.ExpectExitCode(0) +} + +func (suite *InstallIntegrationTestSuite) TestInstall_InvalidCommit() { + suite.OnlyRunForTags(tagsuite.Install) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/small-python", "malformed-commit-id") + cp := ts.SpawnWithOpts(e2e.OptArgs("install", "trender")) + //cp.Expect("Could not find or read the commit file") // re-enable in DX-2307 + cp.Expect("Invalid commit ID") // remove in DX-2307 + cp.ExpectExitCode(1) + + // Re-enable in DX-2307. + //if strings.Count(cp.Snapshot(), " x ") != 1 { + // suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + //} +} + +func (suite *InstallIntegrationTestSuite) TestInstall_NoMatches_NoAlternatives() { + suite.OnlyRunForTags(tagsuite.Install) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") + cp := ts.SpawnWithOpts(e2e.OptArgs("install", "I-dont-exist")) + cp.Expect("No results found for search term") + cp.Expect("find alternatives") // This verifies no alternatives were found + cp.ExpectExitCode(1) + + if strings.Count(strings.ReplaceAll(cp.Snapshot(), " x Failed", ""), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + +func (suite *InstallIntegrationTestSuite) TestInstall_NoMatches_Alternatives() { + suite.OnlyRunForTags(tagsuite.Install) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") + cp := ts.SpawnWithOpts(e2e.OptArgs("install", "database")) + cp.Expect("No results found for search term") + cp.Expect("did you mean") // This verifies alternatives were found + cp.ExpectExitCode(1) + + if strings.Count(strings.ReplaceAll(cp.Snapshot(), " x Failed", ""), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + +func (suite *InstallIntegrationTestSuite) TestInstall_BuildPlannerError() { + suite.OnlyRunForTags(tagsuite.Install) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/small-python", "d8f26b91-899c-4d50-8310-2c338786aa0f") + cp := ts.SpawnWithOpts(e2e.OptArgs("install", "trender@999.0"), e2e.OptAppendEnv(constants.DisableRuntime+"=true")) + cp.Expect("Could not plan build, platform responded with", e2e.RuntimeSourcingTimeoutOpt) + cp.ExpectExitCode(1) + + if strings.Count(strings.ReplaceAll(cp.Snapshot(), " x Failed", ""), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + +func TestInstallIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(InstallIntegrationTestSuite)) +} diff --git a/test/integration/install_scripts_int_test.go b/test/integration/install_scripts_int_test.go index 668a9b94d8..0327904bab 100644 --- a/test/integration/install_scripts_int_test.go +++ b/test/integration/install_scripts_int_test.go @@ -6,13 +6,13 @@ import ( "path/filepath" "runtime" "testing" - "time" - "github.com/ActiveState/termtest" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/thoas/go-funk" + anaConst "github.com/ActiveState/cli/internal/analytics/constants" + "github.com/ActiveState/cli/internal/condition" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" "github.com/ActiveState/cli/internal/fileutils" @@ -66,8 +66,7 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall() { suite.Require().NoError(fileutils.WriteFile(script, b)) // Construct installer command to execute. - installDir := filepath.Join(ts.Dirs.Work, "install") - argsPlain := []string{script, "-t", installDir} + argsPlain := []string{script} if tt.Channel != "" { argsPlain = append(argsPlain, "-b", tt.Channel) } @@ -94,14 +93,16 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall() { if runtime.GOOS != "windows" { cp = ts.SpawnCmdWithOpts( "bash", e2e.OptArgs(argsWithActive...), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), + e2e.OptAppendEnv(fmt.Sprintf("%s=FOO", constants.OverrideSessionTokenEnvVarName)), ) } else { cp = ts.SpawnCmdWithOpts("powershell.exe", e2e.OptArgs(argsWithActive...), e2e.OptAppendEnv("SHELL="), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), + e2e.OptAppendEnv(fmt.Sprintf("%s=FOO", constants.OverrideSessionTokenEnvVarName)), ) } @@ -109,7 +110,7 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall() { if tt.Activate != "" || tt.ActivateByCommand != "" { cp.Expect("Creating a Virtual Environment") - cp.Expect("Quick Start", termtest.OptExpectTimeout(time.Second*60)) + cp.Expect("Quick Start", e2e.RuntimeSourcingTimeoutOpt) // ensure that shell is functional cp.ExpectInput() @@ -117,6 +118,20 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall() { cp.Expect("ActiveState") } + // We get the default install path and use that to directly invoke + // the state tool. This is to avoid inadvertently using the state + // tool that is already on the PATH. + installPath, err := installation.InstallPathForBranch(constants.BranchName) + suite.NoError(err) + + binPath := filepath.Join(installPath, "bin") + + if runtime.GOOS != "windows" { + cp.SendLine("echo $PATH") + } else { + cp.SendLine("echo %PATH%") + } + cp.Expect(installPath) cp.SendLine("state --version") cp.Expect("Version " + constants.Version) cp.Expect("Branch " + constants.BranchName) @@ -125,12 +140,13 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall() { cp.ExpectExitCode(0) - stateExec, err := installation.StateExecFromDir(installDir) + stateExec, err := installation.StateExecFromDir(ts.Dirs.HomeDir) suite.NoError(err) suite.FileExists(stateExec) - suite.assertBinDirContents(filepath.Join(installDir, "bin")) - suite.assertCorrectVersion(ts, installDir, tt.Version, tt.Channel) + suite.assertBinDirContents(binPath) + suite.assertCorrectVersion(ts, binPath, tt.Version, tt.Channel) + suite.assertAnalytics(ts) suite.DirExists(ts.Dirs.Config) // Verify that can install overtop @@ -185,8 +201,10 @@ func (suite *InstallScriptsIntegrationTestSuite) TestInstall_VersionDoesNotExist } else { cp = ts.SpawnCmdWithOpts("powershell.exe", e2e.OptArgs(args...), e2e.OptAppendEnv("SHELL=")) } - cp.Expect("Could not download") - cp.Expect("does-not-exist") + if !condition.OnCI() || runtime.GOOS == "windows" { + // For some reason on Linux and macOS, there is no terminal output on CI. It works locally though. + cp.Expect("Could not download") + } cp.ExpectExitCode(1) } @@ -214,7 +232,7 @@ func scriptPath(t *testing.T, targetDir string) string { func expectStateToolInstallation(cp *e2e.SpawnedCmd) { cp.Expect("Preparing Installer for State Tool Package Manager") - cp.Expect("Installation Complete", termtest.OptExpectTimeout(time.Minute)) + cp.Expect("Installation Complete", e2e.RuntimeSourcingTimeoutOpt) } // assertBinDirContents checks if given files are or are not in the bin directory @@ -257,6 +275,21 @@ func (suite *InstallScriptsIntegrationTestSuite) assertCorrectVersion(ts *e2e.Se } } +func (suite *InstallScriptsIntegrationTestSuite) assertAnalytics(ts *e2e.Session) { + // Verify analytics reported a non-empty sessionToken. + sessionTokenFound := false + events := parseAnalyticsEvents(suite, ts) + suite.Require().NotEmpty(events) + for _, event := range events { + if event.Category == anaConst.CatInstallerFunnel && event.Dimensions != nil { + suite.Assert().NotEmpty(*event.Dimensions.SessionToken) + sessionTokenFound = true + break + } + } + suite.Assert().True(sessionTokenFound, "sessionToken was not found in analytics") +} + func TestInstallScriptsIntegrationTestSuite(t *testing.T) { suite.Run(t, new(InstallScriptsIntegrationTestSuite)) } diff --git a/test/integration/offinstall_int_test.go b/test/integration/offinstall_int_test.go deleted file mode 100644 index 83d21b8cb5..0000000000 --- a/test/integration/offinstall_int_test.go +++ /dev/null @@ -1,509 +0,0 @@ -package integration - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/ActiveState/termtest" - "github.com/mholt/archiver" - - "github.com/ActiveState/cli/internal/analytics/client/sync/reporters" - anaConst "github.com/ActiveState/cli/internal/analytics/constants" - "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/offinstall" - "github.com/ActiveState/cli/internal/osutils" - "github.com/ActiveState/cli/internal/osutils/user" - "github.com/ActiveState/cli/internal/subshell/cmd" - "github.com/ActiveState/cli/internal/testhelpers/e2e" - "github.com/ActiveState/cli/internal/testhelpers/tagsuite" - "github.com/ActiveState/cli/pkg/project" - "github.com/google/uuid" -) - -type OffInstallIntegrationTestSuite struct { - tagsuite.Suite - - installerPath string - uninstallerPath string -} - -const ( - defaultOrg = "ActiveState-Test" - defaultProject = "IntegrationTest" - anotherProject = "Another-IntegrationTest" - defaultArtifactsPayload = "artifacts-payload" - anotherArtifactsPayload = "another-artifacts-payload" - defaultInstalledExecutable = "test-offline-install" - anotherInstalledExecutable = "test-another-offline-install" -) - -func (suite *OffInstallIntegrationTestSuite) TestInstallAndUninstall() { - suite.OnlyRunForTags(tagsuite.OffInstall) - - // Clean up env after test - if runtime.GOOS == "windows" { - env := cmd.NewCmdEnv(true) - origPath, err := env.Get("PATH") - suite.Require().NoError(err) - defer func() { - suite.Require().NoError(env.Set("PATH", origPath)) - }() - } else { - originalPath, exists := os.LookupEnv("PATH") - defer func() { - if !exists { - return - } - suite.Require().NoError(os.Setenv("PATH", originalPath)) - }() - } - - ts := e2e.New(suite.T(), true) - defer ts.Close() - - testReportFilename := filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) - suite.Require().NoFileExists(testReportFilename) - - fmt.Printf("Work dir: %s\n", ts.Dirs.Work) - - suite.preparePayload(ts, defaultArtifactsPayload, defaultProject) - - defaultInstallParentDir, err := offinstall.DefaultInstallParentDir() - suite.Require().NoError(err) - defaultInstallDir := filepath.Join(defaultInstallParentDir, "IntegrationTest") - - env := []string{constants.DisableRuntime + "=false"} - if runtime.GOOS != "windows" { - env = append(env, "SHELL=bash") - } - namespace := project.NewNamespace(defaultOrg, defaultProject, "") - { // Install - suite.runOfflineInstaller(ts, defaultInstallDir, env) - - // Verify that our analytics event was fired - time.Sleep(2 * time.Second) // give time to let rtwatcher detect process has exited - events := parseAnalyticsEvents(suite, ts) - suite.Require().NotEmpty(events) - - heartbeat := suite.filterEvent(events, anaConst.CatRuntimeUsage, anaConst.ActRuntimeHeartbeat) - suite.assertDimensions(heartbeat) - - nDelete := countEvents(events, anaConst.CatRuntimeUsage, anaConst.ActRuntimeDelete, anaConst.SrcOfflineInstaller) - if nDelete != 0 { - suite.FailNow(fmt.Sprintf("Expected 0 delete events, got %d, events:\n%#v", nDelete, events)) - } - - // Ensure shell env is updated - suite.assertShellUpdated(defaultInstallDir, namespace.String(), true, ts) - - // Ensure installation dir looks correct - suite.assertInstallDir(defaultInstallDir, defaultInstalledExecutable, true) - - // Run executable and validate that it has the relocated value - if runtime.GOOS == "windows" { - refreshEnv := filepath.Join(environment.GetRootPathUnsafe(), "test", "integration", "testdata", "tools", "refreshenv", "refreshenv.bat") - tp := ts.SpawnCmd("cmd", "/C", refreshEnv+" && "+defaultInstalledExecutable) - tp.Expect("TEST REPLACEMENT", termtest.OptExpectTimeout(5*time.Second)) - tp.ExpectExitCode(0) - } else { - // Disabled for now: DX-1307 - // tp = ts.SpawnCmd("bash") - // time.Sleep(1 * time.Second) // Give zsh a second to start -- can't use ExpectInput as it doesn't respect a custom HOME dir - // tp.Send("test-offline-install") - // tp.Expect("TEST REPLACEMENT", termtest.OptExpectTimeout(5*time.Second)) - // tp.Send("exit") - // tp.ExpectExitCode(0) - } - } - - { // Uninstall - tp := ts.SpawnCmdWithOpts( - suite.uninstallerPath, - e2e.OptArgs(defaultInstallDir), - e2e.OptAppendEnv(env...), - ) - tp.Expect("continue?") - tp.SendLine("y") - tp.Expect("Uninstall Complete", termtest.OptExpectTimeout(5*time.Second)) - tp.Expect("Press enter to exit") - tp.SendEnter() - tp.ExpectExitCode(0) - - // Ensure shell env is updated - suite.assertShellUpdated(defaultInstallDir, namespace.String(), false, ts) - - // Ensure installation files are removed - suite.assertInstallDir(defaultInstallDir, defaultInstalledExecutable, false) - - // Verify that our analytics event was fired - events := parseAnalyticsEvents(suite, ts) - suite.Require().NotEmpty(events) - nHeartbeat := countEvents(events, anaConst.CatRuntimeUsage, anaConst.ActRuntimeHeartbeat, anaConst.SrcExecutor) - if nHeartbeat != 1 { - suite.FailNow(fmt.Sprintf("Expected 1 heartbeat event, got %d, events:\n%#v", nHeartbeat, events)) - } - del := suite.filterEvent(events, anaConst.CatRuntimeUsage, anaConst.ActRuntimeDelete) - suite.assertDimensions(del) - } -} - -func (suite *OffInstallIntegrationTestSuite) TestInstallNoPermission() { - suite.OnlyRunForTags(tagsuite.OffInstall) - - ts := e2e.New(suite.T(), true) - defer ts.Close() - - suite.preparePayload(ts, defaultArtifactsPayload, defaultProject) - - pathWithNoPermission := "/no-permission" - if runtime.GOOS == "windows" { - pathWithNoPermission = "C:\\Program Files\\No Permission" - } - - tp := ts.SpawnCmdWithOpts( - suite.installerPath, - e2e.OptArgs(pathWithNoPermission), - ) - tp.Expect("Please ensure that the directory is writeable", termtest.OptExpectTimeout(5*time.Second)) - tp.Expect("Press enter to exit", termtest.OptExpectTimeout(5*time.Second)) - tp.SendEnter() - tp.ExpectExitCode(1) -} - -func (suite *OffInstallIntegrationTestSuite) TestInstallMultiple() { - suite.OnlyRunForTags(tagsuite.OffInstall) - - // Clean up env after test - if runtime.GOOS == "windows" { - env := cmd.NewCmdEnv(true) - origPath, err := env.Get("PATH") - suite.Require().NoError(err) - defer func() { - suite.Require().NoError(env.Set("PATH", origPath)) - }() - } else { - originalPath, exists := os.LookupEnv("PATH") - defer func() { - if !exists { - return - } - suite.Require().NoError(os.Setenv("PATH", originalPath)) - }() - } - - ts := e2e.New(suite.T(), true) - defer ts.Close() - - testReportFilename := filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) - suite.Require().NoFileExists(testReportFilename) - - suite.preparePayload(ts, defaultArtifactsPayload, defaultProject) - - defaultInstallParentDir, err := offinstall.DefaultInstallParentDir() - suite.Require().NoError(err) - firstInstallDir := filepath.Join(defaultInstallParentDir, "IntegrationTest") - secondInstallDir := filepath.Join(defaultInstallParentDir, "Another-IntegrationTest") - - firstNamespace := project.NewNamespace(defaultOrg, defaultProject, "") - secondNamespace := project.NewNamespace(defaultOrg, anotherProject, "") - - env := []string{constants.DisableRuntime + "=false"} - if runtime.GOOS != "windows" { - env = append(env, "SHELL=bash") - } - - // Run offline installer for first project - suite.runOfflineInstaller(ts, firstInstallDir, env) - - // Prepare new payload and run offline installer for second project - suite.preparePayload(ts, anotherArtifactsPayload, anotherProject) - suite.runOfflineInstaller(ts, secondInstallDir, env) - - // Assert first projects updates are still in place - suite.assertShellUpdated(firstInstallDir, firstNamespace.String(), true, ts) - suite.assertInstallDir(firstInstallDir, defaultInstalledExecutable, true) - - // Assert second projects updates are also in place - suite.assertShellUpdated(secondInstallDir, firstNamespace.String(), true, ts) - suite.assertInstallDir(secondInstallDir, anotherInstalledExecutable, true) - - // Uninstall first project - suite.runOfflineUninstaller(ts, firstInstallDir, env) - - // Assert first project's update are removed - suite.assertShellUpdated(firstInstallDir, firstNamespace.String(), false, ts) - - // Assert first project's installation files are removed - suite.assertInstallDir(firstInstallDir, defaultInstalledExecutable, false) - - // Uninstall second project - suite.runOfflineUninstaller(ts, secondInstallDir, env) - - // Assert second project's update are removed - suite.assertShellUpdated(secondInstallDir, secondNamespace.String(), false, ts) - - // Assert second project's installation files are removed - suite.assertInstallDir(secondInstallDir, anotherInstalledExecutable, false) -} - -func (suite *OffInstallIntegrationTestSuite) TestInstallTwice() { - suite.OnlyRunForTags(tagsuite.OffInstall) - - // Clean up env after test - if runtime.GOOS == "windows" { - env := cmd.NewCmdEnv(true) - origPath, err := env.Get("PATH") - suite.Require().NoError(err) - defer func() { - suite.Require().NoError(env.Set("PATH", origPath)) - }() - } else { - originalPath, exists := os.LookupEnv("PATH") - defer func() { - if !exists { - return - } - suite.Require().NoError(os.Setenv("PATH", originalPath)) - }() - } - - ts := e2e.New(suite.T(), true) - defer ts.Close() - - suite.preparePayload(ts, defaultArtifactsPayload, defaultProject) - - defaultInstallParentDir, err := offinstall.DefaultInstallParentDir() - suite.Require().NoError(err) - defaultInstallDir := filepath.Join(defaultInstallParentDir, "IntegrationTest") - - env := []string{constants.DisableRuntime + "=false"} - if runtime.GOOS != "windows" { - env = append(env, "SHELL=bash") - } - - suite.runOfflineInstaller(ts, defaultInstallDir, env) - - // Running offline installer again should not cause an error - tp := ts.SpawnCmdWithOpts( - suite.installerPath, - e2e.OptArgs(defaultInstallDir), - e2e.OptAppendEnv(env...), - ) - tp.Expect("Installation directory is not empty") - tp.Send("y") - tp.Expect("Do you accept the ActiveState Runtime Installer License Agreement? (y/N)", termtest.OptExpectTimeout(5*time.Second)) - tp.Send("y") - tp.Expect("Extracting", termtest.OptExpectTimeout(time.Second)) - tp.Expect("Installation complete") - tp.Expect("Press enter to exit") - tp.SendEnter() - tp.ExpectExitCode(0) - - // Uninstall - suite.runOfflineUninstaller(ts, defaultInstallDir, env) -} - -func (suite *OffInstallIntegrationTestSuite) runOfflineInstaller(ts *e2e.Session, installDir string, env []string) { - tp := ts.SpawnCmdWithOpts( - suite.installerPath, - e2e.OptArgs(installDir), - e2e.OptAppendEnv(env...), - ) - tp.Expect("Do you accept the ActiveState Runtime Installer License Agreement? (y/N)", termtest.OptExpectTimeout(5*time.Second)) - tp.Send("y") - tp.Expect("Extracting", termtest.OptExpectTimeout(time.Second)) - tp.Expect("Installing") - tp.Expect("Installation complete") - tp.Expect("Press enter to exit") - tp.SendEnter() - tp.ExpectExitCode(0) -} - -func (suite *OffInstallIntegrationTestSuite) runOfflineUninstaller(ts *e2e.Session, installDir string, env []string) { - tp := ts.SpawnCmdWithOpts( - suite.uninstallerPath, - e2e.OptArgs(installDir), - e2e.OptAppendEnv(env...), - ) - tp.Expect("continue?") - tp.SendLine("y") - tp.Expect("Uninstall Complete", termtest.OptExpectTimeout(5*time.Second)) - tp.Expect("Press enter to exit") - tp.SendEnter() - tp.ExpectExitCode(0) -} - -func (suite *OffInstallIntegrationTestSuite) preparePayload(ts *e2e.Session, payloadName, projectName string) { - root := environment.GetRootPathUnsafe() - - suffix := "-windows" - if runtime.GOOS != "windows" { - suffix = "-nix" - } - - // The payload is an artifact that contains mocked installation files - payloadPath := filepath.Join(root, "test", "integration", "testdata", "offline-install", payloadName+suffix, "artifact") - - // The asset path contains additional files that we want to embed into the executable, such as the license - assetPath := filepath.Join(root, "test", "integration", "testdata", "offline-install", "assets", projectName) - - // The payload archive is effectively double encrypted. We have the artifact itself, as well as the archive that - // wraps it. Our test code only has one artifact, but in the wild there can and most likely will be multiple - artifactMockPath := filepath.Join(ts.Dirs.Work, uuid.New().String()+".tar.gz") - payloadMockPath := filepath.Join(ts.Dirs.Work, "artifacts.tar.gz") - - // The paths of the installer and uninstaller - suite.installerPath = filepath.Join(ts.Dirs.Bin, "offline-installer"+exeutils.Extension) - suite.uninstallerPath = filepath.Join(ts.Dirs.Bin, "uninstall"+exeutils.Extension) - - archiver := archiver.NewTarGz() - { // Create the artifact archive - err := archiver.Archive(fileutils.ListFilesUnsafe(payloadPath), artifactMockPath) - suite.Require().NoError(err) - } - - { // Create the payload archive which contains the artifact - if fileutils.TargetExists(payloadMockPath) { - err := os.RemoveAll(payloadMockPath) - suite.Require().NoError(err) - } - err := archiver.Archive([]string{artifactMockPath}, payloadMockPath) - suite.Require().NoError(err) - } - - { // Use a distinct copy of the installer to test with - err := fileutils.CopyFile(filepath.Join(root, "build", "offline", "offline-installer"+exeutils.Extension), suite.installerPath) - suite.Require().NoError(err) - } - - { // Use a distinct copy of the uninstaller to test with - err := fileutils.CopyFile(filepath.Join(root, "build", "offline", "uninstall"+exeutils.Extension), suite.uninstallerPath) - suite.Require().NoError(err) - } - - // Copy all assets to same dir so gozip doesn't include their relative or absolute paths - buildPath := filepath.Join(ts.Dirs.Work, "build") - suite.Require().NoError(fileutils.MkdirUnlessExists(buildPath)) - suite.Require().NoError(fileutils.CopyMultipleFiles(map[string]string{ - payloadMockPath: filepath.Join(buildPath, filepath.Base(payloadMockPath)), - filepath.Join(assetPath, "installer_config.json"): filepath.Join(buildPath, "installer_config.json"), - filepath.Join(assetPath, "LICENSE.txt"): filepath.Join(buildPath, "LICENSE.txt"), - suite.uninstallerPath: filepath.Join(buildPath, filepath.Base(suite.uninstallerPath)), - })) - - // Append our assets to the installer executable - tp := ts.SpawnCmdWithOpts("gozip", - e2e.OptWD(buildPath), - e2e.OptArgs( - "-c", suite.installerPath, - filepath.Base(payloadMockPath), - "installer_config.json", - "LICENSE.txt", - filepath.Base(suite.uninstallerPath), - ), - ) - tp.ExpectExitCode(0) - - suite.Require().NoError(os.Chmod(suite.installerPath, 0775)) // ensure file is executable - suite.Require().NoError(os.Chmod(suite.uninstallerPath, 0775)) // ensure file is executable -} - -func (suite *OffInstallIntegrationTestSuite) assertShellUpdated(dir, namespace string, exists bool, ts *e2e.Session) { - if runtime.GOOS != "windows" { - // Test bashrc - homeDir, err := user.HomeDir() - suite.Require().NoError(err) - - fname := ".bashrc" - if runtime.GOOS == "darwin" { - fname = ".bash_profile" - } - - assert := suite.Contains - if !exists { - assert = suite.NotContains - } - - fpath := filepath.Join(homeDir, fname) - rcContents := fileutils.ReadFileUnsafe(fpath) - assert(string(rcContents), fmt.Sprintf("%s-%s", constants.RCAppendOfflineInstallStartLine, namespace), fpath) - assert(string(rcContents), fmt.Sprintf("%s-%s", constants.RCAppendOfflineInstallStopLine, namespace), fpath) - assert(string(rcContents), dir) - } else { - // It seems there is a race condition with updating the registry and asserting it was updated - time.Sleep(time.Second) - - // Test registry - isAdmin, err := osutils.IsAdmin() - suite.Require().NoError(err) - regKey := `HKCU\Environment` - if isAdmin { - regKey = `HKLM\SYSTEM\ControlSet001\Control\Session Manager\Environment` - } - out, err := exec.Command("reg", "query", regKey, "/v", "Path").Output() - suite.Require().NoError(err) - - assert := strings.Contains - if !exists { - assert = func(s, substr string) bool { - return !strings.Contains(s, substr) - } - } - - // we need to look for the short and the long version of the target PATH, because Windows translates between them arbitrarily - shortPath, _ := fileutils.GetShortPathName(dir) - longPath, _ := fileutils.GetLongPathName(dir) - if !assert(string(out), shortPath) && !assert(string(out), longPath) && !assert(string(out), dir) { - suite.T().Errorf("registry PATH \"%s\" validation failed for \"%s\", \"%s\" or \"%s\", should contain: %v", out, dir, shortPath, longPath, exists) - } - } -} - -func (suite *OffInstallIntegrationTestSuite) filterEvent(events []reporters.TestLogEntry, category string, action string) reporters.TestLogEntry { - ev := filterEvents(events, func(e reporters.TestLogEntry) bool { - return e.Category == category && e.Action == action - }) - suite.Require().Len(ev, 1) - return ev[0] -} - -func (suite *OffInstallIntegrationTestSuite) assertInstallDir(dir, executable string, exists bool) { - assert := suite.Require().FileExists - if !exists { - assert = suite.Require().NoFileExists - } - if runtime.GOOS == "windows" { - assert(filepath.Join(dir, "bin", fmt.Sprintf("%s.bat", executable))) - } else { - assert(filepath.Join(dir, "bin", fmt.Sprintf("%s", executable))) - } - if runtime.GOOS == "windows" { - assert(filepath.Join(dir, "bin", "shell.bat")) - } - assert(filepath.Join(dir, "LICENSE.txt")) -} - -func (suite *OffInstallIntegrationTestSuite) assertDimensions(event reporters.TestLogEntry) { - evdbg, err := json.Marshal(event) - suite.Require().NoError(err) - dbg := fmt.Sprintf("Event: %s", string(evdbg)) - suite.Require().NotNil(event.Dimensions.ProjectNameSpace, dbg) - suite.Require().NotNil(event.Dimensions.CommitID, dbg) - suite.Require().Equal("ActiveState-Test/IntegrationTest", *event.Dimensions.ProjectNameSpace) - suite.Require().Equal("00000000-0000-0000-0000-000000000000", *event.Dimensions.CommitID) -} - -func TestOffInstallIntegrationTestSuite(t *testing.T) { - t.Skip("Skipping offline installer tests as they will soon live in a separate repo") - // suite.Run(t, new(OffInstallIntegrationTestSuite)) -} diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index 6fc2e8bcba..dcf7ea4abb 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -2,7 +2,6 @@ package integration import ( "fmt" - "io/ioutil" "path/filepath" "runtime" "testing" @@ -88,7 +87,7 @@ func (suite *PackageIntegrationTestSuite) TestPackages_project_invalid() { defer ts.Close() cp := ts.Spawn("packages", "--namespace", "junk/junk") - cp.Expect("The requested project junk/junk could not be found") + cp.Expect("The requested project junk does not exist under junk") cp.ExpectExitCode(1) } @@ -292,46 +291,7 @@ six==1.14.0 ` ) -func (suite *PackageIntegrationTestSuite) TestPackage_import() { - suite.OnlyRunForTags(tagsuite.Package) - ts := e2e.New(suite.T(), false) - defer ts.Close() - - username, _ := ts.CreateNewUser() - namespace := fmt.Sprintf("%s/%s", username, "Python3") - - cp := ts.Spawn("init", "--language", "python", namespace, ts.Dirs.Work) - cp.Expect("successfully initialized") - cp.ExpectExitCode(0) - - reqsFilePath := filepath.Join(cp.WorkDirectory(), reqsFileName) - - suite.Run("invalid requirements.txt", func() { - ts.SetT(suite.T()) - ts.PrepareFile(reqsFilePath, badReqsData) - - cp := ts.Spawn("import", "requirements.txt") - cp.ExpectNotExitCode(0) - }) - - suite.Run("valid requirements.txt", func() { - ts.SetT(suite.T()) - ts.PrepareFile(reqsFilePath, reqsData) - - cp := ts.Spawn("import", "requirements.txt") - cp.ExpectExitCode(0) - - cp = ts.Spawn("push") - cp.ExpectExitCode(0) - - cp = ts.Spawn("import", "requirements.txt") - cp.Expect("Are you sure") - cp.SendLine("n") - cp.ExpectNotExitCode(0) - }) -} - -func (suite *PackageIntegrationTestSuite) TestPackage_headless_operation() { +func (suite *PackageIntegrationTestSuite) TestPackage_detached_operation() { suite.OnlyRunForTags(tagsuite.Package) if runtime.GOOS == "darwin" { suite.T().Skip("Skipping mac for now as the builds are still too unreliable") @@ -448,36 +408,6 @@ scripts: ts.PrepareCommitIdFile("a9d0bc88-585a-49cf-89c1-6c07af781cff") } -func (suite *PackageIntegrationTestSuite) TestInstall_Empty() { - suite.OnlyRunForTags(tagsuite.Package) - if runtime.GOOS == "darwin" { - suite.T().Skip("Skipping mac for now as the builds are still too unreliable") - return - } - - ts := e2e.New(suite.T(), false) - defer ts.Close() - - cp := ts.SpawnWithOpts( - e2e.OptArgs("install", "JSON"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), - ) - cp.Expect("Installing Package") - cp.ExpectExitCode(0, termtest.OptExpectTimeout(120*time.Second)) - - configFilepath := filepath.Join(ts.Dirs.Work, constants.ConfigFileName) - suite.Require().FileExists(configFilepath) - - content, err := ioutil.ReadFile(configFilepath) - suite.Require().NoError(err) - if !suite.Contains(string(content), constants.DashboardCommitURL) { - suite.Fail("activestate.yaml does not contain dashboard commit URL") - } - - commitIdFile := filepath.Join(ts.Dirs.Work, constants.ProjectConfigDirName, constants.CommitIdFileName) - suite.Assert().FileExists(commitIdFile) -} - func (suite *PackageIntegrationTestSuite) TestPackage_UninstallDoesNotExist() { suite.OnlyRunForTags(tagsuite.Package) @@ -502,10 +432,17 @@ func (suite *PackageIntegrationTestSuite) TestJSON() { AssertValidJSON(suite.T(), cp) cp = ts.SpawnWithOpts( - e2e.OptArgs("install", "Text-CSV", "--output", "editor"), + e2e.OptArgs("checkout", "ActiveState-CLI/Packages-Perl", "."), e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), ) - cp.Expect(`{"name":"Text-CSV"`, termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Checked out project") + cp.ExpectExitCode(0) + + cp = ts.SpawnWithOpts( + e2e.OptArgs("install", "Text-CSV", "--output", "editor"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), + ) + cp.Expect(`{"name":"Text-CSV"`, e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) AssertValidJSON(suite.T(), cp) @@ -516,9 +453,9 @@ func (suite *PackageIntegrationTestSuite) TestJSON() { cp = ts.SpawnWithOpts( e2e.OptArgs("uninstall", "Text-CSV", "-o", "json"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect(`{"name":"Text-CSV"`) + cp.Expect(`{"name":"Text-CSV"`, e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) AssertValidJSON(suite.T(), cp) } @@ -545,7 +482,7 @@ func (suite *PackageIntegrationTestSuite) TestNormalize() { cp = ts.SpawnWithOpts( e2e.OptArgs("install", "Charset_normalizer"), e2e.OptWD(dir), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("charset-normalizer") cp.Expect("is different") @@ -565,7 +502,7 @@ func (suite *PackageIntegrationTestSuite) TestNormalize() { cp = ts.SpawnWithOpts( e2e.OptArgs("install", "charset-normalizer"), e2e.OptWD(anotherDir), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("charset-normalizer") cp.ExpectExitCode(0) @@ -586,7 +523,9 @@ func (suite *PackageIntegrationTestSuite) TestInstall_InvalidVersion() { e2e.OptArgs("install", "pytest@999.9999.9999"), e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Error occurred while trying to create a commit") + // User facing error from build planner + // We only assert the state tool curated part of the error as the underlying build planner error may change + cp.Expect("Could not plan build") cp.ExpectExitCode(1) } @@ -607,7 +546,9 @@ func (suite *PackageIntegrationTestSuite) TestUpdate_InvalidVersion() { e2e.OptArgs("install", "pytest@999.9999.9999"), // update e2e.OptAppendEnv(constants.DisableRuntime+"=false"), // We DO want to test the runtime part, just not for every step ) - cp.Expect("Error occurred while trying to create a commit") + // User facing error from build planner + // We only assert the state tool curated part of the error as the underlying build planner error may change + cp.Expect("Could not plan build") cp.ExpectExitCode(1) } diff --git a/test/integration/performance_int_test.go b/test/integration/performance_int_test.go index 173f731cee..a3e6c7a11b 100644 --- a/test/integration/performance_int_test.go +++ b/test/integration/performance_int_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/exeutils" "github.com/ActiveState/termtest" @@ -54,7 +55,7 @@ func performanceTest(commands []string, expect string, samples int, maxTime time for x := 0; x < samples+1; x++ { opts := []e2e.SpawnOptSetter{ e2e.OptArgs(commands...), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_UPDATES=true", "ACTIVESTATE_PROFILE=true"), + e2e.OptAppendEnv(constants.DisableUpdates+"=true", constants.ProfileEnvVarName+"=true"), } termtestLogs := &bytes.Buffer{} if verbose { diff --git a/test/integration/prepare_int_test.go b/test/integration/prepare_int_test.go index fec3affb7e..48f165886a 100644 --- a/test/integration/prepare_int_test.go +++ b/test/integration/prepare_int_test.go @@ -7,9 +7,7 @@ import ( "path/filepath" "runtime" "testing" - "time" - "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" svcApp "github.com/ActiveState/cli/cmd/state-svc/app" @@ -52,7 +50,7 @@ func (suite *PrepareIntegrationTestSuite) TestPrepare() { cp := ts.SpawnWithOpts( e2e.OptArgs("_prepare"), e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.AutostartPathOverrideEnvVarName, autostartDir)), - // e2e.OptAppendEnv(fmt.Sprintf("ACTIVESTATE_CLI_CONFIGDIR=%s", ts.Dirs.Work)), + // e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.ConfigEnvVarName, ts.Dirs.Work)), ) cp.ExpectExitCode(0) @@ -121,7 +119,7 @@ func (suite *PrepareIntegrationTestSuite) AssertConfig(target string) { func (suite *PrepareIntegrationTestSuite) TestResetExecutors() { suite.OnlyRunForTags(tagsuite.Prepare) - ts := e2e.New(suite.T(), true, "ACTIVESTATE_CLI_DISABLE_RUNTIME=false") + ts := e2e.New(suite.T(), true, constants.DisableRuntime+"=false") err := ts.ClearCache() suite.Require().NoError(err) defer ts.Close() @@ -132,7 +130,7 @@ func (suite *PrepareIntegrationTestSuite) TestResetExecutors() { cp.Expect("This project will always be available for use") cp.Expect("Downloading") cp.Expect("Installing") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.SendLine("exit") cp.ExpectExitCode(0) @@ -164,7 +162,7 @@ func (suite *PrepareIntegrationTestSuite) TestResetExecutors() { err = os.RemoveAll(projectExecDir) cp = ts.Spawn("activate") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.SendLine("which python3") cp.SendLine("python3 --version") cp.Expect("ActiveState") diff --git a/test/integration/progress_int_test.go b/test/integration/progress_int_test.go index 7f772287bb..998d4bdd56 100644 --- a/test/integration/progress_int_test.go +++ b/test/integration/progress_int_test.go @@ -3,6 +3,7 @@ package integration import ( "testing" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" @@ -20,20 +21,20 @@ func (suite *ProgressIntegrationTestSuite) TestProgress() { cp := ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/small-python"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect(locale.T("setup_runtime")) - cp.Expect("Checked out") + cp.Expect("Checked out", e2e.RuntimeSourcingTimeoutOpt) suite.Assert().NotContains(cp.Output(), "...") cp.ExpectExitCode(0) cp = ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/small-python", "small-python2", "--non-interactive"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect(locale.T("setup_runtime")) cp.Expect("...") - cp.Expect("Checked out") + cp.Expect("Checked out", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) } diff --git a/test/integration/project_migration_int_test.go b/test/integration/project_migration_int_test.go new file mode 100644 index 0000000000..494b081a42 --- /dev/null +++ b/test/integration/project_migration_int_test.go @@ -0,0 +1,87 @@ +package integration + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/fileutils" + "github.com/ActiveState/cli/internal/testhelpers/e2e" + "github.com/ActiveState/cli/internal/testhelpers/tagsuite" + "github.com/stretchr/testify/suite" +) + +type ProjectMigrationIntegrationTestSuite struct { + tagsuite.Suite +} + +func (suite *ProjectMigrationIntegrationTestSuite) TestPromptMigration() { + suite.OnlyRunForTags(tagsuite.Critical) + suite.T().Skip("Temporarily disabling project migration until DX-2307") // remove in DX-2307 + ts := e2e.New(suite.T(), false) + defer ts.Close() + + commitID := "9090c128-e948-4388-8f7f-96e2c1e00d98" + ts.PrepareActiveStateYAML(`project: https://platform.activestate.com/ActiveState-CLI/test?commitID=` + commitID) + suite.Require().NoError(fileutils.Mkdir(filepath.Join(ts.Dirs.Work, ".git")), "could not mimic this being a git repo") + + // Verify the user is prompted to migrate an unmigrated project. + cp := ts.SpawnWithOpts( + e2e.OptArgs("packages"), + e2e.OptAppendEnv(constants.DisableProjectMigrationPrompt+"=false"), + ) + cp.Expect("migrate") + cp.Expect("? (y/N)") + cp.SendEnter() + cp.Expect("declined") + + // Verify that read-only actions still work for unmigrated projects. + cp.Expect("pylint") + cp.Expect("pytest") + cp.ExpectExitCode(0) + + // Verify activestate.yaml remains unchanged and a .activestate/commit was not created, nor was a + // .gitignore created. + bytes := fileutils.ReadFileUnsafe(filepath.Join(ts.Dirs.Work, constants.ConfigFileName)) + suite.Assert().Contains(string(bytes), commitID, "as.yaml was migrated and does not still contain commitID") + projectConfigDir := filepath.Join(ts.Dirs.Work, constants.ProjectConfigDirName) + suite.Assert().NoDirExists(projectConfigDir, ".activestate dir was created") + gitignoreFile := filepath.Join(ts.Dirs.Work, ".gitignore") + suite.Assert().NoFileExists(gitignoreFile, ".gitignore was created") + + // Verify that migration works. + cp = ts.SpawnWithOpts( + e2e.OptArgs("packages"), + e2e.OptAppendEnv(constants.DisableProjectMigrationPrompt+"=false"), + ) + cp.Expect("migrate") + cp.Expect("? (y/N)") + cp.SendLine("Y") + cp.Expect("success") + + cp.Expect("pylint") + cp.Expect("pytest") + cp.ExpectExitCode(0) + + // Verify .activestate/commit and .gitignore were created. + suite.Require().True(fileutils.DirExists(projectConfigDir), ",migration should have created "+projectConfigDir) + commitIDFile := filepath.Join(projectConfigDir, constants.CommitIdFileName) + suite.Assert().True(fileutils.FileExists(commitIDFile), "commit file not created") + suite.Assert().Contains(string(fileutils.ReadFileUnsafe(commitIDFile)), commitID, "migration did not populate .activestate/commit") + suite.Assert().True(fileutils.FileExists(gitignoreFile), "migration did not create .gitignore") + suite.Assert().Contains(string(fileutils.ReadFileUnsafe(gitignoreFile)), fmt.Sprintf("%s/%s", constants.ProjectConfigDirName, constants.CommitIdFileName), "commit file not added to .gitignore") + + // Verify no prompt for migrated project. + cp = ts.SpawnWithOpts( + e2e.OptArgs("packages"), + e2e.OptAppendEnv(constants.DisableProjectMigrationPrompt+"=false"), + ) + cp.Expect("pylint") + cp.Expect("pytest") + cp.ExpectExitCode(0) +} + +func TestProjectMigrationIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(ProjectMigrationIntegrationTestSuite)) +} diff --git a/test/integration/projects_int_test.go b/test/integration/projects_int_test.go index c514f65b57..80393b4068 100644 --- a/test/integration/projects_int_test.go +++ b/test/integration/projects_int_test.go @@ -148,7 +148,7 @@ func (suite *ProjectsIntegrationTestSuite) TestEdit_Visibility() { ts.LogoutUser() cp = ts.Spawn("checkout", namespace) - cp.Expect("Could not checkout") + cp.Expect("does not exist under ActiveState-CLI") cp.ExpectExitCode(1) ts.LoginAsPersistentUser() diff --git a/test/integration/pull_int_test.go b/test/integration/pull_int_test.go index b4ff02e38d..eaa4eb08a8 100644 --- a/test/integration/pull_int_test.go +++ b/test/integration/pull_int_test.go @@ -12,6 +12,7 @@ import ( "github.com/ActiveState/cli/internal/testhelpers/tagsuite" "github.com/ActiveState/cli/pkg/platform/runtime/buildscript" "github.com/ActiveState/cli/pkg/project" + "github.com/ActiveState/cli/pkg/projectfile" // remove in DX-2307 "github.com/stretchr/testify/suite" ) @@ -24,7 +25,7 @@ func (suite *PullIntegrationTestSuite) TestPull() { ts := e2e.New(suite.T(), false) defer ts.Close() - ts.PrepareProject("ActiveState-CLI/Python3", "") + ts.PrepareProject("ActiveState-CLI/Python3", "59404293-e5a9-4fd0-8843-77cd4761b5b5") cp := ts.Spawn("pull") cp.Expect("Operating on project") @@ -32,9 +33,10 @@ func (suite *PullIntegrationTestSuite) TestPull() { cp.Expect("activestate.yaml has been updated") cp.ExpectExitCode(0) - projectConfigDir := filepath.Join(ts.Dirs.Work, constants.ProjectConfigDirName) - suite.Require().True(fileutils.DirExists(projectConfigDir)) - suite.Assert().True(fileutils.FileExists(filepath.Join(projectConfigDir, constants.CommitIdFileName))) + // Re-enable this block in DX-2307. + //projectConfigDir := filepath.Join(ts.Dirs.Work, constants.ProjectConfigDirName) + //suite.Require().True(fileutils.DirExists(projectConfigDir)) + //suite.Assert().True(fileutils.FileExists(filepath.Join(projectConfigDir, constants.CommitIdFileName))) cp = ts.Spawn("pull") cp.Expect("already up to date") @@ -74,7 +76,7 @@ func (suite *PullIntegrationTestSuite) TestPullSetProjectUnrelated() { cp.ExpectNotExitCode(0) cp = ts.Spawn("pull", "--non-interactive", "--set-project", "ActiveState-CLI/Python3") - cp.Expect("Could not detect common parent") + cp.Expect("no common base") cp.ExpectExitCode(1) } @@ -90,9 +92,14 @@ func (suite *PullIntegrationTestSuite) TestPull_Merge() { pjfilepath := filepath.Join(ts.Dirs.Work, "cli", constants.ConfigFileName) err := fileutils.WriteFile(pjfilepath, []byte(projectLine)) suite.Require().NoError(err) - commitIdFile := filepath.Join(ts.Dirs.Work, "cli", constants.ProjectConfigDirName, constants.CommitIdFileName) - err = fileutils.WriteFile(commitIdFile, []byte(unPulledCommit)) + // Remove the following lines in DX-2307. + pjfile, err := projectfile.Parse(pjfilepath) suite.Require().NoError(err) + suite.Require().NoError(pjfile.LegacySetCommit(unPulledCommit)) + // Re-enable the following lines in DX-2307. + //commitIdFile := filepath.Join(ts.Dirs.Work, "cli", constants.ProjectConfigDirName, constants.CommitIdFileName) + //err = fileutils.WriteFile(commitIdFile, []byte(unPulledCommit)) + //suite.Require().NoError(err) ts.LoginAsPersistentUser() @@ -123,7 +130,7 @@ func (suite *PullIntegrationTestSuite) TestPull_RestoreNamespace() { // Attempt to update to unrelated project. cp := ts.Spawn("pull", "--non-interactive", "--set-project", "ActiveState-CLI/Python3") - cp.Expect("Could not detect common parent") + cp.Expect("no common base") cp.ExpectNotExitCode(0) // Verify namespace is unchanged. @@ -134,6 +141,7 @@ func (suite *PullIntegrationTestSuite) TestPull_RestoreNamespace() { func (suite *PullIntegrationTestSuite) TestMergeBuildScript() { suite.OnlyRunForTags(tagsuite.Pull) + suite.T().Skip("Temporarily disable buildscripts until DX-2307") // remove in DX-2307 ts := e2e.New(suite.T(), false) defer ts.Close() @@ -146,9 +154,9 @@ func (suite *PullIntegrationTestSuite) TestMergeBuildScript() { cp = ts.SpawnWithOpts( e2e.OptArgs("install", "requests"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Package added") + cp.Expect("Package added", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) proj, err := project.FromPath(ts.Dirs.Work) diff --git a/test/integration/push_int_test.go b/test/integration/push_int_test.go index 6b85f2bf2a..ead847ecbb 100644 --- a/test/integration/push_int_test.go +++ b/test/integration/push_int_test.go @@ -13,10 +13,10 @@ import ( "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/fileutils" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/strutils" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" - "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/project" "github.com/ActiveState/cli/pkg/projectfile" ) @@ -73,7 +73,7 @@ func (suite *PushIntegrationTestSuite) TestInitAndPush() { // Check that languages were reset pjfile, err := projectfile.Parse(pjfilepath) suite.Require().NoError(err) - commitID, err := localcommit.Get(filepath.Join(ts.Dirs.Work, namespace)) + commitID, err := commitmediator.Get(pjfile) suite.Require().NoError(err) suite.Require().NotEmpty(commitID.String(), "commitID was not set after running push for project creation") suite.Require().NotEmpty(pjfile.BranchName(), "branch was not set after running push for project creation") @@ -105,58 +105,6 @@ func (suite *PushIntegrationTestSuite) TestInitAndPush() { cp.ExpectExitCode(0) } -// Test pushing to a new project from a headless commit -func (suite *PushIntegrationTestSuite) TestPush_HeadlessConvert_NewProject() { - if runtime.GOOS == "windows" { - suite.T().Skip("Skipped on Windows for now because SendKeyDown() doesnt work (regardless of bash/cmd)") - } - - suite.OnlyRunForTags(tagsuite.Push) - ts := e2e.New(suite.T(), false) - defer ts.Close() - ts.LoginAsPersistentUser() - pname := strutils.UUID() - namespace := fmt.Sprintf("%s/%s", suite.username, pname) - - cp := ts.SpawnWithOpts(e2e.OptArgs("install", suite.extraPackage)) - - cp.Expect("An activestate.yaml has been created", termtest.OptExpectTimeout(time.Second*40)) - switch runtime.GOOS { - case "darwin": - cp.ExpectRe("added|being built", termtest.OptExpectTimeout(60*time.Second)) // while cold storage is off - cp.Wait() - default: - cp.Expect("added", termtest.OptExpectTimeout(60*time.Second)) - cp.ExpectExitCode(0) - } - - pjfilepath := filepath.Join(ts.Dirs.Work, constants.ConfigFileName) - pjfile, err := projectfile.Parse(pjfilepath) - suite.Require().NoError(err) - if !strings.Contains(pjfile.Project, "/commit/") { - suite.FailNow("project field should be headless but isn't: " + pjfile.Project) - } - - cp = ts.SpawnWithOpts(e2e.OptArgs("push")) - cp.Expect("Who would you like the owner of this project to be?") - cp.SendEnter() - cp.Expect("What would you like the name of this project to be?") - cp.SendKeyDown() - cp.Expect("> Other") - cp.SendEnter() - cp.Expect(">") - cp.SendLine(pname.String()) - cp.Expect("Project created") - cp.ExpectExitCode(0) - ts.NotifyProjectCreated(suite.username, pname.String()) - - pjfile, err = projectfile.Parse(pjfilepath) - suite.Require().NoError(err) - if !strings.Contains(pjfile.Project, fmt.Sprintf("/%s?", namespace)) { - suite.FailNow("project field should include project again: " + pjfile.Project) - } -} - // Test pushing without permission, and choosing to create a new project func (suite *PushIntegrationTestSuite) TestPush_NoPermission_NewProject() { if runtime.GOOS == "windows" { @@ -192,7 +140,7 @@ func (suite *PushIntegrationTestSuite) TestPush_NoPermission_NewProject() { cp = ts.SpawnWithOpts(e2e.OptArgs("push")) cp.Expect("not authorized") - cp.Expect("(y/N)") + cp.Expect("(Y/n)") cp.SendLine("y") cp.Expect("Who would you like the owner of this project to be?") cp.SendEnter() @@ -225,10 +173,10 @@ func (suite *PushIntegrationTestSuite) TestCarlisle() { e2e.OptArgs( "activate", suite.baseProject, "--path", wd), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) // The activestate.yaml on Windows runs custom activation to set shortcuts and file associations. - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.SendLine("exit") cp.ExpectExitCode(0) @@ -240,13 +188,14 @@ func (suite *PushIntegrationTestSuite) TestCarlisle() { cp = ts.SpawnWithOpts(e2e.OptArgs( "install", suite.extraPackage), e2e.OptWD(wd), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false")) + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), + ) switch runtime.GOOS { case "darwin": - cp.ExpectRe("added|being built", termtest.OptExpectTimeout(60*time.Second)) // while cold storage is off + cp.ExpectRe("added|being built", e2e.RuntimeSourcingTimeoutOpt) // while cold storage is off cp.Wait() default: - cp.Expect("added", termtest.OptExpectTimeout(60*time.Second)) + cp.Expect("added", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) } @@ -264,6 +213,142 @@ func (suite *PushIntegrationTestSuite) TestCarlisle() { ts.NotifyProjectCreated(suite.username, pname.String()) } +func (suite *PushIntegrationTestSuite) TestPush_NoProject() { + suite.OnlyRunForTags(tagsuite.Push) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.LoginAsPersistentUser() + cp := ts.SpawnWithOpts(e2e.OptArgs("push")) + cp.Expect("No project found") + cp.ExpectExitCode(1) + + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + +func (suite *PushIntegrationTestSuite) TestPush_NoAuth() { + suite.OnlyRunForTags(tagsuite.Push) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/cli", "882ae76e-fbb7-4989-acc9-9a8b87d49388") + + cp := ts.SpawnWithOpts(e2e.OptArgs("push")) + cp.Expect("you need to be authenticated") + cp.ExpectExitCode(1) + + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + +func (suite *PushIntegrationTestSuite) TestPush_NoChanges() { + suite.OnlyRunForTags(tagsuite.Push) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + cp := ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/small-python", ".")) + cp.ExpectExitCode(0) + + ts.LoginAsPersistentUser() + cp = ts.SpawnWithOpts(e2e.OptArgs("push")) + cp.Expect("no local changes to push") + cp.ExpectExitCode(1) + + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + +func (suite *PushIntegrationTestSuite) TestPush_NameInUse() { + suite.OnlyRunForTags(tagsuite.Push) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + // Source project we do not have access to + ts.PrepareProject("ActiveState-Test-DevNull/push-error-test", "2aa0b8fa-04e2-4079-bde1-d46764e3cb53") + + ts.LoginAsPersistentUser() + // Target project already exists + cp := ts.SpawnWithOpts(e2e.OptArgs("push", "-n", "ActiveState-CLI/push-error-test")) + cp.Expect("already in use") + cp.ExpectExitCode(1) + + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + +func (suite *PushIntegrationTestSuite) TestPush_Aborted() { + // Skipped for now due to DX-2244 + suite.T().Skip("Confirming prompt with N not working, must fix first") + + suite.OnlyRunForTags(tagsuite.Push) + + ts := e2e.New(suite.T(), true) + defer ts.Close() + + // Source project we do not have access to + ts.PrepareProject("ActiveState-Test-DevNull/push-error-test", "2aa0b8fa-04e2-4079-bde1-d46764e3cb53") + + ts.LoginAsPersistentUser() + // Target project already exists + cp := ts.SpawnWithOpts(e2e.OptArgs("push")) + cp.Expect("Would you like to create a new project") + cp.SendLine("n") + cp.Expect("Project creation aborted by user", termtest.OptExpectTimeout(5*time.Second)) + cp.ExpectExitCode(1) + + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + +func (suite *PushIntegrationTestSuite) TestPush_InvalidHistory() { + suite.OnlyRunForTags(tagsuite.Push) + + ts := e2e.New(suite.T(), true) + defer ts.Close() + + // Note the commit we're using here is for another project, in order to repro the error + ts.PrepareProject("ActiveState-CLI/small-python", "dbc0415e-91e8-407b-ad36-1de0cc5c0cbb") + + ts.LoginAsPersistentUser() + // Target project already exists + cp := ts.SpawnWithOpts(e2e.OptArgs("push", "ActiveState-CLI/push-error-test")) + cp.Expect("commit history does not match") + cp.ExpectExitCode(1) + + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + +func (suite *PushIntegrationTestSuite) TestPush_PullNeeded() { + suite.OnlyRunForTags(tagsuite.Push) + + ts := e2e.New(suite.T(), true) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/push-error-test", "899c9b4c-d28d-441a-9c28-c84819ba8b1a") + + ts.LoginAsPersistentUser() + // Target project already exists + cp := ts.SpawnWithOpts(e2e.OptArgs("push")) + cp.Expect("changes available that need to be merged") + cp.ExpectExitCode(1) + + if strings.Count(cp.Snapshot(), " x ") != 1 { + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } +} + func (suite *PushIntegrationTestSuite) TestPush_Outdated() { suite.OnlyRunForTags(tagsuite.Push) projectLine := "project: https://platform.activestate.com/ActiveState-CLI/cli?branch=main" @@ -275,8 +360,13 @@ func (suite *PushIntegrationTestSuite) TestPush_Outdated() { wd := filepath.Join(ts.Dirs.Work, "cli") pjfilepath := filepath.Join(ts.Dirs.Work, "cli", constants.ConfigFileName) suite.Require().NoError(fileutils.WriteFile(pjfilepath, []byte(projectLine))) - commitIdFile := filepath.Join(ts.Dirs.Work, "cli", constants.ProjectConfigDirName, constants.CommitIdFileName) - suite.Require().NoError(fileutils.WriteFile(commitIdFile, []byte(unPushedCommit))) + // Remove the following lines in DX-2307. + pjfile, err := projectfile.Parse(pjfilepath) + suite.Require().NoError(err) + suite.Require().NoError(pjfile.LegacySetCommit(unPushedCommit)) + // Re-enable the following lines in DX-2307. + //commitIdFile := filepath.Join(ts.Dirs.Work, "cli", constants.ProjectConfigDirName, constants.CommitIdFileName) + //suite.Require().NoError(fileutils.WriteFile(commitIdFile, []byte(unPushedCommit))) ts.LoginAsPersistentUser() cp := ts.SpawnWithOpts(e2e.OptArgs("push"), e2e.OptWD(wd)) diff --git a/test/integration/refresh_int_test.go b/test/integration/refresh_int_test.go index f4a75ffb78..ce796ad8f4 100644 --- a/test/integration/refresh_int_test.go +++ b/test/integration/refresh_int_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" "github.com/stretchr/testify/suite" @@ -22,15 +23,15 @@ func (suite *RefreshIntegrationTestSuite) TestRefresh() { cp := ts.SpawnWithOpts( e2e.OptArgs("refresh"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Setting Up Runtime") - cp.Expect("Runtime updated") + cp.Expect("Runtime updated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) cp = ts.SpawnWithOpts( e2e.OptArgs("exec", "--", "python3", "-c", "import requests"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("ModuleNotFoundError") cp.ExpectExitCode(1) @@ -38,17 +39,17 @@ func (suite *RefreshIntegrationTestSuite) TestRefresh() { suite.PrepareActiveStateYAML(ts, "ActiveState-CLI/Branches", "secondbranch", "46c83477-d580-43e2-a0c6-f5d3677517f1") cp = ts.SpawnWithOpts( e2e.OptArgs("refresh"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Setting Up Runtime") - cp.Expect("Runtime updated") + cp.Expect("Runtime updated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) cp = ts.SpawnWithOpts( e2e.OptArgs("exec", "--", "python3", "-c", "import requests"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.ExpectExitCode(0) + cp.ExpectExitCode(0, e2e.RuntimeSourcingTimeoutOpt) cp = ts.Spawn("refresh") suite.Assert().NotContains(cp.Output(), "Setting Up Runtime", "Unchanged runtime should not refresh") diff --git a/test/integration/remote_installer_int_test.go b/test/integration/remote_installer_int_test.go index b12fbcf2a3..00ba3d163b 100644 --- a/test/integration/remote_installer_int_test.go +++ b/test/integration/remote_installer_int_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + anaConst "github.com/ActiveState/cli/internal/analytics/constants" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" "github.com/ActiveState/cli/internal/exeutils" @@ -88,6 +89,19 @@ func (suite *RemoteInstallIntegrationTestSuite) TestInstall() { } cp.Expect("Built") cp.ExpectExitCode(0) + + // Verify analytics reported the correct sessionToken. + sessionTokenFound := false + events := parseAnalyticsEvents(suite, ts) + suite.Require().NotEmpty(events) + for _, event := range events { + if event.Category == anaConst.CatUpdates && event.Dimensions != nil { + suite.Assert().Contains(*event.Dimensions.SessionToken, constants.RemoteInstallerVersion) + sessionTokenFound = true + break + } + } + suite.Assert().True(sessionTokenFound, "sessionToken was not found in analytics") }) } } diff --git a/test/integration/revert_int_test.go b/test/integration/revert_int_test.go index f5ac49eef2..751db23d82 100644 --- a/test/integration/revert_int_test.go +++ b/test/integration/revert_int_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" "github.com/stretchr/testify/suite" @@ -42,7 +43,7 @@ func (suite *RevertIntegrationTestSuite) TestRevert() { e2e.OptArgs("history"), e2e.OptWD(wd), ) - cp.Expect("Revert commit " + commitID) + cp.Expect("Reverted commit for commit " + commitID) cp.Expect("- urllib3") cp.Expect("+ argparse") // parent commit cp.Expect("+ urllib3") // commit whose changes were just reverted @@ -51,9 +52,9 @@ func (suite *RevertIntegrationTestSuite) TestRevert() { // Verify that argparse still exists (it was not reverted along with urllib3). cp = ts.SpawnWithOpts( e2e.OptArgs("shell", "Revert"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.ExpectInput() + cp.ExpectInput(e2e.RuntimeSourcingTimeoutOpt) cp.SendLine("python3") cp.Expect("3.9.15") cp.SendLine("import urllib3") @@ -83,7 +84,7 @@ func (suite *RevertIntegrationTestSuite) TestRevert_failsOnCommitNotInHistory() cp.Expect(fmt.Sprintf("Operating on project %s", namespace)) cp.SendLine("Y") cp.Expect(commitID) - cp.Expect("The commit being reverted is not within the current commit's history") + cp.Expect("The target commit is not within the current commit's history") cp.ExpectNotExitCode(0) } @@ -115,7 +116,7 @@ func (suite *RevertIntegrationTestSuite) TestRevertTo() { e2e.OptArgs("history"), e2e.OptWD(wd), ) - cp.Expect("Reverting to commit " + commitID) + cp.Expect("Revert to commit " + commitID) cp.Expect("- argparse") // effectively reverting previous commit cp.Expect("+ argparse") // commit being effectively reverted cp.Expect("+ urllib3") // commit reverted to diff --git a/test/integration/run_int_test.go b/test/integration/run_int_test.go index b3e4b24855..6796288374 100644 --- a/test/integration/run_int_test.go +++ b/test/integration/run_int_test.go @@ -14,6 +14,7 @@ import ( "github.com/ActiveState/termtest" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/testhelpers/e2e" @@ -107,7 +108,7 @@ func (suite *RunIntegrationTestSuite) TestInActivatedEnv() { suite.createProjectFile(ts, 3) cp := ts.Spawn("activate") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput(termtest.OptExpectTimeout(10 * time.Second)) cp.SendLine(fmt.Sprintf("%s run testMultipleLanguages", ts.Exe)) @@ -144,7 +145,7 @@ func (suite *RunIntegrationTestSuite) TestScriptBashSubshell() { suite.createProjectFile(ts, 3) cp := ts.SpawnWithOpts(e2e.OptArgs("activate"), e2e.OptAppendEnv("SHELL=bash")) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput(termtest.OptExpectTimeout(10 * time.Second)) cp.SendLine("helloWorld") @@ -292,11 +293,11 @@ func (suite *RunIntegrationTestSuite) TestRun_Perl_Variable() { cp := ts.SpawnWithOpts( e2e.OptArgs("activate"), e2e.OptAppendEnv( - "ACTIVESTATE_CLI_DISABLE_RUNTIME=false", + constants.DisableRuntime+"=false", "PERL_VERSION=does_not_exist", ), ) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput(termtest.OptExpectTimeout(10 * time.Second)) cp.SendLine("perl -MEnglish -e 'print $PERL_VERSION'") diff --git a/test/integration/runtime_int_test.go b/test/integration/runtime_int_test.go index 2ea4d22cbb..c7ec78a267 100644 --- a/test/integration/runtime_int_test.go +++ b/test/integration/runtime_int_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/exeutils" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" @@ -87,9 +88,9 @@ func (suite *RuntimeIntegrationTestSuite) TestInterruptSetup() { cp := ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/test-interrupt-small-python#863c45e2-3626-49b6-893c-c15e85a17241", "."), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Checked out project") + cp.Expect("Checked out project", e2e.RuntimeSourcingTimeoutOpt) targetDir := target.ProjectDirToTargetDir(ts.Dirs.Work, ts.Dirs.Cache) pythonExe := filepath.Join(setup.ExecDir(targetDir), "python3"+exeutils.Extension) @@ -99,8 +100,8 @@ func (suite *RuntimeIntegrationTestSuite) TestInterruptSetup() { cp = ts.SpawnWithOpts( e2e.OptArgs("pull"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false", - "ACTIVESTATE_CLI_RUNTIME_SETUP_WAIT=true"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false", + constants.RuntimeSetupWaitEnvVarName+"=true"), ) time.Sleep(30 * time.Second) cp.SendCtrlC() // cancel pull/update diff --git a/test/integration/secrets_int_test.go b/test/integration/secrets_int_test.go index 219e342870..c8381736b7 100644 --- a/test/integration/secrets_int_test.go +++ b/test/integration/secrets_int_test.go @@ -71,7 +71,7 @@ func (suite *SecretsIntegrationTestSuite) TestSecret_Expand() { defer clearSecrets(ts, "project.test-secret", "user.test-secret") asyData := strings.TrimSpace(` -project: https://platform.activestate.com/ActiveState-CLI/secrets-test?commitID=c7f8f45d-39e2-4f22-bd2e-4182b914880f +project: https://platform.activestate.com/ActiveState-CLI/secrets-test scripts: - name: project-secret language: bash @@ -84,6 +84,7 @@ scripts: `) ts.PrepareActiveStateYAML(asyData) + ts.PrepareCommitIdFile("c7f8f45d-39e2-4f22-bd2e-4182b914880f") cp := ts.Spawn("secrets", "set", "project.project-secret", "project-value") cp.Expect("Operating on project") diff --git a/test/integration/shell_int_test.go b/test/integration/shell_int_test.go index b44592da04..58a5057713 100644 --- a/test/integration/shell_int_test.go +++ b/test/integration/shell_int_test.go @@ -6,17 +6,13 @@ import ( "path/filepath" "runtime" "testing" - "time" - "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" "github.com/ActiveState/cli/internal/config" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/subshell" - "github.com/ActiveState/cli/internal/subshell/bash" - "github.com/ActiveState/cli/internal/subshell/sscommon" "github.com/ActiveState/cli/internal/subshell/zsh" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" @@ -92,9 +88,9 @@ func (suite *ShellIntegrationTestSuite) TestDefaultShell() { // Use. cp = ts.SpawnWithOpts( e2e.OptArgs("use", "ActiveState-CLI/small-python"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) cp = ts.SpawnWithOpts( @@ -192,9 +188,9 @@ func (suite *ShellIntegrationTestSuite) TestDefaultNoLongerExists() { cp = ts.SpawnWithOpts( e2e.OptArgs("use", "ActiveState-CLI/Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) err := os.RemoveAll(filepath.Join(ts.Dirs.Work, "Python3")) @@ -230,9 +226,9 @@ func (suite *ShellIntegrationTestSuite) TestUseShellUpdates() { cp = ts.SpawnWithOpts( e2e.OptArgs("use", "ActiveState-CLI/Python3"), e2e.OptAppendEnv("SHELL=bash"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) // Ensure both bash and zsh RC files are updated @@ -262,73 +258,8 @@ func (suite *ShellIntegrationTestSuite) SetupRCFile(ts *e2e.Session) { return } - cfg, err := config.New() - suite.Require().NoError(err) - - subshell := subshell.New(cfg) - rcFile, err := subshell.RcFile() - suite.Require().NoError(err) - - err = fileutils.TouchFileUnlessExists(rcFile) - suite.Require().NoError(err) - err = fileutils.CopyFile(rcFile, filepath.Join(ts.Dirs.HomeDir, filepath.Base(rcFile))) - suite.Require().NoError(err) - - zsh := &zsh.SubShell{} - zshRcFile, err := zsh.RcFile() - suite.NoError(err) - err = fileutils.TouchFileUnlessExists(zshRcFile) - suite.NoError(err) - - err = fileutils.CopyFile(rcFile, filepath.Join(ts.Dirs.HomeDir, filepath.Base(zshRcFile))) - suite.Require().NoError(err) -} - -func (suite *ShellIntegrationTestSuite) TestNestedShellNotification() { - if runtime.GOOS == "windows" { - return // cmd.exe does not have an RC file to check for nested shells in - } - suite.OnlyRunForTags(tagsuite.Shell) - ts := e2e.New(suite.T(), false) - defer ts.Close() - - var ss subshell.SubShell - var rcFile string - env := []string{"ACTIVESTATE_CLI_DISABLE_RUNTIME=false"} - switch runtime.GOOS { - case "darwin": - ss = &zsh.SubShell{} - ss.SetBinary("zsh") - rcFile = filepath.Join(ts.Dirs.HomeDir, ".zshrc") - suite.Require().NoError(sscommon.WriteRcFile("zshrc_append.sh", rcFile, sscommon.DefaultID, nil)) - env = append(env, "SHELL=zsh") // override since CI tests are running on bash - case "linux": - ss = &bash.SubShell{} - ss.SetBinary("bash") - rcFile = filepath.Join(ts.Dirs.HomeDir, ".bashrc") - suite.Require().NoError(sscommon.WriteRcFile("bashrc_append.sh", rcFile, sscommon.DefaultID, nil)) - default: - suite.Fail("Unsupported OS") - } - suite.Require().Equal(filepath.Dir(rcFile), ts.Dirs.HomeDir, "rc file not in test suite homedir") - suite.Require().Contains(string(fileutils.ReadFileUnsafe(rcFile)), "State Tool is operating on project") - - cp := ts.Spawn("checkout", "ActiveState-CLI/small-python") - cp.Expect("Checked out project") - cp.ExpectExitCode(0) - - cp = ts.SpawnWithOpts( - e2e.OptArgs("shell", "small-python"), - e2e.OptAppendEnv(env...)) - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) - suite.Assert().NotContains(cp.Output(), "State Tool is operating on project") - cp.SendLine(fmt.Sprintf(`export HOME="%s"`, ts.Dirs.HomeDir)) // some shells do not forward this - - cp.SendLine(ss.Binary()) // platform-specific shell (zsh on macOS, bash on Linux, etc.) - cp.Expect("State Tool is operating on project ActiveState-CLI/small-python") - cp.SendLine("exit") // subshell within a subshell - cp.SendLine("exit") - cp.ExpectExitCode(0) + ts.SetupRCFile() + ts.SetupRCFileCustom(&zsh.SubShell{}) } func (suite *ShellIntegrationTestSuite) TestRuby() { @@ -347,7 +278,7 @@ func (suite *ShellIntegrationTestSuite) TestRuby() { e2e.OptArgs("shell", "Ruby-3.2.2"), e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Activated") + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectInput() cp.SendLine("ruby -v") cp.Expect("3.2.2") diff --git a/test/integration/shells_int_test.go b/test/integration/shells_int_test.go index f8de612c83..06d1c5a81d 100644 --- a/test/integration/shells_int_test.go +++ b/test/integration/shells_int_test.go @@ -4,11 +4,10 @@ import ( "fmt" "runtime" "testing" - "time" - "github.com/ActiveState/termtest" "github.com/stretchr/testify/suite" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" ) @@ -36,9 +35,9 @@ func (suite *ShellsIntegrationTestSuite) TestShells() { // Checkout the first instance. It doesn't matter which shell is used. cp := ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/small-python"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Checked out project") + cp.Expect("Checked out project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) for _, shell := range shells { @@ -56,13 +55,13 @@ func (suite *ShellsIntegrationTestSuite) TestShells() { // There are 2 or more instances checked out, so we should get a prompt in whichever shell we // use. - cp = ts.SpawnShellWithOpts(shell, e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false")) + cp = ts.SpawnShellWithOpts(shell, e2e.OptAppendEnv(constants.DisableRuntime+"=false")) cp.SendLine(e2e.QuoteCommand(shell, ts.ExecutablePath(), "shell", "small-python")) cp.Expect("Multiple project paths") // Just pick the first one and verify the selection prompt works. cp.SendEnter() - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) // Verify that the command prompt contains the right info, except for tcsh, whose prompt does // not behave like other shells'. diff --git a/test/integration/show_int_test.go b/test/integration/show_int_test.go index 3ee2b5581d..c53381b290 100644 --- a/test/integration/show_int_test.go +++ b/test/integration/show_int_test.go @@ -26,9 +26,9 @@ func (suite *ShowIntegrationTestSuite) TestShow() { cp := ts.SpawnWithOpts( e2e.OptArgs("activate"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.ExpectInput() + cp.ExpectInput(e2e.RuntimeSourcingTimeoutOpt) cp = ts.Spawn("show") cp.Expect(`Name`) diff --git a/test/integration/softlimit_int_test.go b/test/integration/softlimit_int_test.go deleted file mode 100644 index b730a6487d..0000000000 --- a/test/integration/softlimit_int_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package integration - -import ( - "testing" - "time" - - "github.com/ActiveState/termtest" - "github.com/stretchr/testify/suite" - - "github.com/ActiveState/cli/internal/constants" - - "github.com/ActiveState/cli/internal/testhelpers/e2e" - "github.com/ActiveState/cli/internal/testhelpers/tagsuite" -) - -type SoftLimitIntegrationTestSuite struct { - tagsuite.Suite -} - -/* -Test several important paths for the soft limit notification to show. -We're not testing all possible paths as it would be too expensive both in terms of testing time as well as maintenance of those tests. -*/ -func (suite *SoftLimitIntegrationTestSuite) TestCheckout() { - suite.OnlyRunForTags(tagsuite.SoftLimit, tagsuite.Critical) - - ts := e2e.New(suite.T(), true) - defer ts.Close() - - ts.LoginAsPersistentUser() - - cp := ts.SpawnWithOpts( - e2e.OptArgs("checkout", "ActiveState-CLI/small-python", "."), - e2e.OptAppendEnv(constants.RuntimeUsageOverrideEnvVarName+"=999"), - e2e.OptAppendEnv(constants.DisableRuntime+"=true"), // We're testing the usage, not the runtime - ) - cp.Expect("You've reached your runtime limit") - cp.ExpectExitCode(0) - - suite.Run("activate", func() { - cp := ts.SpawnWithOpts( - e2e.OptArgs("activate"), - e2e.OptAppendEnv(constants.RuntimeUsageOverrideEnvVarName+"=999"), - e2e.OptAppendEnv(constants.DisableRuntime+"=true"), - ) - cp.Expect("You've reached your runtime limit") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) - cp.ExpectInput() - cp.SendLine("exit 0") - cp.ExpectExitCode(0) - }) - - suite.Run("shell", func() { - cp := ts.SpawnWithOpts( - e2e.OptArgs("shell"), - e2e.OptAppendEnv(constants.RuntimeUsageOverrideEnvVarName+"=999"), - e2e.OptAppendEnv(constants.DisableRuntime+"=true"), - ) - cp.Expect("You've reached your runtime limit") - cp.Expect("Activated", termtest.OptExpectTimeout(120*time.Second)) - cp.ExpectInput() - cp.SendLine("exit 0") - cp.ExpectExitCode(0) - }) -} - -func TestSoftLimitIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(SoftLimitIntegrationTestSuite)) -} diff --git a/test/integration/switch_int_test.go b/test/integration/switch_int_test.go index 7508c78de8..c755cf5897 100644 --- a/test/integration/switch_int_test.go +++ b/test/integration/switch_int_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/runbits/commitmediator" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" - "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/projectfile" "github.com/stretchr/testify/suite" ) @@ -31,7 +31,7 @@ func (suite *SwitchIntegrationTestSuite) TestSwitch_Branch() { pjfile, err := projectfile.Parse(pjfilepath) suite.Require().NoError(err) suite.Require().Equal("main", pjfile.BranchName(), "branch was not set to 'main' after pull") - mainBranchCommitID, err := localcommit.Get(ts.Dirs.Work) + mainBranchCommitID, err := commitmediator.Get(pjfile) suite.Require().NoError(err) cp := ts.SpawnWithOpts(e2e.OptArgs("switch", "secondbranch")) @@ -45,7 +45,7 @@ func (suite *SwitchIntegrationTestSuite) TestSwitch_Branch() { // Check that branch and commitID were updated pjfile, err = projectfile.Parse(pjfilepath) suite.Require().NoError(err) - commitID, err := localcommit.Get(ts.Dirs.Work) + commitID, err := commitmediator.Get(pjfile) suite.Require().NoError(err) suite.Require().NotEqual(mainBranchCommitID, commitID, "commitID was not updated after switching branches") suite.Require().Equal("secondbranch", pjfile.BranchName(), "branch was not updated after switching branches") @@ -65,7 +65,7 @@ func (suite *SwitchIntegrationTestSuite) TestSwitch_CommitID() { pjfile, err := projectfile.Parse(pjfilepath) suite.Require().NoError(err) suite.Require().Equal("main", pjfile.BranchName(), "branch was not set to 'main' after pull") - originalCommitID, err := localcommit.Get(ts.Dirs.Work) + originalCommitID, err := commitmediator.Get(pjfile) suite.Require().NoError(err) cp := ts.SpawnWithOpts(e2e.OptArgs("switch", "efce7c7a-c61a-4b04-bb00-f8e7edfd247f")) @@ -77,7 +77,7 @@ func (suite *SwitchIntegrationTestSuite) TestSwitch_CommitID() { // Check that branch and commitID were updated pjfile, err = projectfile.Parse(pjfilepath) suite.Require().NoError(err) - commitID, err := localcommit.Get(ts.Dirs.Work) + commitID, err := commitmediator.Get(pjfile) suite.Require().NotEqual(originalCommitID, commitID, "commitID was not updated after switching branches") } @@ -95,7 +95,7 @@ func (suite *SwitchIntegrationTestSuite) TestSwitch_CommitID_NotInHistory() { pjfile, err := projectfile.Parse(pjfilepath) suite.Require().NoError(err) suite.Require().Equal("main", pjfile.BranchName(), "branch was not set to 'main' after pull") - originalCommitID, err := localcommit.Get(ts.Dirs.Work) + originalCommitID, err := commitmediator.Get(pjfile) suite.Require().NoError(err) cp := ts.SpawnWithOpts(e2e.OptArgs("switch", "76dff77a-66b9-43e3-90be-dc75917dd661")) @@ -107,7 +107,7 @@ func (suite *SwitchIntegrationTestSuite) TestSwitch_CommitID_NotInHistory() { // Check that branch and commitID were not updated pjfile, err = projectfile.Parse(pjfilepath) suite.Require().NoError(err) - commitID, err := localcommit.Get(ts.Dirs.Work) + commitID, err := commitmediator.Get(pjfile) suite.Require().NoError(err) suite.Equal(originalCommitID, commitID, "commitID was updated after switching branches") } diff --git a/test/integration/testdata/offline-install/another-artifacts-payload-nix/artifact/installdir/bin/test-another-offline-install b/test/integration/testdata/offline-install/another-artifacts-payload-nix/artifact/installdir/bin/test-another-offline-install deleted file mode 100755 index cce31695ba..0000000000 --- a/test/integration/testdata/offline-install/another-artifacts-payload-nix/artifact/installdir/bin/test-another-offline-install +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -echo TEST PLACEHOLDER diff --git a/test/integration/testdata/offline-install/another-artifacts-payload-nix/artifact/installdir/bin/test.hello b/test/integration/testdata/offline-install/another-artifacts-payload-nix/artifact/installdir/bin/test.hello deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/integration/testdata/offline-install/another-artifacts-payload-nix/artifact/runtime.json b/test/integration/testdata/offline-install/another-artifacts-payload-nix/artifact/runtime.json deleted file mode 100644 index 90afca4c3e..0000000000 --- a/test/integration/testdata/offline-install/another-artifacts-payload-nix/artifact/runtime.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "env": [ - { - "env_name": "PATH", - "join": "prepend", - "inherit": true, - "values": [ - "${INSTALLDIR}/bin" - ], - "separator": ";" - } - ], - "file_transforms": [ - { - "pattern": "PLACEHOLDER", - "with": "ANOTHER", - "in": [ - "bin/test-another-offline-install" - ] - } - ], - "installdir": "installdir" -} \ No newline at end of file diff --git a/test/integration/testdata/offline-install/another-artifacts-payload-windows/artifact/installdir/bin/test-another-offline-install.bat b/test/integration/testdata/offline-install/another-artifacts-payload-windows/artifact/installdir/bin/test-another-offline-install.bat deleted file mode 100644 index c14ffd1645..0000000000 --- a/test/integration/testdata/offline-install/another-artifacts-payload-windows/artifact/installdir/bin/test-another-offline-install.bat +++ /dev/null @@ -1 +0,0 @@ -echo TEST PLACEHOLDER \ No newline at end of file diff --git a/test/integration/testdata/offline-install/another-artifacts-payload-windows/artifact/installdir/bin/test.hello b/test/integration/testdata/offline-install/another-artifacts-payload-windows/artifact/installdir/bin/test.hello deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/integration/testdata/offline-install/another-artifacts-payload-windows/artifact/runtime.json b/test/integration/testdata/offline-install/another-artifacts-payload-windows/artifact/runtime.json deleted file mode 100644 index c348000216..0000000000 --- a/test/integration/testdata/offline-install/another-artifacts-payload-windows/artifact/runtime.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "env": [ - { - "env_name": "PATH", - "join": "prepend", - "inherit": true, - "values": [ - "${INSTALLDIR}\\bin" - ], - "separator": ";" - } - ], - "file_transforms": [ - { - "pattern": "PLACEHOLDER", - "with": "ANOTHER", - "in": [ - "bin/test-another-offline-install.bat" - ] - } - ], - "installdir": "installdir" -} \ No newline at end of file diff --git a/test/integration/testdata/offline-install/artifacts-payload-nix/artifact/installdir/bin/test-offline-install b/test/integration/testdata/offline-install/artifacts-payload-nix/artifact/installdir/bin/test-offline-install deleted file mode 100755 index cce31695ba..0000000000 --- a/test/integration/testdata/offline-install/artifacts-payload-nix/artifact/installdir/bin/test-offline-install +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -echo TEST PLACEHOLDER diff --git a/test/integration/testdata/offline-install/artifacts-payload-nix/artifact/installdir/bin/test.hello b/test/integration/testdata/offline-install/artifacts-payload-nix/artifact/installdir/bin/test.hello deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/integration/testdata/offline-install/artifacts-payload-nix/artifact/runtime.json b/test/integration/testdata/offline-install/artifacts-payload-nix/artifact/runtime.json deleted file mode 100644 index e16b19bf70..0000000000 --- a/test/integration/testdata/offline-install/artifacts-payload-nix/artifact/runtime.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "env": [ - { - "env_name": "PATH", - "join": "prepend", - "inherit": true, - "values": [ - "${INSTALLDIR}/bin" - ], - "separator": ";" - } - ], - "file_transforms": [ - { - "pattern": "PLACEHOLDER", - "with": "REPLACEMENT", - "in": [ - "bin/test-offline-install" - ] - } - ], - "installdir": "installdir" -} \ No newline at end of file diff --git a/test/integration/testdata/offline-install/artifacts-payload-windows/artifact/installdir/bin/test-offline-install.bat b/test/integration/testdata/offline-install/artifacts-payload-windows/artifact/installdir/bin/test-offline-install.bat deleted file mode 100644 index c14ffd1645..0000000000 --- a/test/integration/testdata/offline-install/artifacts-payload-windows/artifact/installdir/bin/test-offline-install.bat +++ /dev/null @@ -1 +0,0 @@ -echo TEST PLACEHOLDER \ No newline at end of file diff --git a/test/integration/testdata/offline-install/artifacts-payload-windows/artifact/installdir/bin/test.hello b/test/integration/testdata/offline-install/artifacts-payload-windows/artifact/installdir/bin/test.hello deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/integration/testdata/offline-install/artifacts-payload-windows/artifact/runtime.json b/test/integration/testdata/offline-install/artifacts-payload-windows/artifact/runtime.json deleted file mode 100644 index e04e2f102e..0000000000 --- a/test/integration/testdata/offline-install/artifacts-payload-windows/artifact/runtime.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "env": [ - { - "env_name": "PATH", - "join": "prepend", - "inherit": true, - "values": [ - "${INSTALLDIR}\\bin" - ], - "separator": ";" - } - ], - "file_transforms": [ - { - "pattern": "PLACEHOLDER", - "with": "REPLACEMENT", - "in": [ - "bin/test-offline-install.bat" - ] - } - ], - "installdir": "installdir" -} \ No newline at end of file diff --git a/test/integration/testdata/offline-install/assets/Another-IntegrationTest/LICENSE.txt b/test/integration/testdata/offline-install/assets/Another-IntegrationTest/LICENSE.txt deleted file mode 100644 index f406868075..0000000000 --- a/test/integration/testdata/offline-install/assets/Another-IntegrationTest/LICENSE.txt +++ /dev/null @@ -1,563 +0,0 @@ -ACTIVESTATE® COMMUNITY EDITION LICENSE AGREEMENT - -Version effective date: March 25, 2019 - -This license agreement (the “Agreement”) is made between you (either -an individual or a company or organization, not including its -affiliates or wholly owned subsidiaries) (“You”) and ActiveState -Software Inc. (“ActiveSstate”). This Agreement establishes the terms -under which ActiveState will license the Software (as defined below) -to You and establishes the terms under which You may use, copy, -modify, distribute, and/or Redistribute (as defined below) the -Software. This Agreement does not apply to Maintenance and Support, -anything Beyond Development Use, OEM Distribution (all such -capitalized terms as defined below) each of which requires a separate -agreement with ActiveState. For more information on these types of -agreements, please visit www.activestate.com. The intent of this -Agreement is to allow ActiveState to maintain control over the -development and distribution of the Software while allowing its use in -a variety of ways. If the terms and conditions of this Agreement do -not permit Your proposed use of the Software or if You require -clarification regarding the scope of Your intended use of the -Software, please contact sales@activestate.com. - -PLEASE READ THIS AGREEMENT CAREFULLY BEFORE INSTALLING OR USING THE -SOFTWARE. BY CLICKING ON “YES, ACCEPT” OR BY INSTALLING THE SOFTWARE, -YOU AGREE TO BE BOUND BY THE TERMS AND CONDITIONS OF THIS AGREEMENT. -IF YOU ARE ENTERING INTO THIS AGREEMENT ON BEHALF OF A PERSON, YOUR -ACCEPTANCE REPRESENTS THAT YOU HAVE THE AUTHORITY TO BIND SUCH PERSON -TO THE TERMS AND CONDITIONS OF THIS AGREEMENT, IN WHICH CASE “YOU” OR -“YOUR” WILL REFER TO THE PERSON ON BEHALF OF WHICH YOU ACT (“YOUR -ENTITY”). IF YOU DO NOT AGREE WITH THE TERMS AND CONDITIONS OF THIS -AGREEMENT OR IF YOU DO NOT HAVE THE AUTHORITY TO BIND YOUR ENTITY, YOU -HAVE NO RIGHT TO INSTALL OR USE THE SOFTWARE AND YOU SHOULD (A) -RETURN, DELETE, OR DISABLE THE SOFTWARE OR (B) IF YOU PURCHASED A -PRODUCT FROM ACTIVESTATE OR ITS RESELLER OR DISTRIBUTOR ON WHICH THE -SOFTWARE IS PRE-INSTALLED BY ACTIVESTATE, RETURN THE PURCHASED PRODUCT -TO ACTIVESTATE OR THE APPLICABLE RESELLER OR DISTRIBUTOR FROM WHOM YOU -OBTAINED THE PRODUCT. - -1. Definitions. - -“Accessible Code” means source code contained within the Software that -is licensed under an open source license. - -“Confidential Information” means all information designated in writing -as confidential by each party, or which under the circumstances of -disclosure reasonably ought to be considered as confidential. Without -limiting the foregoing, ActiveState Confidential Information includes -the Software, including all source and object code, and all associated -documentation, but not Accessible Code. - -"Maintenance And Support" means maintenance and support for the -Software provided by ActiveState under separate terms. - -"OEM Distribution" means any distribution to, and/or use of the -Software by, others outside Your organization and distribution and/or -use of the Software as either a bundled add-on to, or embedded -component of another application, with such application being made -available to its users as, but not limited to, an on-premises -application, a hosted application, a software-as-a-service offering or -a subscription service for which the distributor of the application -receives a license fee or any form of direct or indirect compensation -and whether for commercial or non-commercial purposes. - -“Person” means any individual, sole proprietorship, partnership, firm, -entity, unincorporated association, unincorporated syndicate, -unincorporated organization, trust, body corporate or governmental or -regulatory authority, and where the context requires, any of the -foregoing when they are acting as trustee, executor, administrator or -other legal representative. - -“Beyond Development Use” means any use of the Software licensed under -this Agreement beyond software development with the Software. For -greater clarity, any use of the Software licensed under this Agreement -beyond the purpose of developing, prototyping or demonstrating Your -application with the Software or by the Software are not permitted -under this license. - -“Redistribute” means any distribution to, and/or use of the Software -by, others inside or outside Your organization and distribution and/or -use of the Software inside or outside Your organization. - -“Software” means any of ActivePerl, ActivePython, ActiveTcl, ActiveGo, -ActiveRuby, ActiveNode, or ActiveLua software and the accompanying -materials including, but not limited to, source code, binary -executables, documentation, images and scripts, which are distributed -by ActiveState, and derivatives of that collection and/or those files. - -“User Data” means all information and data collected by the Software -or otherwise transmitted by the Software to ActiveState, including any -data, metadata, metrics, statistics, or other information relating to -the performance, operations, resource, health, or other conditions of -the Software, any component thereof (including third party -components), and any related infrastructure, such as network host -names, IP addresses, interpreter used, and system architecture, which -includes filenames, full path, file size, and content hash. - -“Wrapped Application” means a single-file executable in which all -binary components are encapsulated in a single binary without exposing -the base programming language as a scripting language within Your own -application program to end users. - -2. License Grant. - - (a) Subject to the terms and conditions of this Agreement, - ActiveState hereby grants to You a limited, worldwide, - perpetual, paid up, free-of-charge, non-exclusive, - non-transferable, non-assignable, and non-sublicensable - license to install and use the Software on any computing - device, in accordance with the limitations and restrictions - set forth in this Agreement, for research and development - purposes only. You may not use the Software Beyond Development - Use, except as provided in Section 2(b) below. You may not - use the Software for OEM Distribution. You may copy the Software - for archival purposes or as necessary to use the Software as - authorized in this section. You also may modify the - Accessible Code to develop bug fixes, customizations, or - additional features, for the sole purpose of using the Software - as authorized by this Agreement. - - (b) ActiveState may, in its sole discretion, grant You the right to - use the Software Beyond Development Use and/or OEM Distribution - for limited, small-scale, non-commercial and/or open source - projects. To apply for this right, contact sales@activestate.com. - Without the prior approval of ActiveState, you may not use the - Software Beyond Development Use and/or for OEM Distribution. - -3. Restrictions. - - (a) Except as expressly provided in this Agreement, You may not: - - (i) transfer, assign, sublicense, resell, or rent the - Software; - (ii) modify or translate the Software to discover the source - code in the Software or create a functional equivalent in - the Software; - (iii) reverse engineer, decompile, or disassemble (except as - and only to the extent this restriction is prohibited by - applicable law) the Software; - (iv) create derivative works based on the Software; - (v) merge the Software with another product; - (vi) copy the Software; - (vii) remove or obscure any proprietary rights notices or - labels on the Software; - (viii) Redistribute, without entering into a separate agreement - with ActiveState: - I. the Software as a whole, whether as a Wrapped - Application or on a standalone basis; - II. parts of the Software to create a language - distribution; or - III. the Software (other than the Accessible Code) with - Your Wrapped Application; - (ix) distribute the Software by OEM Distribution without - entering into a separate OEM Distribution agreement with - ActiveState; - (x) permit others to use the Software; or - (xi) use the Software: - I. Beyond Development Use on any computing device in - whatever form or manner, whether physical or - virtual and external or internal-facing; - II. on any operating systems other than Windows, OSX, and - Linux; - III. on computing devices used for file and/or application - serving; - IV. on any computing devices used for business continuity - and disaster recovery; or - V. to provide content or functionality through - external-facing servers or internal-facing - production servers. - -4. Confidentiality. - - (a) Except as reasonably required to exercise Your rights under - this Agreement, You agree to prevent any unauthorized copying, - use, distribution, installation or transfer of possession of - Confidential Information received from ActiveState (the - “ACTIVESTATE CONFIDENTIAL INFORMATION”). You do not acquire - any interest in any ActiveState Confidential Information by - reason of this Agreement. ActiveState Confidential Information - does not include any information which (i) becomes part of the - public domain through no act or omission on Your part; (ii) is - lawfully acquired by You from a third party without any breach - of confidentiality; (iii) is independently developed by You - without reference to the ActiveState Confidential Information; - or (iv) is disclosed in accordance with judicial or other - governmental order or timely disclosure requirements imposed - by law or stock exchange policies. Notwithstanding the - foregoing, either party may disclose the terms and conditions - of this Agreement in conjunction with legal proceedings. - Without limiting the generality of the foregoing, You must - take reasonable steps to prevent any personnel from removing - any proprietary or other legend or restrictive notice - contained or included in any material provided by ActiveState - to You. - - (b) You acknowledge that any use or disclosure of the ActiveState - Confidential Information in a manner inconsistent with the - provisions of this Agreement may cause ActiveState irreparable - damage for which remedies other than injunctive relief may be - inadequate. You further agree that ActiveState will be - entitled to attempt to receive from a court of competent - jurisdiction injunctive or other equitable relief to restrain - such use or disclosure in addition to other appropriate - remedies. - -5. Open Source Acknowledgement. The Software is comprised of open - source software, which is subject to the terms of the open source - software license(s) accompanying or otherwise applicable to that - open source software included in the Software (the “Open Source - Components”). For reference, Tcl/Tk open source license terms can - be found in Exhibit A attached to this Agreement or obtained from - this link: https://www.tcl.tk/software/tcltk/license.html. You - acknowledge that Your own distribution or deployment of instances - containing or linking to the Software, including the Open Source - Components, or any other open source software may trigger open - source license requirements for which You are responsible. Nothing - in this Agreement limits Your rights under or grants rights to You - that supersede the terms of any applicable open source software - license for the applicable Open Source Components. - -6. Intellectual Property Ownership. All right, title and interest in - and to the Software and all intellectual property rights embodied - therein, including copyrights, trade names, trademarks, service - marks, product names, trade secrets embodied in the Software's - design and coding methodology and other proprietary materials in - the Software belong exclusively to ActiveState or its third party - licensors. The Software is protected by Canada and United States - copyright laws and international treaty provisions as implemented - locally in different jurisdictions. Except as specifically - provided under this Agreement, You acknowledge that no other - right, title or interest in and to the Software or any parts - thereof is granted to You. ActiveState grants You the limited - right to use the trade names, trademarks, service marks or product - names of ActiveState as required for reasonable and customary use - in describing the origin of the Software. You may not use the - trade names, trademarks, service marks or product names of - ActiveState in any way that might state or imply that ActiveState - endorses Your work, or might state or imply that You created the - Software. - -7. User Data. You acknowledge, agree, and expressly consent to - ActiveState’s collection of Your User Data through the Software. - ActiveState does not claim ownership of any User Data. You hereby - grant to ActiveState and its sublicensees a royalty-free, - perpetual, irrevocable, transferable, worldwide non-exclusive - right to reproduce, analyse, review, process, diagnose, or - otherwise use the User Data (in whole or in part) for the purpose - of supporting, maintaining, and providing the Software, the - Maintenance and Support, if any, and any related services provided - by ActiveState relating to the Software (the “User Data License”). - ActiveState will not disclose the User Data to any third parties - and will only use the User Data in accordance with the User Data - License, except that ActiveState may provide Your User Data to - third parties providing services relating to the Software to - ActiveState (which will protect the User Data on terms and - conditions that are commensurate in scope with this Agreement). In - addition to the rights granted under the User Data License, You - acknowledge and agree that ActiveState has the right to (i) - publicly disclose, in any manner whatsoever, User Data that have - been anonymized; and (ii) review or analyze the User Data and - publicly disclose any results of such review or analysis, - including in the form of reports, blog posts, newsletters, - marketing materials, or otherwise, provided You will not be - identified in such publicly disclosed materials. - -8. Term. This Agreement will be effective upon Your agreement to be - bound by the terms and conditions of this Agreement and will - continue in effect unless otherwise terminated in accordance with - the terms and conditions of this Agreement. - -9. Termination. If You breach any term or condition of this - Agreement, ActiveState may immediately terminate this Agreement - with respect to the Software that You have licensed under this - Agreement by providing notice to You. ActiveState may also - terminate this Agreement, without any liability to You, if any - law, regulations, orders, or legal requirements prohibits - ActiveState’s provision or licensing of the Software to You. Upon - termination of this Agreement by ActiveState, You will immediately - cease all use of the Software and return all copies of the - Software that are under Your control to ActiveState or to delete - all such copies. - -10. Infringement Indemnification. You indemnify, hold harmless, and - defend ActiveState, its licensors, and their respective employees, - agents and distributors against any and all claims, proceedings, - demands and costs resulting from or in any way connected with Your - use of the Software and arising from Your breach of this - Agreement; provided, however, that ActiveState will notify You in - writing of any such claim. ActiveState will not enter into any - settlement or compromise any such claim without Your prior written - consent. You will have sole control of any such action and - settlement negotiations; and ActiveState will provide You with - commercially reasonable information and assistance, at Your - request and expense, necessary to settle or defend such claim. - -11. Disclaimer Of Warranty. - - (a) NEITHER ACTIVESTATE NOR ANY OF ITS SUPPLIERS, LICENSORS, OR - RESELLERS MAKES ANY WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - UNDER THIS AGREEMENT. TO THE MAXIMUM EXTENT PERMITTED UNDER - APPLICABLE LAW, ACTIVESTATE AND ITS SUPPLIERS, LICENSORS, AND - RESELLERS SPECIFICALLY DISCLAIM ALL WARRANTIES AND CONDITIONS - WITH RESPECT TO THE SOFTWARE, EITHER EXPRESS, IMPLIED OR - STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OR - CONDITION OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, - DURABILITY, MERCHANTABLE QUALITY, FITNESS FOR A PARTICULAR - PURPOSE, UPDATES, UPGRADES, SUPPLEMENTS, PRODUCTS, APPLIANCES, - SYSTEM INTEGRATION, DATA ACCURACY AND ANY OTHER ITEMS PROVIDED - HEREUNDER. ACTIVESTATE MAKES NO WARRANTY OR GUARANTEE THAT THE - OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED, ERROR-FREE, - OR VIRUS-FREE, OR THAT THE SOFTWARE WILL MEET ANY PARTICULAR - CRITERIA OF PERFORMANCE, QUALITY, ACCURACY, PURPOSE, OR NEED. - YOU ASSUME THE ENTIRE RISK OF SELECTION, INSTALLATION, AND USE - OF THE SOFTWARE. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN - ESSENTIAL PART OF THIS AGREEMENT. - - (b) TO THE EXTENT ANY IMPLIED WARRANTIES CANNOT BE DISCLAIMED - UNDER APPLICABLE LAW, ANY IMPLIED WARRANTIES ARE LIMITED IN - DURATION TO THE PERIOD REQUIRED BY APPLICABLE LAW. - - (c) SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED - WARRANTIES OR LIMITATIONS ON APPLICABLE STATUTORY RIGHTS OF A - CONSUMER, AND SO SOME OR ALL OF THE EXCLUSION OF IMPLIED - WARRANTIES OR LIMITATIONS SET OUT IN THIS SECTION MAY NOT - APPLY TO YOU. - -12. Limitation Of Liability. - - (a) INDEPENDENT OF THE FOREGOING PROVISIONS, TO THE MAXIMUM EXTENT - PERMITTED UNDER APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL - THEORY, INCLUDING WITHOUT LIMITATION, TORT, CONTRACT, OR - STRICT PRODUCTS LIABILITY, WILL ACTIVESTATE, ITS DIRECTORS, - OFFICERS, EMPLOYEES, AFFILIATES, AGENTS, CONTRACTORS, - PRINCIPALS, SUPPLIERS OR LICENSORS BE LIABLE TO YOU OR ANY - OTHER PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR - CONSEQUENTIAL DAMAGES OF ANY KIND, INCLUDING WITHOUT - LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, - COMPUTER MALFUNCTION, OR ANY OTHER KIND OF COMMERCIAL DAMAGE, - EVEN IF ACTIVESTATE HAS BEEN ADVISED OF THE POSSIBILITY OF - SUCH DAMAGES. - - (b) TO THE MAXIMUM EXTENT PERMITTED UNDER APPLICABLE LAW, IN NO - EVENT WILL ACTIVESTATE BE LIABLE TO YOU FOR DAMAGES UNDER THIS - AGREEMENT FOR ANY CAUSE WHATSOEVER, AND REGARDLESS OF THE FORM - OF ACTION. - -13. Export Controls. You must comply with all export laws and - restrictions and regulations of Canada, the United States or - foreign agencies or authorities, and not to export or re-export - the Software or any direct product thereof in violation of any - such restrictions, laws or regulations, or without all necessary - approvals. As applicable, each party will obtain and bear all - expenses relating to any necessary licenses and/or exemptions with - respect to its own export of the Software from Canada or the U.S. - Neither the Software nor the underlying information or technology - may be electronically transmitted or otherwise exported or - re-exported: into any country subject to Canada or U.S. trade - sanctions covering the Software, to individuals or entities - controlled by such countries, or to nationals or residents of such - countries other than nationals who are lawfully admitted permanent - residents of countries not subject to such sanctions; to anyone on - Canada's Area Control List of the Export and Import Permits Act; - or to anyone on the U.S. Treasury Department's list of Specially - Designated Nationals and Blocked Persons or the U.S. Commerce - Department's Table of Denial Orders. By installing or using the - Software, You agree to the foregoing and represent and warrant - that it complies with these conditions. - -14. U.S. Government End-Users. The Software is a "commercial item," as - that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of - "commercial computer software" and "commercial computer software - documentation," as such terms are used in 48 C.F.R. 12.212 (Sept. - 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 - through 227.7202-4 (June 1995), all U.S. Government End Users - acquire the Software with only those rights as are granted to - all other end users pursuant to the terms and conditions herein. - Unpublished rights are reserved under the copyright laws of Canada - and the United States. - -15. Licensee Outside The U.S. If You are located outside the U.S., - then the following provisions will apply: (a) Les parties aux - presentes confirment leur volonte que cette convention de meme que - tous les documents y compris tout avis qui siy rattache, soient - rediges en langue anglaise (translation: "The parties confirm that - this Agreement and all related documentation is and will be in the - English language."); and (b) You are responsible for complying - with any local laws in Your jurisdiction which might impact Your - right to import, export or use the Software, and You represent - that You have complied with any regulations or registration - procedures required by applicable law to make this license - enforceable. - -16. Entire Agreement. This Agreement constitutes the entire - understanding of the parties with respect to the subject matter of - this Agreement and merges all prior communications, - representations, and agreements. - -17. Severability. If any provision of this Agreement is declared - invalid or unenforceable, such provision will be deemed modified - to the extent necessary and possible to render it valid and - enforceable. In any event, the unenforceability or invalidity of - any provision will not affect any other provision of this - Agreement, and this Agreement will continue in full force and - effect, and be construed and enforced, as if such provision had - not been included, or had been modified as above provided, as the - case may be. - -18. Entire Agreement & Amendment. This Agreement constitutes the - complete agreement between the parties and supersedes all prior or - contemporaneous agreements or representations, written or oral, - concerning the subject matter of this Agreement, appendices and - attachments. ActiveState reserves the right to change this - Agreement at any time, which change shall be effective as of the - effective date for the terms and conditions of this Agreement as - shown on ActiveState’s Website (the “Change Effective Date”). Your - continued use of the Software after the Change Effective Date - constitutes Your acceptance of such changes. This Agreement may - not be otherwise amended without ActiveState's prior written - agreement. You agree to periodically review the terms and - conditions of this Agreement as updated from time to time on - ActiveState’s website. - -19. Arbitration. Except for actions to protect intellectual property - rights and to enforce an arbitrator's decision hereunder, all - disputes, controversies, or claims arising out of or relating to - this Agreement or a breach thereof will be submitted to and be - finally resolved by arbitration under the rules of the American - Arbitration Association ("AAA") then in effect. There will be one - arbitrator, and such arbitrator will be chosen by mutual agreement - of the parties in accordance with AAA rules. The arbitration will - take place in Vancouver, BC, Canada, and may be conducted by - telephone or online. The arbitrator will apply the laws of the - Province of British Columbia, Canada to all issues in dispute. The - controversy or claim will be arbitrated on an individual basis, - and will not be consolidated in any arbitration with any claim or - controversy of any other party. The findings of the arbitrator - will be final and binding on the parties, and may be entered in - any court of competent jurisdiction for enforcement. Enforcements - of any award or judgment will be governed by the United Nations - Convention on the Recognition and Enforcement of Foreign Arbitral - Awards. Should either party file an action contrary to this - provision, the other party may recover legal fees and costs up to - $1,000.00. - -20. Jurisdiction And Venue. The superior courts of Vancouver in the - Province of British Columbia, Canada will be the exclusive - jurisdiction and venue for all legal proceedings that are not - arbitrated under this Agreement. - -21. Force Majeure. Neither party will be liable for damages for any - delay or failure of delivery arising out of causes beyond their - reasonable control and without their fault or negligence, - including, but not limited to, Acts of God, acts of civil or - military authority, fires, riots, wars, embargoes, Internet - disruptions, hacker attacks, or communications failures. - Notwithstanding anything to the contrary contained herein, if - either party is unable to perform hereunder for a period of - thirty (30) consecutive days, then the other party may terminate - this Agreement immediately without liability by ten (10) days’ - written notice to the other. - -22. Publicity And Audit Rights. - - (a) You grant ActiveState the right to include Your name, trade - name, trademark, service mark or logo in its Software - promotional material. You may retract this grant at any time - in writing to marcom@activestate.com, requesting Your name, - trade name, trademark, service mark or logo be excluded from - future releases of ActiveState Software promotional material. - Requests cannot be complied with retroactively and may require - up to thirty (30) days to process. - - (b) If You entered into this Agreement on behalf of a Person, - where such Person has more than 100 employees, if requested by - ActiveState, You will furnish ActiveState with a signed - certification (i) verifying that the Software is being used - pursuant to the terms of this Agreement, including any user - limitations and (ii) listing the locations where the Software - is being used, the version(s) of the Software being used, how - long and how the Software is being used, and the number - computing devices and operating systems the Software is being - used with. You agree to grant ActiveState reasonable access to - Your site(s) and/or systems, upon prior notice during normal - business hours, to audit the use of the Software. Any such - audit shall be at ActiveState’s expense. - -23. Assignment. Except as expressly provided herein, neither this - Agreement nor any rights granted hereunder, nor the use of any of - the Software may be assigned, or otherwise transferred, in whole - or in part, by You, without the prior written consent of - ActiveState. Any permitted assignment by You under this Section - will be conditional upon You delivering all copies of the Software - to the transferee along with a copy of this Agreement, the - transferee accepting the terms and conditions of this Agreement, - and Your license to the Software terminating upon transfer. Any - attempted assignment by You will be void and of no effect unless - permitted by the foregoing. You acknowledge and agree that - ActiveState may assign this Agreement to any third party without - Your prior consent. - -24. Enurement. This Agreement will enure to the benefit of the - parties’ permitted successors and assigns. - -25. Governing Law. This Agreement will be construed under the laws of - the Province of British Columbia and the federal laws of Canada - applicable therein, without regard to the conflict of law rules. - The application of the United Nations Convention of Contracts for - the International Sale of Goods and any local implementations - thereof are expressly excluded. The parties agree that the Uniform - Computer - -Transactions Act or any version thereof, adopted by any jurisdiction, -in any form ("UCITA"), will not apply to this Agreement, and to the -extent that UCITA may be applicable, the parties agree to opt out of -the applicability of UCITA pursuant to the opt-out provision(s) -contained therein. - - - -EXHIBIT A - -Tcl/tk License Terms - - -This software is copyrighted by the Regents of the University of -California, Sun Microsystems, Inc., Scriptics Corporation, and other -parties. The following terms apply to all files associated with the -software unless explicitly disclaimed in individual files. - -The authors hereby grant permission to use, copy, modify, distribute, -and license this software and its documentation for any purpose, -provided that existing copyright notices are retained in all copies -and that this notice is included verbatim in any distributions. No -written agreement, license, or royalty fee is required for any of the -authorized uses. Modifications to this software may be copyrighted by -their authors and need not follow the licensing terms described here, -provided that the new terms are clearly indicated on the first page of -each file where they apply. - -IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY -FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES -ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY -DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, -INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND -NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND -THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE -MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -GOVERNMENT USE: If you are acquiring this software on behalf of the -U.S. government, the Government shall have only "Restricted Rights" -in the software and related documentation as defined in the Federal -Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you -are acquiring the software on behalf of the Department of Defense, -the software shall be classified as "Commercial Computer Software" -and the Government shall have only "Restricted Rights" as defined -in Clause 252.227-7013 (c)(1) of DFARs. Notwithstanding the foregoing, -the authors grant the U.S. Government and others acting in its behalf -permission to use and distribute the software in accordance with the -terms specified in this license. diff --git a/test/integration/testdata/offline-install/assets/Another-IntegrationTest/installer_config.json b/test/integration/testdata/offline-install/assets/Another-IntegrationTest/installer_config.json deleted file mode 100644 index 3fd0854480..0000000000 --- a/test/integration/testdata/offline-install/assets/Another-IntegrationTest/installer_config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "org_name": "ActiveState-Test", - "project_id": "00000000-0000-0000-0000-000000000000", - "project_name": "Another-IntegrationTest", - "commit_id": "00000000-0000-0000-0000-000000000000" -} \ No newline at end of file diff --git a/test/integration/testdata/offline-install/assets/IntegrationTest/LICENSE.txt b/test/integration/testdata/offline-install/assets/IntegrationTest/LICENSE.txt deleted file mode 100644 index f406868075..0000000000 --- a/test/integration/testdata/offline-install/assets/IntegrationTest/LICENSE.txt +++ /dev/null @@ -1,563 +0,0 @@ -ACTIVESTATE® COMMUNITY EDITION LICENSE AGREEMENT - -Version effective date: March 25, 2019 - -This license agreement (the “Agreement”) is made between you (either -an individual or a company or organization, not including its -affiliates or wholly owned subsidiaries) (“You”) and ActiveState -Software Inc. (“ActiveSstate”). This Agreement establishes the terms -under which ActiveState will license the Software (as defined below) -to You and establishes the terms under which You may use, copy, -modify, distribute, and/or Redistribute (as defined below) the -Software. This Agreement does not apply to Maintenance and Support, -anything Beyond Development Use, OEM Distribution (all such -capitalized terms as defined below) each of which requires a separate -agreement with ActiveState. For more information on these types of -agreements, please visit www.activestate.com. The intent of this -Agreement is to allow ActiveState to maintain control over the -development and distribution of the Software while allowing its use in -a variety of ways. If the terms and conditions of this Agreement do -not permit Your proposed use of the Software or if You require -clarification regarding the scope of Your intended use of the -Software, please contact sales@activestate.com. - -PLEASE READ THIS AGREEMENT CAREFULLY BEFORE INSTALLING OR USING THE -SOFTWARE. BY CLICKING ON “YES, ACCEPT” OR BY INSTALLING THE SOFTWARE, -YOU AGREE TO BE BOUND BY THE TERMS AND CONDITIONS OF THIS AGREEMENT. -IF YOU ARE ENTERING INTO THIS AGREEMENT ON BEHALF OF A PERSON, YOUR -ACCEPTANCE REPRESENTS THAT YOU HAVE THE AUTHORITY TO BIND SUCH PERSON -TO THE TERMS AND CONDITIONS OF THIS AGREEMENT, IN WHICH CASE “YOU” OR -“YOUR” WILL REFER TO THE PERSON ON BEHALF OF WHICH YOU ACT (“YOUR -ENTITY”). IF YOU DO NOT AGREE WITH THE TERMS AND CONDITIONS OF THIS -AGREEMENT OR IF YOU DO NOT HAVE THE AUTHORITY TO BIND YOUR ENTITY, YOU -HAVE NO RIGHT TO INSTALL OR USE THE SOFTWARE AND YOU SHOULD (A) -RETURN, DELETE, OR DISABLE THE SOFTWARE OR (B) IF YOU PURCHASED A -PRODUCT FROM ACTIVESTATE OR ITS RESELLER OR DISTRIBUTOR ON WHICH THE -SOFTWARE IS PRE-INSTALLED BY ACTIVESTATE, RETURN THE PURCHASED PRODUCT -TO ACTIVESTATE OR THE APPLICABLE RESELLER OR DISTRIBUTOR FROM WHOM YOU -OBTAINED THE PRODUCT. - -1. Definitions. - -“Accessible Code” means source code contained within the Software that -is licensed under an open source license. - -“Confidential Information” means all information designated in writing -as confidential by each party, or which under the circumstances of -disclosure reasonably ought to be considered as confidential. Without -limiting the foregoing, ActiveState Confidential Information includes -the Software, including all source and object code, and all associated -documentation, but not Accessible Code. - -"Maintenance And Support" means maintenance and support for the -Software provided by ActiveState under separate terms. - -"OEM Distribution" means any distribution to, and/or use of the -Software by, others outside Your organization and distribution and/or -use of the Software as either a bundled add-on to, or embedded -component of another application, with such application being made -available to its users as, but not limited to, an on-premises -application, a hosted application, a software-as-a-service offering or -a subscription service for which the distributor of the application -receives a license fee or any form of direct or indirect compensation -and whether for commercial or non-commercial purposes. - -“Person” means any individual, sole proprietorship, partnership, firm, -entity, unincorporated association, unincorporated syndicate, -unincorporated organization, trust, body corporate or governmental or -regulatory authority, and where the context requires, any of the -foregoing when they are acting as trustee, executor, administrator or -other legal representative. - -“Beyond Development Use” means any use of the Software licensed under -this Agreement beyond software development with the Software. For -greater clarity, any use of the Software licensed under this Agreement -beyond the purpose of developing, prototyping or demonstrating Your -application with the Software or by the Software are not permitted -under this license. - -“Redistribute” means any distribution to, and/or use of the Software -by, others inside or outside Your organization and distribution and/or -use of the Software inside or outside Your organization. - -“Software” means any of ActivePerl, ActivePython, ActiveTcl, ActiveGo, -ActiveRuby, ActiveNode, or ActiveLua software and the accompanying -materials including, but not limited to, source code, binary -executables, documentation, images and scripts, which are distributed -by ActiveState, and derivatives of that collection and/or those files. - -“User Data” means all information and data collected by the Software -or otherwise transmitted by the Software to ActiveState, including any -data, metadata, metrics, statistics, or other information relating to -the performance, operations, resource, health, or other conditions of -the Software, any component thereof (including third party -components), and any related infrastructure, such as network host -names, IP addresses, interpreter used, and system architecture, which -includes filenames, full path, file size, and content hash. - -“Wrapped Application” means a single-file executable in which all -binary components are encapsulated in a single binary without exposing -the base programming language as a scripting language within Your own -application program to end users. - -2. License Grant. - - (a) Subject to the terms and conditions of this Agreement, - ActiveState hereby grants to You a limited, worldwide, - perpetual, paid up, free-of-charge, non-exclusive, - non-transferable, non-assignable, and non-sublicensable - license to install and use the Software on any computing - device, in accordance with the limitations and restrictions - set forth in this Agreement, for research and development - purposes only. You may not use the Software Beyond Development - Use, except as provided in Section 2(b) below. You may not - use the Software for OEM Distribution. You may copy the Software - for archival purposes or as necessary to use the Software as - authorized in this section. You also may modify the - Accessible Code to develop bug fixes, customizations, or - additional features, for the sole purpose of using the Software - as authorized by this Agreement. - - (b) ActiveState may, in its sole discretion, grant You the right to - use the Software Beyond Development Use and/or OEM Distribution - for limited, small-scale, non-commercial and/or open source - projects. To apply for this right, contact sales@activestate.com. - Without the prior approval of ActiveState, you may not use the - Software Beyond Development Use and/or for OEM Distribution. - -3. Restrictions. - - (a) Except as expressly provided in this Agreement, You may not: - - (i) transfer, assign, sublicense, resell, or rent the - Software; - (ii) modify or translate the Software to discover the source - code in the Software or create a functional equivalent in - the Software; - (iii) reverse engineer, decompile, or disassemble (except as - and only to the extent this restriction is prohibited by - applicable law) the Software; - (iv) create derivative works based on the Software; - (v) merge the Software with another product; - (vi) copy the Software; - (vii) remove or obscure any proprietary rights notices or - labels on the Software; - (viii) Redistribute, without entering into a separate agreement - with ActiveState: - I. the Software as a whole, whether as a Wrapped - Application or on a standalone basis; - II. parts of the Software to create a language - distribution; or - III. the Software (other than the Accessible Code) with - Your Wrapped Application; - (ix) distribute the Software by OEM Distribution without - entering into a separate OEM Distribution agreement with - ActiveState; - (x) permit others to use the Software; or - (xi) use the Software: - I. Beyond Development Use on any computing device in - whatever form or manner, whether physical or - virtual and external or internal-facing; - II. on any operating systems other than Windows, OSX, and - Linux; - III. on computing devices used for file and/or application - serving; - IV. on any computing devices used for business continuity - and disaster recovery; or - V. to provide content or functionality through - external-facing servers or internal-facing - production servers. - -4. Confidentiality. - - (a) Except as reasonably required to exercise Your rights under - this Agreement, You agree to prevent any unauthorized copying, - use, distribution, installation or transfer of possession of - Confidential Information received from ActiveState (the - “ACTIVESTATE CONFIDENTIAL INFORMATION”). You do not acquire - any interest in any ActiveState Confidential Information by - reason of this Agreement. ActiveState Confidential Information - does not include any information which (i) becomes part of the - public domain through no act or omission on Your part; (ii) is - lawfully acquired by You from a third party without any breach - of confidentiality; (iii) is independently developed by You - without reference to the ActiveState Confidential Information; - or (iv) is disclosed in accordance with judicial or other - governmental order or timely disclosure requirements imposed - by law or stock exchange policies. Notwithstanding the - foregoing, either party may disclose the terms and conditions - of this Agreement in conjunction with legal proceedings. - Without limiting the generality of the foregoing, You must - take reasonable steps to prevent any personnel from removing - any proprietary or other legend or restrictive notice - contained or included in any material provided by ActiveState - to You. - - (b) You acknowledge that any use or disclosure of the ActiveState - Confidential Information in a manner inconsistent with the - provisions of this Agreement may cause ActiveState irreparable - damage for which remedies other than injunctive relief may be - inadequate. You further agree that ActiveState will be - entitled to attempt to receive from a court of competent - jurisdiction injunctive or other equitable relief to restrain - such use or disclosure in addition to other appropriate - remedies. - -5. Open Source Acknowledgement. The Software is comprised of open - source software, which is subject to the terms of the open source - software license(s) accompanying or otherwise applicable to that - open source software included in the Software (the “Open Source - Components”). For reference, Tcl/Tk open source license terms can - be found in Exhibit A attached to this Agreement or obtained from - this link: https://www.tcl.tk/software/tcltk/license.html. You - acknowledge that Your own distribution or deployment of instances - containing or linking to the Software, including the Open Source - Components, or any other open source software may trigger open - source license requirements for which You are responsible. Nothing - in this Agreement limits Your rights under or grants rights to You - that supersede the terms of any applicable open source software - license for the applicable Open Source Components. - -6. Intellectual Property Ownership. All right, title and interest in - and to the Software and all intellectual property rights embodied - therein, including copyrights, trade names, trademarks, service - marks, product names, trade secrets embodied in the Software's - design and coding methodology and other proprietary materials in - the Software belong exclusively to ActiveState or its third party - licensors. The Software is protected by Canada and United States - copyright laws and international treaty provisions as implemented - locally in different jurisdictions. Except as specifically - provided under this Agreement, You acknowledge that no other - right, title or interest in and to the Software or any parts - thereof is granted to You. ActiveState grants You the limited - right to use the trade names, trademarks, service marks or product - names of ActiveState as required for reasonable and customary use - in describing the origin of the Software. You may not use the - trade names, trademarks, service marks or product names of - ActiveState in any way that might state or imply that ActiveState - endorses Your work, or might state or imply that You created the - Software. - -7. User Data. You acknowledge, agree, and expressly consent to - ActiveState’s collection of Your User Data through the Software. - ActiveState does not claim ownership of any User Data. You hereby - grant to ActiveState and its sublicensees a royalty-free, - perpetual, irrevocable, transferable, worldwide non-exclusive - right to reproduce, analyse, review, process, diagnose, or - otherwise use the User Data (in whole or in part) for the purpose - of supporting, maintaining, and providing the Software, the - Maintenance and Support, if any, and any related services provided - by ActiveState relating to the Software (the “User Data License”). - ActiveState will not disclose the User Data to any third parties - and will only use the User Data in accordance with the User Data - License, except that ActiveState may provide Your User Data to - third parties providing services relating to the Software to - ActiveState (which will protect the User Data on terms and - conditions that are commensurate in scope with this Agreement). In - addition to the rights granted under the User Data License, You - acknowledge and agree that ActiveState has the right to (i) - publicly disclose, in any manner whatsoever, User Data that have - been anonymized; and (ii) review or analyze the User Data and - publicly disclose any results of such review or analysis, - including in the form of reports, blog posts, newsletters, - marketing materials, or otherwise, provided You will not be - identified in such publicly disclosed materials. - -8. Term. This Agreement will be effective upon Your agreement to be - bound by the terms and conditions of this Agreement and will - continue in effect unless otherwise terminated in accordance with - the terms and conditions of this Agreement. - -9. Termination. If You breach any term or condition of this - Agreement, ActiveState may immediately terminate this Agreement - with respect to the Software that You have licensed under this - Agreement by providing notice to You. ActiveState may also - terminate this Agreement, without any liability to You, if any - law, regulations, orders, or legal requirements prohibits - ActiveState’s provision or licensing of the Software to You. Upon - termination of this Agreement by ActiveState, You will immediately - cease all use of the Software and return all copies of the - Software that are under Your control to ActiveState or to delete - all such copies. - -10. Infringement Indemnification. You indemnify, hold harmless, and - defend ActiveState, its licensors, and their respective employees, - agents and distributors against any and all claims, proceedings, - demands and costs resulting from or in any way connected with Your - use of the Software and arising from Your breach of this - Agreement; provided, however, that ActiveState will notify You in - writing of any such claim. ActiveState will not enter into any - settlement or compromise any such claim without Your prior written - consent. You will have sole control of any such action and - settlement negotiations; and ActiveState will provide You with - commercially reasonable information and assistance, at Your - request and expense, necessary to settle or defend such claim. - -11. Disclaimer Of Warranty. - - (a) NEITHER ACTIVESTATE NOR ANY OF ITS SUPPLIERS, LICENSORS, OR - RESELLERS MAKES ANY WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - UNDER THIS AGREEMENT. TO THE MAXIMUM EXTENT PERMITTED UNDER - APPLICABLE LAW, ACTIVESTATE AND ITS SUPPLIERS, LICENSORS, AND - RESELLERS SPECIFICALLY DISCLAIM ALL WARRANTIES AND CONDITIONS - WITH RESPECT TO THE SOFTWARE, EITHER EXPRESS, IMPLIED OR - STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OR - CONDITION OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, - DURABILITY, MERCHANTABLE QUALITY, FITNESS FOR A PARTICULAR - PURPOSE, UPDATES, UPGRADES, SUPPLEMENTS, PRODUCTS, APPLIANCES, - SYSTEM INTEGRATION, DATA ACCURACY AND ANY OTHER ITEMS PROVIDED - HEREUNDER. ACTIVESTATE MAKES NO WARRANTY OR GUARANTEE THAT THE - OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED, ERROR-FREE, - OR VIRUS-FREE, OR THAT THE SOFTWARE WILL MEET ANY PARTICULAR - CRITERIA OF PERFORMANCE, QUALITY, ACCURACY, PURPOSE, OR NEED. - YOU ASSUME THE ENTIRE RISK OF SELECTION, INSTALLATION, AND USE - OF THE SOFTWARE. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN - ESSENTIAL PART OF THIS AGREEMENT. - - (b) TO THE EXTENT ANY IMPLIED WARRANTIES CANNOT BE DISCLAIMED - UNDER APPLICABLE LAW, ANY IMPLIED WARRANTIES ARE LIMITED IN - DURATION TO THE PERIOD REQUIRED BY APPLICABLE LAW. - - (c) SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED - WARRANTIES OR LIMITATIONS ON APPLICABLE STATUTORY RIGHTS OF A - CONSUMER, AND SO SOME OR ALL OF THE EXCLUSION OF IMPLIED - WARRANTIES OR LIMITATIONS SET OUT IN THIS SECTION MAY NOT - APPLY TO YOU. - -12. Limitation Of Liability. - - (a) INDEPENDENT OF THE FOREGOING PROVISIONS, TO THE MAXIMUM EXTENT - PERMITTED UNDER APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL - THEORY, INCLUDING WITHOUT LIMITATION, TORT, CONTRACT, OR - STRICT PRODUCTS LIABILITY, WILL ACTIVESTATE, ITS DIRECTORS, - OFFICERS, EMPLOYEES, AFFILIATES, AGENTS, CONTRACTORS, - PRINCIPALS, SUPPLIERS OR LICENSORS BE LIABLE TO YOU OR ANY - OTHER PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR - CONSEQUENTIAL DAMAGES OF ANY KIND, INCLUDING WITHOUT - LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, - COMPUTER MALFUNCTION, OR ANY OTHER KIND OF COMMERCIAL DAMAGE, - EVEN IF ACTIVESTATE HAS BEEN ADVISED OF THE POSSIBILITY OF - SUCH DAMAGES. - - (b) TO THE MAXIMUM EXTENT PERMITTED UNDER APPLICABLE LAW, IN NO - EVENT WILL ACTIVESTATE BE LIABLE TO YOU FOR DAMAGES UNDER THIS - AGREEMENT FOR ANY CAUSE WHATSOEVER, AND REGARDLESS OF THE FORM - OF ACTION. - -13. Export Controls. You must comply with all export laws and - restrictions and regulations of Canada, the United States or - foreign agencies or authorities, and not to export or re-export - the Software or any direct product thereof in violation of any - such restrictions, laws or regulations, or without all necessary - approvals. As applicable, each party will obtain and bear all - expenses relating to any necessary licenses and/or exemptions with - respect to its own export of the Software from Canada or the U.S. - Neither the Software nor the underlying information or technology - may be electronically transmitted or otherwise exported or - re-exported: into any country subject to Canada or U.S. trade - sanctions covering the Software, to individuals or entities - controlled by such countries, or to nationals or residents of such - countries other than nationals who are lawfully admitted permanent - residents of countries not subject to such sanctions; to anyone on - Canada's Area Control List of the Export and Import Permits Act; - or to anyone on the U.S. Treasury Department's list of Specially - Designated Nationals and Blocked Persons or the U.S. Commerce - Department's Table of Denial Orders. By installing or using the - Software, You agree to the foregoing and represent and warrant - that it complies with these conditions. - -14. U.S. Government End-Users. The Software is a "commercial item," as - that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of - "commercial computer software" and "commercial computer software - documentation," as such terms are used in 48 C.F.R. 12.212 (Sept. - 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 - through 227.7202-4 (June 1995), all U.S. Government End Users - acquire the Software with only those rights as are granted to - all other end users pursuant to the terms and conditions herein. - Unpublished rights are reserved under the copyright laws of Canada - and the United States. - -15. Licensee Outside The U.S. If You are located outside the U.S., - then the following provisions will apply: (a) Les parties aux - presentes confirment leur volonte que cette convention de meme que - tous les documents y compris tout avis qui siy rattache, soient - rediges en langue anglaise (translation: "The parties confirm that - this Agreement and all related documentation is and will be in the - English language."); and (b) You are responsible for complying - with any local laws in Your jurisdiction which might impact Your - right to import, export or use the Software, and You represent - that You have complied with any regulations or registration - procedures required by applicable law to make this license - enforceable. - -16. Entire Agreement. This Agreement constitutes the entire - understanding of the parties with respect to the subject matter of - this Agreement and merges all prior communications, - representations, and agreements. - -17. Severability. If any provision of this Agreement is declared - invalid or unenforceable, such provision will be deemed modified - to the extent necessary and possible to render it valid and - enforceable. In any event, the unenforceability or invalidity of - any provision will not affect any other provision of this - Agreement, and this Agreement will continue in full force and - effect, and be construed and enforced, as if such provision had - not been included, or had been modified as above provided, as the - case may be. - -18. Entire Agreement & Amendment. This Agreement constitutes the - complete agreement between the parties and supersedes all prior or - contemporaneous agreements or representations, written or oral, - concerning the subject matter of this Agreement, appendices and - attachments. ActiveState reserves the right to change this - Agreement at any time, which change shall be effective as of the - effective date for the terms and conditions of this Agreement as - shown on ActiveState’s Website (the “Change Effective Date”). Your - continued use of the Software after the Change Effective Date - constitutes Your acceptance of such changes. This Agreement may - not be otherwise amended without ActiveState's prior written - agreement. You agree to periodically review the terms and - conditions of this Agreement as updated from time to time on - ActiveState’s website. - -19. Arbitration. Except for actions to protect intellectual property - rights and to enforce an arbitrator's decision hereunder, all - disputes, controversies, or claims arising out of or relating to - this Agreement or a breach thereof will be submitted to and be - finally resolved by arbitration under the rules of the American - Arbitration Association ("AAA") then in effect. There will be one - arbitrator, and such arbitrator will be chosen by mutual agreement - of the parties in accordance with AAA rules. The arbitration will - take place in Vancouver, BC, Canada, and may be conducted by - telephone or online. The arbitrator will apply the laws of the - Province of British Columbia, Canada to all issues in dispute. The - controversy or claim will be arbitrated on an individual basis, - and will not be consolidated in any arbitration with any claim or - controversy of any other party. The findings of the arbitrator - will be final and binding on the parties, and may be entered in - any court of competent jurisdiction for enforcement. Enforcements - of any award or judgment will be governed by the United Nations - Convention on the Recognition and Enforcement of Foreign Arbitral - Awards. Should either party file an action contrary to this - provision, the other party may recover legal fees and costs up to - $1,000.00. - -20. Jurisdiction And Venue. The superior courts of Vancouver in the - Province of British Columbia, Canada will be the exclusive - jurisdiction and venue for all legal proceedings that are not - arbitrated under this Agreement. - -21. Force Majeure. Neither party will be liable for damages for any - delay or failure of delivery arising out of causes beyond their - reasonable control and without their fault or negligence, - including, but not limited to, Acts of God, acts of civil or - military authority, fires, riots, wars, embargoes, Internet - disruptions, hacker attacks, or communications failures. - Notwithstanding anything to the contrary contained herein, if - either party is unable to perform hereunder for a period of - thirty (30) consecutive days, then the other party may terminate - this Agreement immediately without liability by ten (10) days’ - written notice to the other. - -22. Publicity And Audit Rights. - - (a) You grant ActiveState the right to include Your name, trade - name, trademark, service mark or logo in its Software - promotional material. You may retract this grant at any time - in writing to marcom@activestate.com, requesting Your name, - trade name, trademark, service mark or logo be excluded from - future releases of ActiveState Software promotional material. - Requests cannot be complied with retroactively and may require - up to thirty (30) days to process. - - (b) If You entered into this Agreement on behalf of a Person, - where such Person has more than 100 employees, if requested by - ActiveState, You will furnish ActiveState with a signed - certification (i) verifying that the Software is being used - pursuant to the terms of this Agreement, including any user - limitations and (ii) listing the locations where the Software - is being used, the version(s) of the Software being used, how - long and how the Software is being used, and the number - computing devices and operating systems the Software is being - used with. You agree to grant ActiveState reasonable access to - Your site(s) and/or systems, upon prior notice during normal - business hours, to audit the use of the Software. Any such - audit shall be at ActiveState’s expense. - -23. Assignment. Except as expressly provided herein, neither this - Agreement nor any rights granted hereunder, nor the use of any of - the Software may be assigned, or otherwise transferred, in whole - or in part, by You, without the prior written consent of - ActiveState. Any permitted assignment by You under this Section - will be conditional upon You delivering all copies of the Software - to the transferee along with a copy of this Agreement, the - transferee accepting the terms and conditions of this Agreement, - and Your license to the Software terminating upon transfer. Any - attempted assignment by You will be void and of no effect unless - permitted by the foregoing. You acknowledge and agree that - ActiveState may assign this Agreement to any third party without - Your prior consent. - -24. Enurement. This Agreement will enure to the benefit of the - parties’ permitted successors and assigns. - -25. Governing Law. This Agreement will be construed under the laws of - the Province of British Columbia and the federal laws of Canada - applicable therein, without regard to the conflict of law rules. - The application of the United Nations Convention of Contracts for - the International Sale of Goods and any local implementations - thereof are expressly excluded. The parties agree that the Uniform - Computer - -Transactions Act or any version thereof, adopted by any jurisdiction, -in any form ("UCITA"), will not apply to this Agreement, and to the -extent that UCITA may be applicable, the parties agree to opt out of -the applicability of UCITA pursuant to the opt-out provision(s) -contained therein. - - - -EXHIBIT A - -Tcl/tk License Terms - - -This software is copyrighted by the Regents of the University of -California, Sun Microsystems, Inc., Scriptics Corporation, and other -parties. The following terms apply to all files associated with the -software unless explicitly disclaimed in individual files. - -The authors hereby grant permission to use, copy, modify, distribute, -and license this software and its documentation for any purpose, -provided that existing copyright notices are retained in all copies -and that this notice is included verbatim in any distributions. No -written agreement, license, or royalty fee is required for any of the -authorized uses. Modifications to this software may be copyrighted by -their authors and need not follow the licensing terms described here, -provided that the new terms are clearly indicated on the first page of -each file where they apply. - -IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY -FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES -ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY -DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, -INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND -NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND -THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE -MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -GOVERNMENT USE: If you are acquiring this software on behalf of the -U.S. government, the Government shall have only "Restricted Rights" -in the software and related documentation as defined in the Federal -Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you -are acquiring the software on behalf of the Department of Defense, -the software shall be classified as "Commercial Computer Software" -and the Government shall have only "Restricted Rights" as defined -in Clause 252.227-7013 (c)(1) of DFARs. Notwithstanding the foregoing, -the authors grant the U.S. Government and others acting in its behalf -permission to use and distribute the software in accordance with the -terms specified in this license. diff --git a/test/integration/testdata/offline-install/assets/IntegrationTest/installer_config.json b/test/integration/testdata/offline-install/assets/IntegrationTest/installer_config.json deleted file mode 100644 index 615cac0a65..0000000000 --- a/test/integration/testdata/offline-install/assets/IntegrationTest/installer_config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "org_name": "ActiveState-Test", - "project_id": "00000000-0000-0000-0000-000000000000", - "project_name": "IntegrationTest", - "commit_id": "00000000-0000-0000-0000-000000000000" -} diff --git a/test/integration/uninstall_int_test.go b/test/integration/uninstall_int_test.go index 1c0436a9ce..79cfd1da75 100644 --- a/test/integration/uninstall_int_test.go +++ b/test/integration/uninstall_int_test.go @@ -11,7 +11,6 @@ import ( "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/installation" "github.com/ActiveState/cli/internal/osutils" - "github.com/ActiveState/cli/internal/osutils/user" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" "github.com/stretchr/testify/suite" @@ -98,6 +97,7 @@ func (suite *UninstallIntegrationTestSuite) testUninstall(all bool) { suite.Fail("State service executable should not exist after uninstall") } + /* Disabled because we never configured anything in the first place: https://activestatef.atlassian.net/browse/DX-2296 if runtime.GOOS == "linux" { // When installed in a non-desktop environment (i.e. on a server), verify the user's ~/.profile was reverted. homeDir, err := user.HomeDir() @@ -105,6 +105,7 @@ func (suite *UninstallIntegrationTestSuite) testUninstall(all bool) { profile := filepath.Join(homeDir, ".profile") suite.NotContains(string(fileutils.ReadFileUnsafe(profile)), ts.SvcExe, "autostart should not be configured for Linux server environment anymore") } + */ if runtime.GOOS == "darwin" { if fileutils.DirExists(filepath.Join(ts.Dirs.Bin, "system")) { diff --git a/test/integration/update_int_test.go b/test/integration/update_int_test.go index 5b3c8c1df2..9f0da8ea5c 100644 --- a/test/integration/update_int_test.go +++ b/test/integration/update_int_test.go @@ -51,13 +51,13 @@ func (suite *UpdateIntegrationTestSuite) env(disableUpdates, forceUpdate bool) [ env := []string{} if disableUpdates { - env = append(env, "ACTIVESTATE_CLI_DISABLE_UPDATES=true") + env = append(env, constants.DisableUpdates+"=true") } else { - env = append(env, "ACTIVESTATE_CLI_DISABLE_UPDATES=false") + env = append(env, constants.DisableUpdates+"=false") } if forceUpdate { - env = append(env, "ACTIVESTATE_FORCE_UPDATE=true") + env = append(env, constants.ForceUpdateEnvVarName+"=true") } dir, err := ioutil.TempDir("", "system*") @@ -296,7 +296,7 @@ func (suite *UpdateIntegrationTestSuite) testAutoUpdate(ts *e2e.Session, baseDir e2e.OptArgs("--version"), e2e.OptAppendEnv(suite.env(false, true)...), e2e.OptAppendEnv(fmt.Sprintf("HOME=%s", fakeHome)), - e2e.OptAppendEnv("ACTIVESTATE_TEST_AUTO_UPDATE=true"), + e2e.OptAppendEnv(constants.TestAutoUpdateEnvVarName + "=true"), } if opts != nil { spawnOpts = append(spawnOpts, opts...) @@ -361,7 +361,7 @@ func (suite *UpdateIntegrationTestSuite) TestAutoUpdateToCurrent() { suite.installLatestReleaseVersion(ts, installDir) - suite.testAutoUpdate(ts, installDir, e2e.OptAppendEnv(fmt.Sprintf("ACTIVESTATE_CLI_UPDATE_BRANCH=%s", constants.BranchName))) + suite.testAutoUpdate(ts, installDir, e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.UpdateBranchEnvVarName, constants.BranchName))) } func (suite *UpdateIntegrationTestSuite) TestUpdateToCurrent() { @@ -379,5 +379,5 @@ func (suite *UpdateIntegrationTestSuite) TestUpdateToCurrent() { suite.installLatestReleaseVersion(ts, installDir) - suite.testUpdate(ts, installDir, e2e.OptAppendEnv(fmt.Sprintf("ACTIVESTATE_CLI_UPDATE_BRANCH=%s", constants.BranchName))) + suite.testUpdate(ts, installDir, e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.UpdateBranchEnvVarName, constants.BranchName))) } diff --git a/test/integration/upgen_int_test.go b/test/integration/upgen_int_test.go index 866b2b4456..a3fba2745c 100644 --- a/test/integration/upgen_int_test.go +++ b/test/integration/upgen_int_test.go @@ -35,7 +35,7 @@ func (suite *UpdateGenIntegrationTestSuite) TestUpdateBits() { } platform := runtime.GOOS + "-" + hostArch - archivePath := filepath.Join(root, "build/update", constants.BranchName, constants.Version, platform, fmt.Sprintf("state-%s-%s%s", platform, constants.Version, ext)) + archivePath := filepath.Join(root, "build/update", constants.BranchName, constants.VersionNumber, platform, fmt.Sprintf("state-%s-%s%s", platform, constants.Version, ext)) suite.Require().FileExists(archivePath, "Make sure you ran 'state run generate-update'") suite.T().Logf("file %s exists\n", archivePath) diff --git a/test/integration/use_int_test.go b/test/integration/use_int_test.go index ffa1a97d8d..242970b361 100644 --- a/test/integration/use_int_test.go +++ b/test/integration/use_int_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/ActiveState/cli/internal/config" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/osutils" "github.com/ActiveState/cli/internal/subshell" @@ -35,9 +36,9 @@ func (suite *UseIntegrationTestSuite) TestUse() { // Use. cp = ts.SpawnWithOpts( e2e.OptArgs("use", "ActiveState-CLI/Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) // Verify runtime works. @@ -55,9 +56,9 @@ func (suite *UseIntegrationTestSuite) TestUse() { // Use it. cp = ts.SpawnWithOpts( e2e.OptArgs("use", "ActiveState-CLI/Python-3.9"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) // Verify the new runtime works. @@ -68,9 +69,9 @@ func (suite *UseIntegrationTestSuite) TestUse() { // Switch back using just the project name. cp = ts.SpawnWithOpts( e2e.OptArgs("use", "Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) // Verify the first runtime is set up correctly and usable. @@ -100,9 +101,9 @@ func (suite *UseIntegrationTestSuite) TestUseCwd() { cp = ts.SpawnWithOpts( e2e.OptArgs("use"), e2e.OptWD(pythonDir), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) emptyDir := filepath.Join(ts.Dirs.Work, "EmptyDir") @@ -121,7 +122,7 @@ func (suite *UseIntegrationTestSuite) TestReset() { ts := e2e.New(suite.T(), false) defer ts.Close() - suite.SetupRCFile(ts) + ts.SetupRCFile() suite.T().Setenv("ACTIVESTATE_HOME", ts.Dirs.HomeDir) cp := ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/Python3")) @@ -131,9 +132,9 @@ func (suite *UseIntegrationTestSuite) TestReset() { cp = ts.SpawnWithOpts( e2e.OptArgs("use", "ActiveState-CLI/Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) python3Exe := filepath.Join(ts.Dirs.DefaultBin, "python3"+osutils.ExeExt) @@ -186,9 +187,9 @@ func (suite *UseIntegrationTestSuite) TestShow() { cp = ts.SpawnWithOpts( e2e.OptArgs("use", "ActiveState-CLI/Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) cp = ts.SpawnWithOpts( @@ -236,10 +237,10 @@ func (suite *UseIntegrationTestSuite) TestSetupNotice() { cp := ts.SpawnWithOpts( e2e.OptArgs("checkout", "ActiveState-CLI/Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Setting Up Runtime") - cp.Expect("Checked out project") + cp.Expect("Checked out project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) suite.Require().NoError(os.RemoveAll(filepath.Join(ts.Dirs.Work, "Python3"))) // runtime marker still exists @@ -253,10 +254,10 @@ func (suite *UseIntegrationTestSuite) TestSetupNotice() { cp = ts.SpawnWithOpts( e2e.OptArgs("use", "Python3"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) cp.Expect("Setting Up Runtime") - cp.Expect("Switched to project") + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) } @@ -272,9 +273,9 @@ func (suite *UseIntegrationTestSuite) TestJSON() { cp = ts.SpawnWithOpts( e2e.OptArgs("use", "-o", "json"), - e2e.OptAppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), ) - cp.Expect(`"namespace":`) + cp.Expect(`"namespace":`, e2e.RuntimeSourcingTimeoutOpt) cp.Expect(`"path":`) cp.Expect(`"executables":`) cp.ExpectExitCode(0) @@ -288,22 +289,6 @@ func (suite *UseIntegrationTestSuite) TestJSON() { AssertValidJSON(suite.T(), cp) } -func (suite *UseIntegrationTestSuite) SetupRCFile(ts *e2e.Session) { - if runtime.GOOS == "windows" { - return - } - - cfg, err := config.New() - suite.Require().NoError(err) - - subshell := subshell.New(cfg) - rcFile, err := subshell.RcFile() - suite.Require().NoError(err) - - err = fileutils.CopyFile(rcFile, filepath.Join(ts.Dirs.HomeDir, filepath.Base(rcFile))) - suite.Require().NoError(err) -} - func TestUseIntegrationTestSuite(t *testing.T) { suite.Run(t, new(UseIntegrationTestSuite)) } diff --git a/test/integration/vscode_int_test.go b/test/integration/vscode_int_test.go deleted file mode 100644 index 137443007d..0000000000 --- a/test/integration/vscode_int_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package integration - -import ( - "encoding/json" - "fmt" - "path/filepath" - "runtime" - "strings" - - "github.com/ActiveState/cli/internal/fileutils" - "github.com/ActiveState/cli/internal/testhelpers/e2e" - "github.com/ActiveState/cli/internal/testhelpers/tagsuite" - "github.com/ActiveState/termtest" -) - -func (suite *PushIntegrationTestSuite) TestInitAndPush_VSCode() { - suite.OnlyRunForTags(tagsuite.Init, tagsuite.Push, tagsuite.VSCode) - - ts := e2e.New(suite.T(), false) - defer ts.Close() - username, _ := ts.CreateNewUser() - - namespace := fmt.Sprintf("%s/%s", username, "Perl") - cp := ts.Spawn( - "--output", "editor", - "init", - "--language", - "perl", - namespace, - filepath.Join(ts.Dirs.Work, namespace), - ) - cp.ExpectExitCode(0) - suite.Contains(cp.Output(), "Skipping runtime setup because it was disabled by an environment variable") - suite.Contains(cp.Output(), "{") - suite.Contains(cp.Output(), "}") - wd := filepath.Join(cp.WorkDirectory(), namespace) - cp = ts.SpawnWithOpts( - e2e.OptArgs("push", "--output", "editor"), - e2e.OptWD(wd), - ) - cp.ExpectExitCode(0) - suite.Equal("", strings.TrimSpace(cp.Snapshot())) - - // check that pushed project exists - cp = ts.Spawn("show", namespace) - cp.ExpectExitCode(0) -} - -func (suite *ShowIntegrationTestSuite) TestShow_VSCode() { - suite.OnlyRunForTags(tagsuite.Show, tagsuite.VSCode) - - ts := e2e.New(suite.T(), false) - defer ts.Close() - - suite.PrepareProject(ts) - - cp := ts.Spawn( - "--output", "editor", - "show", - ) - cp.Expect("}") - cp.ExpectExitCode(0) - - type ShowOutput struct { - Name string `json:"Name"` - Organization string `json:"Organization"` - ProjectURL string `json:"ProjectURL"` - NameSpace string `json:"NameSpace"` - Visibility string `json:"Visibility"` - LastCommit string `json:"LastCommit"` - Scripts map[string]string - Languages []interface{} - Platforms []interface{} - } - - var out ShowOutput - snapshot := cp.StrippedSnapshot() - err := json.Unmarshal([]byte(snapshot), &out) - suite.Require().NoError(err, "Failed to parse JSON from: %s", snapshot) - suite.Equal("Show", out.Name) - suite.Equal(e2e.PersistentUsername, out.Organization) - suite.Equal("Public", out.Visibility) - suite.Len(out.Languages, 1) - suite.Len(out.Scripts, 2) - suite.Len(out.Platforms, 3) - -} - -func (suite *PushIntegrationTestSuite) TestOrganizations_VSCode() { - suite.OnlyRunForTags(tagsuite.Organizations, tagsuite.VSCode) - - ts := e2e.New(suite.T(), false) - defer ts.Close() - - ts.LoginAsPersistentUser() - cp := ts.Spawn("orgs", "--output", "editor") - cp.ExpectExitCode(0) - - // TODO: Response change from "free" to "Community Tier (Free)". Check that vs code extension is okay with that. - // https://www.pivotaltracker.com/story/show/178544144 - org := struct { - Name string `json:"name,omitempty"` - URLName string `json:"URLName,omitempty"` - Tier string `json:"tier,omitempty"` - PrivateProjects bool `json:"privateProjects"` - }{ - "Test-Organization", - "Test-Organization", - "Free Tier", - false, - } - - expected, err := json.Marshal(org) - suite.Require().NoError(err) - - suite.Contains(cp.Output(), string(expected)) -} - -func (suite *AuthIntegrationTestSuite) TestAuth_VSCode() { - suite.OnlyRunForTags(tagsuite.Auth, tagsuite.VSCode) - // TODO: Response change from "free" to "Community Tier (Free)". Check that vs code extension is okay with that. - // https://www.pivotaltracker.com/story/show/178544144 - user := userJSON{ - Username: e2e.PersistentUsername, - URLName: e2e.PersistentUsername, - Tier: "free", - } - data, err := json.Marshal(user) - suite.Require().NoError(err) - expected := string(data) - - ts := e2e.New(suite.T(), false) - defer ts.Close() - - cp := ts.SpawnWithOpts( - e2e.OptArgs("auth", "--username", e2e.PersistentUsername, "--password", e2e.PersistentPassword, "--output", "editor"), - e2e.OptHideArgs(), - ) - cp.Expect(`"privateProjects":false}`) - cp.ExpectExitCode(0) - suite.Equal(string(expected), strings.TrimSpace(cp.Snapshot())) - - cp = ts.Spawn("export", "jwt", "--output", "editor") - cp.ExpectExitCode(0) - suite.Assert().NotEmpty(strings.TrimSpace(cp.Snapshot()), "expected jwt token to be non-empty") -} - -func (suite *PackageIntegrationTestSuite) TestPackages_VSCode() { - suite.OnlyRunForTags(tagsuite.Package, tagsuite.VSCode) - - if runtime.GOOS == "windows" { - suite.T().Skip("Not running on windows cause it has issues parsing json output from termtest") - } - - ts := e2e.New(suite.T(), false) - defer ts.Close() - - suite.PrepareActiveStateYAML(ts) - - cp := ts.Spawn("packages", "--output", "editor") - cp.Expect("]") - cp.ExpectExitCode(0) - - type PackageOutput struct { - Package string `json:"package"` - Version string `json:"version"` - } - - var po []PackageOutput - out := cp.StrippedSnapshot() - err := json.Unmarshal([]byte(out), &po) - suite.Require().NoError(err, "Could not parse JSON from: %s", out) - - suite.Len(po, 2) -} - -func (suite *ProjectsIntegrationTestSuite) TestProjects_VSCode() { - suite.OnlyRunForTags(tagsuite.Projects, tagsuite.VSCode) - ts := e2e.New(suite.T(), false) - defer ts.Close() - - cp := ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/small-python")) - cp.ExpectExitCode(0) - cp = ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/Python3")) - cp.ExpectExitCode(0) - - // Verify separate "local_checkouts" and "executables" fields for editor output. - cp = ts.SpawnWithOpts( - e2e.OptArgs("projects", "--output", "editor"), - e2e.OptTermTest(termtest.OptCols(2000)), // Line breaks make it hard to assert long output - ) - cp.Expect(`"name":"Python3"`) - cp.Expect(`"local_checkouts":["`) - if runtime.GOOS != "windows" { - cp.Expect(filepath.Join(ts.Dirs.Work, "Python3") + `"]`) - } else { - // Windows uses the long path here. - longPath, _ := fileutils.GetLongPathName(filepath.Join(ts.Dirs.Work, "Python3")) - cp.Expect(strings.ReplaceAll(longPath, "\\", "\\\\") + `"]`) - } - cp.Expect(`"executables":["`) - if runtime.GOOS != "windows" { - cp.Expect(ts.Dirs.Cache) - } else { - cp.Expect(strings.ReplaceAll(ts.Dirs.Cache, "\\", "\\\\")) - } - cp.ExpectExitCode(0) -} diff --git a/vendor/github.com/ActiveState/termtest/helpers.go b/vendor/github.com/ActiveState/termtest/helpers.go index 2bb0615df6..3144ad2b1a 100644 --- a/vendor/github.com/ActiveState/termtest/helpers.go +++ b/vendor/github.com/ActiveState/termtest/helpers.go @@ -9,9 +9,9 @@ import ( "time" ) -type voidWriter struct{} +type voidLogger struct{} -func (v voidWriter) Write(p []byte) (n int, err error) { return len(p), nil } +func (v voidLogger) Write(p []byte) (n int, err error) { return len(p), nil } var neverGonnaHappen = time.Hour * 24 * 365 * 100 diff --git a/vendor/github.com/ActiveState/termtest/termtest.go b/vendor/github.com/ActiveState/termtest/termtest.go index 66481e8165..7677bcebdc 100644 --- a/vendor/github.com/ActiveState/termtest/termtest.go +++ b/vendor/github.com/ActiveState/termtest/termtest.go @@ -41,6 +41,10 @@ type Opts struct { var TimeoutError = errors.New("timeout") +var VerboseLogger = log.New(os.Stderr, "TermTest: ", log.LstdFlags|log.Lshortfile) + +var VoidLogger = log.New(voidLogger{}, "", 0) + type SetOpt func(o *Opts) error const DefaultCols = 140 @@ -48,7 +52,7 @@ const DefaultRows = 10 func NewOpts() *Opts { return &Opts{ - Logger: log.New(voidWriter{}, "TermTest: ", log.LstdFlags|log.Lshortfile), + Logger: VoidLogger, ExpectErrorHandler: func(_ *TermTest, err error) error { panic(err) }, @@ -94,9 +98,13 @@ func SilenceErrorHandler() ErrorHandler { } } -func OptVerboseLogging() SetOpt { +func OptVerboseLogger() SetOpt { + return OptLogger(VerboseLogger) +} + +func OptLogger(logger *log.Logger) SetOpt { return func(o *Opts) error { - o.Logger = log.New(os.Stderr, "TermTest: ", log.LstdFlags|log.Lshortfile) + o.Logger = logger return nil } } @@ -183,6 +191,10 @@ func (tt *TermTest) SetErrorHandler(handler ErrorHandler) { tt.opts.ExpectErrorHandler = handler } +func (tt *TermTest) SetLogger(logger *log.Logger) { + tt.opts.Logger = logger +} + func (tt *TermTest) SetTest(t *testing.T) { setTest(tt.opts, t) } diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go index 4f7b42488a..915d5090dd 100644 --- a/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go +++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go @@ -34,8 +34,6 @@ const ( DiffInsert Operation = 1 // DiffEqual item represents an equal diff. DiffEqual Operation = 0 - //IndexSeparator is used to seperate the array indexes in an index string - IndexSeparator = "," ) // Diff represents one diff operation @@ -406,14 +404,11 @@ func (dmp *DiffMatchPatch) DiffLinesToRunes(text1, text2 string) ([]rune, []rune func (dmp *DiffMatchPatch) DiffCharsToLines(diffs []Diff, lineArray []string) []Diff { hydrated := make([]Diff, 0, len(diffs)) for _, aDiff := range diffs { - chars := strings.Split(aDiff.Text, IndexSeparator) - text := make([]string, len(chars)) + runes := []rune(aDiff.Text) + text := make([]string, len(runes)) - for i, r := range chars { - i1, err := strconv.Atoi(r) - if err == nil { - text[i] = lineArray[i1] - } + for i, r := range runes { + text[i] = lineArray[runeToInt(r)] } aDiff.Text = strings.Join(text, "") diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go index 44c4359547..eb727bb594 100644 --- a/vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go +++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go @@ -9,11 +9,16 @@ package diffmatchpatch import ( - "strconv" + "fmt" "strings" "unicode/utf8" ) +const UNICODE_INVALID_RANGE_START = 0xD800 +const UNICODE_INVALID_RANGE_END = 0xDFFF +const UNICODE_INVALID_RANGE_DELTA = UNICODE_INVALID_RANGE_END - UNICODE_INVALID_RANGE_START + 1 +const UNICODE_RANGE_MAX = 0x10FFFF + // unescaper unescapes selected chars for compatibility with JavaScript's encodeURI. // In speed critical applications this could be dropped since the receiving application will certainly decode these fine. Note that this function is case-sensitive. Thus "%3F" would not be unescaped. But this is ok because it is only called with the output of HttpUtility.UrlEncode which returns lowercase hex. Example: "%3f" -> "?", "%24" -> "$", etc. var unescaper = strings.NewReplacer( @@ -93,14 +98,93 @@ func intArrayToString(ns []uint32) string { return "" } - indexSeparator := IndexSeparator[0] - - // Appr. 3 chars per num plus the comma. - b := []byte{} + b := []rune{} for _, n := range ns { - b = strconv.AppendInt(b, int64(n), 10) - b = append(b, indexSeparator) + b = append(b, intToRune(n)) } - b = b[:len(b)-1] return string(b) } + +// These constants define the number of bits representable +// in 1,2,3,4 byte utf8 sequences, respectively. +const ONE_BYTE_BITS = 7 +const TWO_BYTE_BITS = 11 +const THREE_BYTE_BITS = 16 +const FOUR_BYTE_BITS = 21 + +// Helper for getting a sequence of bits from an integer. +func getBits(i uint32, cnt byte, from byte) byte { + return byte((i >> from) & ((1 << cnt) - 1)) +} + +// Converts an integer in the range 0~1112060 into a rune. +// Based on the ranges table in https://en.wikipedia.org/wiki/UTF-8 +func intToRune(i uint32) rune { + if i < (1 << ONE_BYTE_BITS) { + return rune(i) + } + + if i < (1 << TWO_BYTE_BITS) { + r, size := utf8.DecodeRune([]byte{0b11000000 | getBits(i, 5, 6), 0b10000000 | getBits(i, 6, 0)}) + if size != 2 || r == utf8.RuneError { + panic(fmt.Sprintf("Error encoding an int %d with size 2, got rune %v and size %d", size, r, i)) + } + return r + } + + // Last -3 here needed because for some reason 3rd to last codepoint 65533 in this range + // was returning utf8.RuneError during encoding. + if i < ((1 << THREE_BYTE_BITS) - UNICODE_INVALID_RANGE_DELTA - 3) { + if i >= UNICODE_INVALID_RANGE_START { + i += UNICODE_INVALID_RANGE_DELTA + } + + r, size := utf8.DecodeRune([]byte{0b11100000 | getBits(i, 4, 12), 0b10000000 | getBits(i, 6, 6), 0b10000000 | getBits(i, 6, 0)}) + if size != 3 || r == utf8.RuneError { + panic(fmt.Sprintf("Error encoding an int %d with size 3, got rune %v and size %d", size, r, i)) + } + return r + } + + if i < (1<= UNICODE_INVALID_RANGE_END { + return result - UNICODE_INVALID_RANGE_DELTA + } + + return result + } + + if size == 4 { + result := uint32(bytes[0]&0b111)<<18 | uint32(bytes[1]&0b111111)<<12 | uint32(bytes[2]&0b111111)<<6 | uint32(bytes[3]&0b111111) + return result - UNICODE_INVALID_RANGE_DELTA - 3 + } + + panic(fmt.Sprintf("Unexpected state decoding rune=%v size=%d", r, size)) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ce5be23295..a8bedbf9f0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -19,7 +19,7 @@ github.com/ActiveState/graphql # github.com/ActiveState/pty v0.0.0-20230628221854-6fb90eb08a14 ## explicit; go 1.13 github.com/ActiveState/pty -# github.com/ActiveState/termtest v0.7.3-0.20230914171339-6e6462ec3e3f +# github.com/ActiveState/termtest v0.7.3-0.20231006191111-13d903a6f2de ## explicit; go 1.18 github.com/ActiveState/termtest # github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 @@ -389,8 +389,8 @@ github.com/rivo/uniseg # github.com/rollbar/rollbar-go v1.1.0 ## explicit github.com/rollbar/rollbar-go -# github.com/sergi/go-diff v1.3.1 -## explicit; go 1.12 +# github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 +## explicit; go 1.13 github.com/sergi/go-diff/diffmatchpatch # github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 ## explicit diff --git a/version.txt b/version.txt index 4753a5b164..f0938929d6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.42.0-RC1 +0.42.0-RC1 \ No newline at end of file