0f60a7a5db
Build / build (push) Successful in 10m35s
Persists every inbound webhook hit (project + site) so users can debug
"why didn't my deploy fire?" without grepping daemon logs. Surfaces a
14-day rolling history under the WebhookPanel on each project + site
detail page; refreshes every 30s while open. Daily cron prunes records
older than 14 days alongside the existing event log prune.
Schema:
- webhook_deliveries(id, target_type, target_id, target_name, received_at,
source_ip, signature_state, status_code, outcome, detail, body_size)
- indexes on (target_type,target_id,received_at) and (received_at)
Backend:
- store: WebhookDelivery model + Insert/List/Prune helpers
- webhook/handler: deferred recordDelivery() captures the final outcome
on every return path including HMAC rejects, image mismatch, no-stage,
auto_deploy=false, and successful deploys; signatureStateFor()
classifies "unconfigured" vs "missing" vs "invalid" vs "valid"
- api: GET /api/{projects,sites}/{id}/webhook/deliveries with
parseLimit() helper (default 50, max 200)
- main: daily prune cron retains the last 14 days
Frontend:
- WebhookDeliveryLog.svelte: panel with refresh button, status code +
outcome + signature badges, relative time tooltip-on-hover for
absolute time, source IP column
- Mounted below WebhookPanel on project + site detail pages
- en/ru i18n strings for outcome/signature enums and column labels
85 lines
3.0 KiB
Go
85 lines
3.0 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
)
|
|
|
|
// WebhookDelivery is one persisted inbound webhook hit. Recorded after the
|
|
// handler decides what to do so the row reflects the final outcome.
|
|
type WebhookDelivery struct {
|
|
ID int64 `json:"id"`
|
|
TargetType string `json:"target_type"` // "project" or "site"
|
|
TargetID string `json:"target_id"`
|
|
TargetName string `json:"target_name"`
|
|
ReceivedAt string `json:"received_at"`
|
|
SourceIP string `json:"source_ip"`
|
|
SignatureState string `json:"signature_state"` // "valid" / "invalid" / "missing" / "unconfigured"
|
|
StatusCode int `json:"status_code"`
|
|
Outcome string `json:"outcome"` // "deploy" / "skip" / "rejected" / etc.
|
|
Detail string `json:"detail"`
|
|
BodySize int `json:"body_size"`
|
|
}
|
|
|
|
// InsertWebhookDelivery persists a single webhook delivery record. Best-effort
|
|
// — failures here must not block the actual webhook handler, so callers
|
|
// should log and continue rather than propagate.
|
|
func (s *Store) InsertWebhookDelivery(d WebhookDelivery) error {
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO webhook_deliveries
|
|
(target_type, target_id, target_name, source_ip, signature_state,
|
|
status_code, outcome, detail, body_size)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
d.TargetType, d.TargetID, d.TargetName, d.SourceIP, d.SignatureState,
|
|
d.StatusCode, d.Outcome, d.Detail, d.BodySize,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("insert webhook delivery: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListWebhookDeliveriesByTarget returns the most recent N deliveries for a
|
|
// specific target. Used by the per-entity panel on the project / site detail
|
|
// pages.
|
|
func (s *Store) ListWebhookDeliveriesByTarget(targetType, targetID string, limit int) ([]WebhookDelivery, error) {
|
|
if limit <= 0 || limit > 200 {
|
|
limit = 50
|
|
}
|
|
rows, err := s.db.Query(
|
|
`SELECT id, target_type, target_id, target_name, received_at, source_ip,
|
|
signature_state, status_code, outcome, detail, body_size
|
|
FROM webhook_deliveries
|
|
WHERE target_type = ? AND target_id = ?
|
|
ORDER BY id DESC
|
|
LIMIT ?`,
|
|
targetType, targetID, limit,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query webhook deliveries: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
out := []WebhookDelivery{}
|
|
for rows.Next() {
|
|
var d WebhookDelivery
|
|
if err := rows.Scan(&d.ID, &d.TargetType, &d.TargetID, &d.TargetName, &d.ReceivedAt,
|
|
&d.SourceIP, &d.SignatureState, &d.StatusCode, &d.Outcome, &d.Detail, &d.BodySize); err != nil {
|
|
return nil, fmt.Errorf("scan webhook delivery: %w", err)
|
|
}
|
|
out = append(out, d)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// PruneWebhookDeliveriesBefore deletes rows older than the given timestamp.
|
|
// Returns the number of rows removed. Run on the same daily cron as the
|
|
// event log to keep the table from growing without bound.
|
|
func (s *Store) PruneWebhookDeliveriesBefore(beforeTS string) (int64, error) {
|
|
res, err := s.db.Exec(`DELETE FROM webhook_deliveries WHERE received_at < ?`, beforeTS)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("prune webhook deliveries: %w", err)
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
return n, nil
|
|
}
|