diff --git a/README.md b/README.md index 9067a31..2c5d390 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,10 @@ By in argument `` you can Note: the feature has to exist before calling, and it has to be type of `project` ### Conditionally Add Feature -Adds a target feature only to projects that already have a given condition feature. +Adds a target feature to projects based on whether they have a given condition feature. ``` -php cli.php manage:projects-add-feature-conditionally [-f|--force] [--maintainer-id=ID] [--organization-id=ID] [--project-id=ID] +php cli.php manage:projects-add-feature-conditionally [-f|--force] [--maintainer-id=ID] [--organization-id=ID] [--project-id=ID] [--condition-mode=present|absent] ``` The scope is the whole stack by default. You can narrow it down with one of the mutually exclusive options: @@ -68,11 +68,17 @@ The scope is the whole stack by default. You can narrow it down with one of the - `--organization-id` – process all projects of one organization - `--maintainer-id` – process all projects of all organizations of one maintainer -For each project in scope, the target feature is added only when the project already has the condition feature (and does not yet have the target feature). Disabled projects are skipped. +The `` argument accepts one feature or a comma-separated list of features (e.g. `batch1,batch0`). The `--condition-mode` option controls how they are evaluated (default `present`): +- `present` – add the target feature only to projects that have **all** of the condition features +- `absent` – add the target feature only to projects that have **none** of the condition features + +In both modes projects that already have the target feature are skipped, and disabled projects are skipped. Examples: -- `manage:projects-add-feature-conditionally new-billing new-ui` – dry run over the whole stack +- `manage:projects-add-feature-conditionally new-billing new-ui` – dry run, add `new-ui` to projects that have `new-billing` - `manage:projects-add-feature-conditionally -f new-billing new-ui --organization-id=123` +- `manage:projects-add-feature-conditionally -f legacy-billing new-ui --condition-mode=absent` – add `new-ui` to projects that do NOT have `legacy-billing` +- `manage:projects-add-feature-conditionally -f batch1,batch0 batch2 --condition-mode=absent` – add `batch2` to projects that have neither `batch1` nor `batch0` Note: both the condition and target features have to exist before calling, and have to be of type `project`. The options `--maintainer-id`, `--organization-id` and `--project-id` are mutually exclusive. diff --git a/src/Keboola/Console/Command/ProjectsAddFeatureConditionally.php b/src/Keboola/Console/Command/ProjectsAddFeatureConditionally.php index 80d8dc1..a7a2b8a 100644 --- a/src/Keboola/Console/Command/ProjectsAddFeatureConditionally.php +++ b/src/Keboola/Console/Command/ProjectsAddFeatureConditionally.php @@ -16,17 +16,24 @@ class ProjectsAddFeatureConditionally extends ProjectsAddFeature const string OPT_MAINTAINER_ID = 'maintainer-id'; const string OPT_ORGANIZATION_ID = 'organization-id'; const string OPT_PROJECT_ID = 'project-id'; + const string OPT_CONDITION_MODE = 'condition-mode'; + const string CONDITION_MODE_PRESENT = 'present'; + const string CONDITION_MODE_ABSENT = 'absent'; - protected int $projectsWithoutCondition = 0; + protected int $projectsSkippedByCondition = 0; protected function configure(): void { $this ->setName('manage:projects-add-feature-conditionally') - ->setDescription('Add a target feature to projects that already have a given condition feature') + ->setDescription('Add a target feature to projects based on whether they have a given condition feature') ->addArgument(self::ARG_TOKEN, InputArgument::REQUIRED, 'manage token') ->addArgument(self::ARG_URL, InputArgument::REQUIRED, 'Stack URL') - ->addArgument(self::ARG_CONDITION_FEATURE, InputArgument::REQUIRED, 'feature a project must already have') + ->addArgument( + self::ARG_CONDITION_FEATURE, + InputArgument::REQUIRED, + 'condition feature(s), comma-separated for multiple; evaluated together (see --condition-mode)' + ) ->addArgument(self::ARG_TARGET_FEATURE, InputArgument::REQUIRED, 'feature to add') ->addOption( self::OPT_MAINTAINER_ID, @@ -46,14 +53,33 @@ protected function configure(): void InputOption::VALUE_REQUIRED, 'Limit scope to this single project' ) + ->addOption( + self::OPT_CONDITION_MODE, + null, + InputOption::VALUE_REQUIRED, + sprintf( + 'Whether the condition feature must be "%s" or "%s" on the project', + self::CONDITION_MODE_PRESENT, + self::CONDITION_MODE_ABSENT + ), + self::CONDITION_MODE_PRESENT + ) ->addOption(self::OPT_FORCE, 'f', InputOption::VALUE_NONE, 'Will actually do the work, otherwise it\'s dry run'); } protected function execute(InputInterface $input, OutputInterface $output): int { $force = (bool) $input->getOption(self::OPT_FORCE); - $conditionFeature = $input->getArgument(self::ARG_CONDITION_FEATURE); - assert(is_string($conditionFeature)); + $conditionFeaturesArg = $input->getArgument(self::ARG_CONDITION_FEATURE); + assert(is_string($conditionFeaturesArg)); + $conditionFeatures = array_values(array_filter( + array_map('trim', explode(',', $conditionFeaturesArg)), + fn($feature) => $feature !== '' + )); + if (count($conditionFeatures) === 0) { + $output->writeln('ERROR: At least one condition feature must be provided.'); + return 1; + } $targetFeature = $input->getArgument(self::ARG_TARGET_FEATURE); assert(is_string($targetFeature)); $url = $input->getArgument(self::ARG_URL); @@ -74,11 +100,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } + $conditionMode = $input->getOption(self::OPT_CONDITION_MODE); + assert(is_string($conditionMode)); + if (!in_array($conditionMode, [self::CONDITION_MODE_PRESENT, self::CONDITION_MODE_ABSENT], true)) { + $output->writeln(sprintf( + 'ERROR: Option --%s must be either "%s" or "%s".', + self::OPT_CONDITION_MODE, + self::CONDITION_MODE_PRESENT, + self::CONDITION_MODE_ABSENT + )); + return 1; + } + $client = $this->createClient($url, $token); - if (!$this->checkIfFeatureExists($client, $conditionFeature)) { - $output->writeln(sprintf('Condition feature %s does NOT exist', $conditionFeature)); - return 1; + foreach ($conditionFeatures as $conditionFeature) { + if (!$this->checkIfFeatureExists($client, $conditionFeature)) { + $output->writeln(sprintf('Condition feature %s does NOT exist', $conditionFeature)); + return 1; + } } if (!$this->checkIfFeatureExists($client, $targetFeature)) { $output->writeln(sprintf('Target feature %s does NOT exist', $targetFeature)); @@ -87,15 +127,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($projectId !== null) { assert(is_string($projectId)); - $this->processProject($client, $output, $projectId, $conditionFeature, $targetFeature, $force); + $this->processProject($client, $output, $projectId, $conditionFeatures, $targetFeature, $conditionMode, $force); } elseif ($organizationId !== null) { assert(is_string($organizationId)); - $this->processOrganization($client, $output, $organizationId, $conditionFeature, $targetFeature, $force); + $this->processOrganization($client, $output, $organizationId, $conditionFeatures, $targetFeature, $conditionMode, $force); } elseif ($maintainerId !== null) { assert(is_string($maintainerId)); - $this->processMaintainer($client, $output, $maintainerId, $conditionFeature, $targetFeature, $force); + $this->processMaintainer($client, $output, $maintainerId, $conditionFeatures, $targetFeature, $conditionMode, $force); } else { - $this->processAllProjects($client, $output, $conditionFeature, $targetFeature, $force); + $this->processAllProjects($client, $output, $conditionFeatures, $targetFeature, $conditionMode, $force); } $output->writeln("\nDONE with following results:\n"); @@ -111,13 +151,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int * disabled: array{reason: string}, * features: string[] * } $projectInfo + * @param list $conditionFeatures */ protected function addFeatureToProjectConditionally( Client $client, OutputInterface $output, array $projectInfo, - string $conditionFeature, + array $conditionFeatures, string $targetFeature, + string $conditionMode, bool $force ): void { $projectId = (string) $projectInfo['id']; @@ -132,9 +174,27 @@ protected function addFeatureToProjectConditionally( $features = $projectInfo['features']; - if (!in_array($conditionFeature, $features, true)) { - $output->writeln(sprintf(' - condition feature "%s" is NOT set, skipping.', $conditionFeature)); - $this->projectsWithoutCondition++; + $presentConditions = array_values(array_intersect($conditionFeatures, $features)); + $missingConditions = array_values(array_diff($conditionFeatures, $features)); + + // present: project must have ALL condition features; absent: project must have NONE of them + $conditionMet = $conditionMode === self::CONDITION_MODE_PRESENT + ? count($missingConditions) === 0 + : count($presentConditions) === 0; + + if (!$conditionMet) { + if ($conditionMode === self::CONDITION_MODE_PRESENT) { + $output->writeln(sprintf( + ' - condition not met, project is missing feature(s): %s, skipping.', + implode(', ', $missingConditions) + )); + } else { + $output->writeln(sprintf( + ' - condition not met, project has feature(s): %s, skipping.', + implode(', ', $presentConditions) + )); + } + $this->projectsSkippedByCondition++; $output->write("\n"); return; } @@ -151,21 +211,24 @@ protected function addFeatureToProjectConditionally( $output->writeln(sprintf(' - target feature "%s" successfully added.', $targetFeature)); } else { $output->writeln(sprintf( - ' - target feature "%s" CAN be added (project has "%s"). Enable force mode with -f option.', - $targetFeature, - $conditionFeature + ' - target feature "%s" CAN be added (condition met). Enable force mode with -f option.', + $targetFeature )); } $this->projectsUpdated++; $output->write("\n"); } + /** + * @param list $conditionFeatures + */ protected function processProject( Client $client, OutputInterface $output, string $projectId, - string $conditionFeature, + array $conditionFeatures, string $targetFeature, + string $conditionMode, bool $force ): void { try { @@ -178,18 +241,22 @@ protected function processProject( * features: string[] * } $project */ - $this->addFeatureToProjectConditionally($client, $output, $project, $conditionFeature, $targetFeature, $force); + $this->addFeatureToProjectConditionally($client, $output, $project, $conditionFeatures, $targetFeature, $conditionMode, $force); } catch (ClientException $e) { $output->writeln("Error while handling project {$projectId} : " . $e->getMessage()); } } + /** + * @param list $conditionFeatures + */ protected function processOrganization( Client $client, OutputInterface $output, string $organizationId, - string $conditionFeature, + array $conditionFeatures, string $targetFeature, + string $conditionMode, bool $force ): void { $this->orgsChecked++; @@ -204,19 +271,23 @@ protected function processOrganization( * } $project */ try { - $this->addFeatureToProjectConditionally($client, $output, $project, $conditionFeature, $targetFeature, $force); + $this->addFeatureToProjectConditionally($client, $output, $project, $conditionFeatures, $targetFeature, $conditionMode, $force); } catch (ClientException $e) { $output->writeln("Error while handling project {$project['id']} : " . $e->getMessage()); } } } + /** + * @param list $conditionFeatures + */ protected function processMaintainer( Client $client, OutputInterface $output, string $maintainerId, - string $conditionFeature, + array $conditionFeatures, string $targetFeature, + string $conditionMode, bool $force ): void { $this->maintainersChecked++; @@ -226,18 +297,23 @@ protected function processMaintainer( $client, $output, (string) $organization['id'], - $conditionFeature, + $conditionFeatures, $targetFeature, + $conditionMode, $force ); } } + /** + * @param list $conditionFeatures + */ protected function processAllProjects( Client $client, OutputInterface $output, - string $conditionFeature, + array $conditionFeatures, string $targetFeature, + string $conditionMode, bool $force ): void { $maintainers = $client->listMaintainers(); @@ -246,8 +322,9 @@ protected function processAllProjects( $client, $output, (string) $maintainer['id'], - $conditionFeature, + $conditionFeatures, $targetFeature, + $conditionMode, $force ); } @@ -259,13 +336,13 @@ private function printResult(OutputInterface $output, bool $force): void "Checked %d maintainers\n" . "Checked %d organizations\n" . "%d projects were disabled\n" - . "%d projects do not have the condition feature\n" + . "%d projects skipped (condition not met)\n" . "%d projects have the target feature already\n" . '%d ' . ($force ? 'projects updated' : 'projects can be updated in force mode') . "\n", $this->maintainersChecked, $this->orgsChecked, $this->projectsDisabled, - $this->projectsWithoutCondition, + $this->projectsSkippedByCondition, $this->projectsWithFeature, $this->projectsUpdated ));