From 4c35bc49710a974426195e2f8965bb118e9dd11f Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Fri, 19 Dec 2025 14:19:40 +0000 Subject: [PATCH 1/2] Add --app-scope option for variables Adds an `--app-scope` option to `var:create` and `var:update`, allowing variables to be scoped to specific applications within a project. - The option accepts multiple values: `--app-scope app1 --app-scope app2`. - App names are validated against the current deployment when one is available, and validation is skipped for inactive environments without a deployment. - The new `application_scope` property already shows in `variable:get` output without further code changes. Reimplemented from platformsh/legacy-cli#1590 against the 5.x architecture in `legacy/`: the field and `listApps()` helper live in `Service/VariableCommandUtil.php` (the 5.x equivalent of `Command/Variable/VariableCommandBase.php`), and the integration tests were ported to `integration-tests/variable_write_test.go` (replacing the original `go-tests/` location). Co-Authored-By: Claude Opus 4.7 (1M context) --- integration-tests/variable_write_test.go | 99 ++++++++++++++++++++++ legacy/src/Service/VariableCommandUtil.php | 52 ++++++++++++ 2 files changed, 151 insertions(+) diff --git a/integration-tests/variable_write_test.go b/integration-tests/variable_write_test.go index 14989405..e069ec56 100644 --- a/integration-tests/variable_write_test.go +++ b/integration-tests/variable_write_test.go @@ -72,3 +72,102 @@ func TestVariableCreate(t *testing.T) { assert.NoError(t, err) assertTrimmed(t, "false", f.Run("var:get", "-p", projectID, "env:TEST", "-l", "p", "-P", "visible_runtime")) } + +func TestVariableCreateWithAppScope(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + t.Cleanup(authServer.Close) + + apiHandler := mockapi.NewHandler(t) + apiServer := httptest.NewServer(apiHandler) + t.Cleanup(apiServer.Close) + + projectID := mockapi.ProjectID() + + apiHandler.SetProjects([]*mockapi.Project{{ + ID: projectID, + 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"} + main.SetCurrentDeployment(&mockapi.Deployment{ + WebApps: map[string]mockapi.App{ + "app1": {Name: "app1", Type: "golang:1.23"}, + "app2": {Name: "app2", Type: "php:8.3"}, + }, + Routes: map[string]any{}, + Links: mockapi.MakeHALLinks("self=/projects/" + projectID + "/environments/main/deployment/current"), + }) + apiHandler.SetEnvironments([]*mockapi.Environment{main}) + + f := newCommandFactory(t, apiServer.URL, authServer.URL) + + _, stdErr, err := f.RunCombinedOutput("var:create", "-p", projectID, "-l", "p", + "env:SCOPED", "--value", "val", "--app-scope", "app1") + assert.NoError(t, err) + assert.Contains(t, stdErr, "Creating variable env:SCOPED") + + out := f.Run("var:get", "-p", projectID, "-l", "p", "env:SCOPED", "-P", "application_scope") + assert.Contains(t, out, "app1") + + _, _, err = f.RunCombinedOutput("var:create", "-p", projectID, "-l", "p", + "env:MULTI", "--value", "val", "--app-scope", "app1", "--app-scope", "app2") + assert.NoError(t, err) + + out = f.Run("var:get", "-p", projectID, "-l", "p", "env:MULTI", "-P", "application_scope") + assert.Contains(t, out, "app1") + assert.Contains(t, out, "app2") + + _, stdErr, err = f.RunCombinedOutput("var:create", "-p", projectID, "-l", "p", + "env:BAD", "--value", "val", "--app-scope", "nonexistent") + assert.Error(t, err) + assert.Contains(t, stdErr, "was not found") + + _, _, err = f.RunCombinedOutput("var:update", "-p", projectID, "-l", "p", + "env:SCOPED", "--app-scope", "app2") + assert.NoError(t, err) + + out = f.Run("var:get", "-p", projectID, "-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. + authServer := mockapi.NewAuthServer(t) + t.Cleanup(authServer.Close) + + apiHandler := mockapi.NewHandler(t) + apiServer := httptest.NewServer(apiHandler) + t.Cleanup(apiServer.Close) + + projectID := mockapi.ProjectID() + + apiHandler.SetProjects([]*mockapi.Project{{ + ID: projectID, + 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"} + apiHandler.SetEnvironments([]*mockapi.Environment{main}) + + f := newCommandFactory(t, apiServer.URL, authServer.URL) + + _, stdErr, err := f.RunCombinedOutput("var:create", "-p", projectID, "-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", projectID, "-l", "p", "env:ANY_APP", "-P", "application_scope") + assert.Contains(t, out, "anyapp") +} diff --git a/legacy/src/Service/VariableCommandUtil.php b/legacy/src/Service/VariableCommandUtil.php index a86da812..2c2ec604 100644 --- a/legacy/src/Service/VariableCommandUtil.php +++ b/legacy/src/Service/VariableCommandUtil.php @@ -7,8 +7,11 @@ use Platformsh\Cli\Console\AdaptiveTableCell; use Platformsh\Cli\Selector\Selection; 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 +173,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): bool { + $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 +285,27 @@ private function getPrefixOptions(string $name): array 'env:' => 'env: The variable will be exposed directly, e.g. as $' . strtoupper($name) . '.', ]; } + + /** + * Lists application names for validating application_scope values. + * + * @return string[]|false An array of app names, or false if they could not be determined. + */ + private function listApps(Project $project, ?Environment $environment = null): array|false + { + if (!$environment) { + if ($project->default_branch !== '') { + $environment = $this->api->getEnvironment($project->default_branch, $project) ?: null; + } + if (!$environment) { + return false; + } + } + try { + $deployment = $this->api->getCurrentDeployment($environment); + } catch (\Exception) { + return false; + } + return array_keys($deployment->webapps); + } } From d9e5967180ef4fbd8011bd36281227e070723e2b Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Mon, 27 Apr 2026 21:38:54 +0100 Subject: [PATCH 2/2] Update legacy/src/Service/VariableCommandUtil.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- legacy/src/Service/VariableCommandUtil.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy/src/Service/VariableCommandUtil.php b/legacy/src/Service/VariableCommandUtil.php index 2c2ec604..4b831145 100644 --- a/legacy/src/Service/VariableCommandUtil.php +++ b/legacy/src/Service/VariableCommandUtil.php @@ -294,7 +294,7 @@ private function getPrefixOptions(string $name): array private function listApps(Project $project, ?Environment $environment = null): array|false { if (!$environment) { - if ($project->default_branch !== '') { + if (\is_string($project->default_branch) && $project->default_branch !== '') { $environment = $this->api->getEnvironment($project->default_branch, $project) ?: null; } if (!$environment) {