Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
605 changes: 605 additions & 0 deletions agent-tasks/webhook-based-repo-management.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/bootstrap/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ func Bootstrap(templates embed.FS, diggerController controllers.DiggerController

githubApiGroup := apiGroup.Group("/github")
githubApiGroup.POST("/link", controllers.LinkGithubInstallationToOrgApi)
githubApiGroup.POST("/resync", controllers.ResyncGithubInstallationApi)

vcsApiGroup := apiGroup.Group("/connections")
vcsApiGroup.GET("/:id", controllers.GetVCSConnection)
Expand Down
37 changes: 30 additions & 7 deletions backend/controllers/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,37 @@ func (d DiggerController) GithubAppWebHook(c *gin.Context) {
"installationId", *event.Installation.ID,
)

if *event.Action == "deleted" {
err := handleInstallationDeletedEvent(event, appId64)
if err != nil {
slog.Error("Failed to handle installation deleted event", "error", err)
c.String(http.StatusAccepted, "Failed to handle webhook event.")
return
// Run in goroutine to avoid webhook timeouts for large installations
go func(ctx context.Context) {
defer logging.InheritRequestLogger(ctx)()
if *event.Action == "deleted" {
if err := handleInstallationDeletedEvent(event, appId64); err != nil {
slog.Error("Failed to handle installation deleted event", "error", err)
}
} else if *event.Action == "created" || *event.Action == "unsuspended" || *event.Action == "new_permissions_accepted" {
// Use background context so work continues after HTTP response
if err := handleInstallationUpsertEvent(context.Background(), gh, event, appId64); err != nil {
slog.Error("Failed to handle installation upsert event", "error", err)
}
}
}
}(c.Request.Context())

case *github.InstallationRepositoriesEvent:
slog.Info("Processing InstallationRepositoriesEvent",
"action", event.GetAction(),
"installationId", event.Installation.GetID(),
"added", len(event.RepositoriesAdded),
"removed", len(event.RepositoriesRemoved),
)

// Run in goroutine to avoid webhook timeouts for large installations
go func(ctx context.Context) {
defer logging.InheritRequestLogger(ctx)()
// Use background context so work continues after HTTP response
if err := handleInstallationRepositoriesEvent(context.Background(), gh, event, appId64); err != nil {
slog.Error("Failed to handle installation repositories event", "error", err)
}
}(c.Request.Context())
case *github.PushEvent:
slog.Info("Processing PushEvent",
"repo", *event.Repo.FullName,
Expand Down
84 changes: 84 additions & 0 deletions backend/controllers/github_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import (

"github.com/diggerhq/digger/backend/middleware"
"github.com/diggerhq/digger/backend/models"
"github.com/diggerhq/digger/backend/utils"
ci_github "github.com/diggerhq/digger/libs/ci/github"
"github.com/gin-gonic/gin"
"github.com/google/go-github/v61/github"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -85,3 +88,84 @@ func LinkGithubInstallationToOrgApi(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "Successfully created Github installation link"})
return
}

func ResyncGithubInstallationApi(c *gin.Context) {
type ResyncInstallationRequest struct {
InstallationId string `json:"installation_id"`
}

var request ResyncInstallationRequest
if err := c.BindJSON(&request); err != nil {
slog.Error("Error binding JSON for resync", "error", err)
c.JSON(http.StatusBadRequest, gin.H{"status": "Invalid request format"})
return
}

installationId, err := strconv.ParseInt(request.InstallationId, 10, 64)
if err != nil {
slog.Error("Failed to convert InstallationId to int64", "installationId", request.InstallationId, "error", err)
c.JSON(http.StatusBadRequest, gin.H{"status": "installationID should be a valid integer"})
return
}

link, err := models.DB.GetGithubAppInstallationLink(installationId)
if err != nil {
slog.Error("Could not get installation link for resync", "installationId", installationId, "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"status": "Could not get installation link"})
return
}
if link == nil {
slog.Warn("Installation link not found for resync", "installationId", installationId)
c.JSON(http.StatusNotFound, gin.H{"status": "Installation link not found"})
return
}

// Get appId from an existing installation record
var installationRecord models.GithubAppInstallation
if err := models.DB.GormDB.Where("github_installation_id = ?", installationId).Order("updated_at desc").First(&installationRecord).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
slog.Warn("No installation records found for resync", "installationId", installationId)
c.JSON(http.StatusNotFound, gin.H{"status": "No installation records found"})
return
}
slog.Error("Failed to fetch installation record for resync", "installationId", installationId, "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"status": "Could not fetch installation records"})
return
}

appId := installationRecord.GithubAppId
ghProvider := utils.DiggerGithubRealClientProvider{}

client, _, err := ghProvider.Get(appId, installationId)
if err != nil {
slog.Error("Failed to create GitHub client for resync", "installationId", installationId, "appId", appId, "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"status": "Failed to create GitHub client"})
return
}

repos, err := ci_github.ListGithubRepos(client)
if err != nil {
slog.Error("Failed to list repos for resync", "installationId", installationId, "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"status": "Failed to list repos for resync"})
return
}

// Build synthetic InstallationEvent and call upsert handler
installationPayload := &github.Installation{
ID: github.Int64(installationId),
AppID: github.Int64(appId),
}
resyncEvent := &github.InstallationEvent{
Installation: installationPayload,
Repositories: repos,
}

if err := handleInstallationUpsertEvent(c.Request.Context(), ghProvider, resyncEvent, appId); err != nil {
slog.Error("Resync failed", "installationId", installationId, "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"status": "Resync failed"})
return
}

slog.Info("Resync completed", "installationId", installationId, "repoCount", len(repos))
c.JSON(http.StatusOK, gin.H{"status": "Resync completed", "repoCount": len(repos)})
}
Loading
Loading