#!/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/sync_rulesets.php * BRIEF: Apply branch protection rules to all repos via platform adapter */ declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; use MokoEnterprise\Config; use MokoEnterprise\PlatformAdapterFactory; class SyncRulesetsCli extends CliFramework { protected function configure(): void { $this->setDescription('Apply branch protection rules to all repos via platform adapter'); $this->addArgument('--repo', 'Single repository name (default: all repos)', ''); $this->addArgument('--delete', 'Remove existing protections before re-applying', false); } protected function run(): int { $repoName = $this->getArgument('--repo'); $deleteOld = $this->getArgument('--delete'); $config = Config::load(); $adapter = PlatformAdapterFactory::create($config); $org = $config->getString( $adapter->getPlatformName() . '.organization', 'mokoconsulting-tech' ); $platformName = $adapter->getPlatformName(); $ALWAYS_EXCLUDE = ['moko-platform', '.github-private']; // -- Protection rules (platform-agnostic format) -- $PROTECTIONS = [ [ 'name' => 'MAIN — protect default branch', 'branch' => 'main', 'rules' => [ 'required_reviews' => 1, 'dismiss_stale' => true, 'enforce_admins' => true, 'block_on_rejected' => true, 'whitelist_actions_user' => true, ], ], [ 'name' => 'VERSION — immutable snapshots', 'branch' => 'version/*', 'rules' => [ 'required_reviews' => 0, 'enforce_admins' => true, 'whitelist_actions_user' => true, ], ], [ 'name' => 'DEV — prevent branch deletion', 'branch' => 'dev/*', 'rules' => [ 'required_reviews' => 0, 'enforce_admins' => true, 'whitelist_actions_user' => true, ], ], [ 'name' => 'RC — prevent branch deletion', 'branch' => 'rc/*', 'rules' => [ 'required_reviews' => 0, 'enforce_admins' => true, 'whitelist_actions_user' => true, ], ], ]; // -- Build repo list -- $repos = []; if ($repoName !== '') { $repos = [$repoName]; } else { echo "Fetching repositories from {$org} ({$platformName})...\n"; $allRepos = $adapter->listOrgRepos($org, true); // skip archived foreach ($allRepos as $r) { if (!in_array($r['name'], $ALWAYS_EXCLUDE, true)) { $repos[] = $r['name']; } } sort($repos); echo "Found " . count($repos) . " repositories\n\n"; } $created = 0; $skipped = 0; $failed = 0; foreach ($repos as $repo) { echo "Processing {$repo}...\n"; // Check existing protections $existing = $adapter->listBranchProtections($org, $repo); $existingNames = []; if (is_array($existing)) { foreach ($existing as $bp) { $bpName = $bp['name'] ?? $bp['branch_name'] ?? $bp['rule_name'] ?? ''; $bpId = $bp['id'] ?? null; if ($bpName !== '') { $existingNames[$bpName] = $bpId; } } } foreach ($PROTECTIONS as $protection) { $pName = $protection['name']; if ($deleteOld && isset($existingNames[$pName])) { if (!$this->dryRun) { try { // Platform-specific deletion via raw API $adapter->getApiClient()->delete( "/repos/{$org}/{$repo}/" . ($platformName === 'github' ? 'rulesets' : 'branch_protections') . "/{$existingNames[$pName]}" ); } catch (\Exception $e) { /* ignore delete errors */ } } echo " Deleted: {$pName}\n"; unset($existingNames[$pName]); } if (isset($existingNames[$pName])) { echo " Exists: {$pName}\n"; $skipped++; continue; } if ($this->dryRun) { echo " (dry-run) would create: {$pName}\n"; $created++; continue; } try { $adapter->setBranchProtection($org, $repo, $protection['branch'], $protection['rules']); echo " Created: {$pName}\n"; $created++; } catch (\Exception $e) { $msg = $e->getMessage(); if (str_contains($msg, '403')) { echo " Skipped (needs Pro/paid plan): {$pName}\n"; $skipped++; } else { echo " Failed: {$pName} — {$msg}\n"; $failed++; } } } echo "\n"; } echo str_repeat('-', 50) . "\n"; echo "Done: {$created} created, {$skipped} skipped, {$failed} failed\n"; return $failed > 0 ? 1 : 0; } } $app = new SyncRulesetsCli(); exit($app->execute());