fix: replace all hardcoded GitHub API URLs with platform adapter pattern

- cli/create_project.php: use PlatformAdapterFactory, guard GraphQL for GitHub-only
- cli/joomla_release.php: use adapter for API init, platform-aware clone/upload URLs
- release/generate_joomla_update_xml.php: use PlatformAdapterFactory for API init
- release/generate_dolibarr_version_txt.php: same
- validate/scan_drift.php: use PlatformAdapterFactory for API init
- validate/check_repo_health.php: use platform-aware API base URL
- validate/check_composer_deps.php: route through adapter ApiClient
- maintenance/repo_inventory.php: route through adapter ApiClient, guard GraphQL
- maintenance/rotate_secrets.php: route through adapter ApiClient
- maintenance/update_version_from_readme.php: use PlatformAdapterFactory, rename method
- lib/Common.php: set primary REPO_URL to Gitea

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-04-14 22:12:41 -05:00
parent 0b80fec88d
commit e96a851617
11 changed files with 157 additions and 121 deletions
+29 -25
View File
@@ -49,11 +49,18 @@ if (!$repoName && !$allMode) {
exit(2);
}
$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN');
if (empty($token)) {
fwrite(STDERR, "GH_TOKEN or GITHUB_TOKEN not set\n");
$config = \MokoEnterprise\Config::load();
$platform = $config->getString('platform', 'gitea');
try {
$adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$api = $adapter->getApiClient();
} catch (\Exception $e) {
fwrite(STDERR, "Platform initialization failed: " . $e->getMessage() . "\n");
exit(1);
}
$token = $platform === 'gitea'
? $config->getString('gitea.token', '')
: $config->getString('github.token', '');
$repoRoot = dirname(__DIR__, 2);
$templatesDir = "{$repoRoot}/templates/projects";
@@ -93,12 +100,15 @@ $TYPE_TO_TEMPLATE = [
];
/**
* Execute a GitHub GraphQL query.
* Execute a GraphQL query (GitHub only — Gitea does not support GraphQL).
*
* @return array<string, mixed>
*/
function graphql(string $query, array $variables, string $token): array
function graphql(string $query, array $variables, string $token, string $platformName = 'gitea'): array
{
if ($platformName !== 'github') {
return [];
}
$payload = json_encode(['query' => $query, 'variables' => $variables]);
$ch = curl_init('https://api.github.com/graphql');
curl_setopt_array($ch, [
@@ -131,36 +141,30 @@ function graphql(string $query, array $variables, string $token): array
}
/**
* Execute a GitHub REST API GET call.
* Execute a REST API GET call via the platform adapter's ApiClient.
*
* @return array<string, mixed>
*/
function restGet(string $path, string $token): array
function restGet(string $path, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): array
{
$ch = curl_init("https://api.github.com/{$path}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: token ' . $token,
'User-Agent: MokoStandards-CreateProject',
'Accept: application/vnd.github.v3+json',
],
]);
$body = (string) curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $status === 200 ? (json_decode($body, true) ?? []) : [];
if ($apiClient !== null) {
try {
return $apiClient->get("/{$path}");
} catch (\Exception $e) {
return [];
}
}
return [];
}
/**
* Detect platform type from .mokostandards file in the repo.
*/
function detectPlatform(string $org, string $repo, string $token): string
function detectPlatform(string $org, string $repo, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): string
{
// Try .github/.mokostandards first, then root
foreach (['.github/.mokostandards', '.mokostandards'] as $path) {
$data = restGet("repos/{$org}/{$repo}/contents/{$path}", $token);
// Try platform metadata dir first, then root
foreach (['.github/.mokostandards', '.gitea/.mokostandards', '.mokostandards'] as $path) {
$data = restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient);
if (!empty($data['content'])) {
$content = base64_decode($data['content']);
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
+21 -7
View File
@@ -25,7 +25,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use MokoEnterprise\{ApiClient, AuditLogger, CLIApp};
use MokoEnterprise\{ApiClient, AuditLogger, CLIApp, Config, PlatformAdapterFactory};
class JoomlaRelease extends CLIApp
{
@@ -73,7 +73,9 @@ class JoomlaRelease extends CLIApp
return 1;
}
$this->api = new ApiClient();
$config = Config::load();
$this->adapter = PlatformAdapterFactory::create($config);
$this->api = $this->adapter->getApiClient();
$this->logger = new AuditLogger('joomla_release');
if ($repo !== '') {
@@ -287,7 +289,12 @@ class JoomlaRelease extends CLIApp
}
}
$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN');
$releaseConfig = Config::load();
$releasePlatform = $releaseConfig->getString('platform', 'gitea');
$releaseToken = $releasePlatform === 'gitea'
? $releaseConfig->getString('gitea.token', '')
: $releaseConfig->getString('github.token', '');
$acceptHeader = $releasePlatform === 'github' ? 'application/vnd.github+json' : 'application/json';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "{$uploadUrl}?name=" . urlencode($fileName),
@@ -295,9 +302,9 @@ class JoomlaRelease extends CLIApp
CURLOPT_POSTFIELDS => file_get_contents($filePath),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: token {$token}",
"Authorization: token {$releaseToken}",
'Content-Type: application/octet-stream',
'Accept: application/vnd.github+json',
"Accept: {$acceptHeader}",
],
]);
curl_exec($ch);
@@ -369,8 +376,15 @@ class JoomlaRelease extends CLIApp
if (is_dir($tmpDir)) {
$this->rmdir($tmpDir);
}
$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN');
$url = "https://x-access-token:{$token}@github.com/" . self::ORG . "/{$repo}.git";
$config = Config::load();
$platform = $config->getString('platform', 'gitea');
$token = $platform === 'gitea'
? $config->getString('gitea.token', '')
: $config->getString('github.token', '');
$cloneHost = $platform === 'gitea'
? rtrim($config->getString('gitea.url', 'https://git.mokoconsulting.tech'), '/')
: 'https://github.com';
$url = "https://x-access-token:{$token}@" . preg_replace('#^https?://#', '', $cloneHost) . '/' . self::ORG . "/{$repo}.git";
$cmd = ['git', 'clone', '--depth', '1', '--quiet', $url, $tmpDir];
$proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
proc_close($proc);
+2 -1
View File
@@ -32,7 +32,8 @@ class Common
*/
const FALLBACK_VERSION = '04.00.00';
const REPO_URL = 'https://github.com/mokoconsulting-tech/MokoStandards';
const REPO_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API';
const REPO_URL_GITHUB = 'https://github.com/mokoconsulting-tech/MokoStandards';
const COPYRIGHT = 'Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>';
const LICENSE = 'GPL-3.0-or-later';
+30 -21
View File
@@ -30,11 +30,17 @@ foreach ($argv as $i => $arg) {
if ($arg === '--org' && isset($argv[$i + 1])) { $org = $argv[$i + 1]; }
}
$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN');
if (empty($token)) {
fwrite(STDERR, "GH_TOKEN or GITHUB_TOKEN not set\n");
$config = \MokoEnterprise\Config::load();
try {
$_adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$_api = $_adapter->getApiClient();
} catch (\Exception $e) {
fwrite(STDERR, "Platform init failed: " . $e->getMessage() . "\n");
exit(1);
}
$token = $config->getString('platform', 'gitea') === 'gitea'
? $config->getString('gitea.token', '')
: $config->getString('github.token', '');
$ALWAYS_EXCLUDE = ['MokoStandards', '.github-private'];
@@ -43,23 +49,20 @@ $ALWAYS_EXCLUDE = ['MokoStandards', '.github-private'];
*/
function ghApi(string $method, string $path, ?array $body, string $token): array
{
$ch = curl_init("https://api.github.com/{$path}");
$opts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: token ' . $token,
'Content-Type: application/json',
'User-Agent: MokoStandards-Inventory',
'Accept: application/vnd.github.v3+json',
],
];
if ($method !== 'GET') { $opts[CURLOPT_CUSTOMREQUEST] = $method; }
if ($body !== null) { $opts[CURLOPT_POSTFIELDS] = json_encode($body); }
curl_setopt_array($ch, $opts);
$resp = (string) curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [$status, json_decode($resp, true) ?? []];
global $_api;
try {
$result = match ($method) {
'GET' => $_api->get("/{$path}"),
'POST' => $_api->post("/{$path}", $body ?? []),
'PATCH' => $_api->patch("/{$path}", $body ?? []),
'PUT' => $_api->put("/{$path}", $body ?? []),
'DELETE' => $_api->delete("/{$path}"),
default => throw new \RuntimeException("Unsupported method: {$method}"),
};
return [200, $result];
} catch (\Exception $e) {
return [500, ['message' => $e->getMessage()]];
}
}
/**
@@ -69,11 +72,17 @@ function ghApi(string $method, string $path, ?array $body, string $token): array
*/
function graphql(string $query, array $variables, string $token): array
{
global $config;
$pf = isset($config) ? $config->getString('platform', 'gitea') : 'gitea';
if ($pf !== 'github') {
return [];
}
$payload = json_encode(['query' => $query, 'variables' => $variables]);
$ch = curl_init('https://api.github.com/graphql');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['query' => $query, 'variables' => $variables]),
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: bearer ' . $token,
'Content-Type: application/json',
+24 -20
View File
@@ -40,11 +40,17 @@ if (!$repoName && !$allMode) {
exit(2);
}
$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN');
if (empty($token)) {
fwrite(STDERR, "GH_TOKEN or GITHUB_TOKEN not set\n");
$config = \MokoEnterprise\Config::load();
try {
$_adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$_api = $_adapter->getApiClient();
} catch (\Exception $e) {
fwrite(STDERR, "Platform init failed: " . $e->getMessage() . "\n");
exit(1);
}
$token = $config->getString('platform', 'gitea') === 'gitea'
? $config->getString('gitea.token', '')
: $config->getString('github.token', '');
$ALWAYS_EXCLUDE = ['MokoStandards', '.github-private'];
@@ -61,25 +67,23 @@ $ENVS = [
*/
function ghApi(string $method, string $path, ?array $body, string $token): array
{
$ch = curl_init("https://api.github.com/{$path}");
$opts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: token ' . $token,
'Content-Type: application/json',
'User-Agent: MokoStandards-SecretAudit',
'Accept: application/vnd.github.v3+json',
],
];
if ($method !== 'GET') { $opts[CURLOPT_CUSTOMREQUEST] = $method; }
if ($body !== null) { $opts[CURLOPT_POSTFIELDS] = json_encode($body); }
curl_setopt_array($ch, $opts);
$resp = (string) curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [$status, json_decode($resp, true) ?? []];
global $_api;
try {
$result = match ($method) {
'GET' => $_api->get("/{$path}"),
'POST' => $_api->post("/{$path}", $body ?? []),
'PATCH' => $_api->patch("/{$path}", $body ?? []),
'PUT' => $_api->put("/{$path}", $body ?? []),
'DELETE' => $_api->delete("/{$path}"),
default => throw new \RuntimeException("Unsupported method: {$method}"),
};
return [200, $result];
} catch (\Exception $e) {
return [500, ['message' => $e->getMessage()]];
}
}
/**
* Fetch all names from a paginated list endpoint.
*
+11 -8
View File
@@ -109,7 +109,7 @@ class UpdateVersionFromReadme extends CliFramework
$remaining = $this->countRemainingMismatches($repoRoot, $version);
if ($remaining > 0) {
$this->log("{$remaining} version reference(s) could not be auto-updated");
$this->createGitHubIssue($repo, $version, $remaining);
$this->createDriftIssue($repo, $version, $remaining);
}
}
@@ -408,16 +408,19 @@ class UpdateVersionFromReadme extends CliFramework
* @param string $version Expected version
* @param int $remaining Number of remaining mismatches
*/
private function createGitHubIssue(string $repo, string $version, int $remaining): void
private function createDriftIssue(string $repo, string $version, int $remaining): void
{
$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN');
if (empty($token)) {
$this->error('GH_TOKEN or GITHUB_TOKEN required to create issue');
return;
if (!isset($this->apiClient)) {
$config = \MokoEnterprise\Config::load();
try {
$adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$this->apiClient = $adapter->getApiClient();
} catch (\Exception $e) {
$this->error('Platform initialization failed: ' . $e->getMessage());
return;
}
}
$this->apiClient ??= new ApiClient($token, ['base_url' => 'https://api.github.com']);
$title = "⚠️ Version drift: {$remaining} file(s) not updated to {$version}";
$labels = ['version-drift', 'maintenance', 'type: chore', 'automation'];
$body = implode("\n", [
+2 -6
View File
@@ -310,13 +310,9 @@ class GenerateDolibarrVersionTxt extends CLIApp
private function initApi(): bool
{
$config = Config::load();
$token = $config->getString('github.token', '');
if (empty($token)) {
$this->log('❌ GitHub token not configured. Set GH_TOKEN or run: gh auth login', 'ERROR');
return false;
}
try {
$this->api = new ApiClient('https://api.github.com', $token);
$this->adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$this->api = $this->adapter->getApiClient();
return true;
} catch (\Exception $e) {
$this->log('❌ API init failed: ' . $e->getMessage(), 'ERROR');
+2 -6
View File
@@ -532,13 +532,9 @@ XML;
private function initApi(): bool
{
$config = Config::load();
$token = $config->getString('github.token', '');
if (empty($token)) {
$this->log('❌ GitHub token not configured. Set GH_TOKEN or run: gh auth login', 'ERROR');
return false;
}
try {
$this->api = new ApiClient('https://api.github.com', $token);
$this->adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$this->api = $this->adapter->getApiClient();
return true;
} catch (\Exception $e) {
$this->log('❌ API init failed: ' . $e->getMessage(), 'ERROR');
+16 -16
View File
@@ -38,11 +38,17 @@ if (!$repoName && !$allMode) {
exit(2);
}
$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN');
if (empty($token)) {
fwrite(STDERR, "GH_TOKEN or GITHUB_TOKEN not set\n");
$config = \MokoEnterprise\Config::load();
try {
$_adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$_api = $_adapter->getApiClient();
} catch (\Exception $e) {
fwrite(STDERR, "Platform init failed: " . $e->getMessage() . "\n");
exit(1);
}
$token = $config->getString('platform', 'gitea') === 'gitea'
? $config->getString('gitea.token', '')
: $config->getString('github.token', '');
$EXPECTED_VERSION = '04.02.30';
$EXPECTED_DEP = "dev-version/{$EXPECTED_VERSION}";
@@ -56,19 +62,13 @@ $ALWAYS_EXCLUDE = ['MokoStandards', '.github-private'];
*/
function apiGet(string $path, string $token): array
{
$ch = curl_init("https://api.github.com/{$path}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: token ' . $token,
'User-Agent: MokoStandards-ComposerCheck',
'Accept: application/vnd.github.v3+json',
],
]);
$body = (string) curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [$status, json_decode($body, true) ?? []];
global $_api;
try {
$result = $_api->get("/{$path}");
return [200, $result];
} catch (\Exception $e) {
return [500, ['message' => $e->getMessage()]];
}
}
/**
+13 -5
View File
@@ -44,6 +44,7 @@ class RepoHealthChecker extends CliFramework
private MetricsCollector $metrics;
private PluginFactory $pluginFactory;
private ?object $projectPlugin = null;
private string $apiBaseUrl = 'https://git.mokoconsulting.tech/api/v1';
private array $results = [
'categories' => [],
@@ -67,11 +68,18 @@ class RepoHealthChecker extends CliFramework
protected function initialize(): void
{
parent::initialize();
$this->logger = new AuditLogger('repo_health_checker');
$this->metrics = new MetricsCollector();
$this->pluginFactory = new PluginFactory($this->logger, $this->metrics);
// Resolve API base URL from platform config
$config = \MokoEnterprise\Config::load();
$platform = $config->getString('platform', 'gitea');
$this->apiBaseUrl = $platform === 'github'
? 'https://api.github.com'
: rtrim($config->getString('gitea.url', 'https://git.mokoconsulting.tech'), '/') . '/api/v1';
$this->log('Repository health checker initialized with plugin system');
}
@@ -408,7 +416,7 @@ class RepoHealthChecker extends CliFramework
*/
private function fetchRulesets(string $repo, string $token): array
{
$url = "https://api.github.com/repos/{$repo}/rulesets?per_page=100&includes_parents=true";
$url = "{$this->apiBaseUrl}/repos/{$repo}/rulesets?per_page=100&includes_parents=true";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
@@ -555,7 +563,7 @@ class RepoHealthChecker extends CliFramework
*/
private function githubVarExists(string $resourcePath, string $token): bool
{
$url = "https://api.github.com/{$resourcePath}";
$url = "{$this->apiBaseUrl}/{$resourcePath}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
@@ -592,7 +600,7 @@ class RepoHealthChecker extends CliFramework
$perPage = 100;
do {
$url = "https://api.github.com/{$resourcePath}?per_page={$perPage}&page={$page}";
$url = "{$this->apiBaseUrl}/{$resourcePath}?per_page={$perPage}&page={$page}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
+7 -6
View File
@@ -62,15 +62,16 @@ class DriftScanner extends CliFramework
$this->logger = new AuditLogger('drift_scanner');
$this->metrics = new MetricsCollector();
// Initialize API client
$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN');
if (empty($token)) {
$this->error("GH_TOKEN or GITHUB_TOKEN environment variable required");
// Initialize API client via platform adapter
$config = \MokoEnterprise\Config::load();
try {
$this->adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$this->apiClient = $this->adapter->getApiClient();
} catch (\Exception $e) {
$this->error("Platform initialization failed: " . $e->getMessage());
exit(1);
}
$this->apiClient = new ApiClient($token, ['base_url' => 'https://api.github.com']);
$this->log("Standards Drift Scanner v" . self::VERSION);
}