diff --git a/app/Jobs/SetupOpenClawOnServerJob.php b/app/Jobs/SetupOpenClawOnServerJob.php index 91f6e06..72586e9 100644 --- a/app/Jobs/SetupOpenClawOnServerJob.php +++ b/app/Jobs/SetupOpenClawOnServerJob.php @@ -171,17 +171,42 @@ private function captureOpenClawVersion(SshService $sshService): void { try { $output = trim($sshService->exec('openclaw --version')); - // Output is like "openclaw v2026.3.8" — extract the version part - $version = Str::after($output, 'openclaw '); + $version = self::parseOpenClawVersion($output); if ($version) { $this->server->update(['openclaw_version' => $version]); Log::info("OpenClaw {$version} installed on server {$this->server->id}"); + } else { + Log::warning("Could not parse OpenClaw version from output on server {$this->server->id}: {$output}"); } } catch (\RuntimeException $e) { Log::warning("Could not determine OpenClaw version on server {$this->server->id}: {$e->getMessage()}"); } } + /** + * Extract the version string from `openclaw --version` output. + * + * Handles every observed format across OpenClaw releases: + * - "openclaw v2026.3.8" (≤ 5.5, lowercase + "v") + * - "openclaw 2026.5.19 (c97b9f7)" (5.6+, lowercase, no v, build hash) + * - "OpenClaw 2026.5.19" (capital O, no prefix) + * - "v2026.5.19" / "2026.5.19" (bare) + * + * Returns the bare version string (e.g. "2026.5.19") with any leading + * "v" stripped and trailing build hash discarded. Returns null when the + * output doesn't contain a recognisable semver-ish token. + * + * Fixes issue #29. + */ + public static function parseOpenClawVersion(string $output): ?string + { + if (preg_match('/(\d{4}\.\d+\.\d+(?:[-.][a-z0-9.]+)?)/i', $output, $matches)) { + return ltrim($matches[1], 'v'); + } + + return null; + } + private function logProgress(string $step): void { $this->server->events()->create([ diff --git a/tests/Unit/Jobs/SetupOpenClawOnServerJobTest.php b/tests/Unit/Jobs/SetupOpenClawOnServerJobTest.php new file mode 100644 index 0000000..0a92da5 --- /dev/null +++ b/tests/Unit/Jobs/SetupOpenClawOnServerJobTest.php @@ -0,0 +1,49 @@ +toBe('2026.3.8'); +}); + +test('parses lowercase no-prefix format with build hash (5.6+)', function () { + expect(SetupOpenClawOnServerJob::parseOpenClawVersion('openclaw 2026.5.6 (c97b9f7)')) + ->toBe('2026.5.6'); +}); + +test('parses 5.19 format from the actual openclaw binary', function () { + expect(SetupOpenClawOnServerJob::parseOpenClawVersion('openclaw 2026.5.19 (abc1234)')) + ->toBe('2026.5.19'); +}); + +test('parses capital O variant (OpenClaw 2026.5.19)', function () { + expect(SetupOpenClawOnServerJob::parseOpenClawVersion('OpenClaw 2026.5.19')) + ->toBe('2026.5.19'); +}); + +test('parses bare version string with leading v', function () { + expect(SetupOpenClawOnServerJob::parseOpenClawVersion('v2026.5.19')) + ->toBe('2026.5.19'); +}); + +test('parses bare version string without leading v', function () { + expect(SetupOpenClawOnServerJob::parseOpenClawVersion('2026.5.19')) + ->toBe('2026.5.19'); +}); + +test('parses prerelease version (2026.5.20-beta.1)', function () { + expect(SetupOpenClawOnServerJob::parseOpenClawVersion('openclaw 2026.5.20-beta.1')) + ->toBe('2026.5.20-beta.1'); +}); + +test('returns null for unparseable output', function () { + expect(SetupOpenClawOnServerJob::parseOpenClawVersion(''))->toBeNull() + ->and(SetupOpenClawOnServerJob::parseOpenClawVersion('command not found'))->toBeNull() + ->and(SetupOpenClawOnServerJob::parseOpenClawVersion('openclaw'))->toBeNull(); +}); + +test('extracts version when wrapped in noise', function () { + expect(SetupOpenClawOnServerJob::parseOpenClawVersion("Warning: foo\nopenclaw 2026.5.19 (abc)")) + ->toBe('2026.5.19'); +});