Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Ci/Init/InitCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ public function execute(
}
$this->output->plain('');
$this->output->warn('Use --force to overwrite existing files.');
return false;
// The command ran and decided "no-op, hint to use --force".
// That is a user-recoverable outcome, not a failure: return
// true so Module/Ci doesn't translate it into a fatal-error
// exception via the W1 convention.
return true;
}

// Generate files
Expand Down
17 changes: 17 additions & 0 deletions src/Ci/Run/PrCommentReporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,23 @@ private function formatToolFailure(string $toolName, array $result): string
return "{$toolDisplay}: Error - {$result['error']}";
}

// Setup-failed lanes (F30) arrive here as `missing: true` with
// a reason prefixed by "Setup failed: " by RunCommand. The
// ResultCollector::addMissing contract carries that reason in
// the `reason` key. We classify the user-facing rendering by
// category if available; otherwise fall back to the raw reason.
if (!empty($result['missing'])) {
$reason = (string) ($result['reason'] ?? 'Missing result file');
$category = (string) ($result['category'] ?? '');
$tag = match ($category) {
'stability_gate' => '⚠️ Stability-gated',
'platform_missing' => '❌ Platform requirement missing',
'php_version' => '❌ PHP version conflict',
default => '❌ Setup failed',
};
return "{$toolDisplay}: {$tag} ({$reason})";
}

return match ($toolName) {
'phpunit' => sprintf(
'%s: %d failure%s, %d error%s',
Expand Down
16 changes: 14 additions & 2 deletions src/Ci/Run/ResultCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,29 @@ public function addResultFromFile(string $laneName, string $tool, string $jsonFi
* on the lane's PHP version) belong in {@see addSkipped()} with
* `success: true`; they are not modeled here.
*
* `$category` is the F29 classifier output for setup-failed lanes
* (`stability_gate`, `platform_missing`, `php_version`, `unknown`).
* PrCommentReporter uses it to render `stability_gate` failures as
* "⚠️ Stability-gated (working as designed)" rather than the generic
* "❌ Setup failed".
*
* @param string $laneName Lane name
* @param string $tool Tool name
* @param string $reason Reason the result is missing
* @param string $category Classifier output; empty string when N/A
*/
public function addMissing(string $laneName, string $tool, string $reason): void
{
public function addMissing(
string $laneName,
string $tool,
string $reason,
string $category = ''
): void {
$this->results[$laneName][$tool] = [
'success' => false,
'exit_code' => 1,
'missing' => true,
'reason' => $reason,
'category' => $category,
];
}

Expand Down
36 changes: 36 additions & 0 deletions src/Ci/Run/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@ private function runLaneScript(array $lane): void
return;
}

// F30: lane with build/setup-failed.json never had a run-lane.sh
// generated. Stay silent here; aggregateResults emits the ❌ with
// the failure reason.
$setupFailFile = $lane['component_dir'] . '/build/setup-failed.json';
if (file_exists($setupFailFile)) {
$this->output->error("[{$lane['name']}] Setup failed; no lane script to run");
return;
}

$this->output->info("[{$lane['name']}] Executing lane script...");

// Check if script exists
Expand Down Expand Up @@ -260,6 +269,33 @@ private function aggregateResults(array $lanes): void
}
}

// F30: surface setup-failed lanes as ❌ for every tool that
// would have run. The reason from setup-failed.json lands in
// the PR comment so the maintainer sees *why* the lane never
// executed without having to scroll through composer logs.
//
// F29: the category (stability_gate / platform_missing /
// php_version / unknown) lets PrCommentReporter render
// "ecosystem not yet at this stability" failures distinctly
// from real-bug failures.
$setupFailFile = $buildDir . '/setup-failed.json';
if (file_exists($setupFailFile)) {
$failData = json_decode((string) file_get_contents($setupFailFile), true);
if (is_array($failData) && ($failData['setup_failed'] ?? false)) {
$reason = (string) ($failData['reason'] ?? 'Lane setup failed');
$category = (string) ($failData['category'] ?? 'unknown');
foreach ((array) ($failData['tools'] ?? ['phpunit', 'phpstan']) as $tool) {
$this->collector->addMissing(
$lane['name'],
(string) $tool,
'Setup failed: ' . $reason,
$category
);
}
continue;
}
}

// Read PHPUnit results
$phpunitFile = $buildDir . '/phpunit-results-summary.json';
if (file_exists($phpunitFile)) {
Expand Down
56 changes: 56 additions & 0 deletions src/Ci/Setup/ComposerInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,62 @@ private function extractError(string $output): string
return trim(end($nonEmpty));
}

/**
* Classify a composer install failure message into a coarse category
* so downstream reporting can present "stability-gated" failures
* differently from "ext-* missing" and from "no idea".
*
* The classification is best-effort: composer's text is a moving
* target. Categories returned:
*
* - `stability_gate`: a transitive dependency is only available at a
* stability lower than the lane's `minimum-stability`. Composer
* prints `does not match your minimum-stability`. Working as
* designed — the ecosystem is not yet ready for that lane's
* stability level. Maintainer action: wait for the upstream package
* to release at the required stability or accept the failure.
*
* - `platform_missing`: a `ext-*` (or `lib-*`) requirement is not
* installed on the runner. Composer prints `is missing from your
* system. Install or enable PHP's <ext> extension`. The fix lives
* in `.horde.yml`'s `ci-platform` (rerun
* `horde-components dependencies --platform`) or in the bootstrap
* if the resolver caught it but the apt-get install path missed
* the package.
*
* - `php_version`: the resolver couldn't satisfy a PHP version
* constraint. Composer prints `your php version (X.Y.Z) does not
* satisfy that requirement`. Usually a stale `^7` constraint
* surviving on a transitive horde/* package.
*
* - `unknown`: anything else. UI falls back to the generic
* "Setup failed" rendering with the raw message.
*/
public static function classifyError(string $output): string
{
// The composer error messages contain literal newlines and the
// workflow-command-escaped %0A variant; normalise both before
// pattern matching so the classifier works whether the caller
// hands us live output or the lane-prefixed copy from the
// CI log.
$normalised = str_replace(['%0A', '\\n'], "\n", $output);

if (stripos($normalised, 'does not match your minimum-stability') !== false) {
return 'stability_gate';
}
if (preg_match('/is missing from your system\\. Install or enable PHP/i', $normalised) === 1) {
return 'platform_missing';
}
// Composer's pre-resolution platform-requirement rejection.
if (preg_match('/require ext-\\S+ \\* -> it is missing from your system/i', $normalised) === 1) {
return 'platform_missing';
}
if (preg_match('/your php version \\(\\S+\\) does not satisfy/i', $normalised) === 1) {
return 'php_version';
}
return 'unknown';
}

/**
* Verify composer installation succeeded.
*
Expand Down
Loading
Loading