Files
agentic-coding-workflow/internal/jobs/notifier.go
T

81 lines
2.3 KiB
Go

package jobs
import (
"context"
"encoding/json"
"time"
"github.com/yan1h/agent-coding-workflow/internal/infra/clock"
"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
)
// WebhookNotifierConfig drives the WebhookNotifier. Backoff defines the retry
// schedule passed to the worker via job MaxAttempts (len(Backoff)+1, since
// the first attempt is "attempt 1" not a retry). Topics is the whitelist of
// topics that get enqueued; everything else is dropped.
type WebhookNotifierConfig struct {
Enabled bool
Topics []string
Backoff []time.Duration
}
// WebhookNotifier implements notify.Notifier. Its Send method does NOT actually
// deliver — it enqueues a webhook.deliver job. The Worker + WebhookHandler
// performs the real HTTP call. This indirection gives us retry / dead-letter /
// crash safety without coupling notify.Dispatcher to the jobs DB.
type WebhookNotifier struct {
repo Repository
cfg WebhookNotifierConfig
clk clock.Clock
}
// NewWebhookNotifier constructs the notifier.
func NewWebhookNotifier(repo Repository, cfg WebhookNotifierConfig, clk clock.Clock) *WebhookNotifier {
if clk == nil {
clk = clock.Real()
}
return &WebhookNotifier{repo: repo, cfg: cfg, clk: clk}
}
// Name reports the notify.Channel for this notifier.
func (n *WebhookNotifier) Name() notify.Channel { return notify.ChannelWebhook }
// Send enqueues a webhook.deliver job. Disabled config / unmatched topics are
// silently skipped (returning nil, since the dispatcher's contract treats
// per-channel failures as non-fatal anyway).
func (n *WebhookNotifier) Send(ctx context.Context, msg notify.Message) error {
if !n.cfg.Enabled {
return nil
}
if !topicAllowed(n.cfg.Topics, msg.Topic) {
return nil
}
payload, err := json.Marshal(map[string]any{
"topic": msg.Topic,
"severity": string(msg.Severity),
"title": msg.Title,
"body": msg.Body,
"link": msg.Link,
"metadata": msg.Metadata,
"occurred_at": msg.CreatedAt.UTC().Format(time.RFC3339),
})
if err != nil {
return err
}
maxAttempts := int32(len(n.cfg.Backoff) + 1)
if maxAttempts < 1 {
maxAttempts = 1
}
_, err = n.repo.Enqueue(ctx, TypeWebhookDeliver, payload, n.clk.Now(), maxAttempts)
return err
}
func topicAllowed(allow []string, t string) bool {
for _, x := range allow {
if x == t {
return true
}
}
return false
}