Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,28 @@ By in argument `<projects>` 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] <token> <url> <condition-feature> <target-feature> [--maintainer-id=ID] [--organization-id=ID] [--project-id=ID]
php cli.php manage:projects-add-feature-conditionally [-f|--force] <token> <url> <condition-feature> <target-feature> [--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:
- `--project-id` – process a single project
- `--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 `<condition-feature>` 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 <token> <url> new-billing new-ui` – dry run over the whole stack
- `manage:projects-add-feature-conditionally <token> <url> new-billing new-ui` – dry run, add `new-ui` to projects that have `new-billing`
- `manage:projects-add-feature-conditionally -f <token> <url> new-billing new-ui --organization-id=123`
- `manage:projects-add-feature-conditionally -f <token> <url> legacy-billing new-ui --condition-mode=absent` – add `new-ui` to projects that do NOT have `legacy-billing`
- `manage:projects-add-feature-conditionally -f <token> <url> 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.

Expand Down
135 changes: 106 additions & 29 deletions src/Keboola/Console/Command/ProjectsAddFeatureConditionally.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -46,14 +53,33 @@ protected function configure(): void
InputOption::VALUE_REQUIRED,
'Limit scope to this single project'
)
->addOption(
self::OPT_CONDITION_MODE,
Comment thread
ondrajodas marked this conversation as resolved.
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);
Expand All @@ -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));
Expand All @@ -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");
Expand All @@ -111,13 +151,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
* disabled: array{reason: string},
* features: string[]
* } $projectInfo
* @param list<string> $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'];
Expand All @@ -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;
}
Expand All @@ -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<string> $conditionFeatures
*/
protected function processProject(
Client $client,
OutputInterface $output,
string $projectId,
string $conditionFeature,
array $conditionFeatures,
string $targetFeature,
string $conditionMode,
bool $force
): void {
try {
Expand All @@ -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<string> $conditionFeatures
*/
protected function processOrganization(
Client $client,
OutputInterface $output,
string $organizationId,
string $conditionFeature,
array $conditionFeatures,
string $targetFeature,
string $conditionMode,
bool $force
): void {
$this->orgsChecked++;
Expand All @@ -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<string> $conditionFeatures
*/
protected function processMaintainer(
Client $client,
OutputInterface $output,
string $maintainerId,
string $conditionFeature,
array $conditionFeatures,
string $targetFeature,
string $conditionMode,
bool $force
): void {
$this->maintainersChecked++;
Expand All @@ -226,18 +297,23 @@ protected function processMaintainer(
$client,
$output,
(string) $organization['id'],
$conditionFeature,
$conditionFeatures,
$targetFeature,
$conditionMode,
$force
);
}
}

/**
* @param list<string> $conditionFeatures
*/
protected function processAllProjects(
Client $client,
OutputInterface $output,
string $conditionFeature,
array $conditionFeatures,
string $targetFeature,
string $conditionMode,
bool $force
): void {
$maintainers = $client->listMaintainers();
Expand All @@ -246,8 +322,9 @@ protected function processAllProjects(
$client,
$output,
(string) $maintainer['id'],
$conditionFeature,
$conditionFeatures,
$targetFeature,
$conditionMode,
$force
);
}
Expand All @@ -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
));
Expand Down