Public Access
feat: add wiki health check and GitHub wiki mirror sync
- validate/check_wiki_health.php — checks Home page, MokoStandards ref - scripts/sync-wikis-to-github.sh — mirrors Gitea wikis to GitHub - MCP tools: standards_check_wiki, standards_sync_wikis Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -399,6 +399,42 @@ server.tool(
|
||||
},
|
||||
);
|
||||
|
||||
// ── Wiki Health & Sync ──────────────────────────────────────────────────
|
||||
|
||||
server.tool(
|
||||
'standards_check_wiki',
|
||||
'Check wiki health for a repository — verifies Home page exists, has MokoStandards reference, and pages are indexed',
|
||||
{
|
||||
repo_path: z.string().describe('Path to the repository'),
|
||||
},
|
||||
async ({ repo_path }) => {
|
||||
const result = await runner.runValidate('check_wiki_health.php', ['--path', repo_path, '--json']);
|
||||
return textResult(result.stdout || result.stderr);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'standards_sync_wikis',
|
||||
'Sync Gitea wiki repos to their GitHub mirrors. Optionally filter by repo name.',
|
||||
{
|
||||
repo: z.string().optional().describe('Filter to a specific repo name (e.g. "MokoOnyx"). Omit to sync all.'),
|
||||
},
|
||||
async ({ repo }) => {
|
||||
const scriptPath = resolve(config.apiPath, 'scripts', 'sync-wikis-to-github.sh');
|
||||
const args = repo ? [scriptPath, repo] : [scriptPath];
|
||||
const { execFile } = await import('node:child_process');
|
||||
return new Promise((resolvePromise) => {
|
||||
execFile('bash', args, {
|
||||
timeout: 120_000,
|
||||
maxBuffer: 5 * 1024 * 1024,
|
||||
env: { ...process.env, GH_TOKEN: process.env.GH_TOKEN ?? '' },
|
||||
}, (err, stdout, stderr) => {
|
||||
resolvePromise(textResult(stdout || stderr || err?.message || 'No output'));
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// ── Server Info ─────────────────────────────────────────────────────────
|
||||
|
||||
server.tool(
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# sync-wikis-to-github.sh — Mirror Gitea wiki repos to their GitHub counterparts
|
||||
#
|
||||
# Clones each Gitea wiki repo, adds the GitHub wiki as a remote, and force-pushes.
|
||||
# Requires GH_TOKEN env var or ~/.github-token for authentication.
|
||||
#
|
||||
# Usage:
|
||||
# sync-wikis-to-github.sh [repo-name] # sync one repo
|
||||
# sync-wikis-to-github.sh # sync all configured repos
|
||||
#
|
||||
# Requires: git, curl
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||
GITHUB_ORG="${GITHUB_ORG:-mokoconsulting-tech}"
|
||||
GH_TOKEN="${GH_TOKEN:-$(cat ~/.github-token 2>/dev/null || echo "")}"
|
||||
GITEA_TOKEN="${GITEA_TOKEN:-$(cat ~/.gitea-token 2>/dev/null || echo "")}"
|
||||
TMPDIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*"; }
|
||||
|
||||
if [[ -z "$GH_TOKEN" ]]; then
|
||||
log "ERROR: GH_TOKEN not set. Export it or create ~/.github-token"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Repo mappings: gitea_owner/repo → github_repo (same name unless overridden)
|
||||
declare -A REPOS=(
|
||||
["MokoConsulting/moko-platform"]="moko-platform"
|
||||
["MokoConsulting/MokoOnyx"]="MokoOnyx"
|
||||
["MokoConsulting/MokoWaaS"]="MokoWaaS"
|
||||
["MokoConsulting/monitor-mcp"]="monitor-mcp"
|
||||
["MokoConsulting/deploy-mcp"]="deploy-mcp"
|
||||
["MokoConsulting/ssh-mcp"]="ssh-mcp"
|
||||
["MokoConsulting/backup-mcp"]="backup-mcp"
|
||||
["MokoConsulting/joomla-api-mcp"]="joomla-api-mcp"
|
||||
["MokoConsulting/Template-Client-WaaS"]="Template-Client-WaaS"
|
||||
["ClarksvilleFurs/client-waas-clarksvillefurs"]="client-waas-clarksvillefurs"
|
||||
)
|
||||
|
||||
FILTER="${1:-}"
|
||||
SYNCED=0
|
||||
FAILED=0
|
||||
|
||||
for GITEA_REPO in "${!REPOS[@]}"; do
|
||||
GH_REPO="${REPOS[$GITEA_REPO]}"
|
||||
|
||||
# Filter to specific repo if requested
|
||||
if [[ -n "$FILTER" && "$GH_REPO" != "$FILTER" && "$GITEA_REPO" != *"$FILTER"* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
log "Syncing wiki: ${GITEA_REPO} → github/${GH_REPO}"
|
||||
|
||||
WORK="${TMPDIR}/${GH_REPO}.wiki"
|
||||
|
||||
# Clone Gitea wiki
|
||||
GITEA_WIKI_URL="${GITEA_URL}/${GITEA_REPO}.wiki.git"
|
||||
if ! git clone --quiet "$GITEA_WIKI_URL" "$WORK" 2>/dev/null; then
|
||||
log " SKIP: no wiki on Gitea"
|
||||
continue
|
||||
fi
|
||||
|
||||
cd "$WORK"
|
||||
|
||||
# Add GitHub wiki remote
|
||||
GH_WIKI_URL="https://${GH_TOKEN}@github.com/${GITHUB_ORG}/${GH_REPO}.wiki.git"
|
||||
|
||||
# Ensure GitHub wiki exists (create initial page if needed)
|
||||
WIKI_CHECK=$(curl -sf -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_ORG}/${GH_REPO}" 2>/dev/null | \
|
||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('has_wiki', False))" 2>/dev/null || echo "False")
|
||||
|
||||
if [[ "$WIKI_CHECK" != "True" ]]; then
|
||||
log " SKIP: GitHub wiki not enabled for ${GH_REPO}"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
git remote add github "$GH_WIKI_URL" 2>/dev/null || git remote set-url github "$GH_WIKI_URL"
|
||||
|
||||
# Force push to GitHub wiki
|
||||
if git push --force --quiet github master:master 2>/dev/null || git push --force --quiet github main:master 2>/dev/null; then
|
||||
SYNCED=$((SYNCED + 1))
|
||||
log " OK: synced"
|
||||
else
|
||||
FAILED=$((FAILED + 1))
|
||||
log " FAIL: push failed"
|
||||
fi
|
||||
|
||||
cd "$TMPDIR"
|
||||
done
|
||||
|
||||
log "Done. Synced: ${SYNCED}, Failed: ${FAILED}"
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: MokoStandards.Validate
|
||||
* INGROUP: MokoStandards
|
||||
* PATH: /validate/check_wiki_health.php
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: Validate wiki health — checks Home page exists, has MokoStandards link, pages are indexed
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoEnterprise\CLIApp;
|
||||
|
||||
class CheckWikiHealth extends CLIApp
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('check-wiki-health', 'Validate wiki health for a repository', '01.00.00');
|
||||
}
|
||||
|
||||
protected function setupArguments(): array
|
||||
{
|
||||
return [
|
||||
'path:' => 'Repository path (default: current directory)',
|
||||
'gitea-url:' => 'Gitea base URL (default: https://git.mokoconsulting.tech)',
|
||||
'token:' => 'Gitea API token (or set GITEA_TOKEN env var)',
|
||||
];
|
||||
}
|
||||
|
||||
protected function run(): int
|
||||
{
|
||||
$repoPath = realpath($this->getOption('path', '.')) ?: '.';
|
||||
$giteaUrl = $this->getOption('gitea-url', 'https://git.mokoconsulting.tech');
|
||||
$token = $this->getOption('token', getenv('GITEA_TOKEN') ?: '');
|
||||
|
||||
// Detect repo owner/name from git config
|
||||
$configFile = $repoPath . '/.git/config';
|
||||
$remote = '';
|
||||
if (is_file($configFile)) {
|
||||
$config = file_get_contents($configFile);
|
||||
if (preg_match('/url\s*=\s*(.+)/', $config, $m)) {
|
||||
$remote = trim($m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($remote)) {
|
||||
$this->log('Cannot determine git remote — skipping wiki check', 'WARNING');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse owner/repo from remote URL
|
||||
if (preg_match('#[:/]([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) {
|
||||
$owner = $m[1];
|
||||
$repo = $m[2];
|
||||
} else {
|
||||
$this->log("Cannot parse owner/repo from remote: {$remote}", 'WARNING');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->log("Checking wiki: {$owner}/{$repo}");
|
||||
$issues = 0;
|
||||
|
||||
// Check wiki pages via API
|
||||
$apiUrl = "{$giteaUrl}/api/v1/repos/{$owner}/{$repo}/wiki/pages";
|
||||
$headers = $token ? ["Authorization: token {$token}"] : [];
|
||||
$pages = $this->apiGet($apiUrl, $headers);
|
||||
|
||||
if ($pages === null) {
|
||||
$this->log(' No wiki found or API error', 'WARNING');
|
||||
$issues++;
|
||||
if ($this->jsonOutput) {
|
||||
echo json_encode(['status' => 'no_wiki', 'issues' => $issues]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
$pageCount = count($pages);
|
||||
$this->log(" Found {$pageCount} wiki page(s)");
|
||||
|
||||
// Check Home exists
|
||||
$hasHome = false;
|
||||
$pageTitles = [];
|
||||
foreach ($pages as $page) {
|
||||
$title = $page['title'] ?? '';
|
||||
$pageTitles[] = $title;
|
||||
if (strtolower($title) === 'home') {
|
||||
$hasHome = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasHome) {
|
||||
$this->log(' FAIL: No Home page', 'ERROR');
|
||||
$issues++;
|
||||
} else {
|
||||
$this->log(' OK: Home page exists');
|
||||
}
|
||||
|
||||
// Check Home has MokoStandards link
|
||||
if ($hasHome) {
|
||||
$homeUrl = "{$giteaUrl}/api/v1/repos/{$owner}/{$repo}/wiki/page/Home";
|
||||
$home = $this->apiGet($homeUrl, $headers);
|
||||
if ($home) {
|
||||
$content = base64_decode($home['content_base64'] ?? '');
|
||||
if (stripos($content, 'moko-platform/wiki') !== false || stripos($content, 'MokoStandards') !== false) {
|
||||
$this->log(' OK: Has MokoStandards reference');
|
||||
} else {
|
||||
$this->log(' WARN: Home page missing MokoStandards reference', 'WARNING');
|
||||
$issues++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->jsonOutput) {
|
||||
echo json_encode([
|
||||
'repo' => "{$owner}/{$repo}",
|
||||
'pages' => $pageCount,
|
||||
'has_home' => $hasHome,
|
||||
'issues' => $issues,
|
||||
'page_titles' => $pageTitles,
|
||||
], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
return $issues > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
private function apiGet(string $url, array $headers = []): ?array
|
||||
{
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => array_merge(['Accept: application/json'], $headers),
|
||||
'timeout' => 10,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
'ssl' => ['verify_peer' => false],
|
||||
]);
|
||||
|
||||
$response = @file_get_contents($url, false, $ctx);
|
||||
if ($response === false) return null;
|
||||
|
||||
$data = json_decode($response, true);
|
||||
return is_array($data) ? $data : null;
|
||||
}
|
||||
}
|
||||
|
||||
(new CheckWikiHealth())->execute();
|
||||
Reference in New Issue
Block a user