661f84b9c7
Templates moved from MokoStandards repo: - templates/configs/ (editor, linting, composer configs) - templates/docs/ (required + extra doc templates) - templates/gitea/ (issue templates, CLAUDE.md, copilot-instructions) - templates/github/ (legacy, kept for reference) - templates/joomla/ (Joomla component/module/plugin templates) - templates/makefiles/ (build automation) - templates/schemas/ (JSON/XML schemas) - templates/scripts/ (PHP script templates) - templates/stubs/ (Dolibarr/Joomla stubs) - templates/build/, fonts/, images/, licenses/, security/, web/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
163 lines
6.0 KiB
PHP
163 lines
6.0 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.Templates.Scripts.Validate
|
|
* INGROUP: MokoStandards.Templates
|
|
* REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards
|
|
* PATH: /templates/scripts/validate/validate_structure.php
|
|
* VERSION: 04.06.00
|
|
* BRIEF: Validate a repository structure against MokoStandards requirements
|
|
* NOTE: Deployed to bin/validate_structure.php in governed generic/default repos.
|
|
* Run: php bin/validate_structure.php [--path DIR] [--verbose]
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
// Deployed to bin/validate_structure.php in org repos — vendor/ is one level up (repo root)
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
|
use MokoEnterprise\CliFramework;
|
|
|
|
/**
|
|
* Validates a generic repository structure against MokoStandards requirements.
|
|
*
|
|
* Checks performed:
|
|
* - Required root files present (README.md, CHANGELOG.md, LICENSE, CONTRIBUTING.md,
|
|
* SECURITY.md, .gitignore, .editorconfig, composer.json)
|
|
* - Required directories present (src/, docs/, tests/)
|
|
* - .mokostandards.yml governance attachment present
|
|
* - SPDX-License-Identifier header present in all PHP source files
|
|
* - No tab characters in YAML/JSON config files
|
|
* - No Windows path separators in PHP source
|
|
*/
|
|
class ValidateStructure extends CliFramework
|
|
{
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Validate a repository structure against MokoStandards requirements');
|
|
$this->addArgument('--path', 'Repository root to validate', '.');
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$path = rtrim((string) $this->getArgument('--path'), '/\\');
|
|
$passed = 0;
|
|
$failed = 0;
|
|
|
|
if (!is_dir($path)) {
|
|
$this->error("Path does not exist: {$path}");
|
|
}
|
|
|
|
// ── Required root files ───────────────────────────────────────────
|
|
$this->section('Required root files');
|
|
foreach ([
|
|
'README.md',
|
|
'CHANGELOG.md',
|
|
'LICENSE',
|
|
'CONTRIBUTING.md',
|
|
'SECURITY.md',
|
|
'.gitignore',
|
|
'.editorconfig',
|
|
'composer.json',
|
|
] as $file) {
|
|
$ok = file_exists("{$path}/{$file}");
|
|
$this->status($ok, $file);
|
|
$ok ? $passed++ : $failed++;
|
|
}
|
|
|
|
// ── Governance attachment ─────────────────────────────────────────
|
|
$this->section('MokoStandards governance');
|
|
$mokoFile = file_exists("{$path}/.mokostandards.yml");
|
|
$this->status($mokoFile, '.mokostandards.yml');
|
|
$mokoFile ? $passed++ : $failed++;
|
|
|
|
// ── Required directories ──────────────────────────────────────────
|
|
$this->section('Required directories');
|
|
foreach (['src', 'docs', 'tests'] as $dir) {
|
|
$ok = is_dir("{$path}/{$dir}");
|
|
$this->status($ok, $dir . '/');
|
|
$ok ? $passed++ : $failed++;
|
|
}
|
|
|
|
// ── SPDX headers ──────────────────────────────────────────────────
|
|
$this->section('SPDX-License-Identifier headers');
|
|
$phpFiles = $this->findFiles($path . '/src', '*.php');
|
|
$missingHeader = 0;
|
|
foreach ($phpFiles as $file) {
|
|
$head = file_get_contents($file, false, null, 0, 512) ?: '';
|
|
if (!str_contains($head, 'SPDX-License-Identifier')) {
|
|
$missingHeader++;
|
|
$this->log('WARN', 'Missing SPDX header: ' . substr($file, strlen($path) + 1));
|
|
}
|
|
}
|
|
$ok = $missingHeader === 0;
|
|
$this->status($ok, "All PHP files have SPDX header ({$missingHeader} missing)");
|
|
$ok ? $passed++ : $failed++;
|
|
|
|
// ── No tabs in config files ───────────────────────────────────────
|
|
$this->section('No tab characters in config files');
|
|
$configFiles = array_merge(
|
|
$this->findFiles($path, '*.yml'),
|
|
$this->findFiles($path, '*.yaml'),
|
|
$this->findFiles($path, '*.json')
|
|
);
|
|
$tabFiles = 0;
|
|
foreach ($configFiles as $file) {
|
|
if (str_contains((string) file_get_contents($file), "\t")) {
|
|
$tabFiles++;
|
|
$this->log('WARN', 'Tab found: ' . substr($file, strlen($path) + 1));
|
|
}
|
|
}
|
|
$ok = $tabFiles === 0;
|
|
$this->status($ok, "No tabs in YAML/JSON ({$tabFiles} files affected)");
|
|
$ok ? $passed++ : $failed++;
|
|
|
|
// ── No backslash paths in PHP ─────────────────────────────────────
|
|
$this->section('No Windows path separators in PHP source');
|
|
$phpAll = $this->findFiles($path . '/src', '*.php');
|
|
$bsFiles = 0;
|
|
foreach ($phpAll as $file) {
|
|
if (preg_match('/["\'](?:[A-Za-z]:)?\\\\[^\\\\]/', (string) file_get_contents($file))) {
|
|
$bsFiles++;
|
|
$this->log('WARN', 'Backslash path: ' . substr($file, strlen($path) + 1));
|
|
}
|
|
}
|
|
$ok = $bsFiles === 0;
|
|
$this->status($ok, "No backslash paths ({$bsFiles} files affected)");
|
|
$ok ? $passed++ : $failed++;
|
|
|
|
// ── Summary ───────────────────────────────────────────────────────
|
|
$this->printSummary($passed, $failed, $this->elapsed());
|
|
|
|
return $failed > 0 ? 1 : 0;
|
|
}
|
|
|
|
/** Recursively find files matching a glob pattern under a directory. */
|
|
private function findFiles(string $dir, string $pattern): array
|
|
{
|
|
if (!is_dir($dir)) {
|
|
return [];
|
|
}
|
|
$results = [];
|
|
$iter = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
|
|
);
|
|
foreach ($iter as $file) {
|
|
if ($file->isFile() && fnmatch($pattern, $file->getFilename())) {
|
|
$results[] = $file->getPathname();
|
|
}
|
|
}
|
|
return $results;
|
|
}
|
|
}
|
|
|
|
$script = new ValidateStructure('validate_structure', 'Validate a repository structure against MokoStandards requirements');
|
|
exit($script->execute());
|