#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: MokoStandards.Scripts.Validate * INGROUP: MokoStandards * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /validate/check_structure.php * BRIEF: Validates required repository directory and file structure */ declare(strict_types=1); require_once __DIR__ . '/../../vendor/autoload.php'; use MokoEnterprise\CliFramework; /** * Validates that the required directories and files exist in the repository root. */ class CheckStructure extends CliFramework { /** @var list Required directory paths (relative to repo root). */ /** @var list Required directory paths — at least one workflow dir must exist. */ private const REQUIRED_DIRS = ['docs', 'scripts']; /** @var list At least one of these workflow directories must exist. */ private const WORKFLOW_DIRS = ['.github/workflows', '.mokogitea/workflows']; /** @var list Required file paths (relative to repo root). */ private const REQUIRED_FILES = ['README.md', 'LICENSE', 'CONTRIBUTING.md', 'SECURITY.md']; /** Directories searched for CHANGELOG.md (case-insensitive), relative to repo root. */ private const CHANGELOG_DIRS = ['', 'src', 'docs']; /** * Configure available arguments. */ protected function configure(): void { $this->setDescription('Validates required repository directory and file structure'); $this->addArgument('--path', 'Repository path to check', '.'); } /** * Run the structure validation. * * @return int Exit code: 0 if everything is present, 1 if anything is missing. */ protected function run(): int { $path = $this->getArgument('--path'); $missingDirs = []; $missingFiles = []; $passed = 0; $failed = 0; $this->section('Checking required directories'); foreach (self::REQUIRED_DIRS as $dir) { if (!is_dir($path . '/' . $dir)) { $missingDirs[] = $dir; $this->status(false, "Directory: {$dir}"); $failed++; } else { $this->status(true, "Directory: {$dir}"); $passed++; } } // At least one workflow directory must exist $hasWorkflowDir = false; foreach (self::WORKFLOW_DIRS as $wfDir) { if (is_dir($path . '/' . $wfDir)) { $hasWorkflowDir = true; $this->status(true, "Directory: {$wfDir}"); $passed++; break; } } if (!$hasWorkflowDir) { $missingDirs[] = implode(' or ', self::WORKFLOW_DIRS); $this->status(false, 'Directory: ' . implode(' or ', self::WORKFLOW_DIRS)); $failed++; } $this->section('Checking required files'); foreach (self::REQUIRED_FILES as $file) { if (!is_file($path . '/' . $file)) { $missingFiles[] = $file; $this->status(false, "File: {$file}"); $failed++; } else { $this->status(true, "File: {$file}"); $passed++; } } // CHANGELOG.md — accepted at root, src/, or docs/ (case-insensitive) $changelogFound = null; foreach (self::CHANGELOG_DIRS as $sub) { $dir = $sub === '' ? $path : $path . '/' . $sub; $entries = is_dir($dir) ? (@scandir($dir) ?: []) : []; foreach ($entries as $entry) { if (strcasecmp($entry, 'CHANGELOG.md') === 0 && is_file($dir . '/' . $entry)) { $changelogFound = ($sub === '' ? '' : $sub . '/') . $entry; break 2; } } } if ($changelogFound !== null) { $this->status(true, "File: CHANGELOG.md (found: {$changelogFound})"); $passed++; } else { $missingFiles[] = 'CHANGELOG.md'; $this->status(false, 'File: CHANGELOG.md (checked root, src/, docs/)'); $failed++; } $this->printSummary($passed, $failed, $this->elapsed()); if (empty($missingDirs) && empty($missingFiles)) { return 0; } return 1; } } $script = new CheckStructure('check_structure', 'Validates required repository directory and file structure'); exit($script->execute());