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
51 changes: 34 additions & 17 deletions app/Console/Commands/SatisBuild.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand All @@ -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;
}
}
6 changes: 6 additions & 0 deletions app/Jobs/Concerns/ResolvesGitHubToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
78 changes: 54 additions & 24 deletions app/Services/SatisService.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
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;
use Illuminate\Support\Facades\Log;

class SatisService
{
use ResolvesGitHubToken;

protected string $apiUrl;

protected string $apiKey;
Expand All @@ -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));
}

/**
Expand Down Expand Up @@ -100,26 +150,6 @@ public function removePackage(string $packageName): array
}
}

/**
* Get all approved plugins formatted for satis.
*
* @return array<int, array{name: string, repository_url: string, type: string, is_official: bool}>
*/
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<int, array{name: string, repository_url: string, type: string, is_official?: bool}> $plugins
*/
Expand Down
6 changes: 5 additions & 1 deletion tests/Feature/SatisSync/SatisSyncTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
}
}
Loading