Sync comments

This commit is contained in:
Matthew Knight 2026-02-14 01:26:58 -08:00
parent 57a8bb5a5e
commit 2a21f6ba50
No known key found for this signature in database
2 changed files with 88 additions and 36 deletions

View File

@ -54,7 +54,7 @@ func main() {
defer cancel() defer cancel()
go sessionStore.Cleanup(ctx, 30*time.Minute) go sessionStore.Cleanup(ctx, 30*time.Minute)
go forgejosync.SyncUnsyncedTickets(ctx, db, forgejoClient) go forgejosync.SyncUnsynced(ctx, db, forgejoClient)
publicRouter := publichandlers.NewRouter(publichandlers.Dependencies{ publicRouter := publichandlers.NewRouter(publichandlers.Dependencies{
DB: db, DB: db,

View File

@ -17,56 +17,91 @@ const (
maxRetries = 10 maxRetries = 10
) )
// SyncUnsyncedTickets runs a background loop that finds tickets without a // SyncUnsynced runs a background loop that syncs tickets and comments
// Forgejo issue number and creates issues for them, retrying with exponential // that are missing their Forgejo counterparts, retrying with exponential
// backoff. It stops when ctx is cancelled or all tickets are synced. // backoff. It stops when ctx is cancelled or everything is synced.
func SyncUnsyncedTickets(ctx context.Context, db *gorm.DB, client *forgejo.Client) { func SyncUnsynced(ctx context.Context, db *gorm.DB, client *forgejo.Client) {
backoff := initialBackoff backoff := initialBackoff
for attempt := 0; attempt < maxRetries; attempt++ { for attempt := 0; attempt < maxRetries; attempt++ {
ticketsFailed := syncUnsyncedTickets(db, client)
commentsFailed := syncUnsyncedComments(db, client)
if !ticketsFailed && !commentsFailed {
log.Info().Msg("sync: everything synced successfully")
return
}
if !sleep(ctx, backoff) {
return
}
backoff = nextBackoff(backoff)
}
log.Warn().Msg("sync: gave up after max retries, remaining items will be retried on next restart")
}
// syncUnsyncedTickets syncs tickets without a Forgejo issue number.
// Returns true if any tickets failed to sync.
func syncUnsyncedTickets(db *gorm.DB, client *forgejo.Client) bool {
var tickets []models.Ticket var tickets []models.Ticket
if err := db.Preload("Repo").Preload("User"). if err := db.Preload("Repo").Preload("User").
Where("forgejo_issue_number IS NULL"). Where("forgejo_issue_number IS NULL").
Find(&tickets).Error; err != nil { Find(&tickets).Error; err != nil {
log.Error().Err(err).Msg("sync: failed to query unsynced tickets") log.Error().Err(err).Msg("sync: failed to query unsynced tickets")
if !sleep(ctx, backoff) { return true
return
}
backoff = nextBackoff(backoff)
continue
} }
if len(tickets) == 0 { if len(tickets) == 0 {
log.Info().Msg("sync: all tickets synced") return false
return
} }
log.Info().Int("count", len(tickets)).Msg("sync: found unsynced tickets") log.Info().Int("count", len(tickets)).Msg("sync: found unsynced tickets")
allSynced := true anyFailed := false
for _, ticket := range tickets { for _, ticket := range tickets {
if err := syncTicket(ctx, db, client, ticket); err != nil { if err := syncTicket(db, client, ticket); err != nil {
log.Error().Err(err).Str("ticket_id", ticket.ID.String()).Msg("sync: failed to sync ticket") log.Error().Err(err).Str("ticket_id", ticket.ID.String()).Msg("sync: failed to sync ticket")
allSynced = false anyFailed = true
} }
} }
return anyFailed
}
if allSynced { // syncUnsyncedComments syncs comments that belong to tickets with a Forgejo
log.Info().Msg("sync: all tickets synced successfully") // issue but are missing their own Forgejo comment ID.
return // Returns true if any comments failed to sync.
func syncUnsyncedComments(db *gorm.DB, client *forgejo.Client) bool {
var comments []models.TicketComment
if err := db.Preload("Ticket").Preload("Ticket.Repo").Preload("User").
Where("forgejo_comment_id IS NULL").
Find(&comments).Error; err != nil {
log.Error().Err(err).Msg("sync: failed to query unsynced comments")
return true
} }
if !sleep(ctx, backoff) { if len(comments) == 0 {
return return false
}
backoff = nextBackoff(backoff)
} }
log.Warn().Msg("sync: gave up after max retries, remaining unsynced tickets will be retried on next restart") log.Info().Int("count", len(comments)).Msg("sync: found unsynced comments")
anyFailed := false
for _, comment := range comments {
if comment.Ticket.ForgejoIssueNumber == nil {
// Parent ticket hasn't been synced yet; skip until next round
anyFailed = true
continue
}
if err := syncComment(db, client, comment); err != nil {
log.Error().Err(err).Str("comment_id", comment.ID.String()).Msg("sync: failed to sync comment")
anyFailed = true
}
}
return anyFailed
} }
func syncTicket(_ context.Context, db *gorm.DB, client *forgejo.Client, ticket models.Ticket) error { func syncTicket(db *gorm.DB, client *forgejo.Client, ticket models.Ticket) error {
// Look up or create the "customer" label
var labelIDs []int64 var labelIDs []int64
label, err := client.GetOrCreateLabel(ticket.Repo.ForgejoOwner, ticket.Repo.ForgejoRepo, "customer", "#0075ca") label, err := client.GetOrCreateLabel(ticket.Repo.ForgejoOwner, ticket.Repo.ForgejoRepo, "customer", "#0075ca")
if err != nil { if err != nil {
@ -92,6 +127,23 @@ func syncTicket(_ context.Context, db *gorm.DB, client *forgejo.Client, ticket m
return nil return nil
} }
func syncComment(db *gorm.DB, client *forgejo.Client, comment models.TicketComment) error {
repo := comment.Ticket.Repo
forgejoComment, err := client.CreateComment(repo.ForgejoOwner, repo.ForgejoRepo, *comment.Ticket.ForgejoIssueNumber, forgejo.CreateCommentRequest{
Body: comment.Body + "\n\n---\n*Comment by: " + comment.User.Email + "*",
})
if err != nil {
return err
}
if err := db.Model(&comment).Update("forgejo_comment_id", forgejoComment.ID).Error; err != nil {
return err
}
log.Info().Str("comment_id", comment.ID.String()).Int64("forgejo_comment_id", forgejoComment.ID).Msg("sync: comment synced")
return nil
}
func sleep(ctx context.Context, d time.Duration) bool { func sleep(ctx context.Context, d time.Duration) bool {
select { select {
case <-ctx.Done(): case <-ctx.Done():