Add retry mechanism to Git clone and GitHub release fetching methods

This commit is contained in:
crazywhalecc
2026-05-09 14:13:47 +08:00
parent 2ed4b10260
commit 7b79767355
6 changed files with 35 additions and 21 deletions

View File

@@ -23,13 +23,13 @@ class go_win
$pkgroot = PKG_ROOT_PATH; $pkgroot = PKG_ROOT_PATH;
// get version // get version
[$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text', retries: $downloader->getRetry()) ?: '');
if ($version === '') { if ($version === '') {
throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text');
} }
// find SHA256 hash from download page // find SHA256 hash from download page
$page = default_shell()->executeCurl('https://go.dev/dl/'); $page = default_shell()->executeCurl('https://go.dev/dl/', retries: $downloader->getRetry());
if ($page === '' || $page === false) { if ($page === '' || $page === false) {
throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/');
} }

View File

@@ -39,11 +39,11 @@ class go_xcaddy
}; };
// get version and hash // get version and hash
[$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text', retries: $downloader->getRetry()) ?: '');
if ($version === '') { if ($version === '') {
throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text');
} }
$page = default_shell()->executeCurl('https://go.dev/dl/'); $page = default_shell()->executeCurl('https://go.dev/dl/', retries: $downloader->getRetry());
if ($page === '' || $page === false) { if ($page === '' || $page === false) {
throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/');
} }

View File

@@ -20,7 +20,7 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface
// direct branch clone // direct branch clone
if (isset($config['rev'])) { if (isset($config['rev'])) {
default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null, $downloader->getRetry());
$shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false);
$hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse --short HEAD'); $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse --short HEAD');
$hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : '';
@@ -66,7 +66,7 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface
$version = array_key_first($matched_version_branch); $version = array_key_first($matched_version_branch);
$branch = $matched_version_branch[$version]; $branch = $matched_version_branch[$version];
logger()->info("Matched version {$version} from branch {$branch} for {$name}"); logger()->info("Matched version {$version} from branch {$branch} for {$name}");
default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null); default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null, $downloader->getRetry());
return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class);
} }
throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches)."); throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches).");

View File

