@@ -120,22 +120,22 @@ final class EvoBootstrapper
120120 $ memoryLimit = ini_get ('memory_limit ' ) ?: 'Unknown ' ;
121121
122122 $ items = [
123- ['key ' => 'os ' , 'label ' => $ os , 'level ' => 'ok ' ],
124- ['key ' => 'php ' , 'label ' => "PHP - {$ phpVersion }" , 'level ' => $ phpOk ? 'ok ' : 'error ' ],
125- ['key ' => 'composer ' , 'label ' => "Composer " . ($ composerVersion ? " - {$ composerVersion }" : '' ), 'level ' => $ composerOk ? 'ok ' : 'warn ' ],
126- ['key ' => 'pdo ' , 'label ' => 'PDO extension ' , 'level ' => $ pdoOk ? 'ok ' : 'error ' ],
123+ ['key ' => 'os ' , 'label ' => $ os , 'level ' => 'ok ' ],
124+ ['key ' => 'php ' , 'label ' => "PHP - {$ phpVersion }" , 'level ' => $ phpOk ? 'ok ' : 'error ' ],
125+ ['key ' => 'composer ' , 'label ' => "Composer " . ($ composerVersion ? " - {$ composerVersion }" : '' ), 'level ' => $ composerOk ? 'ok ' : 'warn ' ],
126+ ['key ' => 'pdo ' , 'label ' => 'PDO extension ' , 'level ' => $ pdoOk ? 'ok ' : 'error ' ],
127127 // DB drivers: if unavailable, mark as warning (optional until user chooses a DB).
128- ['key ' => 'pdo_mysql ' , 'label ' => 'PDO MySQL driver ' , 'level ' => $ pdoMysqlOk ? 'ok ' : 'warn ' ],
129- ['key ' => 'pdo_pgsql ' , 'label ' => 'PDO PostgreSQL driver ' , 'level ' => $ pdoPgsqlOk ? 'ok ' : 'warn ' ],
130- ['key ' => 'pdo_sqlite ' , 'label ' => 'PDO SQLite driver ' , 'level ' => $ pdoSqliteOk ? 'ok ' : 'warn ' ],
131- ['key ' => 'pdo_sqlsrv ' , 'label ' => 'PDO SQL Server driver ' , 'level ' => $ pdoSqlsrvOk ? 'ok ' : 'warn ' ],
132- ['key ' => 'json ' , 'label ' => 'JSON extension ' , 'level ' => $ jsonOk ? 'ok ' : 'error ' ],
133- ['key ' => 'mysqli ' , 'label ' => 'MySQLi extension ' , 'level ' => $ mysqliOk ? 'ok ' : 'error ' ],
134- ['key ' => 'mbstring ' , 'label ' => 'MBString extension ' , 'level ' => $ mbstringOk ? 'ok ' : 'error ' ],
135- ['key ' => 'curl ' , 'label ' => 'cURL extension ' , 'level ' => $ curlOk ? 'ok ' : 'warn ' ],
136- ['key ' => 'image ' , 'label ' => $ imageLabel , 'level ' => $ imageOk ? 'ok ' : 'warn ' ],
137- ['key ' => 'disk_free ' , 'label ' => 'Disk free - ' . ($ diskFree ?: 'Unknown ' ), 'level ' => $ diskFree !== null ? 'ok ' : 'warn ' ],
138- ['key ' => 'memory_limit ' , 'label ' => 'Memory limit - ' . $ memoryLimit , 'level ' => 'ok ' ],
128+ ['key ' => 'pdo_mysql ' , 'label ' => 'PDO MySQL driver ' , 'level ' => $ pdoMysqlOk ? 'ok ' : 'warn ' ],
129+ ['key ' => 'pdo_pgsql ' , 'label ' => 'PDO PostgreSQL driver ' , 'level ' => $ pdoPgsqlOk ? 'ok ' : 'warn ' ],
130+ ['key ' => 'pdo_sqlite ' , 'label ' => 'PDO SQLite driver ' , 'level ' => $ pdoSqliteOk ? 'ok ' : 'warn ' ],
131+ ['key ' => 'pdo_sqlsrv ' , 'label ' => 'PDO SQL Server driver ' , 'level ' => $ pdoSqlsrvOk ? 'ok ' : 'warn ' ],
132+ ['key ' => 'json ' , 'label ' => 'JSON extension ' , 'level ' => $ jsonOk ? 'ok ' : 'error ' ],
133+ ['key ' => 'mysqli ' , 'label ' => 'MySQLi extension ' , 'level ' => $ mysqliOk ? 'ok ' : 'error ' ],
134+ ['key ' => 'mbstring ' , 'label ' => 'MBString extension ' , 'level ' => $ mbstringOk ? 'ok ' : 'error ' ],
135+ ['key ' => 'curl ' , 'label ' => 'cURL extension ' , 'level ' => $ curlOk ? 'ok ' : 'warn ' ],
136+ ['key ' => 'image ' , 'label ' => $ imageLabel , 'level ' => $ imageOk ? 'ok ' : 'warn ' ],
137+ ['key ' => 'disk_free ' , 'label ' => 'Disk free - ' . ($ diskFree ?: 'Unknown ' ), 'level ' => $ diskFree !== null ? 'ok ' : 'warn ' ],
138+ ['key ' => 'memory_limit ' , 'label ' => 'Memory limit - ' . $ memoryLimit , 'level ' => 'ok ' ],
139139 ];
140140
141141 $ overall = 'ok ' ;
@@ -150,9 +150,9 @@ final class EvoBootstrapper
150150 }
151151
152152 return [
153- 'status ' => $ overall ,
154- 'overall ' => $ overall ,
155- 'items ' => $ items ,
153+ 'status ' => $ overall ,
154+ 'overall ' => $ overall ,
155+ 'items ' => $ items ,
156156 ];
157157 }
158158
@@ -240,7 +240,11 @@ final class EvoBootstrapper
240240 private function selfUpdate (): void
241241 {
242242 $ current = $ this ->getInstalledVersion () ?? '(not installed) ' ;
243- [$ tag ] = $ this ->getLatestReleaseInfo ();
243+ try {
244+ [$ tag ] = $ this ->getLatestReleaseInfo ();
245+ } catch (RuntimeException $ e ) {
246+ $ tag = $ this ->resolveLatestReleaseTagFallback () ?? 'latest ' ;
247+ }
244248 $ this ->out ("Current: {$ current }" );
245249 $ this ->out ("Latest: {$ tag }" );
246250 $ this ->installBinary (true );
@@ -250,11 +254,19 @@ final class EvoBootstrapper
250254 {
251255 $ this ->assertWritableDir ($ this ->binDir );
252256
253- [$ tag , $ assets ] = $ this ->getLatestReleaseInfo ();
254257 [$ expectedAssetName , $ installedBinaryPath ] = $ this ->resolveAssetNameAndBinaryPath ();
255258
256- $ checksumsUrl = $ this ->findAssetUrl ($ assets , 'checksums.txt ' );
257- $ binaryUrl = $ this ->findAssetUrl ($ assets , $ expectedAssetName );
259+ try {
260+ [$ tag , $ assets ] = $ this ->getLatestReleaseInfo ();
261+ $ checksumsUrl = $ this ->findAssetUrl ($ assets , 'checksums.txt ' );
262+ $ binaryUrl = $ this ->findAssetUrl ($ assets , $ expectedAssetName );
263+ } catch (RuntimeException $ e ) {
264+ // GitHub API may return 403 due to rate limits or enterprise firewalls.
265+ // Fall back to direct "latest" asset URLs which do not require API access.
266+ $ tag = $ this ->resolveLatestReleaseTagFallback () ?? 'latest ' ;
267+ $ checksumsUrl = $ this ->getLatestDirectAssetUrl ('checksums.txt ' );
268+ $ binaryUrl = $ this ->getLatestDirectAssetUrl ($ expectedAssetName );
269+ }
258270
259271 $ checksums = $ this ->downloadString ($ checksumsUrl );
260272 $ shaMap = $ this ->parseChecksums ($ checksums );
@@ -385,6 +397,57 @@ final class EvoBootstrapper
385397 return [$ tag , $ assets ];
386398 }
387399
400+ private function getLatestDirectAssetUrl (string $ assetName ): string
401+ {
402+ $ assetName = ltrim ($ assetName , "/ \\" );
403+ $ url = 'https://github.com/ ' . self ::REPO . '/releases/latest/download/ ' . rawurlencode ($ assetName );
404+ $ this ->assertHttpsUrl ($ url );
405+ return $ url ;
406+ }
407+
408+ private function resolveLatestReleaseTagFallback (): ?string
409+ {
410+ $ url = 'https://github.com/ ' . self ::REPO . '/releases/latest ' ;
411+ $ this ->assertHttpsUrl ($ url );
412+
413+ if (function_exists ('curl_init ' )) {
414+ $ ch = curl_init ();
415+ if ($ ch !== false ) {
416+ curl_setopt ($ ch , CURLOPT_URL , $ url );
417+ curl_setopt ($ ch , CURLOPT_NOBODY , true );
418+ curl_setopt ($ ch , CURLOPT_FOLLOWLOCATION , false );
419+ curl_setopt ($ ch , CURLOPT_RETURNTRANSFER , true );
420+ curl_setopt ($ ch , CURLOPT_HEADER , true );
421+ curl_setopt ($ ch , CURLOPT_TIMEOUT , 30 );
422+ curl_setopt ($ ch , CURLOPT_HTTPHEADER , $ this ->createHttpHeaders (false ));
423+ curl_exec ($ ch );
424+ $ redirect = curl_getinfo ($ ch , CURLINFO_REDIRECT_URL );
425+ curl_close ($ ch );
426+
427+ if (is_string ($ redirect ) && $ redirect !== '' ) {
428+ if (preg_match ('~/releases/tag/([^/?#]+)~ ' , $ redirect , $ m )) {
429+ return $ m [1 ];
430+ }
431+ }
432+ }
433+ }
434+
435+ $ headers = @get_headers ($ url , true );
436+ if (is_array ($ headers )) {
437+ $ location = $ headers ['Location ' ] ?? $ headers ['location ' ] ?? null ;
438+ if (is_array ($ location )) {
439+ $ location = end ($ location ) ?: null ;
440+ }
441+ if (is_string ($ location ) && $ location !== '' ) {
442+ if (preg_match ('~/releases/tag/([^/?#]+)~ ' , $ location , $ m )) {
443+ return $ m [1 ];
444+ }
445+ }
446+ }
447+
448+ return null ;
449+ }
450+
388451 /**
389452 * @param array<int, array<string, mixed>> $assets
390453 */
@@ -538,7 +601,7 @@ final class EvoBootstrapper
538601 private function createHttpHeaders (bool $ isApi ): array
539602 {
540603 $ headers = [
541- 'User-Agent: evo-bootstrapper ' ,
604+ 'User-Agent: evo-bootstrapper ' ,
542605 ];
543606 if ($ isApi ) {
544607 $ headers [] = 'Accept: application/vnd.github+json ' ;
@@ -554,17 +617,17 @@ final class EvoBootstrapper
554617 private function createHttpContext (bool $ isApi ): mixed
555618 {
556619 return stream_context_create ([
557- 'http ' => [
558- 'method ' => 'GET ' ,
559- 'header ' => implode ("\r\n" , $ this ->createHttpHeaders ($ isApi )),
560- 'timeout ' => 60 ,
561- 'follow_location ' => 1 ,
562- 'max_redirects ' => 5 ,
563- ],
564- 'ssl ' => [
565- 'verify_peer ' => true ,
566- 'verify_peer_name ' => true ,
567- ],
620+ 'http ' => [
621+ 'method ' => 'GET ' ,
622+ 'header ' => implode ("\r\n" , $ this ->createHttpHeaders ($ isApi )),
623+ 'timeout ' => 60 ,
624+ 'follow_location ' => 1 ,
625+ 'max_redirects ' => 5 ,
626+ ],
627+ 'ssl ' => [
628+ 'verify_peer ' => true ,
629+ 'verify_peer_name ' => true ,
630+ ],
568631 ]);
569632 }
570633
0 commit comments