feat(cli): version pipeline overhaul #206

Merged
jmiller merged 9 commits from dev into main 2026-05-29 09:52:04 +00:00
9 changed files with 333 additions and 45 deletions
+8 -26
View File
@@ -16,6 +16,10 @@ on:
push:
branches:
- dev
- alpha
- beta
- rc
- 'feature/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -57,29 +61,7 @@ jobs:
- name: Bump version
run: |
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; }
# Propagate to platform manifests with -dev suffix
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch dev --stability dev 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
VERSION="${VERSION}-dev"
# 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] <gitea-actions[bot]@mokoconsulting.tech>"
git push origin dev
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"
+5 -7
View File
@@ -165,9 +165,8 @@ jobs:
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20)
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
MAJOR=$(echo "$VERSION" | cut -d. -f1)
# version_set_platform strips suffixes internally when --stability stable
MAJOR=$(echo "$VERSION" | cut -d. -f1 | sed 's/-.*//')
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
@@ -181,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"
@@ -201,8 +200,7 @@ jobs:
MOKO_API="/tmp/moko-platform-api/cli"
php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
VERSION=$(php ${MOKO_API}/version_read.php --path .)
# Strip any pre-release suffix — stable releases have no suffix
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# version_set_platform handles suffix stripping — just pass clean base version
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Bumped to: ${VERSION}"
@@ -376,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=""
+1 -1
View File
@@ -6,7 +6,7 @@ DEFGROUP: MokoStandards.Root
INGROUP: MokoStandards
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /README.md
VERSION: 09.05.00
VERSION: 09.05.04
BRIEF: Project overview and documentation
-->
+1 -1
View File
@@ -392,7 +392,7 @@ class JoomlaRelease extends CliFramework
foreach ($release['assets'] ?? [] as $asset) {
if ($asset['name'] === $fileName) {
$this->api->delete("/repos/{$repo}/releases/assets/{$asset['id']}");
$this->api->delete("/repos/{$repo}/releases/{$release['id']}/assets/{$asset['id']}");
}
}
+137
View File
@@ -0,0 +1,137 @@
#!/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/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] <gitea-actions[bot]@mokoconsulting.tech>\"");
$pushResult = @shell_exec("cd " . escapeshellarg($root) . " && git push origin " . escapeshellarg($branch) . " 2>&1");
echo $pushResult ?? '';
echo "Bumped to {$displayVersion}\n";
exit(0);
+79 -4
View File
@@ -31,7 +31,7 @@ $mokoManifest = "{$root}/.mokogitea/manifest.xml";
$mokoContent = '';
if (file_exists($mokoManifest)) {
$mokoContent = file_get_contents($mokoManifest);
if (preg_match('|<version>(\d{2}\.\d{2}\.\d{2})(?:-((?:(?:dev|alpha|beta|rc)-?)+))?</version>|', $mokoContent, $m)) {
if (preg_match('#<version>(\d{2}\.\d{2}\.\d{2})(?:-((?:(?:dev|alpha|beta|rc)-?)+))?</version>#', $mokoContent, $m)) {
$mokoVersion = $m[1];
$mokoSuffix = isset($m[2]) ? $m[2] : '';
}
@@ -64,7 +64,7 @@ foreach ($manifestFiles as $xmlFile) {
if (strpos($xmlContent, '<extension') === false && strpos($xmlContent, '<version>') === false) {
continue;
}
if (preg_match('|<version>(\d{2}\.\d{2}\.\d{2})((?:-(?:dev|alpha|beta|rc))+)?</version>|', $xmlContent, $xm)) {
if (preg_match('#<version>(\d{2}\.\d{2}\.\d{2})((?:-(?:dev|alpha|beta|rc))+)?</version>#', $xmlContent, $xm)) {
$candidate = $xm[1];
if ($manifestVersion === null || version_compare($candidate, $manifestVersion, '>')) {
$manifestVersion = $candidate;
@@ -120,7 +120,7 @@ $newFull = $new;
// -- Update .mokogitea/manifest.xml (canonical — preserves suffix) --
if (file_exists($mokoManifest) && !empty($mokoContent)) {
$updated = preg_replace(
'|<version>\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?</version>|',
'#<version>\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?</version>#',
"<version>{$newFull}</version>",
$mokoContent,
1
@@ -160,7 +160,7 @@ foreach ($xmlPatterns as $pattern) {
continue;
}
$newContent = preg_replace(
'|<version>\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?</version>|',
'#<version>\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?</version>#',
"<version>{$newFull}</version>",
$content
);
@@ -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);
+99 -3
View File
@@ -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",
@@ -59,7 +99,7 @@ foreach ($xmlGlobs as $glob) {
$xmlContent = file_get_contents($file);
if (strpos($xmlContent, '<extension') === false) continue;
if (preg_match('|<version>(\d{2}\.\d{2}\.\d{2})(?:(?:-(?:dev|alpha|beta|rc))+)?</version>|', $xmlContent, $xm)) {
if (preg_match('#<version>(\d{2}\.\d{2}\.\d{2})(?:(?:-(?:dev|alpha|beta|rc))+)?</version>#', $xmlContent, $xm)) {
$relPath = str_replace($root . '/', '', $file);
$relPath = str_replace($root . '\\', '', $relPath);
$versions[$relPath] = $xm[1];
@@ -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(
'#<version>\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?</version>#',
"<version>{$highestVersion}</version>",
$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(
'|<version>[^<]*</version>|',
'#<version>[^<]*</version>#',
"<version>{$highestVersion}</version>",
$content
);
+2 -2
View File
@@ -66,7 +66,7 @@ foreach ($manifestFiles as $xmlFile) {
if (strpos($xmlContent, '<extension') === false && strpos($xmlContent, '<version>') === false) {
continue;
}
if (preg_match('|<version>(\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?)</version>|', $xmlContent, $xm)) {
if (preg_match('#<version>(\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?)</version>#', $xmlContent, $xm)) {
$candidate = $xm[1];
$candidateBase = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $candidate);
$currentBase = $manifestVersion ? preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $manifestVersion) : null;
@@ -119,7 +119,7 @@ if ($version === null) {
// -- Backfill: if manifest.xml exists but lacks <version>, insert it --
if (file_exists($mokoManifest)) {
$content = file_get_contents($mokoManifest);
if (!preg_match('|<version>\d{2}\.\d{2}\.\d{2}((?:-(?:dev|alpha|beta|rc))+)?</version>|', $content)) {
if (!preg_match('#<version>\d{2}\.\d{2}\.\d{2}((?:-(?:dev|alpha|beta|rc))+)?</version>#', $content)) {
if (strpos($content, '<license') !== false) {
$content = preg_replace(
'|(\s*<license)|',
+1 -1
View File
@@ -263,7 +263,7 @@ class ApiClient
*/
public function delete(string $endpoint, ?array $body = null): array
{
return $this->request('DELETE', $endpoint, $body);
return $this->request('DELETE', $endpoint, $body ?? []);
}
/**