Public Access
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d897ba115 | |||
| 8e6aaaff88 | |||
| fef27eb5d1 | |||
| 6d8e09827d |
@@ -30,6 +30,15 @@ on:
|
|||||||
types: [opened, closed]
|
types: [opened, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- '.mokogitea/workflows/**'
|
||||||
|
- '*.md'
|
||||||
|
- 'wiki/**'
|
||||||
|
- '.editorconfig'
|
||||||
|
- '.gitignore'
|
||||||
|
- '.gitattributes'
|
||||||
|
- '.gitmessage'
|
||||||
|
- 'LICENSE'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
action:
|
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.38.00] --- 2026-06-21
|
||||||
|
|
||||||
## [09.37.00] --- 2026-06-21
|
## [09.37.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