diff --git a/integration-tests/api_curl_test.go b/integration-tests/api_curl_test.go index 414eb34e4..17658947f 100644 --- a/integration-tests/api_curl_test.go +++ b/integration-tests/api_curl_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "os/exec" "strings" + "sync" "testing" "github.com/go-chi/chi/v5" @@ -13,8 +14,41 @@ import ( "github.com/stretchr/testify/assert" ) +// tokenState holds mutable token state shared between the test and HTTP handlers. +type tokenState struct { + mu sync.Mutex + validToken string + tokenFetches int +} + +func (s *tokenState) setToken(token string) { + s.mu.Lock() + defer s.mu.Unlock() + s.validToken = token +} + +func (s *tokenState) getToken() string { + s.mu.Lock() + defer s.mu.Unlock() + return s.validToken +} + +// fetchToken increments the fetch count and returns the current valid token. +func (s *tokenState) fetchToken() string { + s.mu.Lock() + defer s.mu.Unlock() + s.tokenFetches++ + return s.validToken +} + +func (s *tokenState) getFetches() int { + s.mu.Lock() + defer s.mu.Unlock() + return s.tokenFetches +} + func TestApiCurlCommand(t *testing.T) { - validToken := "valid-token" + state := &tokenState{validToken: "valid-token"} mux := chi.NewMux() if testing.Verbose() { @@ -23,7 +57,7 @@ func TestApiCurlCommand(t *testing.T) { mux.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, "/oauth2") { - if r.Header.Get("Authorization") != "Bearer "+validToken { + if r.Header.Get("Authorization") != "Bearer "+state.getToken() { w.WriteHeader(http.StatusUnauthorized) //nolint:lll _ = json.NewEncoder(w).Encode(map[string]any{"error": "invalid_token", "error_description": "Invalid access token."}) @@ -33,11 +67,10 @@ func TestApiCurlCommand(t *testing.T) { next.ServeHTTP(w, r) }) }) - var tokenFetches int mux.Post("/oauth2/token", func(w http.ResponseWriter, _ *http.Request) { + tok := state.fetchToken() w.WriteHeader(http.StatusOK) - tokenFetches++ - _ = json.NewEncoder(w).Encode(map[string]any{"access_token": validToken, "expires_in": 900, "token_type": "bearer"}) + _ = json.NewEncoder(w).Encode(map[string]any{"access_token": tok, "expires_in": 900, "token_type": "bearer"}) }) mux.Get("/users/me", func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{"id": "userID", "email": "me@example.com"}) @@ -52,24 +85,24 @@ func TestApiCurlCommand(t *testing.T) { // Load the first token. assert.Equal(t, "success", f.Run("api:curl", "/fake-api-path")) - assert.Equal(t, 1, tokenFetches) + assert.Equal(t, 1, state.getFetches()) // Revoke the access token and try the command again. // The old token should be considered invalid, so the API call should return 401, // and then the CLI should refresh the token and retry. - validToken = "new-valid-token" + state.setToken("new-valid-token") assert.Equal(t, "success", f.Run("api:curl", "/fake-api-path")) - assert.Equal(t, 2, tokenFetches) + assert.Equal(t, 2, state.getFetches()) assert.Equal(t, "success", f.Run("api:curl", "/fake-api-path")) - assert.Equal(t, 2, tokenFetches) + assert.Equal(t, 2, state.getFetches()) // If --no-retry-401 and --fail are provided then the command should return exit code 22. - validToken = "another-new-valid-token" + state.setToken("another-new-valid-token") stdOut, _, err := f.RunCombinedOutput("api:curl", "/fake-api-path", "--no-retry-401", "--fail") exitErr := &exec.ExitError{} assert.ErrorAs(t, err, &exitErr) assert.Equal(t, 22, exitErr.ExitCode()) assert.Empty(t, stdOut) - assert.Equal(t, 2, tokenFetches) + assert.Equal(t, 2, state.getFetches()) } diff --git a/integration-tests/variable_write_test.go b/integration-tests/variable_write_test.go index 14989405c..3e497701f 100644 --- a/integration-tests/variable_write_test.go +++ b/integration-tests/variable_write_test.go @@ -9,29 +9,54 @@ import ( "github.com/upsun/cli/pkg/mockapi" ) -func TestVariableCreate(t *testing.T) { +// variableTestSetup holds common test infrastructure for variable tests. +type variableTestSetup struct { + authServer *httptest.Server + apiServer *httptest.Server + apiHandler *mockapi.Handler + projectID string + mainEnv *mockapi.Environment + factory *cmdFactory +} + +// setupVariableTest creates the common test infrastructure for variable tests. +func setupVariableTest(t *testing.T) *variableTestSetup { authServer := mockapi.NewAuthServer(t) - defer authServer.Close() + t.Cleanup(authServer.Close) apiHandler := mockapi.NewHandler(t) apiServer := httptest.NewServer(apiHandler) - defer apiServer.Close() + t.Cleanup(apiServer.Close) projectID := mockapi.ProjectID() apiHandler.SetProjects([]*mockapi.Project{{ ID: projectID, - Links: mockapi.MakeHALLinks("self=/projects/"+projectID, - "environments=/projects/"+projectID+"/environments"), + Links: mockapi.MakeHALLinks( + "self=/projects/"+projectID, + "environments=/projects/"+projectID+"/environments", + ), DefaultBranch: "main", }}) - main := makeEnv(projectID, "main", "production", "active", nil) - main.Links["#variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"} - main.Links["#manage-variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"} - envs := []*mockapi.Environment{main} - apiHandler.SetEnvironments(envs) - apiHandler.SetProjectVariables(projectID, []*mockapi.Variable{ + mainEnv := makeEnv(projectID, "main", "production", "active", nil) + mainEnv.Links["#variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"} + mainEnv.Links["#manage-variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"} + + return &variableTestSetup{ + authServer: authServer, + apiServer: apiServer, + apiHandler: apiHandler, + projectID: projectID, + mainEnv: mainEnv, + factory: newCommandFactory(t, apiServer.URL, authServer.URL), + } +} + +func TestVariableCreate(t *testing.T) { + s := setupVariableTest(t) + s.apiHandler.SetEnvironments([]*mockapi.Environment{s.mainEnv}) + s.apiHandler.SetProjectVariables(s.projectID, []*mockapi.Variable{ { Name: "existing", IsSensitive: true, @@ -39,36 +64,99 @@ func TestVariableCreate(t *testing.T) { }, }) - f := newCommandFactory(t, apiServer.URL, authServer.URL) + f, p := s.factory, s.projectID //nolint:lll - _, stdErr, err := f.RunCombinedOutput("var:create", "-p", projectID, "-l", "e", "-e", "main", "env:TEST", "--value", "env-level-value") + _, stdErr, err := f.RunCombinedOutput("var:create", "-p", p, "-l", "e", "-e", "main", "env:TEST", "--value", "env-level-value") assert.NoError(t, err) assert.Contains(t, stdErr, "Creating variable env:TEST on the environment main") - assertTrimmed(t, "env-level-value", f.Run("var:get", "-p", projectID, "-e", "main", "env:TEST", "-P", "value")) + assertTrimmed(t, "env-level-value", f.Run("var:get", "-p", p, "-e", "main", "env:TEST", "-P", "value")) - //nolint:lll - _, stdErr, err = f.RunCombinedOutput("var:create", "-p", projectID, "env:TEST", "-l", "p", "--value", "project-level-value") + _, stdErr, err = f.RunCombinedOutput("var:create", "-p", p, "env:TEST", "-l", "p", "--value", "project-level-value") assert.NoError(t, err) - assert.Contains(t, stdErr, "Creating variable env:TEST on the project "+projectID) + assert.Contains(t, stdErr, "Creating variable env:TEST on the project "+p) - //nolint:lll - assertTrimmed(t, "project-level-value", f.Run("var:get", "-p", projectID, "-e", "main", "env:TEST", "-P", "value", "-l", "p")) - //nolint:lll - assertTrimmed(t, "env-level-value", f.Run("var:get", "-p", projectID, "-e", "main", "env:TEST", "-P", "value", "-l", "e")) + assertTrimmed(t, "project-level-value", f.Run("var:get", "-p", p, "-e", "main", "env:TEST", "-P", "value", "-l", "p")) + assertTrimmed(t, "env-level-value", f.Run("var:get", "-p", p, "-e", "main", "env:TEST", "-P", "value", "-l", "e")) - _, stdErr, err = f.RunCombinedOutput("var:create", "-p", projectID, "existing", "-l", "p", "--value", "test") + _, stdErr, err = f.RunCombinedOutput("var:create", "-p", p, "existing", "-l", "p", "--value", "test") assert.Error(t, err) assert.Contains(t, stdErr, "The variable already exists") - //nolint:lll - _, _, err = f.RunCombinedOutput("var:update", "-p", projectID, "env:TEST", "-l", "p", "--value", "project-level-value2") + _, _, err = f.RunCombinedOutput("var:update", "-p", p, "env:TEST", "-l", "p", "--value", "project-level-value2") assert.NoError(t, err) - assertTrimmed(t, "project-level-value2", f.Run("var:get", "-p", projectID, "env:TEST", "-l", "p", "-P", "value")) + assertTrimmed(t, "project-level-value2", f.Run("var:get", "-p", p, "env:TEST", "-l", "p", "-P", "value")) - assertTrimmed(t, "true", f.Run("var:get", "-p", projectID, "env:TEST", "-l", "p", "-P", "visible_runtime")) - _, _, err = f.RunCombinedOutput("var:update", "-p", projectID, "env:TEST", "-l", "p", "--visible-runtime", "false") + assertTrimmed(t, "true", f.Run("var:get", "-p", p, "env:TEST", "-l", "p", "-P", "visible_runtime")) + _, _, err = f.RunCombinedOutput("var:update", "-p", p, "env:TEST", "-l", "p", "--visible-runtime", "false") assert.NoError(t, err) - assertTrimmed(t, "false", f.Run("var:get", "-p", projectID, "env:TEST", "-l", "p", "-P", "visible_runtime")) + assertTrimmed(t, "false", f.Run("var:get", "-p", p, "env:TEST", "-l", "p", "-P", "visible_runtime")) +} + +func TestVariableCreateWithAppScope(t *testing.T) { + s := setupVariableTest(t) + + // Set up deployment with app names for validation. + s.mainEnv.SetCurrentDeployment(&mockapi.Deployment{ + WebApps: map[string]mockapi.App{ + "app1": {Name: "app1", Type: "golang:1.23"}, + "app2": {Name: "app2", Type: "php:8.3"}, + }, + Routes: make(map[string]any), + Links: mockapi.MakeHALLinks("self=/projects/" + s.projectID + "/environments/main/deployment/current"), + }) + s.apiHandler.SetEnvironments([]*mockapi.Environment{s.mainEnv}) + + f, p := s.factory, s.projectID + + // Test creating project-level variable with single app-scope. + _, stdErr, err := f.RunCombinedOutput("var:create", "-p", p, "-l", "p", + "env:SCOPED", "--value", "val", "--app-scope", "app1") + assert.NoError(t, err) + assert.Contains(t, stdErr, "Creating variable env:SCOPED") + + // Verify application_scope was set. + out := f.Run("var:get", "-p", p, "-l", "p", "env:SCOPED", "-P", "application_scope") + assert.Contains(t, out, "app1") + + // Test creating variable with multiple app scopes. + _, _, err = f.RunCombinedOutput("var:create", "-p", p, "-l", "p", + "env:MULTI", "--value", "val", "--app-scope", "app1", "--app-scope", "app2") + assert.NoError(t, err) + + out = f.Run("var:get", "-p", p, "-l", "p", "env:MULTI", "-P", "application_scope") + assert.Contains(t, out, "app1") + assert.Contains(t, out, "app2") + + // Test validation rejects invalid app names (when deployment exists). + _, stdErr, err = f.RunCombinedOutput("var:create", "-p", p, "-l", "p", + "env:BAD", "--value", "val", "--app-scope", "nonexistent") + assert.Error(t, err) + assert.Contains(t, stdErr, "was not found") + + // Test updating app-scope. + _, _, err = f.RunCombinedOutput("var:update", "-p", p, "-l", "p", + "env:SCOPED", "--app-scope", "app2") + assert.NoError(t, err) + + out = f.Run("var:get", "-p", p, "-l", "p", "env:SCOPED", "-P", "application_scope") + assert.Contains(t, out, "app2") +} + +func TestVariableCreateWithAppScopeNoDeployment(t *testing.T) { + // Uses an environment without a deployment, so app-scope validation is skipped. + s := setupVariableTest(t) + s.apiHandler.SetEnvironments([]*mockapi.Environment{s.mainEnv}) + + f, p := s.factory, s.projectID + + // Without a deployment, any app-scope value should be accepted. + _, stdErr, err := f.RunCombinedOutput("var:create", "-p", p, "-l", "p", + "env:ANY_APP", "--value", "val", "--app-scope", "anyapp") + assert.NoError(t, err) + assert.Contains(t, stdErr, "Creating variable env:ANY_APP") + + out := f.Run("var:get", "-p", p, "-l", "p", "env:ANY_APP", "-P", "application_scope") + assert.Contains(t, out, "anyapp") } diff --git a/legacy/CONTRIBUTING.md b/legacy/CONTRIBUTING.md index 8984f4d2f..15c036039 100644 --- a/legacy/CONTRIBUTING.md +++ b/legacy/CONTRIBUTING.md @@ -1,12 +1,10 @@ # Contributing -Development of the Platform.sh Legacy CLI happens in public in the -[GitHub repository](https://github.com/platformsh/legacy-cli). +Development of the Legacy CLI happens in public in the [GitHub repository](https://github.com/platformsh/legacy-cli). Issues and pull requests submitted via GitHub are very welcome. -In the near future - to be confirmed - this may move to being a subtree split -of the new CLI repository at: https://github.com/platformsh/cli +The principal Upsun CLI repository is: https://github.com/platformsh/cli ## Developing locally @@ -30,8 +28,8 @@ Run tests with: ## Developing in a docker container -If you don't have PHP installed locally or for other reasons want to do development on the -Platform.sh CLI inside a docker container, follow this procedure: +If you don't have PHP installed locally or for other reasons want to do development on the CLI inside a docker +container, follow this procedure: Create a `.env` file based on the default one diff --git a/legacy/Makefile b/legacy/Makefile index 474db6612..8b5498c81 100644 --- a/legacy/Makefile +++ b/legacy/Makefile @@ -1,5 +1,3 @@ -GO_TESTS_DIR=go-tests - .PHONY: composer-dev composer-dev: composer install --no-interaction @@ -16,19 +14,9 @@ lint-phpstan: composer-dev lint-php-cs-fixer: composer-dev ./vendor/bin/php-cs-fixer check --config .php-cs-fixer.dist.php --diff -.PHONY: lint-gofmt -lint-gofmt: - cd $(GO_TESTS_DIR) && go fmt ./... - -.PHONY: lint-golangci -lint-golangci: - command -v golangci-lint >/dev/null || go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) - cd $(GO_TESTS_DIR) && golangci-lint run - .PHONY: lint -lint: lint-gofmt lint-golangci lint-php-cs-fixer lint-phpstan +lint: lint-php-cs-fixer lint-phpstan .PHONY: test test: ./vendor/bin/phpunit --exclude-group slow - cd $(GO_TESTS_DIR) && go test -v -count=1 ./... diff --git a/legacy/README.md b/legacy/README.md index 840fcc6a7..72ccdbe6b 100644 --- a/legacy/README.md +++ b/legacy/README.md @@ -1,4 +1,6 @@ -The **Legacy** Platform.sh CLI is the legacy version of the command-line interface for [Platform.sh](https://platform.sh). For the **current Platform.sh CLI**, check [this repository](https://github.com/platformsh/cli). +The **Legacy** CLI is the legacy version of the command-line interface for [Upsun (formerly Platform.sh)](https://upsun.com). + +For the **current Upsun CLI**, check [this repository](https://github.com/platformsh/cli). ## Install @@ -53,7 +55,7 @@ scoop update platform ## Usage -You can run the Platform.sh CLI in your shell by typing `platform`. +You can run this CLI in your shell by typing `platform`. platform @@ -96,7 +98,7 @@ Other customization is available via environment variables, including: * `PLATFORMSH_CLI_SSH_AUTO_LOAD_CERT`: set to 0 to disable automatic loading of an SSH certificate when running login or SSH commands * `PLATFORMSH_CLI_REPORT_DEPRECATIONS`: set to 1 to enable PHP deprecation notices (suppressed by default). They will only be displayed in debug mode (`-vvv`). * `CLICOLOR_FORCE`: set to 1 or 0 to force colorized output on or off, respectively -* `http_proxy` or `https_proxy`: specify a proxy for connecting to Platform.sh +* `http_proxy` or `https_proxy`: specify an HTTP proxy ## Known issues diff --git a/legacy/config-defaults.yaml b/legacy/config-defaults.yaml index 4f13925f5..256665a74 100644 --- a/legacy/config-defaults.yaml +++ b/legacy/config-defaults.yaml @@ -363,6 +363,7 @@ detection: - Unable to find stack - Cannot build application of type - Invalid deployment + - Could not perform a rolling deployment # Pagination settings. # @@ -425,9 +426,8 @@ browser_login: # css: '' body: | diff --git a/legacy/config.yaml b/legacy/config.yaml index fd13cd114..254c775a4 100644 --- a/legacy/config.yaml +++ b/legacy/config.yaml @@ -1,7 +1,11 @@ -# Platform.sh CLI configuration overrides. +# Upsun CLI (Platform.sh compatibility) configuration overrides # See config-defaults.yaml for the default values and a list of other required keys. +# +# Platform.sh is now Upsun. +# +# These are settings for the 'platform' command, which is available for backwards compatibility. application: - name: 'Platform.sh CLI' + name: 'Upsun CLI (Platform.sh compatibility)' slug: 'platformsh-cli' executable: 'platform' env_prefix: 'PLATFORMSH_CLI_' @@ -17,35 +21,31 @@ application: - self:install - self:update -local: - # A legacy project config file from versions < 3. - project_config_legacy: 'platform-project.yaml' - service: - name: 'Platform.sh' + name: 'Upsun (formerly Platform.sh)' env_prefix: 'PLATFORM_' project_config_dir: '.platform' app_config_file: '.platform.app.yaml' - console_url: 'https://console.platform.sh' + console_url: 'https://console.upsun.com' - docs_url: 'https://docs.platform.sh' - docs_search_url: 'https://docs.platform.sh/search.html?q={{ terms }}' + docs_url: 'https://docs.upsun.com' + docs_search_url: 'https://docs.upsun.com/search.html?q={{ terms }}' - register_url: 'https://auth.api.platform.sh/register' - reset_password_url: 'https://auth.api.platform.sh/reset-password' + register_url: 'https://auth.upsun.com/register' + reset_password_url: 'https://auth.upsun.com/reset-password' - pricing_url: 'https://platform.sh/pricing' + pricing_url: 'https://upsun.com/pricing' activity_type_list_url: 'https://docs.upsun.com/anchors/fixed/integrations/activity-scripts/type/' runtime_operations_help_url: 'https://docs.upsun.com/anchors/fixed/app/runtime-operations/' api: - base_url: 'https://api.platform.sh' + base_url: 'https://api.upsun.com' - auth_url: 'https://auth.api.platform.sh' + auth_url: 'https://auth.upsun.com' oauth2_client_id: 'platform-cli' organization_types: [flexible, fixed] @@ -56,7 +56,7 @@ api: metrics: true teams: true - vendor_filter: 'platformsh' + vendor_filter: 'upsun' ssh: domain_wildcards: ['*.platform.sh'] @@ -64,7 +64,7 @@ ssh: detection: git_remote_name: 'platform' git_domain: 'platform.sh' # matches git.eu-5.platform.sh, etc. - site_domains: ['platform.sh', 'platformsh.site', 'tst.site'] + site_domains: ['platform.sh', 'platformsh.site', 'tst.site', 'upsunapp.com', 'upsun.app'] cluster_header: 'X-Platform-Cluster' migrate: diff --git a/legacy/dist/dev-build-index.php b/legacy/dist/dev-build-index.php index 6f0056581..62e331bf9 100644 --- a/legacy/dist/dev-build-index.php +++ b/legacy/dist/dev-build-index.php @@ -2,7 +2,7 @@ declare(strict_types=1); /** * @file - * This is the index.php script for automated CLI builds on Platform.sh. + * This is the index.php script for automated CLI builds on Upsun. */ use Platformsh\Cli\Service\Config; diff --git a/legacy/dist/manifest.json b/legacy/dist/manifest.json index 8a9c4f62e..f0c6096d9 100644 --- a/legacy/dist/manifest.json +++ b/legacy/dist/manifest.json @@ -1,10 +1,10 @@ [ { - "version": "4.27.0", - "sha1": "62e5f46d69cf191324b2baa4a0ee9aa1487d564a", - "sha256": "77b998915dc64a2141809dec08a7e9988045376e4bbcb99005512249813b9c61", + "version": "4.30.0", + "sha1": "3e442238f815ba68ce46fbdfd1454c3dd644fd26", + "sha256": "9903c7111a1b7b0c94a44a7e5a421c3abb58c3ff53fc39079e9b896d0e93cf0c", "name": "platform.phar", - "url": "https://github.com/platformsh/legacy-cli/releases/download/v4.27.0/platform.phar", + "url": "https://github.com/platformsh/legacy-cli/releases/download/v4.30.0/platform.phar", "php": { "min": "5.5.9" }, @@ -77,7 +77,11 @@ "4.24.0": "New features:\n\n* Support guaranteed resources, adding a new CPU type concept (`shared` or\n `guaranteed`) to the `resources` commands.\n* Support manual deployments:\n - Add an `environment:deploy` command to deploy staged changes.\n - Add an `environment:deploy:type` command to view or change the deployment\n type (between `manual` and `automatic`).\n - Add a `deployment_type` property (read-only) to the `environment:info`\n command.\n - Support the `staged` activity state in activity-related commands.\n\nOther changes:\n\n* Increase the default limits for finding activities.\n* Stop bypassing the organization endpoint for subscriptions.\n* Update Go test dependencies.\n* Add `mark_unwrapped_legacy` config option (defaults to `false`).", "4.25.0": "New features:\n\n* Autoscaling-related features:\n - Add an `autoscaling` command to read autoscaling settings.\n - Mark services with autoscaling in the `resources` command.\n - Disallow changing the instance count in `resources:set` when autoscaling is enabled.\n* Support the OpenTelemetry Protocol (`otlp`) integration, when available on the project.\n* Add a `--strategy` (`-s`) option to the `env:deploy` command (`stopstart` or `rolling`).\n* Require confirmation on `branch` or `env:activate` commands when guaranteed resources are configured.\n\nOther changes:\n\n* Update embedded docs links to the new permalink structure.\n* Avoid writing to stdout when opening a URL.\n* Treat `upsun` and `platformsh` vendors as equivalent in the project list from 2025-09-23.\n* Fix the command recommendation when there are staged activities.\n* Fix the Drush site URL when there is no app route (e.g. with Varnish).", "4.26.0": "New features:\n\n* Add `autoscaling:set` command, to configure CPU-based autoscaling\n* Add support for organization types\n - Display the organization type in the `orgs` list\n - Add a `--type` filter in the `orgs` list\n - Add a `--type` option to `org:create`\n - Display the organization type in the `projects` list\n - Add an `--org-type` filter in the `projects` list\n* Add a `deploy` alias for the `env:deploy` command\n\nOther changes:\n\n* Fix \"This environment is inactive\" during activation, if the deployment cannot \n be fetched.", - "4.27.0": "* Support `memory` as a trigger for autoscaling\n* Set `--fail-with-body` by default in `:curl` commands\n* Update name of `otlp` integration to `otlplog`" + "4.27.0": "* Support `memory` as a trigger for autoscaling\n* Set `--fail-with-body` by default in `:curl` commands\n* Update name of `otlp` integration to `otlplog`", + "4.28.0": "New features:\n\n* Display activities with non-zero exit codes as failed.\n* Allow specifying the deploy strategy on push.\n* Switch to new, more efficient endpoint for querying metrics. \n Output will be mostly the same as the previous `metrics` commands, with these\n exceptions:\n - Rows for the `router` service are now displayed by default.\n - The `---internal---storage` is no longer displayed by default.\n - `inodes` columns are now displayed by default in the `metrics` command. The\n first 5 table columns are the same, and the other commands' default columns\n are the same.\n - The `--interval` option is deprecated and no longer has an effect. The\n default interval displayed changes from 2 minutes to 1 minute.\n\nOther changes:\n\n* Error earlier in non-interactive mode when an organization ID is required.\n* Add an example for adding a column in activity:list command help.\n* Remove security contact from writable properties of billing profile.", + "4.28.2": "* Fix: the resources:build:get command should be hidden when sizing is disabled (#1583)", + "4.29.0": "* Allow configuring autoscaling for all services that support it.\n* Skip the \"type\" question when creating an organization.\n* Restore the metrics --interval option.\n* Return an error for a failed rolling deployment.", + "4.30.0": "* Add `--app-scope` option for variables.\n* Fix `PROCESS privilege(s)` error during `db:dump` of oracle-mysql databases.\n* Add `environment.alert` activity type." } }, { diff --git a/legacy/src/Command/Activity/ActivityListCommand.php b/legacy/src/Command/Activity/ActivityListCommand.php index dfaca47b1..cddc00067 100644 --- a/legacy/src/Command/Activity/ActivityListCommand.php +++ b/legacy/src/Command/Activity/ActivityListCommand.php @@ -94,7 +94,8 @@ protected function configure(): void ->addExample('List recent pushes', '--type push') ->addExample('List all recent activities excluding crons and redeploys', "--exclude-type '*.cron,*.backup*'") ->addExample('List pushes made before 15 March', '--type push --start 2015-03-15') - ->addExample('List up to 25 incomplete activities', '--limit 25 -i'); + ->addExample('List up to 25 incomplete activities', '--limit 25 -i') + ->addExample('Include the activity type in the table', '--columns +type'); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/legacy/src/Command/Autoscaling/AutoscalingSettingsGetCommand.php b/legacy/src/Command/Autoscaling/AutoscalingSettingsGetCommand.php index dc8e0102d..9ab7ebac6 100644 --- a/legacy/src/Command/Autoscaling/AutoscalingSettingsGetCommand.php +++ b/legacy/src/Command/Autoscaling/AutoscalingSettingsGetCommand.php @@ -6,10 +6,12 @@ use Platformsh\Cli\Service\Api; use Platformsh\Cli\Service\Config; use Platformsh\Cli\Service\PropertyFormatter; +use Platformsh\Cli\Service\ResourcesUtil; use Platformsh\Cli\Selector\Selector; use Platformsh\Cli\Command\CommandBase; use Platformsh\Cli\Service\Table; use Platformsh\Client\Exception\EnvironmentStateException; +use Platformsh\Client\Model\Deployment\Service; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -34,7 +36,7 @@ class AutoscalingSettingsGetCommand extends CommandBase /** @var string[] */ protected array $defaultColumns = ['service', 'metric', 'direction', 'threshold', 'duration', 'enabled', 'instance_count']; - public function __construct(private readonly Api $api, private readonly Config $config, private readonly PropertyFormatter $propertyFormatter, private readonly Selector $selector, private readonly Table $table) + public function __construct(private readonly Api $api, private readonly Config $config, private readonly PropertyFormatter $propertyFormatter, private readonly ResourcesUtil $resourcesUtil, private readonly Selector $selector, private readonly Table $table) { parent::__construct(); } @@ -73,13 +75,26 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $autoscalingSettings = $autoscalingSettings->getData(); - $services = array_merge($deployment->webapps, $deployment->workers); + $services = $this->resourcesUtil->allServices($deployment); if (empty($services)) { - $this->stdErr->writeln('No apps or workers found.'); + $this->stdErr->writeln('No apps, workers, or services found.'); return 1; } if (!empty($autoscalingSettings['services'])) { + // Filter autoscaling settings to only show services that are allowed to be configured. + $filteredSettings = $this->filterAutoscalingSettings($autoscalingSettings['services'], $services, $selection->getProject()); + + if (empty($filteredSettings)) { + $this->stdErr->writeln(sprintf('No autoscaling configuration found for the project %s, environment %s.', $this->api->getProjectLabel($selection->getProject()), $this->api->getEnvironmentLabel($environment))); + $isOriginalCommand = $input instanceof ArgvInput; + if ($isOriginalCommand) { + $this->stdErr->writeln(''); + $this->stdErr->writeln(sprintf('You can configure autoscaling by running: %s autoscaling:set', $this->config->getStr('application.executable'))); + } + return 0; + } + if (!$this->table->formatIsMachineReadable()) { $this->stdErr->writeln(sprintf('Autoscaling configuration for the project %s, environment %s:', $this->api->getProjectLabel($selection->getProject()), $this->api->getEnvironmentLabel($environment))); } @@ -87,7 +102,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $empty = $this->table->formatIsMachineReadable() ? '' : 'not set'; $rows = []; - foreach ($autoscalingSettings['services'] as $service => $settings) { + foreach ($filteredSettings as $service => $settings) { $row = [ 'service' => $service, 'metric' => $empty, @@ -137,4 +152,53 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + /** + * Filters autoscaling settings to only include services that are allowed to be configured. + * + * @param array> $autoscalingSettings Autoscaling settings from the API + * @param array $services Array of Service|WebApp|Worker objects from deployment + * @param \Platformsh\Client\Model\Project $project + * + * @return array> Filtered autoscaling settings + */ + protected function filterAutoscalingSettings(array $autoscalingSettings, array $services, \Platformsh\Client\Model\Project $project): array + { + // Force refresh to ensure we have the latest capabilities. + $capabilities = $this->api->getProjectCapabilities($project, true); + $servicesCapabilityEnabled = !empty($capabilities->autoscaling['supports_horizontal_scaling_services']); + + $filtered = []; + foreach ($autoscalingSettings as $serviceName => $settings) { + // Skip if service doesn't exist in deployment + if (!isset($services[$serviceName])) { + continue; + } + + $service = $services[$serviceName]; + $properties = $service->getProperties(); + + // For apps and workers: check supports_horizontal_scaling if present, otherwise allow + if (!($service instanceof Service)) { + if (isset($properties['supports_horizontal_scaling']) && !$properties['supports_horizontal_scaling']) { + continue; + } + $filtered[$serviceName] = $settings; + continue; + } + + // For services: check both the deployment property and the project capability + if (!isset($properties['supports_horizontal_scaling']) || !$properties['supports_horizontal_scaling']) { + continue; + } + + if (!$servicesCapabilityEnabled) { + continue; + } + + $filtered[$serviceName] = $settings; + } + + return $filtered; + } } diff --git a/legacy/src/Command/Autoscaling/AutoscalingSettingsSetCommand.php b/legacy/src/Command/Autoscaling/AutoscalingSettingsSetCommand.php index 171162769..729528fa8 100644 --- a/legacy/src/Command/Autoscaling/AutoscalingSettingsSetCommand.php +++ b/legacy/src/Command/Autoscaling/AutoscalingSettingsSetCommand.php @@ -30,7 +30,7 @@ public function __construct(private readonly Api $api, private readonly Config $ protected function configure(): void { - $this->addOption('service', 's', InputOption::VALUE_REQUIRED, 'Name of the app or worker to configure autoscaling for') + $this->addOption('service', 's', InputOption::VALUE_REQUIRED, 'Name of the app, worker, or service to configure autoscaling for') ->addOption('metric', 'm', InputOption::VALUE_REQUIRED, 'Name of the metric to use for triggering autoscaling') ->addOption('enabled', null, InputOption::VALUE_REQUIRED, 'Enable autoscaling based on the given metric') ->addOption('threshold-up', null, InputOption::VALUE_REQUIRED, 'Threshold over which service will be scaled up') @@ -47,7 +47,7 @@ protected function configure(): void $this->selector->addEnvironmentOption($this->getDefinition()); $helpLines = [ - 'Configure automatic scaling for apps or workers in an environment.', + 'Configure automatic scaling for apps, workers, or services in an environment.', '', sprintf('You can also configure resources statically by running: %s resources:set', $this->config->getStr('application.executable')), ]; @@ -93,9 +93,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $autoscalingSettings = $autoscalingSettings->getData(); - $services = array_merge($deployment->webapps, $deployment->workers); + $services = $this->resourcesUtil->allServices($deployment); if (empty($services)) { - $this->stdErr->writeln('No apps or workers found.'); + $this->stdErr->writeln('No apps, workers, or services found.'); return 1; } @@ -106,6 +106,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $service = $input->getOption('service'); if ($service !== null) { $service = $this->validateService($service, $services); + $this->validateServiceSupportsAutoscaling($service, $services[$service], $selection->getProject()); } $supportedMetrics = $this->getSupportedMetrics($defaults); @@ -188,12 +189,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($showInteractiveForm) { // Interactive mode: let user select services and configure them - $serviceNames = array_keys($services); + // Filter to only show services that support autoscaling + $supportedServices = $this->filterServicesWithAutoscalingSupport($services, $selection->getProject()); + + if (empty($supportedServices)) { + $this->stdErr->writeln('No services that support autoscaling were found.'); + return 1; + } + + $serviceNames = array_keys($supportedServices); if ($service === null) { - // Ask user to select services to configure + // Ask user to select services to configure. $default = $serviceNames[0]; - $text = 'Enter a number to choose an app or worker:' . "\n" . 'Default: ' . $default . ''; + $text = 'Enter a number to choose an app, worker, or service:' . "\n" . 'Default: ' . $default . ''; $serviceNamesIndexed = array_combine($serviceNames, $serviceNames); $selectedService = $this->questionHelper->choose($serviceNamesIndexed, $text, $default); $service = $selectedService; @@ -677,6 +686,83 @@ protected function validateService(string $value, array $services): string throw new InvalidArgumentException(sprintf('Invalid service name %s. Available services: %s', $value, implode(', ', $serviceNames))); } + /** + * Validates that a service supports autoscaling. + * + * @param string $serviceName + * @param Service|WebApp|Worker $service + * @param \Platformsh\Client\Model\Project $project + * + * @throws InvalidArgumentException + */ + protected function validateServiceSupportsAutoscaling(string $serviceName, Service|WebApp|Worker $service, \Platformsh\Client\Model\Project $project): void + { + $properties = $service->getProperties(); + + // For apps and workers: check supports_horizontal_scaling if present, otherwise allow. + if (!($service instanceof Service)) { + if (isset($properties['supports_horizontal_scaling']) && !$properties['supports_horizontal_scaling']) { + throw new InvalidArgumentException(sprintf( + 'The %s %s does not support autoscaling.', + $this->typeName($service), + $serviceName + )); + } + return; + } + + // For services: check both the deployment property and the project capability. + if (!isset($properties['supports_horizontal_scaling']) || !$properties['supports_horizontal_scaling']) { + throw new InvalidArgumentException(sprintf( + 'The service %s does not support autoscaling.', + $serviceName + )); + } + + // Force refresh to ensure we have the latest capabilities. + $capabilities = $this->api->getProjectCapabilities($project, true); + if (empty($capabilities->autoscaling['supports_horizontal_scaling_services'])) { + throw new InvalidArgumentException(sprintf( + 'The service %s does not support autoscaling because the project does not have horizontal scaling enabled for services.', + $serviceName + )); + } + } + + /** + * Filters services to only those that support autoscaling. + * + * @param array $services + * @param \Platformsh\Client\Model\Project $project + * + * @return array + */ + protected function filterServicesWithAutoscalingSupport(array $services, \Platformsh\Client\Model\Project $project): array + { + // Force refresh to ensure we have the latest capabilities. + $capabilities = $this->api->getProjectCapabilities($project, true); + $servicesCapabilityEnabled = !empty($capabilities->autoscaling['supports_horizontal_scaling_services']); + + return array_filter($services, function ($service) use ($servicesCapabilityEnabled) { + $properties = $service->getProperties(); + + // For apps and workers: check supports_horizontal_scaling if present, otherwise allow + if (!($service instanceof Service)) { + if (isset($properties['supports_horizontal_scaling'])) { + return $properties['supports_horizontal_scaling']; + } + return true; + } + + // For services: check both the deployment property and the project capability + if (!isset($properties['supports_horizontal_scaling']) || !$properties['supports_horizontal_scaling']) { + return false; + } + + return $servicesCapabilityEnabled; + }); + } + /** * Return the names of supported metrics. * diff --git a/legacy/src/Command/Db/DbDumpCommand.php b/legacy/src/Command/Db/DbDumpCommand.php index 053b1c681..c7aa57207 100644 --- a/legacy/src/Command/Db/DbDumpCommand.php +++ b/legacy/src/Command/Db/DbDumpCommand.php @@ -224,6 +224,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($schemaOnly) { $dumpCommand .= ' --no-data'; } + if (!$this->relationships->isOracleDB($database)) { + // if ORACLE, don't dump tablespaces + // PROCESS privilege needed and we don't have that + // https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html#option_mysqldump_no-tablespaces + $dumpCommand .= ' --no-tablespaces'; + } foreach ($excludedTables as $table) { $dumpCommand .= ' ' . OsUtil::escapePosixShellArg(sprintf('--ignore-table=%s.%s', $database['path'], $table)); } diff --git a/legacy/src/Command/Environment/EnvironmentPushCommand.php b/legacy/src/Command/Environment/EnvironmentPushCommand.php index bac9247a3..d1eb14000 100644 --- a/legacy/src/Command/Environment/EnvironmentPushCommand.php +++ b/legacy/src/Command/Environment/EnvironmentPushCommand.php @@ -66,7 +66,8 @@ protected function configure(): void ->addHiddenOption('branch', null, InputOption::VALUE_NONE, 'DEPRECATED: alias of --activate') ->addOption('parent', null, InputOption::VALUE_REQUIRED, 'Set the environment parent (only used with --activate)') ->addOption('type', null, InputOption::VALUE_REQUIRED, 'Set the environment type (only used with --activate )') - ->addOption('no-clone-parent', null, InputOption::VALUE_NONE, "Do not clone the parent branch's data (only used with --activate)"); + ->addOption('no-clone-parent', null, InputOption::VALUE_NONE, "Do not clone the parent branch's data (only used with --activate)") + ->addOption('deploy-strategy', 's', InputOption::VALUE_REQUIRED, 'Set the deployment strategy, rolling or stopstart (default)'); $this->resourcesUtil->addOption( $this->getDefinition(), $this->validResourcesInitOptions, @@ -172,6 +173,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $parentId = $input->getOption('parent'); $type = $input->getOption('type'); + $strategy = $input->getOption('deploy-strategy'); + if ($strategy !== null && $strategy !== 'rolling' && $strategy !== 'stopstart') { + $this->stdErr->writeln(sprintf('Invalid deploy strategy %s, should be "rolling" or "stopstart"', $strategy)); + return 1; + } + // Check if the environment may be a production one. $mayBeProduction = $type === 'production' || ($targetEnvironment && $targetEnvironment->type === 'production') @@ -207,6 +214,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + if ($targetEnvironment) { + if (!$targetEnvironment->operationAvailable('deploy', true)) { + $this->stdErr->writeln(sprintf('Deployment strategy: %s', $strategy ?: "stopstart")); + } else { + $this->stdErr->writeln('The activity will be staged, ignoring the deployment strategy.'); + } + } + $this->stdErr->writeln(''); if (!$this->questionHelper->confirm('Are you sure you want to continue?')) { @@ -267,6 +282,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($resourcesInit !== null) { $gitArgs[] = '--push-option=resources.init=' . $resourcesInit; } + if ($strategy !== null) { + $gitArgs[] = '--push-option=deploy.strategy=' . $strategy; + } // Build the SSH command to use with Git. $extraSshOptions = []; diff --git a/legacy/src/Command/Metrics/MetricsCommandBase.php b/legacy/src/Command/Metrics/MetricsCommandBase.php index ef8e92f7b..28074594c 100644 --- a/legacy/src/Command/Metrics/MetricsCommandBase.php +++ b/legacy/src/Command/Metrics/MetricsCommandBase.php @@ -255,6 +255,7 @@ protected function getChooseEnvFilter(): ?callable return null; } + /** * Validates the interval and range input, and finds defaults. * diff --git a/legacy/src/Command/Organization/Billing/OrganizationProfileCommand.php b/legacy/src/Command/Organization/Billing/OrganizationProfileCommand.php index 452619da3..86f31e46a 100644 --- a/legacy/src/Command/Organization/Billing/OrganizationProfileCommand.php +++ b/legacy/src/Command/Organization/Billing/OrganizationProfileCommand.php @@ -122,7 +122,6 @@ private function getType(string $property): string|false $writableProperties = [ 'company_name' => 'string', 'billing_contact' => 'string', - 'security_contact' => 'string', 'vat_number' => 'string', 'default_catalog' => 'string', 'project_options_url' => 'string', diff --git a/legacy/src/Command/Organization/OrganizationCreateCommand.php b/legacy/src/Command/Organization/OrganizationCreateCommand.php index be019793e..219a29ad1 100644 --- a/legacy/src/Command/Organization/OrganizationCreateCommand.php +++ b/legacy/src/Command/Organization/OrganizationCreateCommand.php @@ -54,6 +54,7 @@ private function getForm(): Form $fields['type'] = new OptionsField('Type', [ 'description' => 'The organization type.', 'options' => $options, + 'avoidQuestion' => true, 'default' => $this->config->getWithDefault('api.default_organization_type', key($options)), ]); } diff --git a/legacy/src/Service/ActivityLoader.php b/legacy/src/Service/ActivityLoader.php index ee49be51c..07b925ed4 100644 --- a/legacy/src/Service/ActivityLoader.php +++ b/legacy/src/Service/ActivityLoader.php @@ -164,6 +164,7 @@ public static function getAvailableTypes(): array 'environment.access.add', 'environment.access.remove', 'environment.activate', + 'environment.alert', 'environment.backup', 'environment.backup.delete', 'environment.branch', diff --git a/legacy/src/Service/ActivityMonitor.php b/legacy/src/Service/ActivityMonitor.php index faacdf534..f983fb055 100644 --- a/legacy/src/Service/ActivityMonitor.php +++ b/legacy/src/Service/ActivityMonitor.php @@ -593,7 +593,7 @@ public static function formatResult(Activity $activity, bool $decorate = true): foreach ($activity->commands ?? [] as $command) { if ($command['exit_code'] > 0) { - $name = Activity::RESULT_FAILURE; + $name = self::RESULT_NAMES[Activity::RESULT_FAILURE]; $result = Activity::RESULT_FAILURE; break; } diff --git a/legacy/src/Service/CurlCli.php b/legacy/src/Service/CurlCli.php index 5552e14ef..7e3efc13f 100644 --- a/legacy/src/Service/CurlCli.php +++ b/legacy/src/Service/CurlCli.php @@ -77,7 +77,7 @@ public function run(string $baseUrl, InputInterface $input, OutputInterface $out } if ($type === Process::OUT) { if ($retryOn401) { - // Buffer stdout when we might need to retry on 401. + // Buffer stdout so it can be discarded if a 401 triggers a retry. $stdoutBuffer .= $buffer; } else { $output->write($buffer); @@ -87,7 +87,7 @@ public function run(string $baseUrl, InputInterface $input, OutputInterface $out if ($type === Process::ERR) { if ($retryOn401 && $this->parseCurlStatusCode($buffer) === 401 && $this->api->isLoggedIn()) { $shouldRetry = true; - $stdoutBuffer = ''; // Discard buffered stdout from the 401 response. + $stdoutBuffer = ''; $process->clearErrorOutput(); $process->clearOutput(); @@ -107,6 +107,11 @@ public function run(string $baseUrl, InputInterface $input, OutputInterface $out $process->run($onOutput); + if (!$shouldRetry && $stdoutBuffer !== '') { + $output->write($stdoutBuffer); + $stdoutBuffer = ''; + } + if ($shouldRetry) { // Create a new curl process, replacing the access token. $commandline = $this->buildCurlCommand($url, $newToken, $input); @@ -119,6 +124,11 @@ public function run(string $baseUrl, InputInterface $input, OutputInterface $out $stdErr->writeln(sprintf('Running command: %s', $censor($commandline)), OutputInterface::VERBOSITY_VERBOSE); $process->run($onOutput); + + if ($stdoutBuffer !== '') { // @phpstan-ignore notIdentical.alwaysFalse ($stdoutBuffer is modified by reference in $onOutput) + $output->write($stdoutBuffer); + $stdoutBuffer = ''; + } } // Flush buffered stdout after the final request. diff --git a/legacy/src/Service/Relationships.php b/legacy/src/Service/Relationships.php index 6f82a458a..8c30b1a87 100644 --- a/legacy/src/Service/Relationships.php +++ b/legacy/src/Service/Relationships.php @@ -231,6 +231,17 @@ public function isMariaDB(array $database): bool return isset($database['type']) && (str_starts_with((string) $database['type'], 'mariadb:') || str_starts_with((string) $database['type'], 'mysql:')); } + /** + * Returns whether the database is OracleDB. + * + * @param array $database The database definition from the relationships. + * @return bool + */ + public function isOracleDB(array $database): bool + { + return isset($database['type']) && str_starts_with((string) $database['type'], 'oracle-mysql:'); + } + /** * Returns the correct command to use with a MariaDB client. * diff --git a/legacy/src/Service/VariableCommandUtil.php b/legacy/src/Service/VariableCommandUtil.php index a86da8123..3f48c4ca3 100644 --- a/legacy/src/Service/VariableCommandUtil.php +++ b/legacy/src/Service/VariableCommandUtil.php @@ -6,9 +6,13 @@ use Platformsh\Cli\Console\AdaptiveTableCell; use Platformsh\Cli\Selector\Selection; +use Platformsh\Client\Exception\EnvironmentStateException; use Platformsh\Client\Model\ApiResourceBase; +use Platformsh\Client\Model\Environment; +use Platformsh\Client\Model\Project; use Platformsh\Client\Model\ProjectLevelVariable; use Platformsh\Client\Model\Variable as EnvironmentLevelVariable; +use Platformsh\ConsoleForm\Field\ArrayField; use Platformsh\ConsoleForm\Field\BooleanField; use Platformsh\ConsoleForm\Field\Field; use Platformsh\ConsoleForm\Field\OptionsField; @@ -170,6 +174,32 @@ public function getFields(callable $getSelection): array 'includeAsOption' => false, 'defaultCallback' => fn(): ?string => $getSelection()->hasEnvironment() ? $getSelection()->getEnvironment()->id : null, ]); + $fields['application_scope'] = new ArrayField('Application scope', [ + 'optionName' => 'app-scope', + 'description' => 'A list of application names to which this variable will apply.', + 'questionLine' => 'To which applications should this variable apply?', + 'default' => [], + 'required' => false, + 'avoidQuestion' => true, + 'validator' => function ($values) use ($getSelection) { + $selection = $getSelection(); + $appNames = $this->listApps($selection->getProject(), $selection->hasEnvironment() ? $selection->getEnvironment() : null); + if ($appNames === false) { + // No app names available: skip validation. + return true; + } + foreach ($values as $value) { + if (!in_array($value, $appNames, true)) { + throw new InvalidArgumentException(sprintf( + 'The app "%s" was not found. Valid app names are: %s', + $value, + implode(', ', $appNames) + )); + } + } + return true; + }, + ]); $fields['name'] = new Field('Name', [ 'description' => 'The variable name', 'validators' => [ @@ -256,4 +286,30 @@ private function getPrefixOptions(string $name): array 'env:' => 'env: The variable will be exposed directly, e.g. as $' . strtoupper($name) . '.', ]; } + + /** + * List application names for validating application_scope values. + * + * @param Project $project + * @param Environment|null $environment If not provided, the project's default environment will be used. + * + * @return string[]|false + */ + private function listApps(Project $project, ?Environment $environment = null): array|false + { + if (!$environment) { + if ($project->default_branch !== '') { + $environment = $this->api->getEnvironment($project->default_branch, $project); + } + if (!$environment) { + return false; + } + } + try { + $deployment = $this->api->getCurrentDeployment($environment, false); + } catch (EnvironmentStateException $e) { + return false; + } + return array_keys($deployment->webapps); + } } diff --git a/legacy/tests/Local/LocalProjectTest.php b/legacy/tests/Local/LocalProjectTest.php deleted file mode 100644 index 83a85f651..000000000 --- a/legacy/tests/Local/LocalProjectTest.php +++ /dev/null @@ -1,40 +0,0 @@ -tempDirSetUp(); - $testDir = $this->tempDir; - mkdir("$testDir/1/2/3/4/5", 0o755, true); - - $expectedRoot = "$testDir/1"; - $config = new Config(); - $this->assertTrue($config->has('local.project_config_legacy')); - touch("$expectedRoot/" . $config->getStr('local.project_config_legacy')); - - chdir($testDir); - $localProject = new LocalProject(); - $this->assertFalse($localProject->getProjectRoot()); - $this->assertFalse($localProject->getLegacyProjectRoot()); - - chdir($expectedRoot); - $this->assertFalse($localProject->getProjectRoot()); - $this->assertEquals($expectedRoot, $localProject->getLegacyProjectRoot()); - - chdir("$testDir/1/2/3/4/5"); - $this->assertFalse($localProject->getProjectRoot()); - $this->assertEquals($expectedRoot, $localProject->getLegacyProjectRoot()); - } -} diff --git a/pkg/mockapi/model.go b/pkg/mockapi/model.go index 1f1032436..766257e36 100644 --- a/pkg/mockapi/model.go +++ b/pkg/mockapi/model.go @@ -293,11 +293,12 @@ type Activity struct { } type Variable struct { - Name string `json:"name"` - Value string `json:"value,omitempty"` - IsSensitive bool `json:"is_sensitive"` - VisibleBuild bool `json:"visible_build"` - VisibleRuntime bool `json:"visible_runtime"` + Name string `json:"name"` + Value string `json:"value,omitempty"` + IsSensitive bool `json:"is_sensitive"` + VisibleBuild bool `json:"visible_build"` + VisibleRuntime bool `json:"visible_runtime"` + ApplicationScope []string `json:"application_scope,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"`