You've already forked agentic-coding-workflow
b1ccce567e
Deferred audit retrofits from Plan #5 self-review: - WebhookNotifier now records a job.enqueue audit row after each successful repo.Enqueue (best-effort: failures are warn-logged, not propagated). Added optional audit.Recorder + *slog.Logger params to NewWebhookNotifier. - WorktreePrune.executionPhase now records a worktree.pruned audit row after each successful FinishPruneSuccess. Added optional audit.Recorder param to NewWorktreePrune. - app.go wires the existing auditRec into both constructors. - integration_test.go: drop the _ = auditRec suppression and pass auditRec into the webhook notifier so the closed-loop test exercises the audit emit path.
105 lines
3.0 KiB
Go
105 lines
3.0 KiB
Go
package jobs
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
|
"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
|
|
rec audit.Recorder
|
|
log *slog.Logger
|
|
}
|
|
|
|
// NewWebhookNotifier constructs the notifier. rec is optional; when non-nil the
|
|
// notifier emits a job.enqueue audit row after each successful Enqueue.
|
|
func NewWebhookNotifier(repo Repository, cfg WebhookNotifierConfig, clk clock.Clock, rec audit.Recorder, log *slog.Logger) *WebhookNotifier {
|
|
if clk == nil {
|
|
clk = clock.Real()
|
|
}
|
|
if log == nil {
|
|
log = slog.Default()
|
|
}
|
|
return &WebhookNotifier{repo: repo, cfg: cfg, clk: clk, rec: rec, log: log}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
enqueued, err := n.repo.Enqueue(ctx, TypeWebhookDeliver, payload, n.clk.Now(), maxAttempts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n.rec != nil {
|
|
if rerr := n.rec.Record(ctx, audit.Entry{
|
|
Action: "job.enqueue",
|
|
TargetType: "job",
|
|
TargetID: enqueued.ID.String(),
|
|
Metadata: map[string]any{
|
|
"topic": msg.Topic,
|
|
"job_type": string(TypeWebhookDeliver),
|
|
},
|
|
}); rerr != nil {
|
|
n.log.Warn("webhook_notifier.audit_failed", "job_id", enqueued.ID, "err", rerr.Error())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func topicAllowed(allow []string, t string) bool {
|
|
for _, x := range allow {
|
|
if x == t {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|