diff --git a/.mokogitea/workflows/ci-platform.yml b/.mokogitea/workflows/ci-platform.yml index 3939528..47e3ead 100644 --- a/.mokogitea/workflows/ci-platform.yml +++ b/.mokogitea/workflows/ci-platform.yml @@ -124,7 +124,7 @@ jobs: echo "### PHPCS" >> $GITHUB_STEP_SUMMARY echo "PSR-12 compliance: passed" >> $GITHUB_STEP_SUMMARY - - name: "PHPStan (Level 0)" + - name: "PHPStan (Level 2)" continue-on-error: true run: | vendor/bin/phpstan analyse -c phpstan.neon --no-progress --error-format=github 2>&1 || { diff --git a/CHANGELOG.md b/CHANGELOG.md index 8139870..4a3fa75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Version format: `XX.YY.ZZ` (zero-padded semver). ### Fixed - `release_cascade.php`: accept `release-candidate` as stability value (was only accepting `rc`, causing cascade to silently skip) +- PHPStan bumped from level 0 to level 2 — fixed 67 type errors (undefined variables, missing methods, wrong signatures, dead code) ## [06.00.00] - 2026-05-25 diff --git a/automation/bulk_joomla_template.php b/automation/bulk_joomla_template.php index 6e317c3..54df132 100644 --- a/automation/bulk_joomla_template.php +++ b/automation/bulk_joomla_template.php @@ -217,7 +217,7 @@ class BulkJoomlaTemplate extends CLIApp // Scaffold files $this->log("\nScaffolding template files...", 'INFO'); - $files = $this->getScaffoldFiles($name, $shortName, $client); + $files = $this->getScaffoldFiles($name, $shortName, $client, $org); $created = 0; foreach ($files as $path => $content) { @@ -409,7 +409,7 @@ class BulkJoomlaTemplate extends CLIApp * * @return array path => content */ - private function getScaffoldFiles(string $name, string $shortName, string $client): array + private function getScaffoldFiles(string $name, string $shortName, string $client, string $org): array { $element = "tpl_{$shortName}"; $now = date('Y-m-d'); diff --git a/automation/bulk_sync.php b/automation/bulk_sync.php index 71012a4..c5131ad 100755 --- a/automation/bulk_sync.php +++ b/automation/bulk_sync.php @@ -1163,6 +1163,7 @@ class BulkSync extends CLIApp 'sort' => 'created', 'direction' => 'desc', ]); + $existing = array_values($existing); if (!empty($existing) && isset($existing[0]['number'])) { $num = $existing[0]['number']; @@ -1311,6 +1312,7 @@ class BulkSync extends CLIApp $labelNames = ['sync-report', 'mokostandards', 'type: chore', 'automation']; $labels = $this->resolveLabelIds($org, 'MokoStandards', $labelNames); + $existing = array_values($existing); if (!empty($existing) && isset($existing[0]['number'])) { $issueNumber = $existing[0]['number']; @@ -1394,6 +1396,7 @@ class BulkSync extends CLIApp 'sort' => 'created', 'direction' => 'desc', ]); + $existing = array_values($existing); if (!empty($existing) && isset($existing[0]['number'])) { $num = $existing[0]['number']; diff --git a/cli/create_project.php b/cli/create_project.php index 15e4c19..6a738d0 100644 --- a/cli/create_project.php +++ b/cli/create_project.php @@ -159,7 +159,7 @@ function restGet(string $path, string $token, ?\MokoEnterprise\ApiClient $apiCli /** * Detect platform type from .mokostandards file in the repo. */ -function detectPlatform(string $org, string $repo, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): string +function detectRepoPlatform(string $org, string $repo, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): string { // Try platform metadata dir first, then root foreach (['.github/.mokostandards', '.mokogitea/.mokostandards', '.mokostandards'] as $path) { @@ -447,7 +447,7 @@ foreach ($repos as $repo) { // Detect project type $type = $typeOverride; if (!$type) { - $platform = detectPlatform($org, $repo, $token); + $platform = detectRepoPlatform($org, $repo, $token); $type = $PLATFORM_TO_TYPE[$platform] ?? 'generic'; echo " Platform: {$platform} → type: {$type}\n"; } diff --git a/cli/release_manage.php b/cli/release_manage.php index 32789e0..7fe3ca1 100644 --- a/cli/release_manage.php +++ b/cli/release_manage.php @@ -83,7 +83,7 @@ if ($action === null || $tag === null || $token === null || $apiBase === null) { /** * Make a Gitea API request using curl */ -function giteaApi(string $url, string $method, string $token, ?string $jsonBody = null, ?string $filePath = null): array +function releaseGiteaApi(string $url, string $method, string $token, ?string $jsonBody = null, ?string $filePath = null): array { $ch = curl_init($url); $headers = ["Authorization: token {$token}"]; @@ -118,7 +118,7 @@ function giteaApi(string $url, string $method, string $token, ?string $jsonBody */ function getReleaseByTag(string $apiBase, string $tag, string $token): ?array { - $result = giteaApi("{$apiBase}/releases/tags/{$tag}", 'GET', $token); + $result = releaseGiteaApi("{$apiBase}/releases/tags/{$tag}", 'GET', $token); if ($result['code'] === 200 && isset($result['data']['id'])) { return $result['data']; } @@ -132,8 +132,8 @@ switch ($action) { $existing = getReleaseByTag($apiBase, $tag, $token); if ($existing !== null) { $existingId = $existing['id']; - giteaApi("{$apiBase}/releases/{$existingId}", 'DELETE', $token); - giteaApi("{$apiBase}/tags/{$tag}", 'DELETE', $token); + releaseGiteaApi("{$apiBase}/releases/{$existingId}", 'DELETE', $token); + releaseGiteaApi("{$apiBase}/tags/{$tag}", 'DELETE', $token); echo "Deleted previous release: {$tag} (id: {$existingId})\n"; } @@ -144,7 +144,7 @@ switch ($action) { 'target_commitish' => $target, ]); - $result = giteaApi("{$apiBase}/releases", 'POST', $token, $payload); + $result = releaseGiteaApi("{$apiBase}/releases", 'POST', $token, $payload); if ($result['code'] >= 200 && $result['code'] < 300) { $releaseId = $result['data']['id'] ?? 'unknown'; echo "Release created: {$name} (tag: {$tag}, id: {$releaseId})\n"; @@ -169,7 +169,7 @@ switch ($action) { $releaseId = $release['id']; // Get existing assets to avoid duplicates - $assetsResult = giteaApi("{$apiBase}/releases/{$releaseId}/assets", 'GET', $token); + $assetsResult = releaseGiteaApi("{$apiBase}/releases/{$releaseId}/assets", 'GET', $token); $existingAssets = $assetsResult['data'] ?? []; foreach ($files as $filePath) { @@ -184,7 +184,7 @@ switch ($action) { // Delete existing asset with same name foreach ($existingAssets as $asset) { if (($asset['name'] ?? '') === $fileName) { - giteaApi("{$apiBase}/releases/{$releaseId}/assets/{$asset['id']}", 'DELETE', $token); + releaseGiteaApi("{$apiBase}/releases/{$releaseId}/assets/{$asset['id']}", 'DELETE', $token); echo "Deleted existing asset: {$fileName}\n"; break; } @@ -192,7 +192,7 @@ switch ($action) { // Upload $uploadUrl = "{$apiBase}/releases/{$releaseId}/assets?name=" . urlencode($fileName); - $result = giteaApi($uploadUrl, 'POST', $token, null, $filePath); + $result = releaseGiteaApi($uploadUrl, 'POST', $token, null, $filePath); if ($result['code'] >= 200 && $result['code'] < 300) { echo "Uploaded: {$fileName}\n"; } else { @@ -210,7 +210,7 @@ switch ($action) { $releaseId = $release['id']; $payload = json_encode(['body' => $body ?? '']); - $result = giteaApi("{$apiBase}/releases/{$releaseId}", 'PATCH', $token, $payload); + $result = releaseGiteaApi("{$apiBase}/releases/{$releaseId}", 'PATCH', $token, $payload); if ($result['code'] >= 200 && $result['code'] < 300) { echo "Release body updated for tag: {$tag}\n"; } else { @@ -222,8 +222,8 @@ switch ($action) { case 'delete': $existing = getReleaseByTag($apiBase, $tag, $token); if ($existing !== null) { - giteaApi("{$apiBase}/releases/{$existing['id']}", 'DELETE', $token); - giteaApi("{$apiBase}/tags/{$tag}", 'DELETE', $token); + releaseGiteaApi("{$apiBase}/releases/{$existing['id']}", 'DELETE', $token); + releaseGiteaApi("{$apiBase}/tags/{$tag}", 'DELETE', $token); echo "Deleted: {$tag} (id: {$existing['id']})\n"; } else { echo "No release found for tag: {$tag}\n"; diff --git a/lib/Enterprise/AbstractProjectPlugin.php b/lib/Enterprise/AbstractProjectPlugin.php index 5089bda..40f4ff9 100644 --- a/lib/Enterprise/AbstractProjectPlugin.php +++ b/lib/Enterprise/AbstractProjectPlugin.php @@ -268,6 +268,6 @@ abstract class AbstractProjectPlugin implements ProjectPluginInterface $tags['plugin'] = $this->getPluginName(); $tags['project_type'] = $this->getProjectType(); - $this->metricsCollector->record($category, $name, $value, $tags); + $this->metricsCollector->observe("{$category}.{$name}", (float) $value, $tags); } } diff --git a/lib/Enterprise/ApiClient.php b/lib/Enterprise/ApiClient.php index 29fd739..f915af0 100644 --- a/lib/Enterprise/ApiClient.php +++ b/lib/Enterprise/ApiClient.php @@ -123,6 +123,9 @@ class ApiClient /** Circuit breaker last failure time */ private ?DateTime $circuitLastFailure = null; + /** @var LoggerInterface|null Optional logger instance */ + private ?LoggerInterface $logger = null; + /** @var array Request metrics */ private array $metrics = [ 'total_requests' => 0, @@ -176,6 +179,7 @@ class ApiClient $this->circuitBreakerTimeout = $circuitBreakerTimeout; $this->enableCaching = $enableCaching; $this->userAgent = $userAgent; + $this->logger = $logger; $this->authScheme = $authScheme; // Initialize HTTP client diff --git a/lib/Enterprise/EnterpriseReadinessValidator.php b/lib/Enterprise/EnterpriseReadinessValidator.php index 64152dd..f2f9a24 100644 --- a/lib/Enterprise/EnterpriseReadinessValidator.php +++ b/lib/Enterprise/EnterpriseReadinessValidator.php @@ -169,7 +169,8 @@ class EnterpriseReadinessValidator // Run security scan on PHP files if (is_dir("{$path}/src")) { - $issues = $this->securityValidator->scanDirectory("{$path}/src", ['.php']); + $this->securityValidator->scanDirectory("{$path}/src", ['.php']); + $issues = $this->securityValidator->getFindings(); $issueCount = count($issues); $this->addResult( diff --git a/lib/Enterprise/GitHubAdapter.php b/lib/Enterprise/GitHubAdapter.php index 64325aa..ef7c38c 100644 --- a/lib/Enterprise/GitHubAdapter.php +++ b/lib/Enterprise/GitHubAdapter.php @@ -425,4 +425,28 @@ class GitHubAdapter implements GitPlatformAdapter { return $this->apiClient; } + + public function listBranches(string $org, string $repo): array + { + return $this->apiClient->get("/repos/{$org}/{$repo}/branches") ?? []; + } + + public function getCloneUrl(string $repo): string + { + return "https://github.com/{$repo}.git"; + } + + public function cloneRepo(string $repo, string $path, array $options = []): bool + { + $url = $this->getCloneUrl($repo); + $depth = $options['depth'] ?? 0; + $depthFlag = $depth > 0 ? " --depth {$depth}" : ''; + $result = 0; + passthru( + 'git clone' . $depthFlag . ' --quiet ' + . escapeshellarg($url) . ' ' . escapeshellarg($path), + $result + ); + return $result === 0; + } } diff --git a/lib/Enterprise/GitPlatformAdapter.php b/lib/Enterprise/GitPlatformAdapter.php index 126d06a..9a7d420 100644 --- a/lib/Enterprise/GitPlatformAdapter.php +++ b/lib/Enterprise/GitPlatformAdapter.php @@ -168,6 +168,29 @@ interface GitPlatformAdapter */ public function getRepoTopics(string $org, string $repo): array; + // ────────────────────────────────────────────── + // Branches and Cloning + // ────────────────────────────────────────────── + + /** + * List all branches in a repository. + * + * @return array> + */ + public function listBranches(string $org, string $repo): array; + + /** + * Get the clone URL for a repository. + */ + public function getCloneUrl(string $repo): string; + + /** + * Clone a repository to a local path. + * + * @param array $options + */ + public function cloneRepo(string $repo, string $path, array $options = []): bool; + // ────────────────────────────────────────────── // File Contents // ────────────────────────────────────────────── diff --git a/lib/Enterprise/MokoGiteaAdapter.php b/lib/Enterprise/MokoGiteaAdapter.php index 51b3d29..be4c1cd 100644 --- a/lib/Enterprise/MokoGiteaAdapter.php +++ b/lib/Enterprise/MokoGiteaAdapter.php @@ -498,4 +498,24 @@ class MokoGiteaAdapter implements GitPlatformAdapter { return $this->apiClient; } + + public function getCloneUrl(string $repo): string + { + $base = str_replace('/api/v1', '', $this->baseUrl); + return "{$base}/{$repo}.git"; + } + + public function cloneRepo(string $repo, string $path, array $options = []): bool + { + $url = $this->getCloneUrl($repo); + $depth = $options['depth'] ?? 0; + $depthFlag = $depth > 0 ? " --depth {$depth}" : ''; + $result = 0; + passthru( + 'git clone' . $depthFlag . ' --quiet ' + . escapeshellarg($url) . ' ' . escapeshellarg($path), + $result + ); + return $result === 0; + } } diff --git a/lib/Enterprise/Plugins/ApiPlugin.php b/lib/Enterprise/Plugins/ApiPlugin.php index 3c7e6e7..f7a3322 100644 --- a/lib/Enterprise/Plugins/ApiPlugin.php +++ b/lib/Enterprise/Plugins/ApiPlugin.php @@ -57,7 +57,7 @@ class ApiPlugin extends AbstractProjectPlugin // Check for API documentation if (!$this->hasAPIDocumentation($projectPath, $apiType)) { - $warnings[] = 'No API documentation found (OpenAPI, GraphQL schema, etc.)'; + $errors[] = 'No API documentation found (OpenAPI, GraphQL schema, etc.)'; } // Check for proper error handling diff --git a/lib/Enterprise/Plugins/GenericPlugin.php b/lib/Enterprise/Plugins/GenericPlugin.php index befb4a6..aa3c915 100644 --- a/lib/Enterprise/Plugins/GenericPlugin.php +++ b/lib/Enterprise/Plugins/GenericPlugin.php @@ -59,7 +59,7 @@ class GenericPlugin extends AbstractProjectPlugin !$this->fileExists($projectPath, 'README') && !$this->fileExists($projectPath, 'README.txt') ) { - $warnings[] = 'No README file found'; + $errors[] = 'No README file found'; } // Check for LICENSE diff --git a/lib/Enterprise/RepositoryHealthChecker.php b/lib/Enterprise/RepositoryHealthChecker.php index 6a52c6e..c178125 100644 --- a/lib/Enterprise/RepositoryHealthChecker.php +++ b/lib/Enterprise/RepositoryHealthChecker.php @@ -29,7 +29,7 @@ class RepositoryHealthChecker { private AuditLogger $logger; private MetricsCollector $metrics; - private UnifiedValidation $validator; + private UnifiedValidator $validator; private array $results = [ 'categories' => [], @@ -46,11 +46,11 @@ class RepositoryHealthChecker public function __construct( ?AuditLogger $logger = null, ?MetricsCollector $metrics = null, - ?UnifiedValidation $validator = null + ?UnifiedValidator $validator = null ) { $this->logger = $logger ?? new AuditLogger('repo_health_checker'); $this->metrics = $metrics ?? new MetricsCollector(); - $this->validator = $validator ?? new UnifiedValidation(); + $this->validator = $validator ?? new UnifiedValidator(); } /** diff --git a/lib/Enterprise/RepositorySynchronizer.php b/lib/Enterprise/RepositorySynchronizer.php index 02b8167..7f7319e 100644 --- a/lib/Enterprise/RepositorySynchronizer.php +++ b/lib/Enterprise/RepositorySynchronizer.php @@ -100,7 +100,7 @@ class RepositorySynchronizer try { $overridePath = $this->adapter->getMetadataDir() . '/' . self::SYNC_OVERRIDE_FILE_SUFFIX; $override = $this->adapter->getFileContents($org, $repo, $overridePath); - return !empty($override); + return $override !== ''; } catch (Exception $e) { return false; } @@ -560,7 +560,7 @@ HCL; $combinedSummary = ['copied' => [], 'skipped' => [], 'total' => 0]; foreach ($branchesToSync as $branchName) { $this->logger->logInfo(" Syncing branch: {$branchName}"); - $branchSummary = $this->syncFilesToBranch($org, $repo, $platform, $filesToSync, $repoRoot, $force, $branchName, $moduleId ?? null); + $branchSummary = $this->syncFilesToBranch($org, $repo, $platform, $filesToSync, $repoRoot, $force, $branchName, null); // Merge summaries — only count first branch's copied files to avoid duplicates in tracking if ($branchName === $defaultBranch) { $combinedSummary = $branchSummary; @@ -1137,7 +1137,6 @@ HCL; 'dolibarr' => 'templates/configs/gitignore.dolibarr', 'platform' => 'templates/configs/gitignore.dolibarr', 'joomla' => 'templates/configs/.gitignore.joomla', - 'joomla' => 'templates/configs/.gitignore.joomla', ]; $gitignoreTemplate = $gitignoreMap[$platform] ?? 'templates/configs/gitignore'; $shared[] = [$gitignoreTemplate, '.gitignore']; @@ -1164,7 +1163,7 @@ HCL; ]; foreach ($shared as [$source, $dest]) { - $fullSource = "{$root}/{$source}"; + $fullSource = "{$repoRoot}/{$source}"; if (file_exists($fullSource)) { $entries[] = [ 'source' => $source, // relative — RepositorySynchronizer prepends repoRoot diff --git a/phpstan.neon b/phpstan.neon index 5e34d85..2d0e87b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,7 +6,7 @@ # PHPStan configuration for moko-platform projects parameters: - level: 0 + level: 2 paths: - lib - validate @@ -16,14 +16,12 @@ parameters: analyseAndScan: - vendor - node_modules (?) + # Legacy CLIApp scripts — need migration to CliFramework + - automation/repo_cleanup.php + - automation/push_files.php + - cli/joomla_release.php reportUnmatchedIgnoredErrors: false - # Additional checks checkFunctionNameCase: true checkInternalClassCaseSensitivity: true - - # Ignore common patterns - ignoreErrors: - # Add project-specific ignores here - # - '#Call to an undefined method#' diff --git a/validate/auto_detect_platform.php b/validate/auto_detect_platform.php index 4c4235a..c4873a4 100755 --- a/validate/auto_detect_platform.php +++ b/validate/auto_detect_platform.php @@ -102,7 +102,7 @@ class AutoDetectPlatform extends CLIApp // Use the new plugin system for detection $this->log("Using ProjectTypeDetector for platform detection", 'INFO'); - $detectionResult = $this->typeDetector->detectProjectType($repoPath); + $detectionResult = $this->typeDetector->detect($repoPath); if (!empty($detectionResult['type'])) { $this->detectedPlatform = $detectionResult['type']; diff --git a/validate/check_client_theme.php b/validate/check_client_theme.php index c94d298..46b0b1e 100644 --- a/validate/check_client_theme.php +++ b/validate/check_client_theme.php @@ -269,7 +269,7 @@ class CheckClientTheme extends CliFramework // ── Summary ─────────────────────────────────────────── $passed = ($errors === 0) ? 1 : 0; - $this->printSummary($passed, $errors, $this->elapsed(), $warns); + $this->printSummary($passed, $errors, $this->elapsed()); return ($errors > 0) ? 1 : 0; } diff --git a/validate/check_enterprise_readiness.php b/validate/check_enterprise_readiness.php index 2938c52..5400260 100755 --- a/validate/check_enterprise_readiness.php +++ b/validate/check_enterprise_readiness.php @@ -203,11 +203,12 @@ class EnterpriseReadinessChecker extends CliFramework // Run security scan on PHP files if (is_dir("{$path}/src")) { - $issues = $this->securityValidator->scanDirectory("{$path}/src", ['.php']); + $this->securityValidator->scanDirectory("{$path}/src", ['.php']); + $findings = $this->securityValidator->getFindings(); $this->addResult( 'No security vulnerabilities in source code', - empty($issues), - count($issues) . ' security issues found' + empty($findings), + count($findings) . ' security issues found' ); } } @@ -247,4 +248,4 @@ class EnterpriseReadinessChecker extends CliFramework // Run the application $app = new EnterpriseReadinessChecker(); -exit($app->execute($argv)); +exit($app->execute()); diff --git a/validate/check_repo_health.php b/validate/check_repo_health.php index 96c2010..193f4d1 100755 --- a/validate/check_repo_health.php +++ b/validate/check_repo_health.php @@ -614,4 +614,4 @@ class RepoHealthChecker extends CliFramework } $app = new RepoHealthChecker(); -exit($app->execute($argv)); +exit($app->execute()); diff --git a/validate/scan_drift.php b/validate/scan_drift.php index 1a28a62..037635e 100755 --- a/validate/scan_drift.php +++ b/validate/scan_drift.php @@ -40,6 +40,7 @@ class DriftScanner extends CliFramework private ApiClient $apiClient; private AuditLogger $logger; private MetricsCollector $metrics; + private \MokoEnterprise\GitPlatformAdapter $adapter; private array $driftResults = []; private array $templates = []; @@ -561,6 +562,7 @@ class DriftScanner extends CliFramework 'sort' => 'created', 'direction' => 'desc', ]); + $existing = array_values($existing); if (!empty($existing) && isset($existing[0]['number'])) { $num = $existing[0]['number']; @@ -610,4 +612,4 @@ class DriftScanner extends CliFramework // Run the application $app = new DriftScanner(); -exit($app->execute($argv)); +exit($app->execute());