#!/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.CLI * INGROUP: MokoStandards * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/sync_rulesets.php * BRIEF: Apply branch protection rules to all repos via platform adapter * * USAGE * php cli/sync_rulesets.php # Apply to all repos * php cli/sync_rulesets.php --repo MokoCRM # Single repo * php cli/sync_rulesets.php --dry-run # Preview only * php cli/sync_rulesets.php --delete # Remove then re-apply * * NOTE: On GitHub, this creates rulesets via the rulesets API. * On Gitea, this creates branch_protections via the branch protection API. */ declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; use MokoEnterprise\Config; use MokoEnterprise\PlatformAdapterFactory; $dryRun = in_array('--dry-run', $argv); $deleteOld = in_array('--delete', $argv); $repoName = null; foreach ($argv as $i => $arg) { if ($arg === '--repo' && isset($argv[$i + 1])) { $repoName = $argv[$i + 1]; } } $config = Config::load(); $adapter = PlatformAdapterFactory::create($config); $org = $config->getString( $adapter->getPlatformName() . '.organization', 'mokoconsulting-tech' ); $platformName = $adapter->getPlatformName(); $ALWAYS_EXCLUDE = ['MokoStandards', '.github-private']; // ── Protection rules (platform-agnostic format) ───────────────────────── // On GitHub → rulesets API. On Gitea → branch_protections API. $PROTECTIONS = [ [ 'name' => 'MAIN — protect default branch', 'branch' => 'main', 'rules' => [ 'required_reviews' => 1, 'dismiss_stale' => true, 'enforce_admins' => true, 'block_on_rejected' => true, ], ], [ 'name' => 'VERSION — immutable snapshots', 'branch' => 'version/*', 'rules' => [ 'required_reviews' => 0, 'enforce_admins' => true, ], ], [ 'name' => 'DEV — prevent branch deletion', 'branch' => 'dev/*', 'rules' => [ 'required_reviews' => 0, 'enforce_admins' => true, ], ], [ 'name' => 'RC — prevent branch deletion', 'branch' => 'rc/*', 'rules' => [ 'required_reviews' => 0, 'enforce_admins' => 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 (!$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 ($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"; exit($failed > 0 ? 1 : 0);