From 78d7291929abef279c8ccc939c399c3086527d54 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:56:13 +0200 Subject: [PATCH] fix(github): keep provided api_url on GitHub app updates --- app/Http/Controllers/Api/GithubController.php | 4 +- bootstrap/helpers/github.php | 55 +++++++++++++++++++ tests/Feature/GithubAppsListApiTest.php | 8 +++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index fa166d4f7..ac1e9cb6c 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -254,7 +254,7 @@ public function create_github_app(Request $request) $payload = [ 'uuid' => Str::uuid(), 'name' => $request->input('name'), - 'organization' => normalizeGithubOrganization($request->input('organization')), + 'organization' => $request->input('organization'), 'api_url' => githubApiUrlFromHtmlUrl($request->input('html_url')), 'html_url' => $request->input('html_url'), 'custom_user' => $request->input('custom_user', 'git'), @@ -650,8 +650,6 @@ public function update_github_app(Request $request, $github_app_id) } if (isset($payload['html_url'])) { $payload['api_url'] = githubApiUrlFromHtmlUrl($payload['html_url']); - } elseif (isset($payload['api_url'])) { - $payload['api_url'] = githubApiUrlFromHtmlUrl($githubApp->html_url); } // Handle private_key_uuid -> private_key_id conversion diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 476707cec..f1e131a1d 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -13,6 +13,12 @@ use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token\Builder; +/** + * Extract and normalize the hostname from a GitHub URL. + * + * @param string|null $url The URL to parse + * @return string|null The lowercase hostname, or null if the URL is blank or has no parseable host + */ function githubUrlHost(?string $url): ?string { if (blank($url)) { @@ -28,6 +34,17 @@ function githubUrlHost(?string $url): ?string return strtolower($host); } +/** + * Build the scheme://host[:port] origin for a GitHub URL. + * + * When the host cannot be parsed (e.g. a scheme-less or malformed URL such as + * "not-a-url"), the input is returned verbatim with any trailing slashes + * trimmed. Callers should pass already-validated URLs (see SafeExternalUrl), + * so this fallback only guards against unexpected input. + * + * @param string $url The URL to derive the origin from + * @return string The normalized origin, or the trimmed input when the host is unparseable + */ function githubUrlOrigin(string $url): string { $scheme = parse_url($url, PHP_URL_SCHEME) ?: 'https'; @@ -41,11 +58,21 @@ function githubUrlOrigin(string $url): string return $scheme.'://'.$host.($port ? ":{$port}" : ''); } +/** + * Determine whether the URL points at github.com. + * + * @param string|null $htmlUrl The GitHub HTML URL to check + */ function isGithubDotComHost(?string $htmlUrl): bool { return githubUrlHost($htmlUrl) === 'github.com'; } +/** + * Determine whether the URL points at a *.ghe.com GitHub Enterprise Cloud host. + * + * @param string|null $htmlUrl The GitHub HTML URL to check + */ function isGheDotComHost(?string $htmlUrl): bool { $host = githubUrlHost($htmlUrl); @@ -55,16 +82,32 @@ function isGheDotComHost(?string $htmlUrl): bool && ! Str::startsWith($host, 'api.'); } +/** + * Determine whether the URL belongs to GitHub's cloud family (github.com or *.ghe.com). + * + * @param string|null $htmlUrl The GitHub HTML URL to check + */ function isGithubCloudFamilyHost(?string $htmlUrl): bool { return isGithubDotComHost($htmlUrl) || isGheDotComHost($htmlUrl); } +/** + * Determine whether the URL belongs to a self-hosted GitHub Enterprise Server. + * + * @param string|null $htmlUrl The GitHub HTML URL to check + */ function isGithubEnterpriseServerHost(?string $htmlUrl): bool { return filled($htmlUrl) && ! isGithubCloudFamilyHost($htmlUrl); } +/** + * Derive the GitHub REST API base URL from a GitHub HTML URL. + * + * @param string $htmlUrl The GitHub HTML URL + * @return string The API base URL (api.github.com, api. for *.ghe.com, or /api/v3 for GHES) + */ function githubApiUrlFromHtmlUrl(string $htmlUrl): string { if (isGithubDotComHost($htmlUrl)) { @@ -78,6 +121,12 @@ function githubApiUrlFromHtmlUrl(string $htmlUrl): string return githubUrlOrigin($htmlUrl).'/api/v3'; } +/** + * Normalize a GitHub organization slug by trimming surrounding slashes and whitespace. + * + * @param string|null $organization The raw organization value + * @return string|null The trimmed organization, or null when blank + */ function normalizeGithubOrganization(?string $organization): ?string { if (blank($organization)) { @@ -87,6 +136,12 @@ function normalizeGithubOrganization(?string $organization): ?string return trim((string) $organization, "/ \t\n\r\0\x0B"); } +/** + * URL-encode a single GitHub path segment. + * + * @param string $segment The raw path segment + * @return string The raw-URL-encoded segment + */ function encodeGithubPathSegment(string $segment): string { return rawurlencode($segment); diff --git a/tests/Feature/GithubAppsListApiTest.php b/tests/Feature/GithubAppsListApiTest.php index 781c444d8..56deae733 100644 --- a/tests/Feature/GithubAppsListApiTest.php +++ b/tests/Feature/GithubAppsListApiTest.php @@ -27,6 +27,14 @@ ]); }); +/** + * Generate a temporary 2048-bit RSA private key for GitHub Apps API tests. + * + * Generated in-process so tests do not depend on external files or secrets; + * the key is used only as a signing fixture and is never persisted. + * + * @return string PEM-encoded RSA private key + */ function validGithubAppsApiPrivateKey(): string { $key = openssl_pkey_new([