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
@@ -19,6 +19,19 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type ListHookTaskOptions struct {
|
||||
db.ListOptions
|
||||
HookID int64
|
||||
}
|
||||
|
||||
func (opts ListHookTaskOptions) ToConds() builder.Cond {
|
||||
return builder.Eq{"hook_task.hook_id": opts.HookID}
|
||||
}
|
||||
|
||||
func (opts ListHookTaskOptions) ToOrders() string {
|
||||
return "hook_task.id DESC"
|
||||
}
|
||||
|
||||
// ___ ___ __ ___________ __
|
||||
// / | \ ____ ____ | | _\__ ___/____ _____| | __
|
||||
// / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
|
||||
@@ -116,6 +129,19 @@ func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error)
|
||||
Find(&tasks)
|
||||
}
|
||||
|
||||
func GetHookTaskByUUID(ctx context.Context, hookID int64, uuid string) (*HookTask, error) {
|
||||
task, exist, err := db.Get[HookTask](ctx, builder.Eq{"hook_id": hookID, "uuid": uuid})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
return nil, ErrHookTaskNotExist{
|
||||
HookID: hookID,
|
||||
UUID: uuid,
|
||||
}
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CreateHookTask creates a new hook task,
|
||||
// it handles conversion from Payload to PayloadContent.
|
||||
func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) {
|
||||
@@ -152,14 +178,9 @@ func UpdateHookTask(ctx context.Context, t *HookTask) error {
|
||||
|
||||
// ReplayHookTask copies a hook task to get re-delivered
|
||||
func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) {
|
||||
task, exist, err := db.Get[HookTask](ctx, builder.Eq{"hook_id": hookID, "uuid": uuid})
|
||||
task, err := GetHookTaskByUUID(ctx, hookID, uuid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
return nil, ErrHookTaskNotExist{
|
||||
HookID: hookID,
|
||||
UUID: uuid,
|
||||
}
|
||||
}
|
||||
|
||||
return CreateHookTask(ctx, &HookTask{
|
||||
|
||||
@@ -277,6 +277,23 @@ func TestHookTasks(t *testing.T) {
|
||||
hookTasks, err = HookTasks(t.Context(), unittest.NonexistentID, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, hookTasks)
|
||||
|
||||
tasks, count, err := db.FindAndCount[HookTask](t.Context(), ListHookTaskOptions{HookID: hook.ID})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, count)
|
||||
require.Len(t, tasks, 3)
|
||||
|
||||
task, err := GetHookTaskByUUID(t.Context(), hook.ID, tasks[0].UUID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tasks[0].ID, task.ID)
|
||||
|
||||
tasks, count, err = db.FindAndCount[HookTask](t.Context(), ListHookTaskOptions{HookID: unittest.NonexistentID})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
assert.Empty(t, tasks)
|
||||
|
||||
_, err = GetHookTaskByUUID(t.Context(), hook.ID, "non-existent-uuid")
|
||||
assert.True(t, IsErrHookTaskNotExist(err))
|
||||
}
|
||||
|
||||
func TestCreateHookTask(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user