From 15d95840c8d79150851b70a3b00e4423fe4113ef Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 4 Mar 2026 15:27:47 +0000 Subject: [PATCH] feat: add sync-forge command and update config defaults - Add SyncForgeCommand to register Forge repos as tracked vendors - Register SyncForgeCommand in onConsole() event handler - Update gitea config defaults: forge.lthn.ai URL, FORGE_TOKEN fallback, core org - Fix PHP 8.5 parse error: use string concatenation for ANSI-tagged output Co-Authored-By: Virgil --- Boot.php | 1 + Console/SyncForgeCommand.php | 183 +++++++++++++++++++++++++++++++++++ config.php | 6 +- 3 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 Console/SyncForgeCommand.php diff --git a/Boot.php b/Boot.php index 9f39249..1b72204 100644 --- a/Boot.php +++ b/Boot.php @@ -98,6 +98,7 @@ class Boot extends ServiceProvider $event->command(Console\IssuesCommand::class); $event->command(Console\CheckUpdatesCommand::class); $event->command(Console\SendDigestsCommand::class); + $event->command(Console\SyncForgeCommand::class); } /** diff --git a/Console/SyncForgeCommand.php b/Console/SyncForgeCommand.php new file mode 100644 index 0000000..1a7de63 --- /dev/null +++ b/Console/SyncForgeCommand.php @@ -0,0 +1,183 @@ +option('org') ?? config('upstream.gitea.org', 'core'); + $dryRun = $this->option('dry-run'); + + if (! $token) { + $this->error('No Forge token configured. Set FORGE_TOKEN or GITEA_TOKEN in .env'); + + return self::FAILURE; + } + + $this->info("Fetching repositories from {$baseUrl}/api/v1/orgs/{$org}/repos..."); + + $repos = $this->fetchAllRepos($baseUrl, $token, $org); + + if ($repos === null) { + $this->error('Failed to fetch repositories from Forge API.'); + + return self::FAILURE; + } + + $this->info(count($repos) . ' repositories found.'); + $this->newLine(); + + $created = 0; + $skipped = 0; + $updated = 0; + + foreach ($repos as $repo) { + $fullName = $repo['full_name']; + $slug = Str::slug($fullName, '-'); + $repoUrl = "{$baseUrl}/{$fullName}"; + + $existing = Vendor::withTrashed()->where('slug', $slug)->first(); + + if ($existing && ! $existing->trashed()) { + // Update git_repo_url if it changed + if ($existing->git_repo_url !== $repoUrl) { + if (! $dryRun) { + $existing->update(['git_repo_url' => $repoUrl]); + } + $this->line(' Updated ' . $fullName); + $updated++; + } else { + $this->line(' Exists ' . $fullName); + $skipped++; + } + + continue; + } + + if ($existing && $existing->trashed()) { + if (! $dryRun) { + $existing->restore(); + $existing->update([ + 'git_repo_url' => $repoUrl, + 'is_active' => true, + ]); + } + $this->line(' Restored ' . $fullName); + $created++; + + continue; + } + + // Detect language hint for platform + $platform = $this->detectPlatform($repo); + + if (! $dryRun) { + Vendor::create([ + 'slug' => $slug, + 'name' => $repo['name'], + 'vendor_name' => $org, + 'source_type' => Vendor::SOURCE_OSS, + 'plugin_platform' => $platform, + 'git_repo_url' => $repoUrl, + 'current_version' => null, + 'target_repo' => $fullName, + 'target_branch' => $repo['default_branch'] ?? 'main', + 'is_active' => true, + 'path_mapping' => [], + 'ignored_paths' => ['.git/*', 'vendor/*', 'node_modules/*'], + 'priority_paths' => [], + ]); + } + + $this->line(' Created ' . $fullName . ' (' . ($platform ?? 'oss') . ')'); + $created++; + } + + $this->newLine(); + $prefix = $dryRun ? '[DRY RUN] ' : ''; + $this->info("{$prefix}Sync complete: {$created} created, {$updated} updated, {$skipped} unchanged."); + + return self::SUCCESS; + } + + /** + * Fetch all repositories from a Forge organisation, handling pagination. + * + * @return array>|null + */ + private function fetchAllRepos(string $baseUrl, string $token, string $org): ?array + { + $allRepos = []; + $page = 1; + $limit = 50; + + do { + $response = Http::withHeaders(['Authorization' => "token {$token}"]) + ->timeout(30) + ->get("{$baseUrl}/api/v1/orgs/{$org}/repos", [ + 'page' => $page, + 'limit' => $limit, + ]); + + if (! $response->successful()) { + $this->error("Forge API error: {$response->status()}"); + + return null; + } + + $repos = $response->json(); + + if (empty($repos)) { + break; + } + + $allRepos = array_merge($allRepos, $repos); + $page++; + } while (count($repos) === $limit); + + return $allRepos; + } + + /** + * Detect the platform type from repository metadata. + */ + private function detectPlatform(array $repo): ?string + { + $name = $repo['name'] ?? ''; + + if (str_starts_with($name, 'php-') || str_starts_with($name, 'core-')) { + return Vendor::PLATFORM_LARAVEL; + } + + return Vendor::PLATFORM_OTHER; + } +} diff --git a/config.php b/config.php index 46b369c..74cfe69 100644 --- a/config.php +++ b/config.php @@ -192,9 +192,9 @@ return [ */ 'gitea' => [ 'enabled' => env('UPSTREAM_GITEA_ENABLED', true), - 'url' => env('GITEA_URL', 'https://git.host.uk'), - 'token' => env('GITEA_TOKEN'), - 'org' => env('GITEA_ORG', 'host-uk'), + 'url' => env('GITEA_URL', 'https://forge.lthn.ai'), + 'token' => env('GITEA_TOKEN', env('FORGE_TOKEN')), + 'org' => env('GITEA_ORG', 'core'), ], /*