Compare commits

...

2 Commits

Author SHA1 Message Date
Jonathan Miller 76bc91a383 fix: route all decorative CLI output to stderr
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
All display methods (banner, progress, section, status, log, table,
summary box, divider, step) now write to stderr via a new display()
helper. This ensures stdout is reserved for machine-readable data,
fixing CI pipelines that capture CLI output with $() or redirect to
$GITHUB_OUTPUT.

Root cause: version_read.php banner was written to stdout, so
  VERSION=$(php version_read.php --path .)
captured the box-drawing characters along with the version string,
corrupting $GITHUB_OUTPUT and breaking downstream release steps.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 10:01:16 -05:00
Jonathan Miller b53846f6f4 fix: prevent version_read banner from corrupting XML manifests
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 38s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
- Add --quiet flag to version_read.php call in version_auto_bump.php
  so the CliFramework banner doesn't pollute stdout
- Parse version output by matching XX.YY.ZZ pattern instead of
  blindly taking the first line
- Add version format validation in version_set_platform.php to reject
  non-XX.YY.ZZ values before writing to XML files

Root cause: exec() captured the decorative banner output from
version_read.php and version_set_platform.php's regex replacement
injected it into <version> tags across all Joomla manifests.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:01:19 -05:00
3 changed files with 80 additions and 50 deletions
+11 -3
View File
@@ -109,10 +109,18 @@ class VersionAutoBumpCli extends CliFramework
echo "{$line}\n";
}
// Step 2: Read version
// Step 2: Read version (--quiet suppresses banner so only the version is output)
$versionOutput = [];
exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " 2>&1", $versionOutput, $versionRc);
$version = trim($versionOutput[0] ?? '');
exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " --quiet 2>&1", $versionOutput, $versionRc);
// Take the last non-empty line — the version is always the final output
$version = '';
foreach (array_reverse($versionOutput) as $line) {
$line = trim($line);
if (preg_match('/^\d{2}\.\d{2}\.\d{2}/', $line)) {
$version = $line;
break;
}
}
if (empty($version)) {
echo "No version found — skipping\n";
+6
View File
@@ -53,6 +53,12 @@ class VersionSetPlatformCli extends CliFramework
// Strip any existing suffix(es) before applying the correct one
$version = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $version);
// Validate version format — must be XX.YY.ZZ to prevent XML corruption
if (!preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $version)) {
$this->log('ERROR', "Invalid version format: '{$version}' — expected XX.YY.ZZ");
return 1;
}
// Append stability suffix for non-stable releases
$stabilitySuffixMap = [
'stable' => '',
+63 -47
View File
@@ -141,6 +141,26 @@ abstract class CliFramework
/** @var float Script start time for elapsed-time reporting. */
private float $startTime;
// =========================================================================
// Display output — all decorative output goes to stderr
// =========================================================================
/**
* Write decorative/diagnostic output to stderr.
*
* All non-data output (banners, progress bars, section headers, status
* lines, log messages) MUST use this method so that stdout is reserved
* for machine-readable data. This ensures that shell captures like
* VERSION=$(php version_read.php --path .)
* only receive the actual data, not decorative text.
*
* @since 04.00.16
*/
protected function display(string $text): void
{
fwrite(STDERR, $text);
}
// =========================================================================
// Constructor
// =========================================================================
@@ -326,14 +346,14 @@ abstract class CliFramework
protected function printHelp(): void
{
$w = $this->termWidth();
echo $this->c(self::C_BOLD . self::C_CYAN, $this->scriptName);
$this->display($this->c(self::C_BOLD . self::C_CYAN, $this->scriptName));
if ($this->description !== '') {
echo ' — ' . $this->description;
$this->display(' — ' . $this->description);
}
echo "\n";
echo $this->c(self::C_DIM, str_repeat(self::BOX_H, $w)) . "\n\n";
echo $this->c(self::C_BOLD, 'Usage:') . " php {$this->scriptName}.php [options]\n\n";
echo $this->c(self::C_BOLD, 'Options:') . "\n";
$this->display("\n");
$this->display($this->c(self::C_DIM, str_repeat(self::BOX_H, $w)) . "\n\n");
$this->display($this->c(self::C_BOLD, 'Usage:') . " php {$this->scriptName}.php [options]\n\n");
$this->display($this->c(self::C_BOLD, 'Options:') . "\n");
$builtIn = [
'--help' => ['desc' => 'Show this help message', 'default' => null],
@@ -348,16 +368,16 @@ abstract class CliFramework
$hint = ($default !== null && $default !== false)
? $this->c(self::C_DIM, " (default: {$default})")
: '';
printf(
$this->display(sprintf(
" %s%-22s%s%s%s\n",
self::C_CYAN,
$name,
self::C_RESET,
$def['desc'],
$hint
);
));
}
echo "\n";
$this->display("\n");
}
// =========================================================================
@@ -378,23 +398,23 @@ abstract class CliFramework
$titleLine = $this->padRight($titleStyled, $inner, strlen($titleRaw));
$descLine = ($desc !== '') ? $this->padRight(" {$desc}", $inner) : null;
echo "\n";
echo $this->c(
$this->display("\n");
$this->display($this->c(
self::C_CYAN,
self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR
) . "\n";
echo $this->c(self::C_CYAN, self::BOX_V)
) . "\n");
$this->display($this->c(self::C_CYAN, self::BOX_V)
. $this->c(self::C_BOLD, $titleLine)
. $this->c(self::C_CYAN, self::BOX_V) . "\n";
. $this->c(self::C_CYAN, self::BOX_V) . "\n");
if ($descLine !== null) {
echo $this->c(self::C_CYAN, self::BOX_V)
$this->display($this->c(self::C_CYAN, self::BOX_V)
. $this->c(self::C_DIM, $descLine)
. $this->c(self::C_CYAN, self::BOX_V) . "\n";
. $this->c(self::C_CYAN, self::BOX_V) . "\n");
}
echo $this->c(
$this->display($this->c(
self::C_CYAN,
self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR
) . "\n\n";
) . "\n\n");
}
/** Print the dry-run notice box. */
@@ -403,18 +423,18 @@ abstract class CliFramework
$w = min($this->termWidth(), 70);
$msg = ' ' . self::ICON_DRY . ' DRY-RUN MODE — no changes will be written ';
$row = $this->padRight($msg, $w - 2);
echo $this->c(
$this->display($this->c(
self::C_YELLOW . self::C_BOLD,
self::BOX_TL . str_repeat(self::BOX_H, $w - 2) . self::BOX_TR
) . "\n";
echo $this->c(
) . "\n");
$this->display($this->c(
self::C_YELLOW . self::C_BOLD,
self::BOX_V . $row . self::BOX_V
) . "\n";
echo $this->c(
) . "\n");
$this->display($this->c(
self::C_YELLOW . self::C_BOLD,
self::BOX_BL . str_repeat(self::BOX_H, $w - 2) . self::BOX_BR
) . "\n\n";
) . "\n\n");
}
// =========================================================================
@@ -435,11 +455,11 @@ abstract class CliFramework
$w = $this->termWidth();
$text = " {$title} ";
$fill = max(0, $w - strlen($text) - 4);
echo "\n";
echo $this->c(
$this->display("\n");
$this->display($this->c(
self::C_CYAN,
str_repeat(self::BOX_H, 2) . $text . str_repeat(self::BOX_H, $fill)
) . "\n\n";
) . "\n\n");
}
/** Print a plain horizontal divider. */
@@ -449,7 +469,7 @@ abstract class CliFramework
return;
}
$this->clearProgress();
echo $this->c(self::C_DIM, str_repeat(self::BOX_H, $this->termWidth())) . "\n";
$this->display($this->c(self::C_DIM, str_repeat(self::BOX_H, $this->termWidth())) . "\n");
}
// =========================================================================
@@ -495,11 +515,7 @@ abstract class CliFramework
$line = "{$ts} {$icon} {$badge} {$text}\n";
if ($level === 'ERROR') {
fwrite(STDERR, $line);
} else {
echo $line;
}
$this->display($line);
}
/** Log a success message. */
@@ -564,7 +580,7 @@ abstract class CliFramework
? ' ' . $this->c(self::C_DIM, "{$detail}")
: '';
echo ' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n";
$this->display(' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n");
}
// =========================================================================
@@ -601,10 +617,10 @@ abstract class CliFramework
$line = " [{$bar}] {$percent} {$counter}{$suffix}";
if ($newline) {
echo "\r{$line}\n";
$this->display("\r{$line}\n");
$this->progressActive = false;
} else {
echo "\r{$line}";
$this->display("\r{$line}");
$this->progressActive = true;
}
}
@@ -613,7 +629,7 @@ abstract class CliFramework
protected function clearProgress(): void
{
if ($this->progressActive) {
echo "\r" . str_repeat(' ', $this->termWidth()) . "\r";
$this->display("\r" . str_repeat(' ', $this->termWidth()) . "\r");
$this->progressActive = false;
}
}
@@ -644,8 +660,8 @@ abstract class CliFramework
$maxKey = max(array_map('strlen', array_keys($rows)));
$inner = $maxKey + 20;
echo "\n";
echo $this->c($color, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR) . "\n";
$this->display("\n");
$this->display($this->c($color, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR) . "\n");
foreach ($rows as $label => $value) {
$valStr = (string) $value;
@@ -653,10 +669,10 @@ abstract class CliFramework
$padding = $inner - strlen($label) - $valVis - 4;
$row = ' ' . $this->c(self::C_BOLD, $label)
. str_repeat(' ', max(1, $padding)) . $valStr . ' ';
echo $this->c($color, self::BOX_V) . $row . $this->c($color, self::BOX_V) . "\n";
$this->display($this->c($color, self::BOX_V) . $row . $this->c($color, self::BOX_V) . "\n");
}
echo $this->c($color, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR) . "\n\n";
$this->display($this->c($color, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR) . "\n\n");
}
/**
@@ -702,7 +718,7 @@ abstract class CliFramework
$this->clearProgress();
$badge = $this->c(self::C_BOLD . self::C_MAGENTA, "Step {$current}/{$total}");
$arrow = $this->c(self::C_DIM, self::ICON_INFO);
echo "\n{$badge} {$arrow} {$title}\n";
$this->display("\n{$badge} {$arrow} {$title}\n");
}
// =========================================================================
@@ -964,13 +980,13 @@ abstract class CliFramework
}
// Header.
echo $sep . "\n";
$this->display($sep . "\n");
$headerLine = '|';
foreach ($headers as $i => $h) {
$headerLine .= ' ' . $this->c(self::C_BOLD, str_pad($h, $widths[$i])) . ' |';
}
echo $headerLine . "\n";
echo $sep . "\n";
$this->display($headerLine . "\n");
$this->display($sep . "\n");
// Rows.
foreach ($rows as $row) {
@@ -978,9 +994,9 @@ abstract class CliFramework
foreach ($row as $i => $cell) {
$line .= ' ' . str_pad((string) $cell, $widths[$i]) . ' |';
}
echo $line . "\n";
$this->display($line . "\n");
}
echo $sep . "\n";
$this->display($sep . "\n");
}
}