private async Task SyncTimerCallback(object state) { if (DateTimeOffset.UtcNow.Subtract(_lastSyncInterest) > SyncIdle) { DeactivateOnIdle(); return; } var users = await GetUsersWithAccess(); if (!users.Any()) { DeactivateOnIdle(); return; } var github = new GitHubActorPool(_grainFactory, users.Select(x => x.UserId)); IGitHubOrganizationAdmin admin = null; if (users.Any(x => x.IsAdmin)) { admin = _grainFactory.GetGrain <IGitHubActor>(users.First(x => x.IsAdmin).UserId); } var updater = new DataUpdater(_contextFactory, _mapper); try { await UpdateDetails(updater, github); await UpdateAdmins(updater, github); await UpdateProjects(updater, github); // Webhooks if (admin != null) { using (var context = _contextFactory.CreateInstance()) { updater.UnionWithExternalChanges(await AddOrUpdateOrganizationWebhooks(context, admin)); } } } catch (GitHubPoolEmptyException) { // Nothing to do. // No need to also catch GithubRateLimitException, it's handled by GitHubActorPool } // Send Changes. await updater.Changes.Submit(_queueClient); // Save await Save(); }
public async Task <IChangeSummary> AddOrUpdateOrganizationWebhooks(ShipHubContext context, IGitHubOrganizationAdmin admin) { var changes = ChangeSummary.Empty; var hook = await context.Hooks.AsNoTracking().SingleOrDefaultAsync(x => x.OrganizationId == _orgId); context.Database.Connection.Close(); // If our last operation on this repo hook resulted in error, delay. if (hook?.LastError != null && hook?.LastError.Value > DateTimeOffset.UtcNow.Subtract(HookErrorDelay)) { return(changes); // Wait to try later. } // There are now a few cases to handle // If there is no record of a hook, try to make one. // If there is an incomplete record, try to make it. // If there is an errored record, sleep or retry if (hook?.GitHubId == null) { // GitHub will immediately send a ping when the webhook is created. // To avoid any chance for a race, add the Hook to the DB first, then // create on GitHub. HookTableType newHook = null; if (hook == null) { newHook = await context.CreateHook(Guid.NewGuid(), string.Join(",", RequiredEvents), organizationId : _orgId); } else { newHook = new HookTableType() { Id = hook.Id, Secret = hook.Secret, Events = string.Join(",", RequiredEvents), }; } // Assume failure until we succeed newHook.LastError = DateTimeOffset.UtcNow; try { var hookList = await admin.OrganizationWebhooks(_login); if (!hookList.IsOk) { this.Info($"Unable to list hooks for {_login}. {hookList.Status} {hookList.Error}"); return(changes); } var existingHooks = hookList.Result .Where(x => x.Name.Equals("web")) .Where(x => x.Config.Url.StartsWith($"https://{_apiHostName}/", StringComparison.OrdinalIgnoreCase)); // Delete any existing hooks that already point back to us - don't // want to risk adding multiple Ship hooks. foreach (var existingHook in existingHooks) { var deleteResponse = await admin.DeleteOrganizationWebhook(_login, existingHook.Id); if (!deleteResponse.Succeeded) { this.Info($"Failed to delete existing hook ({existingHook.Id}) for org '{_login}' {deleteResponse.Status} {deleteResponse.Error}"); } } var addHookResponse = await admin.AddOrganizationWebhook( _login, new gh.Webhook() { Name = "web", Active = true, Events = RequiredEvents, Config = new gh.WebhookConfiguration() { Url = $"https://{_apiHostName}/webhook/org/{_orgId}", ContentType = "json", Secret = newHook.Secret.ToString(), }, }); if (addHookResponse.Succeeded) { newHook.GitHubId = addHookResponse.Result.Id; newHook.LastError = null; changes = await context.BulkUpdateHooks(hooks : new[] { newHook }); } else { this.Error($"Failed to add hook for org '{_login}' ({_orgId}): {addHookResponse.Status} {addHookResponse.Error}"); } } catch (Exception e) { e.Report($"Failed to add hook for org '{_login}' ({_orgId})"); // Save LastError await context.BulkUpdateHooks(hooks : new[] { newHook }); } } else if (!RequiredEvents.SetEquals(hook.Events.Split(','))) { var editHook = new HookTableType() { Id = hook.Id, GitHubId = hook.GitHubId, Secret = hook.Secret, Events = hook.Events, LastError = DateTimeOffset.UtcNow, // Default to faulted. }; try { this.Info($"Updating webhook {_login}/{hook.GitHubId} from [{hook.Events}] to [{string.Join(",", RequiredEvents)}]"); var editResponse = await admin.EditOrganizationWebhookEvents(_login, (long)hook.GitHubId, RequiredEvents); if (editResponse.Succeeded) { editHook.LastError = null; editHook.GitHubId = editResponse.Result.Id; editHook.Events = string.Join(",", editResponse.Result.Events); await context.BulkUpdateHooks(hooks : new[] { editHook }); } else if (editResponse.Status == HttpStatusCode.NotFound) { // Our record is out of date. this.Info($"Failed to edit hook for org '{_login}'. Deleting our hook record. {editResponse.Status} {editResponse.Error}"); changes = await context.BulkUpdateHooks(deleted : new[] { editHook.Id }); } else { throw new Exception($"Failed to edit hook for org '{_login}': {editResponse.Status} {editResponse.Error}"); } } catch (Exception e) { e.Report(); // Save LastError await context.BulkUpdateHooks(hooks : new[] { editHook }); } } return(changes); }