34ef05bd6e
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 46s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
- Replace all .github/workflows refs in definitions with .mokogitea/workflows - Add all 13 universal workflows to every platform definition - Fix ISSUE_TEMPLATE paths: .github/ → .mokogitea/ - Fix template source paths: templates/github/ → templates/mokogitea/ - Remove 60+ dead template references pointing to non-existent files - Rename templates/gitea/ directory to templates/mokogitea/ - Add orphaned workflows (ci-platform, issue-branch) to definitions Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
210 lines
6.3 KiB
PHP
210 lines
6.3 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: moko-platform.CLI
|
|
* INGROUP: moko-platform
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /cli/theme_lint.php
|
|
* BRIEF: Lint theme files — CSS syntax, image sizes, hardcoded URLs
|
|
*
|
|
* Usage:
|
|
* php theme_lint.php --path /repo
|
|
* php theme_lint.php --path /repo --max-image-kb 500
|
|
* php theme_lint.php --path /repo --github-output
|
|
*
|
|
* Options:
|
|
* --path Repository root (default: .)
|
|
* --max-image-kb Maximum image file size in KB (default: 500)
|
|
* --github-output Export results to $GITHUB_OUTPUT
|
|
* --strict Exit 1 on any warning (default: only on errors)
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
$path = '.';
|
|
$maxImageKb = 500;
|
|
$ghOutput = false;
|
|
$strict = false;
|
|
|
|
foreach ($argv as $i => $arg) {
|
|
if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1];
|
|
if ($arg === '--max-image-kb' && isset($argv[$i + 1])) $maxImageKb = (int)$argv[$i + 1];
|
|
if ($arg === '--github-output') $ghOutput = true;
|
|
if ($arg === '--strict') $strict = true;
|
|
}
|
|
|
|
$root = realpath($path) ?: $path;
|
|
$errors = 0;
|
|
$warnings = 0;
|
|
|
|
// ── Find source directory ───────────────────────────────────────────────
|
|
$srcDir = null;
|
|
foreach (['src', 'htdocs'] as $d) {
|
|
if (is_dir("{$root}/{$d}")) { $srcDir = "{$root}/{$d}"; break; }
|
|
}
|
|
if ($srcDir === null) {
|
|
fwrite(STDERR, "No src/ or htdocs/ directory in {$root}\n");
|
|
exit(1);
|
|
}
|
|
|
|
echo "Theme Lint: {$srcDir}\n\n";
|
|
|
|
// ── Check 1: CSS syntax validation ──────────────────────────────────────
|
|
echo "--- CSS Syntax ---\n";
|
|
$cssFiles = findFiles($srcDir, '*.css');
|
|
$cssMinFiles = findFiles($srcDir, '*.min.css');
|
|
$cssToCheck = array_diff($cssFiles, $cssMinFiles);
|
|
|
|
if (empty($cssToCheck)) {
|
|
echo " No CSS files to check\n";
|
|
} else {
|
|
foreach ($cssToCheck as $file) {
|
|
$content = file_get_contents($file);
|
|
$relPath = str_replace($root . '/', '', $file);
|
|
|
|
// Check for unmatched braces
|
|
$openBraces = substr_count($content, '{');
|
|
$closeBraces = substr_count($content, '}');
|
|
if ($openBraces !== $closeBraces) {
|
|
echo " ERROR: {$relPath}: unmatched braces (open={$openBraces}, close={$closeBraces})\n";
|
|
$errors++;
|
|
}
|
|
|
|
// Check for empty rules
|
|
if (preg_match_all('/\{[\s]*\}/', $content, $m)) {
|
|
$count = count($m[0]);
|
|
echo " WARN: {$relPath}: {$count} empty rule(s)\n";
|
|
$warnings++;
|
|
}
|
|
|
|
// Check for !important abuse (more than 10 in one file)
|
|
$importantCount = substr_count($content, '!important');
|
|
if ($importantCount > 10) {
|
|
echo " WARN: {$relPath}: {$importantCount} !important declarations (consider refactoring)\n";
|
|
$warnings++;
|
|
}
|
|
}
|
|
|
|
if ($errors === 0) {
|
|
echo " OK: " . count($cssToCheck) . " CSS file(s) checked\n";
|
|
}
|
|
}
|
|
|
|
// ── Check 2: Image file sizes ───────────────────────────────────────────
|
|
echo "\n--- Image Sizes (max {$maxImageKb}KB) ---\n";
|
|
$imageExts = ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.webp', '*.svg', '*.bmp'];
|
|
$images = [];
|
|
foreach ($imageExts as $ext) {
|
|
$images = array_merge($images, findFiles($srcDir, $ext));
|
|
}
|
|
// Also check root images/ directory
|
|
if (is_dir("{$root}/images")) {
|
|
foreach ($imageExts as $ext) {
|
|
$images = array_merge($images, findFiles("{$root}/images", $ext));
|
|
}
|
|
}
|
|
|
|
$oversized = 0;
|
|
$totalSize = 0;
|
|
foreach ($images as $file) {
|
|
$size = filesize($file);
|
|
$totalSize += $size;
|
|
$relPath = str_replace($root . '/', '', $file);
|
|
$sizeKb = round($size / 1024);
|
|
|
|
if ($sizeKb > $maxImageKb) {
|
|
echo " WARN: {$relPath}: {$sizeKb}KB (exceeds {$maxImageKb}KB limit)\n";
|
|
$oversized++;
|
|
$warnings++;
|
|
}
|
|
}
|
|
|
|
$totalMb = round($totalSize / 1024 / 1024, 1);
|
|
echo " " . count($images) . " image(s), {$totalMb}MB total";
|
|
if ($oversized > 0) {
|
|
echo ", {$oversized} oversized";
|
|
}
|
|
echo "\n";
|
|
|
|
// ── Check 3: Hardcoded URLs in CSS/JS ───────────────────────────────────
|
|
echo "\n--- Hardcoded URLs ---\n";
|
|
$codeFiles = array_merge(
|
|
findFiles($srcDir, '*.css'),
|
|
findFiles($srcDir, '*.js')
|
|
);
|
|
// Exclude minified files
|
|
$codeFiles = array_filter($codeFiles, function($f) {
|
|
return !preg_match('/\.min\.(css|js)$/', $f);
|
|
});
|
|
|
|
$urlPatterns = [
|
|
'/https?:\/\/clarksvillefurs\.com/' => 'hardcoded production URL',
|
|
'/https?:\/\/[a-z]+\.dev\.mokoconsulting\.tech/' => 'hardcoded dev URL',
|
|
'/https?:\/\/localhost/' => 'localhost reference',
|
|
];
|
|
|
|
$urlIssues = 0;
|
|
foreach ($codeFiles as $file) {
|
|
$content = file_get_contents($file);
|
|
$relPath = str_replace($root . '/', '', $file);
|
|
|
|
foreach ($urlPatterns as $pattern => $desc) {
|
|
if (preg_match_all($pattern, $content, $matches)) {
|
|
$count = count($matches[0]);
|
|
echo " WARN: {$relPath}: {$count} {$desc}\n";
|
|
$urlIssues++;
|
|
$warnings++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($urlIssues === 0) {
|
|
echo " OK: No hardcoded URLs found\n";
|
|
}
|
|
|
|
// ── Summary ─────────────────────────────────────────────────────────────
|
|
echo "\n=== Summary ===\n";
|
|
echo "Errors: {$errors}\n";
|
|
echo "Warnings: {$warnings}\n";
|
|
|
|
if ($ghOutput) {
|
|
$ghFile = getenv('GITHUB_OUTPUT');
|
|
if ($ghFile) {
|
|
file_put_contents($ghFile, "lint_errors={$errors}\n", FILE_APPEND);
|
|
file_put_contents($ghFile, "lint_warnings={$warnings}\n", FILE_APPEND);
|
|
file_put_contents($ghFile, "lint_images=" . count($images) . "\n", FILE_APPEND);
|
|
file_put_contents($ghFile, "lint_css=" . count($cssToCheck) . "\n", FILE_APPEND);
|
|
}
|
|
}
|
|
|
|
if ($errors > 0) {
|
|
exit(1);
|
|
}
|
|
if ($strict && $warnings > 0) {
|
|
exit(1);
|
|
}
|
|
exit(0);
|
|
|
|
// ── Helper: recursively find files matching a glob pattern ──────────────
|
|
function findFiles(string $dir, string $pattern): array
|
|
{
|
|
$results = [];
|
|
if (!is_dir($dir)) return $results;
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if (fnmatch($pattern, $file->getFilename())) {
|
|
$results[] = $file->getPathname();
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|