From 54b41591b951cddf62c5851c4624289a97314374 Mon Sep 17 00:00:00 2001 From: boban Date: Mon, 22 Jun 2026 23:14:34 +0200 Subject: [PATCH] =?UTF-8?q?feat(release):=20ReleaseBridge=20=E2=80=94=20wr?= =?UTF-8?q?ite=20staging=20request,=20read=20host=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- app/Services/ReleaseBridge.php | 64 ++++++++++++++++++++++++++++ tests/Feature/ReleaseBridgeTest.php | 66 +++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 app/Services/ReleaseBridge.php create mode 100644 tests/Feature/ReleaseBridgeTest.php diff --git a/app/Services/ReleaseBridge.php b/app/Services/ReleaseBridge.php new file mode 100644 index 0000000..0e1ac03 --- /dev/null +++ b/app/Services/ReleaseBridge.php @@ -0,0 +1,64 @@ + $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'] ?? ''), + ]; + } +} diff --git a/tests/Feature/ReleaseBridgeTest.php b/tests/Feature/ReleaseBridgeTest.php new file mode 100644 index 0000000..c74a406 --- /dev/null +++ b/tests/Feature/ReleaseBridgeTest.php @@ -0,0 +1,66 @@ +dir = storage_path('app/restart-signal'); + @mkdir($this->dir, 0775, true); + @unlink($this->dir.'/release-request.json'); + } + + protected function tearDown(): void + { + @unlink($this->dir.'/release-request.json'); + foreach (glob($this->dir.'/release-result-*.json') ?: [] as $f) { + @unlink($f); + } + parent::tearDown(); + } + + public function test_request_writes_a_validated_stage_request_and_returns_an_id(): void + { + $id = app(ReleaseBridge::class)->requestStaging('0.10.0'); + + $this->assertNotNull($id); + $this->assertMatchesRegularExpression('/^[A-Za-z0-9]{32}$/', $id); + $payload = json_decode((string) file_get_contents($this->dir.'/release-request.json'), true); + $this->assertSame('stage', $payload['action']); + $this->assertSame('0.10.0', $payload['target']); + $this->assertSame($id, $payload['id']); + } + + public function test_request_rejects_a_non_semver_target(): void + { + $this->expectException(\InvalidArgumentException::class); + app(ReleaseBridge::class)->requestStaging('garbage'); + } + + public function test_result_reads_the_host_result_for_the_issued_id(): void + { + $id = app(ReleaseBridge::class)->requestStaging('0.10.0'); + file_put_contents( + $this->dir."/release-result-{$id}.json", + json_encode(['id' => $id, 'ok' => true, 'tag' => 'v0.10.0-beta1', 'message' => 'ok']) + ); + + $res = app(ReleaseBridge::class)->result($id); + + $this->assertTrue($res['ok']); + $this->assertSame('v0.10.0-beta1', $res['tag']); + } + + public function test_result_is_null_until_the_host_replies(): void + { + $id = app(ReleaseBridge::class)->requestStaging('0.10.0'); + $this->assertNull(app(ReleaseBridge::class)->result($id)); + } +}