mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
feat(api): add REST API endpoints for repository webhook delivery history (#38775)
Webhook delivery history (request/response, status, timestamp) was only
reachable through the repo settings UI, which shows the last ~10
deliveries with no filtering. There was no way to fetch this data
programmatically for debugging integrations or auditing webhook
activity.
This adds two read endpoints at the repository level:
- `GET /repos/{owner}/{repo}/hooks/{id}/deliveries` — paginated list of
a hook's deliveries, most recent first
- `GET /repos/{owner}/{repo}/hooks/{id}/deliveries/{uuid}` — a single
delivery, including request/response headers, body, and status
Both reuse the existing `hook_task` storage and retention policy (the
`CleanupHookTaskTable` cron job) that already back the "Recent
Deliveries" web UI, and are gated by the same `reqAdmin()` +
`reqWebhooksEnabled()` guard as the existing hook endpoints.
Org, user, and admin/system-level webhooks are intentionally out of
scope for this PR to keep it reviewable; they can follow as separate
PRs using the same pattern.
## Usage
```bash
curl -H "Authorization: token $TOKEN" \
https://gitea.example.com/api/v1/repos/{owner}/{repo}/hooks/{id}/deliveries
curl -H "Authorization: token $TOKEN" \
https://gitea.example.com/api/v1/repos/{owner}/{repo}/hooks/{id}/deliveries/{uuid}
```
## Testing
- Unit tests for the new model-layer query functions
(`ListHookTasks`, `GetHookTaskByUUID`)
- Integration tests covering: list + get, an undelivered task's response
is `null`, a hook/delivery UUID from another repository 404s instead
of leaking across repos, and pagination (`page`/`limit`, `Link` and
`X-Total-Count` headers, correct ordering/no overlap across pages)
- `make lint-go`, `make generate-swagger`
Fixes: https://github.com/go-gitea/gitea/issues/38463.
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
11 files changed
+824
-6
No files matched your search
@@ -6,6 +6,7 @@ package webhook
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"maps"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
@@ -461,3 +462,39 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
||||
BranchFilter: w.BranchFilter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToHookDelivery(t *webhook_model.HookTask) *api.HookDelivery {
|
||||
delivery := &api.HookDelivery{
|
||||
UUID: t.UUID,
|
||||
EventType: string(t.EventType),
|
||||
IsDelivered: t.IsDelivered,
|
||||
IsSucceeded: t.IsSucceed,
|
||||
}
|
||||
if t.IsDelivered {
|
||||
delivery.Delivered = new(t.Delivered.AsTime())
|
||||
}
|
||||
if t.RequestInfo != nil {
|
||||
delivery.Request = &api.HookDeliveryRequest{
|
||||
URL: t.RequestInfo.URL,
|
||||
HTTPMethod: t.RequestInfo.HTTPMethod,
|
||||
Headers: maps.Clone(t.RequestInfo.Headers),
|
||||
Body: t.RequestInfo.Body,
|
||||
}
|
||||
for name := range delivery.Request.Headers {
|
||||
if strings.EqualFold(name, "Authorization") {
|
||||
delivery.Request.Headers[name] = "******"
|
||||
}
|
||||
}
|
||||
if t.PayloadVersion == 1 && delivery.Request.Body == "" {
|
||||
delivery.Request.Body = t.PayloadContent
|
||||
}
|
||||
}
|
||||
if t.ResponseInfo != nil {
|
||||
delivery.Response = &api.HookDeliveryResponse{
|
||||
Status: t.ResponseInfo.Status,
|
||||
Headers: t.ResponseInfo.Headers,
|
||||
Body: t.ResponseInfo.Body,
|
||||
}
|
||||
}
|
||||
return delivery
|
||||
}
|
||||
@@ -6,11 +6,47 @@ package webhook
|
||||
import (
|
||||
"testing"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
api "gitea.dev/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToHookDelivery(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
payloadVersion int
|
||||
requestContent string
|
||||
body string
|
||||
}{
|
||||
{"legacy payload", 1, `{"headers":{"Authorization":"stored credential","authorization":"lowercase credential","Content-Type":"application/json"}}`, "payload"},
|
||||
{"recorded body", 1, `{"body":"sent body"}`, "sent body"},
|
||||
{"empty current body", 2, `{}`, ""},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
task := &webhook_model.HookTask{
|
||||
PayloadVersion: test.payloadVersion,
|
||||
PayloadContent: "payload",
|
||||
RequestContent: test.requestContent,
|
||||
}
|
||||
task.AfterLoad()
|
||||
delivery := ToHookDelivery(task)
|
||||
require.NotNil(t, delivery.Request)
|
||||
assert.Equal(t, test.body, delivery.Request.Body)
|
||||
if task.RequestInfo.Headers != nil {
|
||||
assert.Equal(t, map[string]string{
|
||||
"Authorization": "******",
|
||||
"authorization": "******",
|
||||
"Content-Type": "application/json",
|
||||
}, delivery.Request.Headers)
|
||||
assert.Equal(t, "stored credential", task.RequestInfo.Headers["Authorization"])
|
||||
assert.Equal(t, "lowercase credential", task.RequestInfo.Headers["authorization"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createTestPayload() *api.CreatePayload {
|
||||
return &api.CreatePayload{
|
||||
Sha: "2020558fe2e34debb818a514715839cabd25e777",
|
||||
|
||||
Reference in new issue
Block a user