@@ -21,13 +21,13 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckU
private ?string $version = null; private ?string $version = null;
public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true, ?string $query = null): array public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true, ?string $query = null, int $retries = 0): array
{ {
logger()->debug("Fetching {$name} GitHub releases from {$repo}"); logger()->debug("Fetching {$name} GitHub releases from {$repo}");
$url = str_replace('{repo}', $repo, self::API_URL); $url = str_replace('{repo}', $repo, self::API_URL);
$url .= ($query ?? ''); $url .= ($query ?? '');
$headers = $this->getGitHubTokenHeaders(); $headers = $this->getGitHubTokenHeaders();
$data2 = default_shell()->executeCurl($url, headers: $headers); $data2 = default_shell()->executeCurl($url, headers: $headers, retries: $retries);
$data = json_decode($data2 ?: '', true); $data = json_decode($data2 ?: '', true);
if (!is_array($data)) { if (!is_array($data)) {
throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}");
@@ -46,13 +46,13 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckU
* Get the latest GitHub release assets for a given repository. * Get the latest GitHub release assets for a given repository.
* match_asset is provided, only return the asset that matches the regex. * match_asset is provided, only return the asset that matches the regex.
*/ */
public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null): array public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null, int $retries = 0): array
{ {
logger()->debug("Fetching {$name} GitHub release from {$repo}"); logger()->debug("Fetching {$name} GitHub release from {$repo}");
$url = str_replace('{repo}', $repo, self::API_URL); $url = str_replace('{repo}', $repo, self::API_URL);
$url .= ($query ?? ''); $url .= ($query ?? '');
$headers = $this->getGitHubTokenHeaders(); $headers = $this->getGitHubTokenHeaders();
$data2 = default_shell()->executeCurl($url, headers: $headers); $data2 = default_shell()->executeCurl($url, headers: $headers, retries: $retries);
$data = json_decode($data2 ?: '', true); $data = json_decode($data2 ?: '', true);
if (!is_array($data)) { if (!is_array($data)) {
throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}");
@@ -84,7 +84,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckU
if (!isset($config['match'])) { if (!isset($config['match'])) {
throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}");
} }
$rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); $rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null, $downloader->getRetry());
// download file using curl // download file using curl
$asset_url = str_replace(['{repo}', '{id}'], [$config['repo'], $rel['id']], self::ASSET_URL); $asset_url = str_replace(['{repo}', '{id}'], [$config['repo'], $rel['id']], self::ASSET_URL);
@@ -124,7 +124,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckU
if (!isset($config['match'])) { if (!isset($config['match'])) {
throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}");
} }
$this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null, $downloader->getRetry());
$new_version = $this->version ?? $old_version ?? ''; $new_version = $this->version ?? $old_version ?? '';
return new CheckUpdateResult( return new CheckUpdateResult(
old: $old_version, old: $old_version,

View File

@@ -22,11 +22,11 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface
* Get the GitHub tarball URL for a given repository and release type. * Get the GitHub tarball URL for a given repository and release type.
* If match_url is provided, only return the tarball that matches the regex. * If match_url is provided, only return the tarball that matches the regex.
*/ */
public function getGitHubTarballInfo(string $name, string $repo, string $rel_type, bool $prefer_stable = true, ?string $match_url = null, ?string $basename = null, ?string $query = null): array public function getGitHubTarballInfo(string $name, string $repo, string $rel_type, bool $prefer_stable = true, ?string $match_url = null, ?string $basename = null, ?string $query = null, int $retries = 0): array
{ {
if ($rel_type === 'releases' && $match_url === null && $query === null && $prefer_stable) { if ($rel_type === 'releases' && $match_url === null && $query === null && $prefer_stable) {
$api_url = str_replace(['{repo}', '{rel_type}'], [$repo, 'releases/latest'], self::API_URL); $api_url = str_replace(['{repo}', '{rel_type}'], [$repo, 'releases/latest'], self::API_URL);
$data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders()); $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders(), retries: $retries);
$data = json_decode($data ?: '', true); $data = json_decode($data ?: '', true);
if (!is_array($data) || empty($data['tarball_url'])) { if (!is_array($data) || empty($data['tarball_url'])) {
throw new DownloaderException("Failed to get GitHub latest release for {$repo} from {$api_url}"); throw new DownloaderException("Failed to get GitHub latest release for {$repo} from {$api_url}");
@@ -36,7 +36,7 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface
} else { } else {
$api_url = str_replace(['{repo}', '{rel_type}'], [$repo, $rel_type], self::API_URL); $api_url = str_replace(['{repo}', '{rel_type}'], [$repo, $rel_type], self::API_URL);
$api_url .= ($query ?? ''); $api_url .= ($query ?? '');
$data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders()); $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders(), retries: $retries);
$data = json_decode($data ?: '', true); $data = json_decode($data ?: '', true);
if (!is_array($data)) { if (!is_array($data)) {
throw new DownloaderException("Failed to get GitHub tarball URL for {$repo} from {$api_url}"); throw new DownloaderException("Failed to get GitHub tarball URL for {$repo} from {$api_url}");
@@ -65,7 +65,7 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface
} }
$this->version = $version ?? null; $this->version = $version ?? null;
} }
$head = default_shell()->executeCurl($rel_url, 'HEAD', headers: $this->getGitHubTokenHeaders()) ?: ''; $head = default_shell()->executeCurl($rel_url, 'HEAD', headers: $this->getGitHubTokenHeaders(), retries: $retries) ?: '';
preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?<filename>.+\.tar\.gz)\1/im', $head, $matches); preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?<filename>.+\.tar\.gz)\1/im', $head, $matches);
if ($matches) { if ($matches) {
$filename = $matches['filename']; $filename = $matches['filename'];
@@ -84,9 +84,9 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface
'ghtagtar' => 'tags', 'ghtagtar' => 'tags',
default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"),
}; };
[$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null, $downloader->getRetry());
$path = DOWNLOAD_PATH . "/{$filename}"; $path = DOWNLOAD_PATH . "/{$filename}";
default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders(), retries: $downloader->getRetry());
return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version, downloader: static::class); return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version, downloader: static::class);
} }
@@ -97,7 +97,7 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface
'ghtagtar' => 'tags', 'ghtagtar' => 'tags',
default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"),
}; };
$this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null, $downloader->getRetry());
$new_version = $this->version ?? $old_version ?? ''; $new_version = $this->version ?? $old_version ?? '';
return new CheckUpdateResult( return new CheckUpdateResult(
old: $old_version, old: $old_version,

View File

@@ -84,7 +84,7 @@ class DefaultShell extends Shell
/** /**
* Execute a Git clone command to clone a repository. * Execute a Git clone command to clone a repository.
*/ */
public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null): void public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null, int $retries = 0): void
{ {
$path = FileSystem::convertPath($path); $path = FileSystem::convertPath($path);
if (file_exists($path)) { if (file_exists($path)) {
@@ -99,7 +99,21 @@ class DefaultShell extends Shell
$cmd = clean_spaces("{$git} clone -c http.lowSpeedLimit=1 -c http.lowSpeedTime=3600 --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); $cmd = clean_spaces("{$git} clone -c http.lowSpeedLimit=1 -c http.lowSpeedTime=3600 --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}");
$this->logCommandInfo($cmd); $this->logCommandInfo($cmd);
logger()->debug("[GIT CLONE] {$cmd}"); logger()->debug("[GIT CLONE] {$cmd}");
$this->passthru($cmd, $this->console_putput); try {
$this->passthru($cmd, $this->console_putput);
} catch (InterruptException $e) {
throw $e;
} catch (\Throwable $e) {
if ($retries > 0) {
logger()->warning("Git clone failed, retrying... ({$retries} retries left)");
if (is_dir($path)) {
FileSystem::removeDir($path);
}
$this->executeGitClone($url, $branch, $path, $shallow, $submodules, $retries - 1);
return;
}
throw $e;
}
if ($submodules !== null) { if ($submodules !== null) {
$depth_flag = $shallow ? '--depth 1' : ''; $depth_flag = $shallow ? '--depth 1' : '';
foreach ($submodules as $submodule) { foreach ($submodules as $submodule) {