feat: release pipeline rework #211

Merged
jmiller merged 3 commits from dev into main 2026-05-30 05:49:36 +00:00
9 changed files with 393 additions and 402 deletions
+1 -3
View File
@@ -16,10 +16,8 @@ on:
push:
branches:
- dev
- alpha
- beta
- rc
- 'feature/**'
- 'patch/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+22 -126
View File
@@ -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] <gitea-actions[bot]@mokoconsulting.tech>"
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] <gitea-actions[bot]@mokoconsulting.tech>"
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}" \
+2 -2
View File
@@ -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
+22 -2
View File
@@ -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:
+3 -194
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* 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);
+275
View File
@@ -0,0 +1,275 @@
#!/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/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 <dev|alpha|beta|rc|stable> --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] <gitea-actions[bot]@mokoconsulting.tech>\"");
@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);
+32 -71
View File
@@ -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 <tag> 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());
}
}
}
+33 -1
View File
@@ -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");
+3 -3
View File
@@ -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 = [];