clusev/app/Services/ReleaseBridge.php

65 lines
2.1 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Str;
/**
* The app side of the release bridge. requestStaging() writes a strictly-validated request to
* run/release-request.json (a host watcher runs it); result() reads the host-written result for an
* id this app issued. Validation here is the first gate — the host re-validates every value.
* Reuses the same bind-mounted signal dir as the WireGuard/restart bridges.
*/
class ReleaseBridge
{
private function dir(): string
{
return storage_path('app/restart-signal');
}
/**
* Write a staging-release request for a bare X.Y.Z target; returns the request id, or null when
* the signal dir is not writable. Throws on a non-semver target (defence-in-depth).
*/
public function requestStaging(string $target): ?string
{
if (preg_match('/^\d+\.\d+\.\d+$/', $target) !== 1) {
throw new \InvalidArgumentException('invalid target');
}
$id = Str::random(32);
$payload = ['id' => $id, 'action' => 'stage', 'target' => $target, 'at' => time()];
@mkdir($this->dir(), 0775, true);
$ok = @file_put_contents($this->dir().'/release-request.json', json_encode($payload, JSON_UNESCAPED_SLASHES));
return $ok === false ? null : $id;
}
/**
* Read the host result for an id THIS app issued. Null until the result exists. Never throws.
*
* @return array{ok:bool, tag:?string, message:string}|null
*/
public function result(string $id): ?array
{
if (preg_match('/^[A-Za-z0-9]{16,64}$/', $id) !== 1) {
return null;
}
$file = $this->dir()."/release-result-{$id}.json";
if (! is_file($file)) {
return null;
}
$data = json_decode((string) @file_get_contents($file), true);
if (! is_array($data) || ($data['id'] ?? null) !== $id) {
return null;
}
return [
'ok' => (bool) ($data['ok'] ?? false),
'tag' => isset($data['tag']) && is_string($data['tag']) ? $data['tag'] : null,
'message' => (string) ($data['message'] ?? ''),
];
}
}