Merge pull request 'fix: PHPStan level 0 to 2 + 67 type errors fixed' (#93) from dev into main
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Repo Health / Release configuration (push) Successful in 9s
Generic: Repo Health / Scripts governance (push) Successful in 9s
Generic: Repo Health / Repository health (push) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 1m5s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 4s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Failing after 34s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 36s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Failing after 37s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 38s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Failing after 39s

This commit was merged in pull request #93.
This commit is contained in:
2026-05-26 00:32:29 +00:00
22 changed files with 118 additions and 42 deletions
+1 -1
View File
@@ -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 || {
+1
View File
@@ -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
+2 -2
View File
@@ -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<string, string> 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');
+3
View File
@@ -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'];
+2 -2
View File
@@ -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";
}
+11 -11
View File
@@ -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";
+1 -1
View File
@@ -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);
}
}
+4
View File
@@ -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<string, mixed> 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
@@ -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(
+24
View File
@@ -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;
}
}
+23
View File
@@ -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<int, array<string, mixed>>
*/
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<string, mixed> $options
*/
public function cloneRepo(string $repo, string $path, array $options = []): bool;
// ──────────────────────────────────────────────
// File Contents
// ──────────────────────────────────────────────
+20
View File
@@ -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;
}
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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();
}
/**
+3 -4
View File
@@ -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
+5 -7
View File
@@ -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#'
+1 -1
View File
@@ -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'];
+1 -1
View File
@@ -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;
}
+5 -4
View File
@@ -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());
+1 -1
View File
@@ -614,4 +614,4 @@ class RepoHealthChecker extends CliFramework
}
$app = new RepoHealthChecker();
exit($app->execute($argv));
exit($app->execute());
+3 -1
View File
@@ -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());