#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: MokoStandards.Scripts.CLI * INGROUP: MokoStandards * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/bulk_workflow_trigger.php * VERSION: 01.00.00 * BRIEF: Trigger a workflow across multiple repos at once */ declare(strict_types=1); final class BulkWorkflowTrigger { private string $giteaUrl = 'https://git.mokoconsulting.tech'; private string $token = ''; private string $reposFile = ''; private string $org = ''; private string $workflow = ''; private string $ref = 'main'; private string $inputs = ''; private bool $dryRun = false; public function run(): int { $this->parseArgs(); if ($this->token === '') { $this->log('ERROR: --token is required.'); $this->printUsage(); return 1; } if ($this->workflow === '') { $this->log('ERROR: --workflow is required.'); $this->printUsage(); return 1; } if ($this->reposFile === '' && $this->org === '') { $this->log('ERROR: Either --repos or --org is required.'); $this->printUsage(); return 1; } // Build repo list $repos = $this->buildRepoList(); if ($repos === null || count($repos) === 0) { $this->log('ERROR: No repos found to process.'); return 1; } $this->log("Triggering workflow \"{$this->workflow}\" on ref \"{$this->ref}\" across " . count($repos) . " repo(s)."); $this->log("Gitea URL: {$this->giteaUrl}"); if ($this->dryRun) { $this->log('[DRY RUN] No requests will be sent.'); } $this->log(''); // Parse inputs $inputsDecoded = null; if ($this->inputs !== '') { $inputsDecoded = json_decode($this->inputs, true); if (!is_array($inputsDecoded)) { $this->log('ERROR: --inputs must be valid JSON.'); return 1; } } // Print header $this->log(sprintf('%-40s | %s', 'Repo', 'Status')); $this->log(str_repeat('-', 60)); $failCount = 0; foreach ($repos as $repo) { $repo = trim($repo); if ($repo === '' || strpos($repo, '/') === false) { continue; } [$owner, $repoName] = explode('/', $repo, 2); if ($this->dryRun) { $this->log(sprintf('%-40s | %s', $repo, 'DRY RUN (skipped)')); continue; } $payload = ['ref' => $this->ref]; if ($inputsDecoded !== null) { $payload['inputs'] = $inputsDecoded; } $response = $this->apiRequest( 'POST', "/api/v1/repos/{$owner}/{$repoName}/actions/workflows/{$this->workflow}/dispatches", json_encode($payload) ); if ($response['code'] >= 200 && $response['code'] < 300) { $status = 'TRIGGERED'; } elseif ($response['code'] === 404) { $status = 'FAILED (not found)'; $failCount++; } elseif ($response['code'] === 422) { $status = 'SKIPPED (unprocessable)'; } else { $status = "FAILED (HTTP {$response['code']})"; $failCount++; } $this->log(sprintf('%-40s | %s', $repo, $status)); } $this->log(''); $this->log('Done. ' . ($failCount > 0 ? "{$failCount} failure(s)." : 'All succeeded.')); return $failCount > 0 ? 1 : 0; } private function parseArgs(): void { $args = $_SERVER['argv'] ?? []; $count = count($args); for ($i = 1; $i < $count; $i++) { switch ($args[$i]) { case '--gitea-url': $this->giteaUrl = rtrim($args[++$i] ?? '', '/'); break; case '--token': $this->token = $args[++$i] ?? ''; break; case '--repos': $this->reposFile = $args[++$i] ?? ''; break; case '--org': $this->org = $args[++$i] ?? ''; break; case '--workflow': $this->workflow = $args[++$i] ?? ''; break; case '--ref': $this->ref = $args[++$i] ?? 'main'; break; case '--inputs': $this->inputs = $args[++$i] ?? ''; break; case '--dry-run': $this->dryRun = true; break; case '--help': case '-h': $this->printUsage(); exit(0); default: $this->log("WARNING: Unknown argument: {$args[$i]}"); break; } } } private function printUsage(): void { $this->log('Usage: bulk_workflow_trigger.php --token --workflow [options]'); $this->log(''); $this->log('Options:'); $this->log(' --gitea-url Gitea URL (default: https://git.mokoconsulting.tech)'); $this->log(' --token Gitea API token'); $this->log(' --repos File with newline-separated owner/repo list'); $this->log(' --org Trigger on all repos in an org'); $this->log(' --workflow Workflow file (e.g., "sync-servers.yml")'); $this->log(' --ref Branch ref (default: "main")'); $this->log(' --inputs Workflow inputs as JSON string'); $this->log(' --dry-run Show what would be done without triggering'); $this->log(' --help, -h Show this help'); } private function buildRepoList(): ?array { if ($this->reposFile !== '') { if (!file_exists($this->reposFile)) { $this->log("ERROR: Repos file not found: {$this->reposFile}"); return null; } $content = file_get_contents($this->reposFile); $lines = array_filter(array_map('trim', explode("\n", $content)), function (string $line): bool { return $line !== '' && $line[0] !== '#'; }); return array_values($lines); } // Fetch all repos from org $this->log("Fetching repos from org: {$this->org}"); $page = 1; $repos = []; while (true) { $response = $this->apiRequest('GET', "/api/v1/orgs/{$this->org}/repos?limit=50&page={$page}"); if ($response['code'] < 200 || $response['code'] >= 300) { if ($page === 1) { $this->log("ERROR: Could not fetch repos for org (HTTP {$response['code']})."); return null; } break; } $data = json_decode($response['body'], true); if (!is_array($data) || count($data) === 0) { break; } foreach ($data as $repo) { $fullName = $repo['full_name'] ?? ''; if ($fullName !== '') { $repos[] = $fullName; } } $page++; } $this->log('Found ' . count($repos) . " repo(s) in org \"{$this->org}\"."); return $repos; } private function apiRequest(string $method, string $endpoint, ?string $body = null): array { $url = $this->giteaUrl . $endpoint; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Accept: application/json', "Authorization: token {$this->token}", ]); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $responseBody = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $error = curl_error($ch); curl_close($ch); return ['code' => 0, 'body' => "cURL error: {$error}"]; } curl_close($ch); return ['code' => $httpCode, 'body' => $responseBody]; } private function log(string $message): void { fwrite(STDERR, $message . PHP_EOL); } } $app = new BulkWorkflowTrigger(); exit($app->run());