68 ILogger<GitHubClientFactory>
logger,
69 IOptions<GeneralConfiguration> generalConfigurationOptions)
72 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
73 generalConfiguration = generalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(generalConfigurationOptions));
75 clientCache =
new Dictionary<string, (GitHubClient, DateTimeOffset)>();
108 async ValueTask<IGitHubClient?>
GetOrCreateClient(
string? accessTokenOrSerializedPem,
long? installationRepositoryId, CancellationToken cancellationToken)
109#pragma warning restore CA1506
113 DateTimeOffset? lastUsed;
117 if (String.IsNullOrWhiteSpace(accessTokenOrSerializedPem))
119 accessTokenOrSerializedPem =
null;
123 cacheKey = accessTokenOrSerializedPem;
125 cacheHit =
clientCache.TryGetValue(cacheKey, out var tuple);
127 var now = DateTimeOffset.UtcNow;
130 logger.LogTrace(
"Creating new GitHubClient...");
132 client =
new GitHubClient(
133 new ProductHeaderValue(
137 if (accessTokenOrSerializedPem !=
null)
139 if (installationRepositoryId.HasValue)
141 logger.LogTrace(
"Performing GitHub App authentication for installation on repository {installationRepositoryId}", installationRepositoryId.Value);
142 var splits = accessTokenOrSerializedPem.Split(
':');
143 if (splits.Length != 2)
145 logger.LogError(
"Failed to parse serialized Client ID & PEM! Expected 2 chunks, got {chunkCount}", splits.Length);
152 pemBytes = Convert.FromBase64String(splits[1]);
156 logger.LogError(ex,
"Failed to parse supposed base64 PEM!");
160 var pem = Encoding.UTF8.GetString(pemBytes);
162 using var rsa = RSA.Create();
163 rsa.ImportFromPem(pem);
165 var signingCredentials =
new SigningCredentials(
new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
166 var jwtSecurityTokenHandler =
new JwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation =
false };
168 var nowDateTime = DateTime.UtcNow;
170 var jwt = jwtSecurityTokenHandler.CreateToken(
new SecurityTokenDescriptor
173 Expires = nowDateTime.AddMinutes(10),
174 IssuedAt = nowDateTime,
175 SigningCredentials = signingCredentials,
178 var jwtStr = jwtSecurityTokenHandler.WriteToken(jwt);
180 client.Credentials =
new Credentials(jwtStr, AuthenticationType.Bearer);
182 Installation installation;
185 installation = await client.GitHubApps.GetRepositoryInstallationForCurrent(installationRepositoryId.Value);
189 logger.LogError(ex,
"Failed to perform app authentication!");
193 cancellationToken.ThrowIfCancellationRequested();
196 var installToken = await client.GitHubApps.CreateInstallationToken(installation.Id);
198 client.Credentials =
new Credentials(installToken.Token);
202 logger.LogError(ex,
"Failed to perform installation authentication!");
207 client.Credentials =
new Credentials(accessTokenOrSerializedPem);
210 clientCache.Add(cacheKey, (Client: client, LastUsed: now));
215 logger.LogTrace(
"Cache hit for GitHubClient");
216 client = tuple.Client;
217 lastUsed = tuple.LastUsed;
218 tuple.LastUsed = now;
230 if (tuple.LastUsed <= purgeAfter)
239 "Pruned {count} expired GitHub client(s) from cache that haven't been used in {purgeAfterHours} days.",
244 var rateLimitInfo = client.GetLastApiInfo()?.RateLimit;
245 if (rateLimitInfo !=
null)
246 if (rateLimitInfo.Remaining == 0)
248 "Requested GitHub client has no requests remaining! Limit resets at {resetTime}",
249 rateLimitInfo.Reset.ToString(
"o"));
250 else if (rateLimitInfo.Remaining < 25)
252 "Requested GitHub client has only {remainingRequests} requests remaining after the usage at {lastUse}! Limit resets at {resetTime}",
253 rateLimitInfo.Remaining,
255 rateLimitInfo.Reset.ToString(
"o"));
258 "Requested GitHub client has {remainingRequests} requests remaining after the usage at {lastUse}. Limit resets at {resetTime}",
259 rateLimitInfo.Remaining,
261 rateLimitInfo.Reset.ToString(
"o"));