Fixes
This commit is contained in:
parent
61e9f00b1c
commit
57a8bb5a5e
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/mattnite/forgejo-tickets/internal/forgejo"
|
"github.com/mattnite/forgejo-tickets/internal/forgejo"
|
||||||
adminhandlers "github.com/mattnite/forgejo-tickets/internal/handlers/admin"
|
adminhandlers "github.com/mattnite/forgejo-tickets/internal/handlers/admin"
|
||||||
publichandlers "github.com/mattnite/forgejo-tickets/internal/handlers/public"
|
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/mattnite/forgejo-tickets/internal/templates"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -53,6 +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)
|
||||||
|
|
||||||
publicRouter := publichandlers.NewRouter(publichandlers.Dependencies{
|
publicRouter := publichandlers.NewRouter(publichandlers.Dependencies{
|
||||||
DB: db,
|
DB: db,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<a href="/tickets/{{.ID}}" class="text-sm font-medium text-blue-600 hover:text-blue-500">{{.Title}}</a>
|
<a href="/tickets/{{.ID}}" class="text-sm font-medium text-blue-600 hover:text-blue-500">{{.Title}}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-sm text-gray-500">{{.RepoName}}</td>
|
<td class="px-4 py-3 text-sm text-gray-500">{{.Repo.Name}}</td>
|
||||||
<td class="px-4 py-3">{{statusBadge (print .Status)}}</td>
|
<td class="px-4 py-3">{{statusBadge (print .Status)}}</td>
|
||||||
<td class="px-4 py-3 text-sm text-gray-500">{{formatDate .CreatedAt}}</td>
|
<td class="px-4 py-3 text-sm text-gray-500">{{formatDate .CreatedAt}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<a href="/tickets/{{.ID}}" class="text-sm font-medium text-blue-600 hover:text-blue-500">{{.Title}}</a>
|
<a href="/tickets/{{.ID}}" class="text-sm font-medium text-blue-600 hover:text-blue-500">{{.Title}}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-sm text-gray-500">{{.RepoName}}</td>
|
<td class="px-4 py-3 text-sm text-gray-500">{{.Repo.Name}}</td>
|
||||||
<td class="px-4 py-3">{{statusBadge (print .Status)}}</td>
|
<td class="px-4 py-3">{{statusBadge (print .Status)}}</td>
|
||||||
<td class="px-4 py-3 text-sm text-gray-500">{{formatDate .CreatedAt}}</td>
|
<td class="px-4 py-3 text-sm text-gray-500">{{formatDate .CreatedAt}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue