From db06dc31cc5cb12ae9ca8d0987d16bb30f23a1b8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 29 May 2026 04:50:22 -0500 Subject: [PATCH] =?UTF-8?q?feat(cli):=20version=20pipeline=20overhaul=20?= =?UTF-8?q?=E2=80=94=20multi-branch=20stability,=20generic=20file=20scanni?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: version_bump.php — scan CHANGELOG.md and all text files for VERSION: patterns Phase 2: version_check.php — check manifest.xml, package.json, pyproject.toml, CHANGELOG Phase 3: version_auto_bump.php — new CLI tool replacing inline workflow bash Phase 4: auto-release.yml — replace python3 calls with PHP Phase 5: auto-bump.yml — slim to single CLI call, support all branch types Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/auto-bump.yml | 44 +-------- .mokogitea/workflows/auto-release.yml | 4 +- cli/version_auto_bump.php | 137 ++++++++++++++++++++++++++ cli/version_bump.php | 75 ++++++++++++++ cli/version_check.php | 100 ++++++++++++++++++- 5 files changed, 316 insertions(+), 44 deletions(-) create mode 100644 cli/version_auto_bump.php diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index fb99033..8673649 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -61,43 +61,7 @@ jobs: - name: Bump version run: | - BRANCH="${GITHUB_REF_NAME}" - - # Map branch name to stability suffix - # Feature branches (feature/*) use dev stability - case "$BRANCH" in - dev) STABILITY="dev" ;; - alpha) STABILITY="alpha" ;; - beta) STABILITY="beta" ;; - rc) STABILITY="rc" ;; - feature/*) STABILITY="dev" ;; - *) STABILITY="dev" ;; - esac - - BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true - echo "$BUMP" - - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true - [ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; } - - # version_set_platform strips existing suffixes internally - # Propagate to platform manifests with stability suffix - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - VERSION="${VERSION}-${STABILITY}" - - # Commit if anything changed - if git diff --quiet && git diff --cached --quiet; then - echo "No version changes to commit" - exit 0 - fi - - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " - git push origin "$BRANCH" - echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY + php ${MOKO_CLI}/version_auto_bump.php \ + --path . --branch "${GITHUB_REF_NAME}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 871ecde..618cb76 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -180,7 +180,7 @@ jobs: API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}") - RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) + RC_ID=$(echo "$RC_JSON" | php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo \$d['id'] ?? '';" 2>/dev/null || true) if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then echo "promote=true" >> "$GITHUB_OUTPUT" @@ -374,7 +374,7 @@ jobs: API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ - python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \ + php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo base64_decode(\$d['content'] ?? '');" \ > updates.xml 2>/dev/null || true SHA_FLAG="" diff --git a/cli/version_auto_bump.php b/cli/version_auto_bump.php new file mode 100644 index 0000000..ecb4450 --- /dev/null +++ b/cli/version_auto_bump.php @@ -0,0 +1,137 @@ +#!/usr/bin/env php + + * + * 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/version_auto_bump.php + * VERSION: 01.00.00 + * BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash + * + * Usage: + * php version_auto_bump.php --path . --branch dev + * php version_auto_bump.php --path . --branch feature/my-feature --token TOKEN --repo-url URL + * php version_auto_bump.php --path . --branch alpha --dry-run + */ + +declare(strict_types=1); + +$path = '.'; +$branch = null; +$token = ''; +$repoUrl = ''; +$dryRun = false; + +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--branch' && isset($argv[$i + 1])) $branch = $argv[$i + 1]; + if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1]; + if ($arg === '--repo-url' && isset($argv[$i + 1])) $repoUrl = $argv[$i + 1]; + if ($arg === '--dry-run') $dryRun = true; +} + +// Auto-detect branch from git or CI env +if ($branch === null) { + $branch = getenv('GITHUB_REF_NAME') ?: trim((string) @shell_exec('git rev-parse --abbrev-ref HEAD 2>/dev/null')); + if (empty($branch) || $branch === 'HEAD') { + fwrite(STDERR, "Cannot detect branch — pass --branch\n"); + exit(1); + } +} + +// Map branch to stability suffix +$stabilityMap = [ + 'dev' => 'dev', + 'alpha' => 'alpha', + 'beta' => 'beta', + 'rc' => 'rc', +]; + +if (array_key_exists($branch, $stabilityMap)) { + $stability = $stabilityMap[$branch]; +} elseif (str_starts_with($branch, 'feature/')) { + $stability = 'dev'; +} else { + $stability = 'dev'; +} + +$cli = __DIR__; +$php = PHP_BINARY; + +// Step 1: Patch bump +$bumpOutput = []; +exec("{$php} {$cli}/version_bump.php --path " . escapeshellarg($path) . " 2>&1", $bumpOutput, $bumpRc); +foreach ($bumpOutput as $line) { + echo "{$line}\n"; +} + +// Step 2: Read version +$versionOutput = []; +exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " 2>&1", $versionOutput, $versionRc); +$version = trim($versionOutput[0] ?? ''); + +if (empty($version)) { + echo "No version found — skipping\n"; + exit(0); +} + +echo "Version: {$version} | Branch: {$branch} | Stability: {$stability}\n"; + +// Step 3: Set platform version with stability suffix +exec("{$php} {$cli}/version_set_platform.php --path " . escapeshellarg($path) + . " --version " . escapeshellarg($version) + . " --branch " . escapeshellarg($branch) + . " --stability " . escapeshellarg($stability) . " 2>&1", $setPlatOutput); +foreach ($setPlatOutput as $line) { + echo "{$line}\n"; +} + +// Step 4: Version consistency check and fix +exec("{$php} {$cli}/version_check.php --path " . escapeshellarg($path) . " --fix 2>&1", $checkOutput); + +// Re-read version (now includes suffix from version_set_platform) +$suffixMap = [ + 'dev' => '-dev', + 'alpha' => '-alpha', + 'beta' => '-beta', + 'rc' => '-rc', +]; +$displayVersion = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $version) . ($suffixMap[$stability] ?? ''); + +if ($dryRun) { + echo "[DRY-RUN] Would commit and push {$displayVersion} to {$branch}\n"; + exit(0); +} + +// Step 5: Git commit and push +$root = realpath($path) ?: $path; + +// Check if anything changed +$diffStatus = trim((string) @shell_exec("cd " . escapeshellarg($root) . " && git diff --quiet && git diff --cached --quiet 2>&1 && echo clean || echo dirty")); +if ($diffStatus === 'clean') { + echo "No version changes to commit\n"; + exit(0); +} + +// Configure git +@shell_exec("cd " . escapeshellarg($root) . " && git config --local user.email \"gitea-actions[bot]@mokoconsulting.tech\""); +@shell_exec("cd " . escapeshellarg($root) . " && git config --local user.name \"gitea-actions[bot]\""); + +if (!empty($repoUrl)) { + @shell_exec("cd " . escapeshellarg($root) . " && git remote set-url origin " . escapeshellarg($repoUrl)); +} + +@shell_exec("cd " . escapeshellarg($root) . " && git add -A"); +$commitMsg = "chore(version): auto-bump patch {$displayVersion} [skip ci]"; +@shell_exec("cd " . escapeshellarg($root) . " && git commit -m " . escapeshellarg($commitMsg) + . " --author=\"gitea-actions[bot] \""); + +$pushResult = @shell_exec("cd " . escapeshellarg($root) . " && git push origin " . escapeshellarg($branch) . " 2>&1"); +echo $pushResult ?? ''; + +echo "Bumped to {$displayVersion}\n"; +exit(0); diff --git a/cli/version_bump.php b/cli/version_bump.php index 33f5775..afe3713 100644 --- a/cli/version_bump.php +++ b/cli/version_bump.php @@ -205,5 +205,80 @@ if (file_exists($pyprojectFile)) { } } +// -- Update CHANGELOG.md -- +$changelogFile = "{$root}/CHANGELOG.md"; +if (file_exists($changelogFile)) { + $clContent = file_get_contents($changelogFile); + $updatedCl = preg_replace( + '/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?/m', + '${1}' . $newFull, + $clContent + ); + if ($updatedCl !== null && $updatedCl !== $clContent) { + file_put_contents($changelogFile, $updatedCl); + fwrite(STDERR, "Updated CHANGELOG.md\n"); + } +} + +// -- Generic VERSION: pattern scan across all text files -- +$scanExtensions = ['php', 'yml', 'yaml', 'md', 'txt', 'xml', 'sh', 'toml', 'ini', 'css', 'js']; +$excludeDirs = ['.git', 'vendor', 'node_modules', 'build', 'dist', '.claude']; +$versionPattern = '/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?/m'; + +$directory = new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS); +$filter = new RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) use ($excludeDirs) { + if ($current->isDir() && in_array($current->getFilename(), $excludeDirs, true)) { + return false; + } + return true; +}); +$iterator = new RecursiveIteratorIterator($filter); + +$genericUpdated = []; +foreach ($iterator as $fileInfo) { + if ($fileInfo->isDir()) { + continue; + } + + $ext = strtolower($fileInfo->getExtension()); + if (!in_array($ext, $scanExtensions, true)) { + continue; + } + + $filePath = $fileInfo->getPathname(); + + // Skip files already handled above + $relPath = str_replace([$root . '/', $root . '\\'], '', $filePath); + if (in_array($relPath, ['README.md', 'CHANGELOG.md', 'package.json', 'pyproject.toml'], true)) { + continue; + } + if (in_array($relPath, $updatedFiles ?? [], true)) { + continue; + } + if (strpos($relPath, '.mokogitea/manifest.xml') !== false) { + continue; + } + + $content = @file_get_contents($filePath); + if ($content === false) { + continue; + } + + // Skip synced files — they have their own version managed by their source repo + if (preg_match('/^#\s*REPO:\s*https?:\/\//m', $content)) { + continue; + } + + $updated = preg_replace($versionPattern, '${1}' . $newFull, $content); + if ($updated !== null && $updated !== $content) { + file_put_contents($filePath, $updated); + $genericUpdated[] = $relPath; + } +} + +if (!empty($genericUpdated)) { + fwrite(STDERR, "Updated VERSION: in " . count($genericUpdated) . " file(s): " . implode(', ', $genericUpdated) . "\n"); +} + echo "{$old} -> {$newFull}\n"; exit(0); diff --git a/cli/version_check.php b/cli/version_check.php index 0c49c64..1cc3e01 100644 --- a/cli/version_check.php +++ b/cli/version_check.php @@ -34,6 +34,19 @@ $root = realpath($path) ?: $path; $errors = 0; $versions = []; +// ── Read .mokogitea/manifest.xml (canonical) ──────────────────────────────── +$mokoManifest = "{$root}/.mokogitea/manifest.xml"; +if (file_exists($mokoManifest)) { + $xml = @simplexml_load_file($mokoManifest); + if ($xml !== false) { + $v = (string)($xml->identity->version ?? ''); + $base = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $v); + if (preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $base)) { + $versions['.mokogitea/manifest.xml'] = $base; + } + } +} + // ── Read README.md version ─────────────────────────────────────────────────── $readme = "{$root}/README.md"; if (file_exists($readme)) { @@ -43,6 +56,33 @@ if (file_exists($readme)) { } } +// ── Read CHANGELOG.md version ─────────────────────────────────────────────── +$changelog = "{$root}/CHANGELOG.md"; +if (file_exists($changelog)) { + $content = file_get_contents($changelog); + if (preg_match('/VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) { + $versions['CHANGELOG.md'] = $m[1]; + } +} + +// ── Read package.json version ─────────────────────────────────────────────── +$packageJson = "{$root}/package.json"; +if (file_exists($packageJson)) { + $pkg = json_decode(file_get_contents($packageJson), true); + if (isset($pkg['version']) && preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $pkg['version'])) { + $versions['package.json'] = $pkg['version']; + } +} + +// ── Read pyproject.toml version ───────────────────────────────────────────── +$pyproject = "{$root}/pyproject.toml"; +if (file_exists($pyproject)) { + $content = file_get_contents($pyproject); + if (preg_match('/^version\s*=\s*"(\d{2}\.\d{2}\.\d{2})"/m', $content, $m)) { + $versions['pyproject.toml'] = $m[1]; + } +} + // ── Read manifest XML versions ─────────────────────────────────────────────── $xmlGlobs = [ "{$root}/src/pkg_*.xml", @@ -111,9 +151,65 @@ if (count($uniqueVersions) === 1) { echo " Fixed: README.md -> {$highestVersion}\n"; } + // Fix .mokogitea/manifest.xml + if (isset($versions['.mokogitea/manifest.xml']) && $versions['.mokogitea/manifest.xml'] !== $highestVersion) { + $content = file_get_contents($mokoManifest); + $updated = preg_replace( + '#\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?#', + "{$highestVersion}", + $content + ); + if ($updated !== null) { + file_put_contents($mokoManifest, $updated); + } + echo " Fixed: .mokogitea/manifest.xml -> {$highestVersion}\n"; + } + + // Fix CHANGELOG.md + if (isset($versions['CHANGELOG.md']) && $versions['CHANGELOG.md'] !== $highestVersion) { + $content = file_get_contents($changelog); + $updated = preg_replace( + '/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?/m', + '${1}' . $highestVersion, + $content + ); + if ($updated !== null) { + file_put_contents($changelog, $updated); + } + echo " Fixed: CHANGELOG.md -> {$highestVersion}\n"; + } + + // Fix package.json + if (isset($versions['package.json']) && $versions['package.json'] !== $highestVersion) { + $content = file_get_contents($packageJson); + $updated = preg_replace( + '/("version"\s*:\s*")\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m', + '${1}' . $highestVersion . '${2}', + $content + ); + if ($updated !== null) { + file_put_contents($packageJson, $updated); + } + echo " Fixed: package.json -> {$highestVersion}\n"; + } + + // Fix pyproject.toml + if (isset($versions['pyproject.toml']) && $versions['pyproject.toml'] !== $highestVersion) { + $content = file_get_contents($pyproject); + $updated = preg_replace( + '/^(version\s*=\s*")\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m', + '${1}' . $highestVersion . '${2}', + $content + ); + if ($updated !== null) { + file_put_contents($pyproject, $updated); + } + echo " Fixed: pyproject.toml -> {$highestVersion}\n"; + } + // Fix XML manifests foreach ($versions as $source => $ver) { - if ($source === 'README.md') continue; + if (in_array($source, ['README.md', 'CHANGELOG.md', '.mokogitea/manifest.xml', 'package.json', 'pyproject.toml'], true)) continue; if ($ver === $highestVersion) continue; $file = "{$root}/{$source}"; @@ -121,7 +217,7 @@ if (count($uniqueVersions) === 1) { $content = file_get_contents($file); $updated = preg_replace( - '|[^<]*|', + '#[^<]*#', "{$highestVersion}", $content );