fix(github): paginate PR files for watch paths and reuse installation tokens

- Fetch every page of pull request files so watch paths match PRs with more than 30 files.
- Deploy the preview and log a warning when the changed-file list is unknown (API error or truncated compare list).
- Log when watch paths decline a preview deployment.
- Keep the GitHub App installation token in memory per instance, so a deployment requests one token instead of 3-5.

Closes #12169
This commit is contained in:
Andras Bacsai
2026-10-15 14:45:45 +02:00
parent 5f78e3dfb3
commit 2c973d68c4
6 changed files with 384 additions and 30 deletions
+30 -22
View File
@@ -216,7 +216,7 @@ function generateGithubToken(GithubApp $source, string $type)
function generateGithubInstallationToken(GithubApp $source)
{
return generateGithubToken($source, 'installation');
return $source->installationToken();
}
function generateGithubJwt(GithubApp $source)
@@ -397,28 +397,29 @@ function loadRepositoryByPage(GithubApp $source, string $token, int $page)
'repositories' => $json['repositories'],
];
}
function getGithubCommitRangeFiles(?GithubApp $source, string $owner, string $repo, string $beforeSha, string $afterSha): array
/**
* Returns null when the full list of changed files is unknown (API error or truncated list).
*/
function getGithubCommitRangeFiles(?GithubApp $source, string $owner, string $repo, string $beforeSha, string $afterSha): ?array
{
try {
if (! $source) {
// Manual webhooks don't have GitHub App authentication
// Return empty array so watch paths are ignored (current behavior)
return [];
}
$endpoint = "/repos/{$owner}/{$repo}/compare/{$beforeSha}...{$afterSha}";
$response = githubApi($source, $endpoint, 'get', null, false);
if (! $response) {
return [];
}
$response = githubApi($source, $endpoint);
$files = collect(data_get($response, 'data.files', []));
// The compare API returns at most 300 files and cannot paginate them.
if ($files->count() >= 300) {
return null;
}
return $files->pluck('filename')->filter()->values()->toArray();
} catch (Exception $e) {
return [];
return null;
}
}
@@ -444,27 +445,34 @@ function getGithubCommitMessage(?GithubApp $source, string $owner, string $repo,
}
}
function getGithubPullRequestFiles(?GithubApp $source, string $owner, string $repo, int $pullRequestId): array
/**
* Returns null when the full list of changed files is unknown (API error or truncated list).
*/
function getGithubPullRequestFiles(?GithubApp $source, string $owner, string $repo, int $pullRequestId): ?array
{
try {
if (! $source) {
// Manual webhooks don't have GitHub App authentication
// Return empty array so watch paths are ignored (current behavior)
return [];
}
$endpoint = "/repos/{$owner}/{$repo}/pulls/{$pullRequestId}/files";
$response = githubApi($source, $endpoint, 'get', null, false);
// GitHub returns at most 100 files per page and 3000 files per pull request.
$perPage = 100;
$maxPages = 30;
$files = collect();
if (! $response) {
return [];
for ($page = 1; $page <= $maxPages; $page++) {
$endpoint = "/repos/{$owner}/{$repo}/pulls/{$pullRequestId}/files?per_page={$perPage}&page={$page}";
$pageFiles = collect(data_get(githubApi($source, $endpoint), 'data', []));
$files = $files->merge($pageFiles->pluck('filename'));
if ($pageFiles->count() < $perPage) {
return $files->filter()->values()->toArray();
}
}
$files = collect(data_get($response, 'data', []));
return $files->pluck('filename')->filter()->values()->toArray();
return null;
} catch (Exception $e) {
return [];
return null;
}
}