From 889c9492b1ad359bb8c990916c7dc82bea4715ae Mon Sep 17 00:00:00 2001 From: Joshua Kite Date: Fri, 21 Nov 2025 12:30:26 +0000 Subject: [PATCH] Fix GitHub App OAuth flow for repository updates - Make code parameter optional in callback handler - Add webhook handler for InstallationRepositoriesEvent - Add test coverage for repository add/remove events Fixes issue where users cannot add/remove individual repositories from existing GitHub App installations. GitHub only provides OAuth code during fresh installations, not during repository updates. --- backend/controllers/github.go | 14 +++++ backend/controllers/github_callback.go | 10 +++- backend/controllers/github_installation.go | 68 +++++++++++++++++++++- backend/controllers/github_test.go | 31 ++++++++++ 4 files changed, 120 insertions(+), 3 deletions(-) diff --git a/backend/controllers/github.go b/backend/controllers/github.go index 6accb0c34..42cf16580 100644 --- a/backend/controllers/github.go +++ b/backend/controllers/github.go @@ -74,6 +74,20 @@ func (d DiggerController) GithubAppWebHook(c *gin.Context) { return } } + case *github.InstallationRepositoriesEvent: + slog.Info("Processing InstallationRepositoriesEvent", + "action", *event.Action, + "installationId", *event.Installation.ID, + "repositoriesAdded", len(event.RepositoriesAdded), + "repositoriesRemoved", len(event.RepositoriesRemoved), + ) + + err := handleInstallationRepositoriesEvent(event, appId64) + if err != nil { + slog.Error("Failed to handle installation repositories event", "error", err) + c.String(http.StatusAccepted, "Failed to handle webhook event.") + return + } case *github.PushEvent: slog.Info("Processing PushEvent", "repo", *event.Repo.FullName, diff --git a/backend/controllers/github_callback.go b/backend/controllers/github_callback.go index 7b6d34ee3..e4810fa48 100644 --- a/backend/controllers/github_callback.go +++ b/backend/controllers/github_callback.go @@ -28,13 +28,19 @@ func (d DiggerController) GithubAppCallbackPage(c *gin.Context) { c.String(http.StatusBadRequest, "installation_id parameter for github app is empty") return } + //setupAction := c.Request.URL.Query()["setup_action"][0] codeParams, codeExists := c.Request.URL.Query()["code"] + + // Code parameter is only provided for fresh installations, not for updates + // If code is missing, this is likely an update - just show success page + // The actual repository changes will be handled by the InstallationRepositoriesEvent webhook if !codeExists || len(codeParams) == 0 { - slog.Error("There was no code in the url query parameters") - c.String(http.StatusBadRequest, "could not find the code query parameter for github app") + slog.Info("No code parameter - likely an installation update, showing success page") + c.HTML(http.StatusOK, "github_success.tmpl", gin.H{}) return } + code := codeParams[0] if len(code) < 1 { slog.Error("Code parameter is empty") diff --git a/backend/controllers/github_installation.go b/backend/controllers/github_installation.go index 1db95b13a..d0546350c 100644 --- a/backend/controllers/github_installation.go +++ b/backend/controllers/github_installation.go @@ -1,9 +1,10 @@ package controllers import ( + "log/slog" + "github.com/diggerhq/digger/backend/models" "github.com/google/go-github/v61/github" - "log/slog" ) func handleInstallationDeletedEvent(installation *github.InstallationEvent, appId int64) error { @@ -47,3 +48,68 @@ func handleInstallationDeletedEvent(installation *github.InstallationEvent, appI slog.Info("Successfully handled installation deleted event", "installationId", installationId) return nil } + +func handleInstallationRepositoriesEvent(event *github.InstallationRepositoriesEvent, appId int64) error { + installationId := *event.Installation.ID + action := *event.Action + + slog.Info("Handling installation repositories event", + "installationId", installationId, + "appId", appId, + "action", action, + "repositoriesAdded", len(event.RepositoriesAdded), + "repositoriesRemoved", len(event.RepositoriesRemoved), + ) + + // Handle removed repositories + for _, repo := range event.RepositoriesRemoved { + repoFullName := *repo.FullName + slog.Info("Removing repository from installation", + "installationId", installationId, + "repoFullName", repoFullName, + ) + + _, err := models.DB.GithubRepoRemoved(installationId, appId, repoFullName) + if err != nil { + slog.Error("Error removing GitHub repo", + "installationId", installationId, + "repoFullName", repoFullName, + "error", err, + ) + return err + } + } + + // Handle added repositories + for _, repo := range event.RepositoriesAdded { + repoFullName := *repo.FullName + slog.Info("Adding repository to installation", + "installationId", installationId, + "repoFullName", repoFullName, + ) + + _, err := models.DB.GithubRepoAdded( + installationId, + appId, + *event.Installation.Account.Login, + *event.Installation.Account.ID, + repoFullName, + ) + if err != nil { + slog.Error("Error adding GitHub repo", + "installationId", installationId, + "repoFullName", repoFullName, + "error", err, + ) + return err + } + } + + slog.Info("Successfully handled installation repositories event", + "installationId", installationId, + "repositoriesAdded", len(event.RepositoriesAdded), + "repositoriesRemoved", len(event.RepositoriesRemoved), + ) + + return nil +} diff --git a/backend/controllers/github_test.go b/backend/controllers/github_test.go index 15955f9ed..846710e78 100644 --- a/backend/controllers/github_test.go +++ b/backend/controllers/github_test.go @@ -866,3 +866,34 @@ func TestJobsTreeWithThreeLevels(t *testing.T) { assert.NoError(t, err) assert.Equal(t, result["333"].DiggerJobID, parentLinks[0].ParentDiggerJobId) } + +func TestHandleInstallationRepositoriesEvent(t *testing.T) { + teardownSuite, database := setupSuite(t) + defer teardownSuite(t) + + // Test repositories added event + var addedEvent github.InstallationRepositoriesEvent + err := json.Unmarshal([]byte(installationRepositoriesAddedPayload), &addedEvent) + assert.NoError(t, err) + + err = handleInstallationRepositoriesEvent(&addedEvent, 360162) + assert.NoError(t, err) + + // Verify repo was added + installation, err := database.GetGithubAppInstallationByIdAndRepo(41584295, "diggerhq/test-github-action") + assert.NoError(t, err) + assert.NotNil(t, installation) + assert.Equal(t, "diggerhq/test-github-action", installation.Repo) + + // Test repositories removed event + var removedEvent github.InstallationRepositoriesEvent + err = json.Unmarshal([]byte(installationRepositoriesDeletedPayload), &removedEvent) + assert.NoError(t, err) + + err = handleInstallationRepositoriesEvent(&removedEvent, 360162) + assert.NoError(t, err) + + // Verify repo was removed (soft deleted) + installation, err = database.GetGithubAppInstallationByIdAndRepo(41584295, "diggerhq/test-github-action") + assert.Error(t, err) // Should error because repo is deleted +}