feat(features): support percentage rollouts in feature:enable (#592)

## What

`feature:enable <feature> <target>` now accepts a percentage like `25%`
as the target, in addition to a user email or `all`.

```bash
php artisan feature:enable AiConsentSettings 25%
```

This activates the feature for a random `ceil(total * N/100)` sample of
the **current** users, persisted in Pennant's `features` table — no need
to edit the feature class and redeploy for a percentage-based rollout.

## Notes

- It targets the current user base. New users keep the feature's default
(`false`) unless the command is run again.
- Percentage is validated to be between 1 and 100.
- `feature:disable` was left unchanged — disabling for a random subset
has no clear use case.

## Test plan

- [x] `feature:enable AiConsentSettings 40%` enables the feature for
exactly 4 of 10 users
- [x] An out-of-range percentage (`0%`) fails
This commit is contained in:
Víctor Falcón 2026-06-25 12:46:30 +02:00 committed by GitHub
parent 9a458b1031
commit f72e2a64ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 48 additions and 1 deletions

View File

@ -11,7 +11,7 @@ class FeatureEnableCommand extends Command
{
use ResolvesFeatures;
protected $signature = 'feature:enable {feature : The feature name (class name or string-based feature)} {target : User email or "all" for everyone}';
protected $signature = 'feature:enable {feature : The feature name (class name or string-based feature)} {target : User email, "all" for everyone, or a percentage like "25%" for a random rollout}';
protected $description = 'Enable a feature for a specific user or all users';
@ -36,6 +36,10 @@ class FeatureEnableCommand extends Command
return self::SUCCESS;
}
if (preg_match('/^(\d{1,3})%$/', $target, $matches)) {
return $this->enableForPercentage($featureClass, $featureName, (int) $matches[1]);
}
$user = User::where('email', $target)->first();
if (! $user) {
@ -49,4 +53,23 @@ class FeatureEnableCommand extends Command
return self::SUCCESS;
}
private function enableForPercentage(string $featureClass, string $featureName, int $percentage): int
{
if ($percentage < 1 || $percentage > 100) {
$this->error('Percentage must be between 1 and 100.');
return self::FAILURE;
}
$count = (int) ceil(User::count() * $percentage / 100);
User::inRandomOrder()
->take($count)
->each(fn (User $user) => Feature::for($user)->activate($featureClass));
$this->info("Feature '{$featureName}' enabled for {$count} users (~{$percentage}% of current users).");
return self::SUCCESS;
}
}

View File

@ -0,0 +1,24 @@
<?php
use App\Features\AiConsentSettings;
use App\Models\User;
use Laravel\Pennant\Feature;
test('enables a feature for a percentage of users', function () {
$users = User::factory()->count(10)->create();
$this->artisan('feature:enable', ['feature' => 'AiConsentSettings', 'target' => '40%'])
->expectsOutputToContain('enabled for 4 users')
->assertSuccessful();
$enabled = $users->filter(fn (User $user) => Feature::for($user)->active(AiConsentSettings::class));
expect($enabled)->toHaveCount(4);
});
test('rejects an out-of-range percentage', function () {
User::factory()->create();
$this->artisan('feature:enable', ['feature' => 'AiConsentSettings', 'target' => '0%'])
->assertFailed();
});