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
29 changes: 27 additions & 2 deletions app/Jobs/SetupOpenClawOnServerJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
49 changes: 49 additions & 0 deletions tests/Unit/Jobs/SetupOpenClawOnServerJobTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

use App\Jobs\SetupOpenClawOnServerJob;

test('parses lowercase v-prefix format (pre-5.6 OpenClaw)', function () {
expect(SetupOpenClawOnServerJob::parseOpenClawVersion('openclaw v2026.3.8'))
->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');
});
Loading