Public Access
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d897ba115 | |||
| 8e6aaaff88 | |||
| fef27eb5d1 | |||
| 6d8e09827d |
@@ -30,6 +30,15 @@ on:
|
||||
types: [opened, closed]
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.mokogitea/workflows/**'
|
||||
- '*.md'
|
||||
- 'wiki/**'
|
||||
- '.editorconfig'
|
||||
- '.gitignore'
|
||||
- '.gitattributes'
|
||||
- '.gitmessage'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Enforce branch protection rules across all org repos.
|
||||
# Runs weekly and on manual dispatch.
|
||||
|
||||
name: "Org: Enforce Branch Protections"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Every Monday at 6am UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Dry run (show changes without applying)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
enforce:
|
||||
name: Enforce Branch Protections
|
||||
runs-on: release
|
||||
|
||||
steps:
|
||||
- name: Checkout MokoCLI
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
|
||||
- name: Setup PHP
|
||||
run: |
|
||||
if ! command -v php > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-curl > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: Run branch protection enforcement
|
||||
run: |
|
||||
DRY_RUN=""
|
||||
if [ "${{ inputs.dry_run }}" = "true" ]; then
|
||||
DRY_RUN="--dry-run"
|
||||
fi
|
||||
php cli/branch_protect_org.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--org "MokoConsulting" \
|
||||
$DRY_RUN
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Branch Protection Enforcement" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All repos checked for main, dev, rc, beta, alpha protections" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Push whitelist: jmiller only" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -18,6 +18,8 @@ BRIEF: Release changelog
|
||||
|
||||
## [09.38.00] --- 2026-06-21
|
||||
|
||||
## [09.38.00] --- 2026-06-21
|
||||
|
||||
## [09.37.00] --- 2026-06-21
|
||||
|
||||
## [09.37.00] --- 2026-06-21
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoCLI
|
||||
* @subpackage cli
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Enforce branch protection rules across all repos in the org.
|
||||
*
|
||||
* Usage:
|
||||
* php cli/branch_protect_org.php --token TOKEN [--org MokoConsulting] [--dry-run]
|
||||
*
|
||||
* Branch flow: feature/* -> dev -> rc -> main
|
||||
* main, dev, rc: push whitelist only (no direct push)
|
||||
* alpha, beta: push whitelist only (pre-release)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$options = getopt('', ['token:', 'org:', 'api-base:', 'dry-run', 'help']);
|
||||
|
||||
if (isset($options['help']) || empty($options['token'])) {
|
||||
echo "Usage: php cli/branch_protect_org.php --token TOKEN [--org ORG] [--api-base URL] [--dry-run]\n";
|
||||
echo "\n";
|
||||
echo "Options:\n";
|
||||
echo " --token Gitea API token (required)\n";
|
||||
echo " --org Organization name (default: MokoConsulting)\n";
|
||||
echo " --api-base API base URL (default: https://git.mokoconsulting.tech/api/v1)\n";
|
||||
echo " --dry-run Show what would be changed without making changes\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$token = $options['token'];
|
||||
$org = $options['org'] ?? 'MokoConsulting';
|
||||
$apiBase = rtrim($options['api-base'] ?? 'https://git.mokoconsulting.tech/api/v1', '/');
|
||||
$dryRun = isset($options['dry-run']);
|
||||
|
||||
// Protected branches and their rules
|
||||
$branchRules = [
|
||||
// Primary branches (flow: feature/* -> dev -> rc -> main)
|
||||
'main' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
'dev' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
'rc' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
'beta' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
'alpha' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
|
||||
// Synonyms (prevent bypass via alternate names)
|
||||
'master' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
'develop' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
'release' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
'production' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
'stable' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
'staging' => ['enable_push' => true, 'enable_push_whitelist' => true, 'push_whitelist_usernames' => ['jmiller']],
|
||||
];
|
||||
|
||||
function apiRequest(string $method, string $url, string $token, ?array $body = null): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: token ' . $token,
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
],
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
if ($body !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
return [
|
||||
'status' => $httpCode,
|
||||
'data' => json_decode($response, true) ?: [],
|
||||
];
|
||||
}
|
||||
|
||||
// 1. List all org repos
|
||||
echo "Fetching repos for {$org}...\n";
|
||||
$page = 1;
|
||||
$repos = [];
|
||||
|
||||
do {
|
||||
$result = apiRequest('GET', "{$apiBase}/orgs/{$org}/repos?limit=50&page={$page}", $token);
|
||||
$batch = $result['data'];
|
||||
$repos = array_merge($repos, $batch);
|
||||
$page++;
|
||||
} while (count($batch) === 50);
|
||||
|
||||
echo sprintf("Found %d repos\n\n", count($repos));
|
||||
|
||||
$summary = ['protected' => 0, 'added' => 0, 'skipped' => 0, 'errors' => 0];
|
||||
|
||||
foreach ($repos as $repo) {
|
||||
$repoName = $repo['name'];
|
||||
|
||||
if ($repo['archived'] ?? false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get existing protections
|
||||
$existing = apiRequest('GET', "{$apiBase}/repos/{$org}/{$repoName}/branch_protections", $token);
|
||||
$existingNames = array_map(fn($p) => $p['branch_name'] ?? '', $existing['data'] ?: []);
|
||||
|
||||
$added = [];
|
||||
$skipped = [];
|
||||
|
||||
foreach ($branchRules as $branch => $rules) {
|
||||
if (in_array($branch, $existingNames, true)) {
|
||||
$skipped[] = $branch;
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$added[] = $branch;
|
||||
$summary['added']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$body = array_merge($rules, ['branch_name' => $branch]);
|
||||
$result = apiRequest('POST', "{$apiBase}/repos/{$org}/{$repoName}/branch_protections", $token, $body);
|
||||
|
||||
if ($result['status'] >= 200 && $result['status'] < 300) {
|
||||
$added[] = $branch;
|
||||
$summary['added']++;
|
||||
} elseif ($result['status'] === 422) {
|
||||
$skipped[] = $branch;
|
||||
$summary['skipped']++;
|
||||
} else {
|
||||
$added[] = "{$branch}(ERR:{$result['status']})";
|
||||
$summary['errors']++;
|
||||
}
|
||||
}
|
||||
|
||||
$summary['protected']++;
|
||||
|
||||
if (!empty($added)) {
|
||||
$prefix = $dryRun ? '[DRY-RUN] ' : '';
|
||||
echo sprintf(" %s%-35s added: %s\n", $prefix, $repoName, implode(', ', $added));
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
echo sprintf("Summary: %d repos, %d rules added, %d already existed, %d errors\n",
|
||||
$summary['protected'], $summary['added'], $summary['skipped'], $summary['errors']);
|
||||
|
||||
if ($dryRun) {
|
||||
echo "\n(Dry run - no changes made)\n";
|
||||
}
|
||||
Reference in New Issue
Block a user