4cc3f5bee4
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 14s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been skipped
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
- Convert tabs to spaces (3,413 violations) - Fix line endings, trailing whitespace, brace placement - Break lines exceeding 150-char absolute limit - Replace heredoc tab closers with spaces - Fix empty elseif, forbidden function calls - Update phpcs.xml: exclude rules inappropriate for CLI scripts (SideEffects, MissingNamespace, MultipleClasses, HeaderOrder, empty catch blocks) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
301 lines
12 KiB
PHP
301 lines
12 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: MokoStandards.Automation
|
|
* INGROUP: MokoStandards
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /automation/migrate_to_gitea.php
|
|
* BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance
|
|
*
|
|
* USAGE
|
|
* php automation/migrate_to_gitea.php --dry-run
|
|
* php automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods
|
|
* php automation/migrate_to_gitea.php --exclude MokoStandards --skip-archived
|
|
* php automation/migrate_to_gitea.php --resume
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
|
use MokoEnterprise\CheckpointManager;
|
|
use MokoEnterprise\CliFramework;
|
|
use MokoEnterprise\Config;
|
|
use MokoEnterprise\PlatformAdapterFactory;
|
|
use MokoEnterprise\GitHubAdapter;
|
|
use MokoEnterprise\MokoGiteaAdapter;
|
|
|
|
/**
|
|
* Gitea Migration Script
|
|
*
|
|
* Migrates repositories from GitHub to a self-hosted Gitea instance.
|
|
* Uses Gitea's built-in migration endpoint for git history, tags, releases,
|
|
* issues, and labels. Post-migration applies branch protection, topics,
|
|
* and workflow conversion.
|
|
*/
|
|
class MigrateToGitea extends CliFramework
|
|
{
|
|
private ?GitHubAdapter $github = null;
|
|
private ?MokoGiteaAdapter $gitea = null;
|
|
private ?CheckpointManager $checkpoints = null;
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Migrate repositories from GitHub to Gitea');
|
|
$this->addArgument('--dry-run', 'Show what would be migrated without making changes', false);
|
|
$this->addArgument('--repos', 'Specific repositories to migrate (space-separated)', '');
|
|
$this->addArgument('--exclude', 'Repositories to exclude (space-separated)', '');
|
|
$this->addArgument('--skip-archived', 'Skip archived repositories', false);
|
|
$this->addArgument('--resume', 'Resume from last checkpoint', false);
|
|
$this->addArgument('--github-token', 'GitHub token override', '');
|
|
$this->addArgument('--gitea-token', 'Gitea token override', '');
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$dryRun = (bool) $this->getArgument('--dry-run');
|
|
$specificRepos = array_filter(explode(' ', (string) $this->getArgument('--repos')));
|
|
$excludeRepos = array_filter(explode(' ', (string) $this->getArgument('--exclude')));
|
|
$skipArchived = (bool) $this->getArgument('--skip-archived');
|
|
$resume = (bool) $this->getArgument('--resume');
|
|
|
|
$config = Config::load();
|
|
|
|
// Override tokens if provided
|
|
$ghToken = (string) $this->getArgument('--github-token');
|
|
$giteaToken = (string) $this->getArgument('--gitea-token');
|
|
if ($ghToken !== '') {
|
|
$config->set('github.token', $ghToken);
|
|
}
|
|
if ($giteaToken !== '') {
|
|
$config->set('gitea.token', $giteaToken);
|
|
}
|
|
|
|
// Create both adapters
|
|
try {
|
|
$adapters = PlatformAdapterFactory::createBoth($config);
|
|
$this->github = $adapters['github'];
|
|
$this->gitea = $adapters['gitea'];
|
|
} catch (\RuntimeException $e) {
|
|
$this->log('ERROR', $e->getMessage());
|
|
return 1;
|
|
}
|
|
|
|
$this->checkpoints = new CheckpointManager('.checkpoints/migration');
|
|
$org = $config->getString('github.organization', 'MokoConsulting');
|
|
$giteaOrg = $config->getString('gitea.organization', 'MokoConsulting');
|
|
|
|
echo "=== Gitea Migration Tool ===\n";
|
|
echo "Source: GitHub ({$org})\n";
|
|
echo "Destination: Gitea ({$giteaOrg}) at " . $config->getString('gitea.url') . "\n";
|
|
echo "Mode: " . ($dryRun ? 'DRY RUN' : 'LIVE') . "\n\n";
|
|
|
|
// ── Phase 1: Discovery ──────────────────────────────────────────
|
|
$this->section('Phase 1: Discovery');
|
|
|
|
$ghRepos = $this->github->listOrgRepos($org, $skipArchived);
|
|
echo "Found " . count($ghRepos) . " repositories on GitHub\n";
|
|
|
|
// Filter repos
|
|
if (!empty($specificRepos)) {
|
|
$ghRepos = array_filter($ghRepos, fn($r) => in_array($r['name'], $specificRepos, true));
|
|
}
|
|
if (!empty($excludeRepos)) {
|
|
$ghRepos = array_filter($ghRepos, fn($r) => !in_array($r['name'], $excludeRepos, true));
|
|
}
|
|
|
|
// Check which already exist on Gitea
|
|
$giteaRepos = [];
|
|
try {
|
|
$existing = $this->gitea->listOrgRepos($giteaOrg);
|
|
foreach ($existing as $r) {
|
|
$giteaRepos[$r['name']] = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
echo "Note: Could not list Gitea repos (org may not exist yet): {$e->getMessage()}\n";
|
|
}
|
|
|
|
$toMigrate = [];
|
|
$toSkip = [];
|
|
foreach ($ghRepos as $repo) {
|
|
$name = $repo['name'];
|
|
if (isset($giteaRepos[$name])) {
|
|
$toSkip[] = $name;
|
|
} else {
|
|
$toMigrate[] = $repo;
|
|
}
|
|
}
|
|
|
|
echo "\nMigration plan:\n";
|
|
echo " Migrate: " . count($toMigrate) . " repositories\n";
|
|
echo " Skip: " . count($toSkip) . " (already on Gitea)\n";
|
|
if (!empty($toSkip)) {
|
|
echo " Skipped: " . implode(', ', $toSkip) . "\n";
|
|
}
|
|
echo "\n";
|
|
|
|
if (empty($toMigrate)) {
|
|
echo "Nothing to migrate.\n";
|
|
return 0;
|
|
}
|
|
|
|
if ($dryRun) {
|
|
echo "Repositories to migrate:\n";
|
|
foreach ($toMigrate as $repo) {
|
|
$vis = $repo['private'] ? 'private' : 'public';
|
|
echo " - {$repo['name']} ({$vis})\n";
|
|
}
|
|
echo "\nDry run complete. Use without --dry-run to execute.\n";
|
|
return 0;
|
|
}
|
|
|
|
// ── Phase 2: Migrate ────────────────────────────────────────────
|
|
$this->section('Phase 2: Migration');
|
|
|
|
$ghToken = $config->getString('github.token');
|
|
$results = ['migrated' => [], 'failed' => [], 'skipped' => $toSkip];
|
|
|
|
// Resume support
|
|
$checkpoint = $resume ? $this->checkpoints->loadCheckpoint('gitea_migration') : null;
|
|
$startFrom = $checkpoint['last_completed'] ?? '';
|
|
$skipUntil = !empty($startFrom);
|
|
|
|
foreach ($toMigrate as $index => $repo) {
|
|
$name = $repo['name'];
|
|
|
|
if ($skipUntil) {
|
|
if ($name === $startFrom) {
|
|
$skipUntil = false;
|
|
}
|
|
echo " Skipping {$name} (already migrated)\n";
|
|
continue;
|
|
}
|
|
|
|
echo "\n [{$index}/{" . count($toMigrate) . "}] Migrating {$name}...\n";
|
|
|
|
try {
|
|
// Shallow migration — copy current branch state only, no past
|
|
// commit history. This gives every repo a clean start on Gitea.
|
|
$this->gitea->migrateRepository([
|
|
'clone_addr' => "https://github.com/{$org}/{$name}.git",
|
|
'repo_name' => $name,
|
|
'repo_owner' => $giteaOrg,
|
|
'service' => 'github',
|
|
'auth_token' => $ghToken,
|
|
'mirror' => false,
|
|
'private' => $repo['private'],
|
|
'issues' => false,
|
|
'labels' => true,
|
|
'milestones' => false,
|
|
'releases' => false,
|
|
'pull_requests' => false,
|
|
'wiki' => false,
|
|
]);
|
|
|
|
echo " Migrated successfully\n";
|
|
$results['migrated'][] = $name;
|
|
|
|
// Save checkpoint after each successful migration
|
|
$this->checkpoints->saveCheckpoint('gitea_migration', [
|
|
'last_completed' => $name,
|
|
'migrated' => $results['migrated'],
|
|
'failed' => $results['failed'],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
echo " FAILED: " . $e->getMessage() . "\n";
|
|
$results['failed'][] = ['name' => $name, 'error' => $e->getMessage()];
|
|
$this->gitea->getApiClient()->resetCircuitBreaker();
|
|
}
|
|
}
|
|
|
|
// ── Phase 3: Post-migration ─────────────────────────────────────
|
|
$this->section('Phase 3: Post-migration');
|
|
|
|
foreach ($results['migrated'] as $name) {
|
|
echo " Post-processing {$name}...\n";
|
|
|
|
try {
|
|
// Apply topics from GitHub
|
|
$ghTopics = $this->github->getRepoTopics($org, $name);
|
|
if (!empty($ghTopics)) {
|
|
$this->gitea->setRepoTopics($giteaOrg, $name, $ghTopics);
|
|
echo " Topics applied\n";
|
|
}
|
|
|
|
// Apply branch protection
|
|
$this->gitea->setBranchProtection($giteaOrg, $name, 'main', [
|
|
'required_reviews' => 1,
|
|
'dismiss_stale' => true,
|
|
'block_on_rejected' => true,
|
|
]);
|
|
echo " Branch protection applied\n";
|
|
} catch (\Exception $e) {
|
|
echo " Warning: post-processing issue: " . $e->getMessage() . "\n";
|
|
$this->gitea->getApiClient()->resetCircuitBreaker();
|
|
}
|
|
}
|
|
|
|
// ── Phase 4: Verification ───────────────────────────────────────
|
|
$this->section('Phase 4: Verification');
|
|
|
|
$report = "## Migration Report\n\n";
|
|
$report .= "**Date:** " . gmdate('Y-m-d H:i:s') . " UTC\n";
|
|
$report .= "**Source:** GitHub ({$org})\n";
|
|
$report .= "**Destination:** Gitea ({$giteaOrg})\n\n";
|
|
|
|
$report .= "### Results\n\n";
|
|
$report .= "| Status | Count |\n|--------|-------|\n";
|
|
$report .= "| Migrated | " . count($results['migrated']) . " |\n";
|
|
$report .= "| Failed | " . count($results['failed']) . " |\n";
|
|
$report .= "| Skipped (existing) | " . count($results['skipped']) . " |\n\n";
|
|
|
|
if (!empty($results['migrated'])) {
|
|
$report .= "### Migrated Repositories\n\n";
|
|
foreach ($results['migrated'] as $name) {
|
|
$report .= "- {$name}\n";
|
|
}
|
|
$report .= "\n";
|
|
}
|
|
|
|
if (!empty($results['failed'])) {
|
|
$report .= "### Failed Repositories\n\n";
|
|
foreach ($results['failed'] as $fail) {
|
|
$report .= "- **{$fail['name']}**: {$fail['error']}\n";
|
|
}
|
|
$report .= "\n";
|
|
}
|
|
|
|
echo $report;
|
|
|
|
// Create summary issue on Gitea
|
|
try {
|
|
$this->gitea->createIssue(
|
|
$giteaOrg,
|
|
'MokoStandards',
|
|
'chore: GitHub → Gitea migration report — ' . count($results['migrated']) . ' repos migrated',
|
|
$report,
|
|
['labels' => ['automation', 'type: chore']]
|
|
);
|
|
echo "Migration report issue created on Gitea.\n";
|
|
} catch (\Exception $e) {
|
|
echo "Could not create report issue: " . $e->getMessage() . "\n";
|
|
}
|
|
|
|
echo "\nMigration complete: " . count($results['migrated']) . " migrated, "
|
|
. count($results['failed']) . " failed, "
|
|
. count($results['skipped']) . " skipped\n";
|
|
|
|
return count($results['failed']) > 0 ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
$script = new MigrateToGitea('migrate_to_gitea', 'Migrate repositories from GitHub to Gitea');
|
|
exit($script->execute());
|