From 6fe7597da05f1b9a6dc5785499df6ebba67cb680 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 30 May 2026 03:38:53 +0000 Subject: [PATCH 1/3] chore(release): build 09.09.00 [skip ci] --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a0167..0a7fed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ BRIEF: Release changelog # Changelog ## [Unreleased] +## [09.09.00] --- 2026-05-30 + ## [09.09.00] --- 2026-05-29 ## [09.08.00] --- 2026-05-29 @@ -19,5 +21,3 @@ BRIEF: Release changelog ## [09.07.00] --- 2026-05-29 ## [09.06.00] --- 2026-05-29 - -## [09.05.00] --- 2026-05-29 -- 2.52.0 From c24050c7e36ee99120cba120d107966118777289 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 30 May 2026 00:48:00 -0500 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20release=20pipeline=20rework=20?= =?UTF-8?q?=E2=80=94=20independent=20streams,=20CLI-driven=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - release_publish.php: new CLI — publishes release + copies for all lesser streams, updates all updates.xml streams, syncs to all branches - updates_xml_build.php: write single channel only (no cascade) - release_cascade.php: deprecated (no-op) - updates_xml_sync.php: add --all flag for auto-discovery - auto-bump.yml: remove alpha/beta/rc triggers, add patch/* - auto-release.yml: thin CLI wrappers for promote-rc and release jobs - version_auto_bump.php: all branches get patch bumps, add patch/* Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/auto-bump.yml | 4 +- .mokogitea/workflows/auto-release.yml | 148 +++----------- cli/release_cascade.php | 197 +----------------- cli/release_publish.php | 275 ++++++++++++++++++++++++++ cli/updates_xml_build.php | 103 +++------- cli/updates_xml_sync.php | 34 +++- cli/version_auto_bump.php | 6 +- 7 files changed, 369 insertions(+), 398 deletions(-) create mode 100644 cli/release_publish.php diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index a397a9e..dee20d6 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -16,10 +16,8 @@ on: push: branches: - dev - - alpha - - beta - - rc - 'feature/**' + - 'patch/**' env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 5c16f42..663c4aa 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -82,71 +82,33 @@ jobs: cd /tmp/moko-platform-api composer install --no-dev --no-interaction --quiet - - name: Rename source branch to rc + - name: Rename branch to rc run: | - SOURCE_BRANCH="${{ github.event.pull_request.head.ref || 'dev' }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - PR_NUM="${{ github.event.pull_request.number }}" php /tmp/moko-platform-api/cli/branch_rename.php \ - --from "$SOURCE_BRANCH" --to rc \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" \ - --pr "$PR_NUM" + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" - - name: Set RC version on renamed branch + - name: Checkout rc and configure git run: | - # Checkout the new rc branch git fetch origin rc git checkout rc - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - MOKO_CLI="/tmp/moko-platform-api/cli" + 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" - VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true - [ -z "$VERSION" ] && { echo "No version — skipping"; exit 0; } - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch rc --stability rc 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - if ! git diff --quiet || ! git diff --cached --quiet; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add -A - git commit -m "chore(version): set RC stability suffix [skip ci]" \ - --author="gitea-actions[bot] " - git push origin rc - fi - - - name: Build RC release + - name: Publish RC release run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - MOKO_CLI="/tmp/moko-platform-api/cli" - VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true - - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "release-candidate" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch rc 2>&1 || true - - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "release-candidate" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp 2>&1 || true - - - name: Cascade lesser channels - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability release-candidate \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" - name: Summary if: always() run: | echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Draft PR opened — branch renamed to rc, RC release built" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── release: @@ -390,64 +352,14 @@ jobs: echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY # -- STEP 8: Build packages and upload to release ---------------------------- - - name: "Step 8: Build package and upload" - id: package + - name: "Publish stable release (+ copies for all lesser streams)" if: >- steps.version.outputs.skip != 'true' && steps.rc.outputs.promote != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_package.php \ - --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - # -- STEP 5: Write update stream (after build so SHA-256 is available) ----- - - name: "Step 5: Write update stream" - if: steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - # Fetch latest updates.xml from main so preserve logic has current channels - GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ - "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ - php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo base64_decode(\$d['content'] ?? '');" \ - > updates.xml 2>/dev/null || rm -f updates.xml - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php /tmp/moko-platform-api/cli/updates_xml_build.php \ - --path . --version "${VERSION}" --stability stable \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} --github-output - - # Commit updates.xml if changed - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " - git push origin HEAD:refs/heads/main 2>&1 || true - fi - - # -- STEP 8b: Update release description with changelog ---------------------- - - name: "Step 8b: Update release body" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - php /tmp/moko-platform-api/cli/release_body_update.php \ - --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - 2>&1 || true - echo "Release body updated" >> $GITHUB_STEP_SUMMARY + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability stable --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - name: "Step 9: Mirror release to GitHub" @@ -484,33 +396,17 @@ jobs: && echo "main branch pushed to GitHub mirror" \ || echo "WARNING: GitHub mirror push failed" - # -- Clean up lesser pre-releases (cascade) --------------------------------- - # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev - - name: "Delete lesser pre-release channels" - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability stable \ - --version "${VERSION}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" 2>/dev/null || true - - - name: "Step 11: Clean up pre-release branches and recreate dev from main" + - name: "Step 11: Delete rc branch and recreate dev from main" if: steps.version.outputs.skip != 'true' continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - # Delete ephemeral pre-release branches (rc, alpha, beta) - for EPHEMERAL in rc alpha beta; do - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/${EPHEMERAL}" 2>/dev/null \ - && echo "Deleted ${EPHEMERAL} branch" \ - || echo "${EPHEMERAL} branch not found" - done + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" # Delete dev branch curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ diff --git a/cli/release_cascade.php b/cli/release_cascade.php index db96629..1179602 100644 --- a/cli/release_cascade.php +++ b/cli/release_cascade.php @@ -1,6 +1,5 @@ #!/usr/bin/env php * * SPDX-License-Identifier: GPL-3.0-or-later @@ -10,199 +9,9 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/release_cascade.php - * BRIEF: Delete lesser pre-release channels from Gitea when promoting stability - * - * Usage: - * php release_cascade.php --stability stable --token TOKEN --api-base URL - * php release_cascade.php --stability rc --token TOKEN --api-base URL - * php release_cascade.php --stability stable --version 09.01.00 --token TOKEN --api-base URL - * - * Cascade rules: - * stable -> deletes development, alpha, beta, release-candidate - * rc -> deletes development, alpha, beta - * beta -> deletes development, alpha - * alpha -> deletes development - * - * When --version is given, also deletes releases on any channel whose version - * is lower than the specified version (prevents stale pre-releases lingering). + * VERSION: 02.00.00 + * BRIEF: DEPRECATED — cascade behavior removed. Each release stream is independent. */ -declare(strict_types=1); - -$stability = null; -$token = null; -$apiBase = null; -$version = null; - -foreach ($argv as $i => $arg) { - if ($arg === '--stability' && isset($argv[$i + 1])) { - $stability = $argv[$i + 1]; - } - if ($arg === '--token' && isset($argv[$i + 1])) { - $token = $argv[$i + 1]; - } - if ($arg === '--api-base' && isset($argv[$i + 1])) { - $apiBase = $argv[$i + 1]; - } - if ($arg === '--version' && isset($argv[$i + 1])) { - $version = $argv[$i + 1]; - } -} - -// Allow token from environment -if ($token === null) { - $token = getenv('MOKOGITEA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null; -} - -if ($stability === null || $token === null || $apiBase === null) { - fwrite(STDERR, "Usage: release_cascade.php --stability [stable|rc|beta|alpha] --token TOKEN --api-base URL\n"); - fwrite(STDERR, " --api-base: e.g. https://git.mokoconsulting.tech/api/v1/repos/Org/Repo\n"); - fwrite(STDERR, " Token can also be set via MOKOGITEA_TOKEN or GITEA_TOKEN env var\n"); - exit(1); -} - -// Define cascade hierarchy -$cascadeMap = [ - 'stable' => ['development', 'alpha', 'beta', 'release-candidate'], - 'release-candidate' => ['development', 'alpha', 'beta'], - 'rc' => ['development', 'alpha', 'beta'], - 'beta' => ['development', 'alpha'], - 'alpha' => ['development'], -]; - -if (!isset($cascadeMap[$stability])) { - fwrite(STDERR, "Unknown stability level: {$stability}\n"); - fwrite(STDERR, "Valid options: stable, rc, beta, alpha\n"); - exit(1); -} - -$tagsToDelete = $cascadeMap[$stability]; -$deleted = 0; - -foreach ($tagsToDelete as $tag) { - // Get release by tag - $ch = curl_init("{$apiBase}/releases/tags/{$tag}"); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], - CURLOPT_TIMEOUT => 30, - ]); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($httpCode !== 200 || empty($response)) { - continue; - } - - $data = json_decode($response, true); - $releaseId = $data['id'] ?? null; - - if ($releaseId === null) { - continue; - } - - // Delete release - $ch = curl_init("{$apiBase}/releases/{$releaseId}"); - curl_setopt_array($ch, [ - CURLOPT_CUSTOMREQUEST => 'DELETE', - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], - CURLOPT_TIMEOUT => 30, - ]); - curl_exec($ch); - curl_close($ch); - - // Delete tag - $ch = curl_init("{$apiBase}/tags/{$tag}"); - curl_setopt_array($ch, [ - CURLOPT_CUSTOMREQUEST => 'DELETE', - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], - CURLOPT_TIMEOUT => 30, - ]); - curl_exec($ch); - curl_close($ch); - - echo "Deleted: {$tag} (release id: {$releaseId})\n"; - $deleted++; -} - -// ── Version-aware cleanup: delete releases with lesser version numbers ─────── -if ($version !== null) { - // Normalize version for comparison (strip any suffix) - $baseVersion = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $version); - - // Check all channels (including ones not in the cascade map for this stability) - $allChannels = ['development', 'alpha', 'beta', 'release-candidate', 'stable']; - foreach ($allChannels as $tag) { - // Skip the current stability channel - if ($tag === $stability) { - continue; - } - // Skip channels already deleted by cascade above - if (in_array($tag, $tagsToDelete, true)) { - continue; - } - - $ch = curl_init("{$apiBase}/releases/tags/{$tag}"); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], - CURLOPT_TIMEOUT => 30, - ]); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($httpCode !== 200 || empty($response)) { - continue; - } - - $data = json_decode($response, true); - $releaseId = $data['id'] ?? null; - $releaseName = $data['name'] ?? ''; - if ($releaseId === null) { - continue; - } - - // Extract version from release name (e.g. "element 09.00.01 (development)") - $releaseVersion = null; - if (preg_match('/(\d{2}\.\d{2}\.\d{2})/', $releaseName, $vm)) { - $releaseVersion = $vm[1]; - } - - if ($releaseVersion === null) { - continue; - } - - // Delete if release version is less than the promoted version - if (version_compare($releaseVersion, $baseVersion, '<')) { - $delCh = curl_init("{$apiBase}/releases/{$releaseId}"); - curl_setopt_array($delCh, [ - CURLOPT_CUSTOMREQUEST => 'DELETE', - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], - CURLOPT_TIMEOUT => 30, - ]); - curl_exec($delCh); - curl_close($delCh); - - $tagCh = curl_init("{$apiBase}/tags/{$tag}"); - curl_setopt_array($tagCh, [ - CURLOPT_CUSTOMREQUEST => 'DELETE', - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], - CURLOPT_TIMEOUT => 30, - ]); - curl_exec($tagCh); - curl_close($tagCh); - - echo "Deleted: {$tag} — version {$releaseVersion} < {$baseVersion}\n"; - $deleted++; - } - } -} - -echo "Cleaned up {$deleted} pre-release channel(s)\n"; +echo "release_cascade.php: No-op (cascade behavior removed — each stream is independent)\n"; exit(0); diff --git a/cli/release_publish.php b/cli/release_publish.php new file mode 100644 index 0000000..b161f05 --- /dev/null +++ b/cli/release_publish.php @@ -0,0 +1,275 @@ +#!/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/release_publish.php + * VERSION: 01.00.00 + * BRIEF: Publish a release and create copies for all lesser stability streams. + * + * When a release is published at a given stability, copies are created for all + * lower stability streams with the same base version and their respective suffix. + * updates.xml is updated for ALL streams and synced to ALL branches. + * + * Usage: + * php release_publish.php --path . --stability stable --token TOKEN + * php release_publish.php --path . --stability rc --token TOKEN --bump minor + * php release_publish.php --path . --stability dev --token TOKEN --bump patch + * php release_publish.php --path . --stability stable --token TOKEN --dry-run + * + * Options: + * --path Repository root (default: .) + * --stability Target stability: dev|alpha|beta|rc|stable (required) + * --token Gitea API token (required) + * --bump Version bump type before release: patch|minor|none (default: none) + * --branch Current branch (default: auto-detect) + * --gitea-url Gitea URL (default: env GITEA_URL) + * --org Organization (default: env GITEA_ORG) + * --repo Repository name (default: env GITEA_REPO) + * --dry-run Preview without making changes + */ + +declare(strict_types=1); + +$path = '.'; +$stability = ''; +$token = ''; +$bumpType = 'none'; +$branch = ''; +$giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech'; +$org = getenv('GITEA_ORG') ?: ''; +$repo = getenv('GITEA_REPO') ?: ''; +$dryRun = false; + +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--stability' && isset($argv[$i + 1])) $stability = $argv[$i + 1]; + if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1]; + if ($arg === '--bump' && isset($argv[$i + 1])) $bumpType = $argv[$i + 1]; + if ($arg === '--branch' && isset($argv[$i + 1])) $branch = $argv[$i + 1]; + if ($arg === '--gitea-url' && isset($argv[$i + 1])) $giteaUrl = $argv[$i + 1]; + if ($arg === '--org' && isset($argv[$i + 1])) $org = $argv[$i + 1]; + if ($arg === '--repo' && isset($argv[$i + 1])) $repo = $argv[$i + 1]; + if ($arg === '--dry-run') $dryRun = true; +} + +if (empty($stability) || empty($token)) { + fwrite(STDERR, "Usage: release_publish.php --stability --token TOKEN [options]\n"); + exit(1); +} + +$cli = __DIR__; +$php = PHP_BINARY; +$giteaUrl = rtrim($giteaUrl, '/'); + +// Auto-detect org/repo from git remote if not set +if (empty($org) || empty($repo)) { + $remote = trim((string) @shell_exec("cd " . escapeshellarg($path) . " && git remote get-url origin 2>/dev/null")); + if (preg_match('#/([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) { + if (empty($org)) $org = $m[1]; + if (empty($repo)) $repo = $m[2]; + } +} + +// Auto-detect branch +if (empty($branch)) { + $branch = getenv('GITHUB_REF_NAME') ?: trim((string) @shell_exec("cd " . escapeshellarg($path) . " && git rev-parse --abbrev-ref HEAD 2>/dev/null")); +} + +$apiBase = "{$giteaUrl}/api/v1/repos/{$org}/{$repo}"; + +// Stability ordering and suffix mapping +$allStabilities = ['dev', 'alpha', 'beta', 'rc', 'stable']; +$suffixMap = [ + 'dev' => '-dev', + 'alpha' => '-alpha', + 'beta' => '-beta', + 'rc' => '-rc', + 'stable' => '', +]; +$releaseTagMap = [ + 'dev' => 'development', + 'alpha' => 'alpha', + 'beta' => 'beta', + 'rc' => 'release-candidate', + 'stable' => 'stable', +]; + +$stabilityIndex = array_search($stability, $allStabilities); +if ($stabilityIndex === false) { + fwrite(STDERR, "Invalid stability: {$stability}\n"); + exit(1); +} + +echo "=== Release Publish ===\n"; +echo "Stability: {$stability} | Bump: {$bumpType} | Branch: {$branch}\n"; +echo "Repo: {$org}/{$repo}\n"; + +// -- Step 1: Version bump (if requested) -- +if ($bumpType !== 'none') { + $bumpFlag = $bumpType === 'minor' ? '--minor' : ''; + echo "\n--- Step 1: Version bump ({$bumpType}) ---\n"; + if (!$dryRun) { + passthru("{$php} {$cli}/version_bump.php --path " . escapeshellarg($path) . " {$bumpFlag} 2>&1"); + } else { + echo "[DRY-RUN] Would run version_bump.php {$bumpFlag}\n"; + } +} + +// -- Step 2: Read version and set stability suffix -- +echo "\n--- Step 2: Set version suffix ---\n"; +$versionOutput = []; +exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " 2>/dev/null", $versionOutput); +$version = trim($versionOutput[0] ?? ''); +if (empty($version)) { + fwrite(STDERR, "No version found\n"); + exit(1); +} +// Strip existing suffix to get base version +$baseVersion = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $version); + +if (!$dryRun) { + passthru("{$php} {$cli}/version_set_platform.php --path " . escapeshellarg($path) + . " --version " . escapeshellarg($baseVersion) + . " --branch " . escapeshellarg($branch) + . " --stability " . escapeshellarg($stability) . " 2>&1"); + passthru("{$php} {$cli}/version_check.php --path " . escapeshellarg($path) . " --fix 2>/dev/null"); +} + +$releaseVersion = $baseVersion . $suffixMap[$stability]; +echo "Release version: {$releaseVersion}\n"; + +// -- Step 3: Build release package -- +echo "\n--- Step 3: Build and upload release ---\n"; +$releaseTag = $releaseTagMap[$stability]; +$sha256 = ''; + +if (!$dryRun) { + // Create release + passthru("{$php} {$cli}/release_create.php --path " . escapeshellarg($path) + . " --version " . escapeshellarg($releaseVersion) + . " --tag " . escapeshellarg($releaseTag) + . " --token " . escapeshellarg($token) + . " --api-base " . escapeshellarg($apiBase) + . " --repo " . escapeshellarg($repo) + . " --branch " . escapeshellarg($branch) . " 2>&1"); + + // Build and upload package + $packageOutput = []; + exec("{$php} {$cli}/release_package.php --path " . escapeshellarg($path) + . " --version " . escapeshellarg($releaseVersion) + . " --tag " . escapeshellarg($releaseTag) + . " --token " . escapeshellarg($token) + . " --api-base " . escapeshellarg($apiBase) + . " --repo " . escapeshellarg($repo) + . " --output /tmp 2>&1", $packageOutput); + foreach ($packageOutput as $line) { + echo $line . "\n"; + // Extract SHA from output + if (preg_match('/sha256_zip=([a-f0-9]{64})/i', $line, $m)) { + $sha256 = $m[1]; + } + } + // Also check GITHUB_OUTPUT + $ghOutput = getenv('GITHUB_OUTPUT'); + if ($ghOutput && file_exists($ghOutput)) { + $ghContent = file_get_contents($ghOutput); + if (preg_match('/sha256_zip=([a-f0-9]{64})/i', $ghContent, $m)) { + $sha256 = $m[1]; + } + } +} else { + echo "[DRY-RUN] Would build and upload {$releaseVersion} to {$releaseTag}\n"; +} + +// -- Step 4: Create copies for all lesser stability streams -- +echo "\n--- Step 4: Create copies for lesser streams ---\n"; +for ($i = 0; $i < $stabilityIndex; $i++) { + $lesserStability = $allStabilities[$i]; + $lesserTag = $releaseTagMap[$lesserStability]; + $lesserVersion = $baseVersion . $suffixMap[$lesserStability]; + + echo " Creating {$lesserStability} release: {$lesserVersion}\n"; + + if (!$dryRun) { + // Create or update the lesser release with the same package + passthru("{$php} {$cli}/release_create.php --path " . escapeshellarg($path) + . " --version " . escapeshellarg($lesserVersion) + . " --tag " . escapeshellarg($lesserTag) + . " --token " . escapeshellarg($token) + . " --api-base " . escapeshellarg($apiBase) + . " --repo " . escapeshellarg($repo) + . " --branch " . escapeshellarg($branch) . " 2>&1"); + + // Upload the same package to the lesser release + passthru("{$php} {$cli}/release_package.php --path " . escapeshellarg($path) + . " --version " . escapeshellarg($lesserVersion) + . " --tag " . escapeshellarg($lesserTag) + . " --token " . escapeshellarg($token) + . " --api-base " . escapeshellarg($apiBase) + . " --repo " . escapeshellarg($repo) + . " --output /tmp 2>&1"); + } +} + +// -- Step 5: Update ALL streams in updates.xml -- +echo "\n--- Step 5: Update updates.xml for ALL streams ---\n"; +// Write entry for the primary stream and all lesser streams +$streamsToWrite = array_slice($allStabilities, 0, $stabilityIndex + 1); + +foreach ($streamsToWrite as $stream) { + $streamVersion = $baseVersion . $suffixMap[$stream]; + $shaFlag = !empty($sha256) ? "--sha {$sha256}" : ''; + + echo " Writing {$stream} stream: {$streamVersion}\n"; + if (!$dryRun) { + passthru("{$php} {$cli}/updates_xml_build.php --path " . escapeshellarg($path) + . " --version " . escapeshellarg($streamVersion) + . " --stability " . escapeshellarg($stream) + . " --gitea-url " . escapeshellarg($giteaUrl) + . " --org " . escapeshellarg($org) + . " --repo " . escapeshellarg($repo) + . " {$shaFlag} 2>&1"); + } +} + +// -- Step 6: Commit updates.xml and sync to all branches -- +echo "\n--- Step 6: Commit and sync updates.xml ---\n"; +$root = realpath($path) ?: $path; + +if (!$dryRun) { + $diffCheck = trim((string) @shell_exec("cd " . escapeshellarg($root) . " && git diff --quiet updates.xml 2>&1 && echo clean || echo dirty")); + if ($diffCheck === 'dirty') { + @shell_exec("cd " . escapeshellarg($root) . " && git add updates.xml"); + @shell_exec("cd " . escapeshellarg($root) . " && git commit -m " . escapeshellarg("chore: update channels for {$releaseVersion} [skip ci]") + . " --author=\"gitea-actions[bot] \""); + @shell_exec("cd " . escapeshellarg($root) . " && git push origin " . escapeshellarg($branch) . " 2>&1"); + echo " Committed updates.xml\n"; + } + + // Sync to all branches + passthru("{$php} {$cli}/updates_xml_sync.php --path " . escapeshellarg($path) + . " --current " . escapeshellarg($branch) . " --all" + . " --version " . escapeshellarg($releaseVersion) + . " --token " . escapeshellarg($token) + . " --gitea-url " . escapeshellarg($giteaUrl) + . " --org " . escapeshellarg($org) + . " --repo " . escapeshellarg($repo) . " 2>&1"); +} else { + echo "[DRY-RUN] Would commit updates.xml and sync to all branches\n"; +} + +echo "\n=== Release published: {$releaseVersion} ===\n"; + +// Output for CI +$ghOutput = getenv('GITHUB_OUTPUT'); +if ($ghOutput) { + file_put_contents($ghOutput, "version={$releaseVersion}\nbase_version={$baseVersion}\n", FILE_APPEND); +} + +exit(0); diff --git a/cli/updates_xml_build.php b/cli/updates_xml_build.php index af5bf18..60e7616 100644 --- a/cli/updates_xml_build.php +++ b/cli/updates_xml_build.php @@ -413,63 +413,35 @@ function buildEntry( return implode("\n", $lines); } -// -- Determine which channels to write ---------------------------------------- -// Stable cascades to all channels; pre-releases cascade down to lower channels. -// Each channel entry represents "latest release available at this stability or higher". -// When stable releases, ALL channels point to stable (it's the newest for everyone). -// When RC releases, rc/beta/alpha/dev point to RC; stable is preserved. -// When dev releases, only dev is updated; everything else is preserved. -$allChannels = ['development', 'alpha', 'beta', 'rc', 'stable']; -$stabilityIndex = array_search($stability === 'development' ? 'development' : $stability, $allChannels); -if ($stabilityIndex === false) { - $stabilityIndex = 4; // default to stable -} - -// Write entries for the current channel AND all lower channels (cascade down) -// All cascaded entries point to the CURRENT release (the highest stability being built) +// -- Write ONLY the single channel being released -------------------------------- +// No cascading. Each update stream is independent. +// When dev releases, only the dev entry is written/updated. +// When stable releases, only the stable entry is written/updated. +// All other channel entries are preserved exactly as-is. $entries = []; $giteaTag = $releaseTagMap[$stability] ?? $stability; $channelVersion = $version . ($stabilitySuffixMap[$stability] ?? ''); $channelDownloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$giteaTag}/{$typePrefix}{$extElement}-{$channelVersion}.zip"; $channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$giteaTag}"; +$joomlaTag = $stabilityTagMap[$stability] ?? $stability; +$changelogUrl = "{$giteaUrl}/{$org}/{$repo}/raw/branch/main/CHANGELOG.md"; -// Stability labels for descriptions -$stabilityLabelMap = [ - 'stable' => 'stable', - 'rc' => 'rc', - 'beta' => 'beta', - 'alpha' => 'alpha', - 'development' => 'development', -]; - -for ($i = 0; $i <= $stabilityIndex; $i++) { - $channelName = $allChannels[$i]; - $joomlaTag = $stabilityTagMap[$channelName] ?? $channelName; - $stabilityLabel = $stabilityLabelMap[$channelName] ?? $channelName; - - // All cascaded entries use the SAME version as the highest-stability package. - // The version MUST match what's inside the ZIP (Joomla reads it post-install). - // The differentiates channels; the version is always the release version. - // Changelog URL: points to the CHANGELOG.md on main branch - $changelogUrl = "{$giteaUrl}/{$org}/{$repo}/raw/branch/main/CHANGELOG.md"; - - $entries[] = buildEntry( - $joomlaTag, - $entryVersion, - $channelDownloadUrl, - $displayName, - $stabilityLabel, - $extElement, - $extType, - $clientTag, - $folderTag, - $channelInfoUrl, - $targetPlatform, - $phpTag, - $shaTag, - $changelogUrl - ); -} +$entries[] = buildEntry( + $joomlaTag, + $channelVersion, + $channelDownloadUrl, + $displayName, + $stability, + $extElement, + $extType, + $clientTag, + $folderTag, + $channelInfoUrl, + $targetPlatform, + $phpTag, + $shaTag, + $changelogUrl +); // -- Preserve existing entries for channels not being updated ----------------- $dest = $outputFile ?? "{$root}/updates.xml"; @@ -478,32 +450,21 @@ $preservedEntries = []; if (file_exists($dest)) { $existingXml = @simplexml_load_file($dest); if ($existingXml) { - // Joomla tags we're writing — don't preserve these - $writtenChannels = []; - for ($i = 0; $i <= $stabilityIndex; $i++) { - $writtenChannels[] = $stabilityTagMap[$allChannels[$i]] ?? $allChannels[$i]; - } - // Also match legacy/alternate tag names (e.g. 'development' = 'dev') - $writtenChannels[] = 'development'; // alias for 'dev' + // Only the channel we're writing gets replaced — everything else is preserved + $writtenTag = $joomlaTag; + // Also match legacy alternate (e.g. 'development' = 'dev') + $writtenAliases = [$writtenTag]; + if ($writtenTag === 'dev') $writtenAliases[] = 'development'; + if ($writtenTag === 'development') $writtenAliases[] = 'dev'; foreach ($existingXml->update as $existingUpdate) { $existingTag = ''; if (isset($existingUpdate->tags->tag)) { $existingTag = (string) $existingUpdate->tags->tag; } - $existingVersion = (string) ($existingUpdate->version ?? ''); - // Strip suffixes for comparison - $existingBase = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $existingVersion); - $currentBase = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $version); - - // Keep entries for channels we're NOT overwriting, - // but ONLY if their version is >= current (never preserve stale entries) - if (!empty($existingTag) && !in_array($existingTag, $writtenChannels, true)) { - if (version_compare($existingBase, $currentBase, '>=')) { - $preservedEntries[] = ' ' . trim($existingUpdate->asXML()); - } else { - echo "Discarding stale {$existingTag} entry (v{$existingVersion} < v{$version})\n"; - } + // Keep ALL entries except the one channel we're overwriting + if (!empty($existingTag) && !in_array($existingTag, $writtenAliases, true)) { + $preservedEntries[] = ' ' . trim($existingUpdate->asXML()); } } } diff --git a/cli/updates_xml_sync.php b/cli/updates_xml_sync.php index 0754068..8b7236a 100644 --- a/cli/updates_xml_sync.php +++ b/cli/updates_xml_sync.php @@ -17,11 +17,12 @@ * * Usage: * php updates_xml_sync.php --path /repo --branches main,dev --current dev - * php updates_xml_sync.php --path /repo --branches main --current dev --version 02.01.27 + * php updates_xml_sync.php --path /repo --all --current dev --version 02.01.27 * * Options: * --path Repository root containing updates.xml (default: .) * --branches Comma-separated target branches to sync to (default: main,dev) + * --all Auto-discover all branches via Gitea API (overrides --branches) * --current Current branch to skip (required) * --version Version string for commit message (optional) * --token Gitea API token (default: env MOKOGITEA_TOKEN) @@ -41,10 +42,12 @@ $token = getenv('MOKOGITEA_TOKEN') ?: ''; $giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech'; $org = getenv('GITEA_ORG') ?: ''; $repo = getenv('GITEA_REPO') ?: ''; +$discoverAll = false; foreach ($argv as $i => $arg) { if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; if ($arg === '--branches' && isset($argv[$i + 1])) $branches = $argv[$i + 1]; + if ($arg === '--all') $discoverAll = true; if ($arg === '--current' && isset($argv[$i + 1])) $current = $argv[$i + 1]; if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1]; if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1]; @@ -68,6 +71,35 @@ if ($org === '' || $repo === '') { exit(1); } +// Auto-discover branches if --all flag is set +if ($discoverAll) { + $apiUrl = "{$giteaUrl}/api/v1/repos/{$org}/{$repo}/branches?limit=50"; + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ["Authorization: token {$token}", 'Accept: application/json'], + CURLOPT_TIMEOUT => 15, + ]); + $response = curl_exec($ch); + curl_close($ch); + $branchList = json_decode($response ?: '[]', true) ?: []; + $discovered = []; + foreach ($branchList as $b) { + $name = $b['name'] ?? ''; + if ($name !== '' && $name !== $current + && !str_starts_with($name, 'version/') + && !str_starts_with($name, 'feature/') + && !str_starts_with($name, 'patch/') + ) { + $discovered[] = $name; + } + } + if (!empty($discovered)) { + $branches = implode(',', $discovered); + echo "Discovered branches: {$branches}\n"; + } +} + $updatesFile = rtrim($path, '/') . '/updates.xml'; if (!file_exists($updatesFile)) { fwrite(STDERR, "No updates.xml found at {$updatesFile}\n"); diff --git a/cli/version_auto_bump.php b/cli/version_auto_bump.php index c16e035..32d7f1b 100644 --- a/cli/version_auto_bump.php +++ b/cli/version_auto_bump.php @@ -53,7 +53,7 @@ $stabilityMap = [ if (array_key_exists($branch, $stabilityMap)) { $stability = $stabilityMap[$branch]; -} elseif (str_starts_with($branch, 'feature/')) { +} elseif (str_starts_with($branch, 'feature/') || str_starts_with($branch, 'patch/')) { $stability = 'dev'; } else { $stability = 'dev'; @@ -62,8 +62,8 @@ if (array_key_exists($branch, $stabilityMap)) { $cli = __DIR__; $php = PHP_BINARY; -// Step 1: Patch bump (skip on alpha/beta/rc — those only change the suffix) -$shouldBump = !in_array($branch, ['alpha', 'beta', 'rc'], true); +// Step 1: Patch bump — all branches get patch bumps +$shouldBump = true; if ($shouldBump) { $bumpOutput = []; -- 2.52.0 From 983b06d434a2a35f64bed95961251c0e6ef52ad7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 30 May 2026 00:48:39 -0500 Subject: [PATCH 3/3] docs: update CONTRIBUTING.md with release stream copies and version flow Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- CONTRIBUTING.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea9a5fc..c0b4858 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,13 +88,33 @@ Each branch appends a suffix to indicate stability: ### Auto version bump -On every push to `dev`, `alpha`, `beta`, `rc`, or `feature/*`: +On every push to `dev`, `feature/*`, or `patch/*`: 1. Patch version incremented -2. Stability suffix applied based on branch name +2. Stability suffix `-dev` applied 3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.) 4. Commit created with `[skip ci]` to avoid loops +### Release version flow + +Version bumps happen at specific release events: + +| Event | Bump | Example | +|-------|------|---------| +| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` | +| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` | +| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) | +| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` | + +### Release stream copies + +When a higher-stability release is published, copies are created for all lesser streams with the same base version: + +- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta` +- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc` + +This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed). + ### Version files The version tools update all files containing version stamps: -- 2.52.0