ReleaseManager.php•5.48 kB
<?php
declare(strict_types=1);
namespace Butschster\ContextGenerator\Lib\GithubClient;
use Butschster\ContextGenerator\Lib\GithubClient\Model\GithubRepository;
use Butschster\ContextGenerator\Lib\GithubClient\Model\Release;
use Butschster\ContextGenerator\Lib\HttpClient\Exception\HttpException;
use Butschster\ContextGenerator\Lib\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface;
/**
 * Manages GitHub releases for a repository.
 */
final readonly class ReleaseManager
{
    public function __construct(
        private HttpClientInterface $httpClient,
        private GithubRepository $repository,
        private ?string $token = null,
        private BinaryNameBuilder $binaryNameBuilder = new BinaryNameBuilder(),
        private ?LoggerInterface $logger = null,
    ) {}
    /**
     * Get the latest release from GitHub.
     *
     * @throws \RuntimeException If the request fails
     */
    public function getLatestRelease(): Release
    {
        $url = \sprintf(
            'https://api.github.com/repos/%s/releases/latest',
            $this->repository->repository,
        );
        $response = $this->httpClient->get(
            $url,
            $this->getHeaders(),
        );
        if (!$response->isSuccess()) {
            throw new \RuntimeException(
                "Failed to fetch latest release. Server returned status code {$response->getStatusCode()}",
            );
        }
        try {
            $data = $response->getJson();
            return Release::fromApiResponse($data);
        } catch (HttpException $e) {
            throw new \RuntimeException("Failed to parse GitHub response: {$e->getMessage()}", 0, $e);
        }
    }
    /**
     * Attempts to download the appropriate binary for the current platform.
     * First tries the platform-specific binary, then falls back to the generic binary.
     */
    public function downloadBinary(string $version, string $binaryName, string $type, string $destinationPath): bool
    {
        // First try platform-specific binary
        try {
            $fileName = $this->binaryNameBuilder->buildPlatformSpecificName($binaryName, $version, $type);
            $this->logger?->info("Attempting to download platform-specific binary: {$fileName}");
            $assetUrl = $this->getAssetUrlOrFail($fileName);
            $this->downloadAsset($assetUrl, $destinationPath);
            $this->logger?->info("Successfully downloaded platform-specific binary: {$fileName}");
            return true;
        } catch (\Throwable $e) {
            $this->logger?->warning("Failed to download platform-specific binary: {$e->getMessage()}");
            // Fall back to generic binary
            try {
                $fileName = $this->binaryNameBuilder->buildGenericName($binaryName, $type);
                $this->logger?->info("Falling back to generic binary: {$fileName}");
                $assetUrl = $this->getAssetUrlOrFail($fileName);
                $this->downloadAsset($assetUrl, $destinationPath);
                $this->logger?->info("Successfully downloaded generic binary: {$fileName}");
                return true;
            } catch (\Throwable $e2) {
                $this->logger?->error("Failed to download generic binary: {$e2->getMessage()}");
                throw new \RuntimeException("Failed to download binary: {$e2->getMessage()}", 0, $e2);
            }
        }
    }
    /**
     * Download a release asset to a local file.
     */
    private function downloadAsset(string $assetUrl, string $destinationPath): void
    {
        $response = $this->httpClient->getWithRedirects(
            $assetUrl,
            $this->getHeaders(),
        );
        if (!$response->isSuccess()) {
            throw new \RuntimeException(
                "Failed to download asset. Server returned status code {$response->getStatusCode()}",
            );
        }
        // Write the file
        if (!\file_put_contents($destinationPath, $response->getBody())) {
            throw new \RuntimeException("Failed to write file: {$destinationPath}");
        }
        // Make the file executable if it's not a Windows system
        if (\PHP_OS_FAMILY !== 'Windows') {
            if (!\chmod($destinationPath, 0755)) {
                throw new \RuntimeException("Failed to set executable permissions on the file: {$destinationPath}");
            }
        }
    }
    /**
     * Gets the asset URL for a specific filename or throws an exception if not found.
     *
     * @param string $fileName Name of the file
     * @return string The asset URL
     * @throws \RuntimeException If the asset is not found
     */
    private function getAssetUrlOrFail(string $fileName): string
    {
        $release = $this->getLatestRelease();
        $assetUrl = $release->getAssetUrl($fileName);
        if ($assetUrl === null) {
            throw new \RuntimeException("Could not find asset '{$fileName}' in release {$release->getVersion()}");
        }
        return $assetUrl;
    }
    /**
     * Generate standard headers for GitHub API requests.
     *
     * @return array<string, string> Headers
     */
    private function getHeaders(): array
    {
        $headers = [
            'Accept' => 'application/vnd.github.v3+json',
            'User-Agent' => 'ContextGenerator',
        ];
        if ($this->token) {
            $headers['Authorization'] = 'token ' . $this->token;
        }
        return $headers;
    }
}