diff --git a/app/Console/Commands/SatisBuild.php b/app/Console/Commands/SatisBuild.php index d7b06650..be482cb1 100644 --- a/app/Console/Commands/SatisBuild.php +++ b/app/Console/Commands/SatisBuild.php @@ -31,7 +31,7 @@ public function handle(SatisService $satisService): int $pluginName = $this->option('plugin'); if ($pluginName) { - $plugin = Plugin::where('name', $pluginName)->first(); + $plugin = Plugin::with('user')->where('name', $pluginName)->first(); if (! $plugin) { $this->error("Plugin '{$pluginName}' not found."); @@ -46,32 +46,49 @@ public function handle(SatisService $satisService): int } $this->info("Triggering Satis build for: {$pluginName}"); - $result = $satisService->build([$plugin]); - } else { - $this->info('Triggering Satis build for all approved plugins...'); - $result = $satisService->buildAll(); - } + $result = $satisService->buildForPlugin($plugin); + + if ($result['success'] ?? false) { + $this->info('Build triggered successfully!'); + $this->line("Job ID: {$result['job_id']}"); + + return self::SUCCESS; + } - if ($result['success']) { - $this->info('Build triggered successfully!'); - $this->line("Job ID: {$result['job_id']}"); + $this->error('Build trigger failed: '.($result['error'] ?? 'Unknown error')); - if (isset($result['plugins_count'])) { - $this->line("Plugins: {$result['plugins_count']}"); + if (isset($result['status'])) { + $this->line("HTTP Status: {$result['status']}"); } + $this->line('API URL: '.config('services.satis.url')); + $this->line('API Key configured: '.(config('services.satis.api_key') ? 'Yes' : 'No')); + + return self::FAILURE; + } + + $this->info('Triggering individual Satis builds for all approved paid plugins...'); + $result = $satisService->buildAll(); + + $count = $result['plugins_count'] ?? 0; + $failed = $result['failed'] ?? []; + + if ($count === 0) { + $this->warn($result['error'] ?? 'No plugins to build.'); + return self::SUCCESS; } - $this->error('Build trigger failed: '.$result['error']); + $this->line('Dispatched '.($count - count($failed))."/{$count} plugin build(s)."); + + if (! empty($failed)) { + $this->error('Failed to dispatch: '.implode(', ', $failed)); - if (isset($result['status'])) { - $this->line("HTTP Status: {$result['status']}"); + return self::FAILURE; } - $this->line('API URL: '.config('services.satis.url')); - $this->line('API Key configured: '.(config('services.satis.api_key') ? 'Yes' : 'No')); + $this->info('All builds triggered successfully!'); - return self::FAILURE; + return self::SUCCESS; } } diff --git a/app/Jobs/Concerns/ResolvesGitHubToken.php b/app/Jobs/Concerns/ResolvesGitHubToken.php index cf1d8f70..e3b3dbd3 100644 --- a/app/Jobs/Concerns/ResolvesGitHubToken.php +++ b/app/Jobs/Concerns/ResolvesGitHubToken.php @@ -11,6 +11,12 @@ protected function getGitHubToken(): ?string { /** @var Plugin $plugin */ $plugin = $this->plugin; + + return $this->resolveGitHubTokenFor($plugin); + } + + protected function resolveGitHubTokenFor(Plugin $plugin): ?string + { $user = $plugin->user; if ($user && $user->hasGitHubToken()) { diff --git a/app/Services/SatisService.php b/app/Services/SatisService.php index 2d317ec0..29a9c64e 100644 --- a/app/Services/SatisService.php +++ b/app/Services/SatisService.php @@ -3,6 +3,7 @@ namespace App\Services; use App\Enums\PluginType; +use App\Jobs\Concerns\ResolvesGitHubToken; use App\Models\Plugin; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; @@ -10,6 +11,8 @@ class SatisService { + use ResolvesGitHubToken; + protected string $apiUrl; protected string $apiKey; @@ -21,13 +24,60 @@ public function __construct() } /** - * Trigger a full satis build with all approved plugins. + * Rebuild every approved paid plugin. + * + * Each plugin is built individually using its owner's GitHub token. A single + * satis build can only authenticate to github.com as one identity, but the + * plugins live in private repos across different owners/orgs — so one shared + * token can't clone them all and falls back to unauthenticated requests that + * hit the 60/hour rate limit. Per-plugin builds are partial (merging) on the + * satis side, so a failure never overwrites the published index with an + * incomplete set. */ - public function buildAll(?string $githubToken = null): array + public function buildAll(): array { - $plugins = $this->getApprovedPlugins(); + $plugins = Plugin::query() + ->approved() + ->where('type', PluginType::Paid) + ->with('user') + ->get(); + + if ($plugins->isEmpty()) { + return [ + 'success' => false, + 'error' => 'No plugins to build', + 'plugins_count' => 0, + 'failed' => [], + 'results' => [], + ]; + } + + $results = []; + $failed = []; + + foreach ($plugins as $plugin) { + $result = $this->buildForPlugin($plugin); + $results[$plugin->name] = $result; + + if (! ($result['success'] ?? false)) { + $failed[] = $plugin->name; + } + } - return $this->triggerBuild($plugins, $githubToken, fullBuild: true); + return [ + 'success' => empty($failed), + 'plugins_count' => $plugins->count(), + 'failed' => $failed, + 'results' => $results, + ]; + } + + /** + * Build a single plugin using its owner's GitHub token. + */ + public function buildForPlugin(Plugin $plugin): array + { + return $this->build([$plugin], $this->resolveGitHubTokenFor($plugin)); } /** @@ -100,26 +150,6 @@ public function removePackage(string $packageName): array } } - /** - * Get all approved plugins formatted for satis. - * - * @return array - */ - protected function getApprovedPlugins(): array - { - return Plugin::query() - ->approved() - ->where('type', PluginType::Paid) - ->get() - ->map(fn (Plugin $plugin) => [ - 'name' => $plugin->name, - 'repository_url' => $plugin->repository_url, - 'is_official' => $plugin->is_official ?? false, - ]) - ->values() - ->all(); - } - /** * @param array $plugins */ diff --git a/tests/Feature/SatisSync/SatisSyncTest.php b/tests/Feature/SatisSync/SatisSyncTest.php index f3d99360..44fd1b36 100644 --- a/tests/Feature/SatisSync/SatisSyncTest.php +++ b/tests/Feature/SatisSync/SatisSyncTest.php @@ -141,13 +141,17 @@ public function test_build_all_only_includes_paid_plugins(): void $this->assertTrue($result['success']); $this->assertEquals(1, $result['plugins_count']); + // buildAll dispatches an individual partial build per paid plugin, each + // authenticated with the owner's token, so a single failure can never + // authoritatively overwrite the published index. + Http::assertSentCount(1); Http::assertSent(function ($request) use ($paidPlugin) { $data = $request->data(); $plugins = $data['plugins'] ?? []; return count($plugins) === 1 && $plugins[0]['name'] === $paidPlugin->name - && $data['full_build'] === true; + && $data['full_build'] === false; }); } }