Files
moko-platform/cli/release_cascade.php
T
Jonathan Miller 07ea171af9
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 43s
feat: release promotion pipeline, 5 new CLI tools, workflow refactoring
New CLI tools:
- manifest_element.php — extract element/type/prefix from any platform manifest
- release_create.php — create/overwrite Gitea releases with proper naming
- release_package.php — build ZIP+tar.gz, SHA-256, upload assets
- release_promote.php — promote releases between channels (dev→RC→stable)
- version_reset_dev.php — reset platform version on dev branch after release

Updated CLI tools:
- version_bump.php — now writes to manifests, Dolibarr mod, composer.json (not just README)
- release_cascade.php — added --version for version-aware deletion of stale releases
- release_validate.php — auto-detect platform, --github-output, source dir check

Workflow changes (auto-release.yml):
- Draft PR to main → auto-promote highest pre-release to RC
- Merged PR to main → promote RC to stable (skip rebuild when RC exists)
- Removed paths filter for Go/Node/generic repo compatibility
- Fixed cascade --api-base parameter bug

Workflow changes (pre-release.yml):
- Auto-trigger development pre-release on feature branch merge to dev
- Removed paths filter

Infrastructure:
- RepositorySynchronizer: fixed template repo names, .mokogitea/workflows path,
  universal workflow cascade (Template-Generic → other templates)
- bulk_sync.php: syncs universal workflows to templates before repo sync
- PHPDoc added to 4 classes missing class-level docs
- Version bump 09.00.00 → 09.01.00

Closes #152 #153 #154 #155 #156 #157 #158 #159 #161 #162

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 14:29:32 -05:00

209 lines
6.6 KiB
PHP

#!/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_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).
*/
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('GA_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 GA_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('/-[a-z]+$/', '', $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";
exit(0);