diff --git a/cmd/server/main.go b/cmd/server/main.go index 2659d4e..8fa56f5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -16,6 +16,7 @@ import ( "github.com/mattnite/forgejo-tickets/internal/forgejo" adminhandlers "github.com/mattnite/forgejo-tickets/internal/handlers/admin" publichandlers "github.com/mattnite/forgejo-tickets/internal/handlers/public" + forgejosync "github.com/mattnite/forgejo-tickets/internal/sync" "github.com/mattnite/forgejo-tickets/internal/templates" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -53,6 +54,7 @@ func main() { defer cancel() go sessionStore.Cleanup(ctx, 30*time.Minute) + go forgejosync.SyncUnsyncedTickets(ctx, db, forgejoClient) publicRouter := publichandlers.NewRouter(publichandlers.Dependencies{ DB: db, diff --git a/internal/sync/sync.go b/internal/sync/sync.go new file mode 100644 index 0000000..e8aad89 --- /dev/null +++ b/internal/sync/sync.go @@ -0,0 +1,110 @@ +package sync + +import ( + "context" + "math" + "time" + + "github.com/mattnite/forgejo-tickets/internal/forgejo" + "github.com/mattnite/forgejo-tickets/internal/models" + "github.com/rs/zerolog/log" + "gorm.io/gorm" +) + +const ( + initialBackoff = 5 * time.Second + maxBackoff = 5 * time.Minute + maxRetries = 10 +) + +// SyncUnsyncedTickets runs a background loop that finds tickets without a +// Forgejo issue number and creates issues for them, retrying with exponential +// backoff. It stops when ctx is cancelled or all tickets are synced. +func SyncUnsyncedTickets(ctx context.Context, db *gorm.DB, client *forgejo.Client) { + backoff := initialBackoff + + for attempt := 0; attempt < maxRetries; attempt++ { + var tickets []models.Ticket + if err := db.Preload("Repo").Preload("User"). + Where("forgejo_issue_number IS NULL"). + Find(&tickets).Error; err != nil { + log.Error().Err(err).Msg("sync: failed to query unsynced tickets") + if !sleep(ctx, backoff) { + return + } + backoff = nextBackoff(backoff) + continue + } + + if len(tickets) == 0 { + log.Info().Msg("sync: all tickets synced") + return + } + + log.Info().Int("count", len(tickets)).Msg("sync: found unsynced tickets") + + allSynced := true + for _, ticket := range tickets { + if err := syncTicket(ctx, db, client, ticket); err != nil { + log.Error().Err(err).Str("ticket_id", ticket.ID.String()).Msg("sync: failed to sync ticket") + allSynced = false + } + } + + if allSynced { + log.Info().Msg("sync: all tickets synced successfully") + return + } + + if !sleep(ctx, backoff) { + return + } + backoff = nextBackoff(backoff) + } + + log.Warn().Msg("sync: gave up after max retries, remaining unsynced tickets will be retried on next restart") +} + +func syncTicket(_ context.Context, db *gorm.DB, client *forgejo.Client, ticket models.Ticket) error { + // Look up or create the "customer" label + var labelIDs []int64 + label, err := client.GetOrCreateLabel(ticket.Repo.ForgejoOwner, ticket.Repo.ForgejoRepo, "customer", "#0075ca") + if err != nil { + log.Error().Err(err).Str("ticket_id", ticket.ID.String()).Msg("sync: failed to get/create label") + } else { + labelIDs = append(labelIDs, label.ID) + } + + issue, err := client.CreateIssue(ticket.Repo.ForgejoOwner, ticket.Repo.ForgejoRepo, forgejo.CreateIssueRequest{ + Title: ticket.Title, + Body: ticket.Description + "\n\n---\n*Submitted by: " + ticket.User.Email + "*", + Labels: labelIDs, + }) + if err != nil { + return err + } + + if err := db.Model(&ticket).Update("forgejo_issue_number", issue.Number).Error; err != nil { + return err + } + + log.Info().Str("ticket_id", ticket.ID.String()).Int64("issue_number", issue.Number).Msg("sync: ticket synced") + return nil +} + +func sleep(ctx context.Context, d time.Duration) bool { + select { + case <-ctx.Done(): + return false + case <-time.After(d): + return true + } +} + +func nextBackoff(current time.Duration) time.Duration { + next := time.Duration(float64(current) * math.Phi) + if next > maxBackoff { + return maxBackoff + } + return next +} diff --git a/web/templates/pages/admin/users/detail.html b/web/templates/pages/admin/users/detail.html index e2d3139..7071dc5 100644 --- a/web/templates/pages/admin/users/detail.html +++ b/web/templates/pages/admin/users/detail.html @@ -67,7 +67,7 @@ {{.Title}} - {{.RepoName}} + {{.Repo.Name}} {{statusBadge (print .Status)}} {{formatDate .CreatedAt}} diff --git a/web/templates/pages/tickets/list.html b/web/templates/pages/tickets/list.html index d326260..7f33936 100644 --- a/web/templates/pages/tickets/list.html +++ b/web/templates/pages/tickets/list.html @@ -24,7 +24,7 @@ {{.Title}} - {{.RepoName}} + {{.Repo.Name}} {{statusBadge (print .Status)}} {{formatDate .CreatedAt}}