#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: moko-platform.CLI * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/bulk_workflow_push.php * VERSION: 09.22.00 * BRIEF: Push a workflow file to all governed repos via the Gitea Contents API */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class BulkWorkflowPushCli extends CliFramework { private int $updated = 0; private int $created = 0; private int $skipped = 0; private int $errors = 0; protected function configure(): void { $this->setDescription('Push a workflow file to all governed repos via the Gitea Contents API'); $this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech'); $this->addArgument('--token', 'Gitea API token', ''); $this->addArgument('--org', 'Target organization', ''); $this->addArgument('--file', 'Local workflow file to push', ''); $this->addArgument('--dest', 'Destination path in repos (default: .mokogitea/workflows/)', ''); $this->addArgument('--branch', 'Target branch (default: main)', 'main'); } protected function run(): int { $giteaUrl = rtrim($this->getArgument('--gitea-url'), '/'); $token = $this->getArgument('--token'); $org = $this->getArgument('--org'); $workflowFile = $this->getArgument('--file'); $destPath = $this->getArgument('--dest'); $branch = $this->getArgument('--branch'); if ($token === '') { $this->log('ERROR', '--token is required.'); return 1; } if ($workflowFile === '') { $this->log('ERROR', '--file is required.'); return 1; } if (!file_exists($workflowFile)) { $this->log('ERROR', "File not found: {$workflowFile}"); return 1; } if ($org === '') { $this->log('ERROR', '--org is required.'); return 1; } if ($destPath === '') { $destPath = '.mokogitea/workflows/' . basename($workflowFile); } $localContent = file_get_contents($workflowFile); if ($localContent === false) { $this->log('ERROR', "Could not read file: {$workflowFile}"); return 1; } $this->log('INFO', "Pushing: {$workflowFile}"); $this->log('INFO', " -> {$destPath} (branch: {$branch})"); $this->log('INFO', " -> Org: {$org} @ {$giteaUrl}"); if ($this->dryRun) { $this->log('INFO', '[DRY RUN] No changes will be made.'); } echo "\n"; $repos = $this->fetchOrgRepos($giteaUrl, $token, $org); if ($repos === null) { return 1; } $this->log('INFO', "Found " . count($repos) . " repo(s) in \"{$org}\"."); echo "\n"; fprintf(STDERR, "%-45s | %s\n", 'Repo', 'Status'); fprintf(STDERR, "%s\n", str_repeat('-', 70)); $encodedContent = base64_encode($localContent); foreach ($repos as $repo) { $this->pushToRepo($giteaUrl, $token, $repo, $encodedContent, $localContent, $destPath, $branch); } echo "\n"; $this->log('INFO', "Done: {$this->created} created, {$this->updated} updated, " . "{$this->skipped} skipped, {$this->errors} error(s)."); return $this->errors > 0 ? 1 : 0; } private function pushToRepo( string $giteaUrl, string $token, string $repoFullName, string $encodedContent, string $localContent, string $destPath, string $branch ): void { [$owner, $repoName] = explode('/', $repoFullName, 2); $existing = $this->apiRequest( $giteaUrl, $token, 'GET', "/api/v1/repos/{$owner}/{$repoName}/contents/" . "{$destPath}?ref={$branch}" ); if ($existing['code'] === 200) { $data = json_decode($existing['body'], true); $remoteSha = $data['sha'] ?? ''; $remoteContent = base64_decode($data['content'] ?? ''); if ($remoteContent === $localContent) { fprintf(STDERR, "%-45s | %s\n", $repoFullName, 'IDENTICAL (skipped)'); $this->skipped++; return; } if ($this->dryRun) { fprintf(STDERR, "%-45s | %s\n", $repoFullName, 'WOULD UPDATE'); $this->updated++; return; } $payload = json_encode([ 'content' => $encodedContent, 'sha' => $remoteSha, 'message' => "chore: sync {$destPath} " . "from moko-platform [skip ci]", 'branch' => $branch, ]); $response = $this->apiRequest( $giteaUrl, $token, 'PUT', "/api/v1/repos/{$owner}/{$repoName}/contents/" . $destPath, $payload ); if ($response['code'] === 200) { fprintf(STDERR, "%-45s | %s\n", $repoFullName, 'UPDATED'); $this->updated++; } else { fprintf(STDERR, "%-45s | %s\n", $repoFullName, "ERROR (HTTP {$response['code']})"); $this->errors++; } } elseif ($existing['code'] === 404) { if ($this->dryRun) { fprintf(STDERR, "%-45s | %s\n", $repoFullName, 'WOULD CREATE'); $this->created++; return; } $payload = json_encode([ 'content' => $encodedContent, 'message' => "chore: add {$destPath} " . "from moko-platform [skip ci]", 'branch' => $branch, ]); $response = $this->apiRequest( $giteaUrl, $token, 'POST', "/api/v1/repos/{$owner}/{$repoName}/contents/" . $destPath, $payload ); if ($response['code'] === 201) { fprintf(STDERR, "%-45s | %s\n", $repoFullName, 'CREATED'); $this->created++; } else { fprintf(STDERR, "%-45s | %s\n", $repoFullName, "ERROR (HTTP {$response['code']})"); $this->errors++; } } else { fprintf(STDERR, "%-45s | %s\n", $repoFullName, "ERROR (HTTP {$existing['code']})"); $this->errors++; } } private function fetchOrgRepos(string $giteaUrl, string $token, string $org): ?array { $this->log('INFO', "Fetching repos from org: {$org}"); $page = 1; $repos = []; while (true) { $response = $this->apiRequest( $giteaUrl, $token, 'GET', "/api/v1/orgs/{$org}/repos?" . "limit=50&page={$page}" ); if ($response['code'] < 200 || $response['code'] >= 300) { if ($page === 1) { $this->log('ERROR', "Could not fetch repos " . "(HTTP {$response['code']})."); return null; } break; } $data = json_decode($response['body'], true); if (!is_array($data) || count($data) === 0) { break; } foreach ($data as $repo) { if (!empty($repo['archived'])) { continue; } $fullName = $repo['full_name'] ?? ''; if ($fullName !== '') { $repos[] = $fullName; } } $page++; } return $repos; } private function apiRequest( string $giteaUrl, string $token, string $method, string $endpoint, ?string $body = null ): array { $url = $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 {$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]; } } $app = new BulkWorkflowPushCli(); exit($app->execute());