mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-04 11:30:51 +09:00
Add an optional Name field to webhooks so users can give them human-readable labels instead of relying only on URLs. The webhook overview page now displays names when available, or falls back to the URL for unnamed webhooks. Fixes #37025 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -404,6 +404,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(327, "Add disabled state to action runners", v1_26.AddDisabledToActionRunner),
|
||||
newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob),
|
||||
newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge),
|
||||
newMigration(330, "Add name column to webhook", v1_26.AddNameToWebhook),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
16
models/migrations/v1_26/v330.go
Normal file
16
models/migrations/v1_26/v330.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddNameToWebhook(x *xorm.Engine) error {
|
||||
type Webhook struct {
|
||||
Name string `xorm:"VARCHAR(255) NOT NULL DEFAULT ''"`
|
||||
}
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(Webhook))
|
||||
return err
|
||||
}
|
||||
@@ -126,6 +126,7 @@ type Webhook struct {
|
||||
OwnerID int64 `xorm:"INDEX"`
|
||||
IsSystemWebhook bool
|
||||
URL string `xorm:"url TEXT"`
|
||||
Name string `xorm:"VARCHAR(255) NOT NULL DEFAULT ''"`
|
||||
HTTPMethod string `xorm:"http_method"`
|
||||
ContentType HookContentType
|
||||
Secret string `xorm:"TEXT"`
|
||||
|
||||
@@ -19,6 +19,8 @@ var ErrInvalidReceiveHook = errors.New("Invalid JSON payload received over webho
|
||||
type Hook struct {
|
||||
// The unique identifier of the webhook
|
||||
ID int64 `json:"id"`
|
||||
// Optional human-readable name for the webhook
|
||||
Name string `json:"name"`
|
||||
// The type of the webhook (e.g., gitea, slack, discord)
|
||||
Type string `json:"type"`
|
||||
// Branch filter pattern to determine which branches trigger the webhook
|
||||
@@ -66,6 +68,8 @@ type CreateHookOption struct {
|
||||
// default: false
|
||||
// Whether the webhook should be active upon creation
|
||||
Active bool `json:"active"`
|
||||
// Optional human-readable name for the webhook
|
||||
Name string `json:"name" binding:"MaxSize(255)"`
|
||||
}
|
||||
|
||||
// EditHookOption options when modify one hook
|
||||
@@ -80,6 +84,8 @@ type EditHookOption struct {
|
||||
AuthorizationHeader string `json:"authorization_header"`
|
||||
// Whether the webhook is active and will be triggered
|
||||
Active *bool `json:"active"`
|
||||
// Optional human-readable name
|
||||
Name *string `json:"name,omitzero" binding:"MaxSize(255)"`
|
||||
}
|
||||
|
||||
// Payloader payload is some part of one hook
|
||||
|
||||
@@ -2258,6 +2258,9 @@
|
||||
"repo.settings.payload_url": "Target URL",
|
||||
"repo.settings.http_method": "HTTP Method",
|
||||
"repo.settings.content_type": "POST Content Type",
|
||||
"repo.settings.webhook.name": "Webhook name",
|
||||
"repo.settings.webhook.name_helper": "Optionally give this webhook a friendly name",
|
||||
"repo.settings.webhook.name_empty": "Unnamed Webhook",
|
||||
"repo.settings.secret": "Secret",
|
||||
"repo.settings.webhook_secret_desc": "If the webhook server supports using secret, you can follow the webhook's manual and fill in a secret here.",
|
||||
"repo.settings.slack_username": "Username",
|
||||
|
||||
@@ -215,6 +215,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
|
||||
w := &webhook.Webhook{
|
||||
OwnerID: ownerID,
|
||||
RepoID: repoID,
|
||||
Name: strings.TrimSpace(form.Name),
|
||||
URL: form.Config["url"],
|
||||
ContentType: webhook.ToHookContentType(form.Config["content_type"]),
|
||||
Secret: form.Config["secret"],
|
||||
@@ -392,6 +393,10 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
|
||||
w.IsActive = *form.Active
|
||||
}
|
||||
|
||||
if form.Name != nil {
|
||||
w.Name = strings.TrimSpace(*form.Name)
|
||||
}
|
||||
|
||||
if err := webhook.UpdateWebhook(ctx, w); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return false
|
||||
|
||||
@@ -234,6 +234,7 @@ func createWebhook(ctx *context.Context, params webhookParams) {
|
||||
w := &webhook.Webhook{
|
||||
RepoID: orCtx.RepoID,
|
||||
URL: params.URL,
|
||||
Name: strings.TrimSpace(params.WebhookForm.Name),
|
||||
HTTPMethod: params.HTTPMethod,
|
||||
ContentType: params.ContentType,
|
||||
Secret: params.WebhookForm.Secret,
|
||||
@@ -288,6 +289,7 @@ func editWebhook(ctx *context.Context, params webhookParams) {
|
||||
}
|
||||
|
||||
w.URL = params.URL
|
||||
w.Name = strings.TrimSpace(params.WebhookForm.Name)
|
||||
w.ContentType = params.ContentType
|
||||
w.Secret = params.WebhookForm.Secret
|
||||
w.HookEvent = ParseHookEvent(params.WebhookForm)
|
||||
|
||||
@@ -206,6 +206,7 @@ type ProtectBranchPriorityForm struct {
|
||||
|
||||
// WebhookForm form for changing web hook
|
||||
type WebhookForm struct {
|
||||
Name string `binding:"MaxSize(255)"`
|
||||
Events string
|
||||
Create bool
|
||||
Delete bool
|
||||
|
||||
@@ -411,6 +411,7 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
||||
|
||||
return &api.Hook{
|
||||
ID: w.ID,
|
||||
Name: w.Name,
|
||||
Type: w.Type,
|
||||
URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID),
|
||||
Active: w.IsActive,
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
<div class="item">
|
||||
<span class="{{if eq .LastStatus 1}}tw-text-green{{else if eq .LastStatus 2}}tw-text-red{{else}}tw-text-text-light{{end}}">{{svg "octicon-dot-fill" 22}}</span>
|
||||
<div class="gt-ellipsis tw-flex-1">
|
||||
<a title="{{.URL}}" href="{{$.BaseLink}}/{{.ID}}">{{.URL}}</a>
|
||||
<a title="{{.URL}}" href="{{$.BaseLink}}/{{.ID}}">{{or .Name (ctx.Locale.Tr "repo.settings.webhook.name_empty")}}</a>
|
||||
<span class="tw-ml-2 tw-text-grey-light">{{.URL}}</span>
|
||||
</div>
|
||||
<a class="muted tw-p-2" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
|
||||
<a class="tw-text-red tw-p-2 link-action"
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
*/}}
|
||||
{{$isNew := not .Webhook.ID}}
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.webhook.name"}}</label>
|
||||
<input name="name" type="text" value="{{.Webhook.Name}}" maxlength="255">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.webhook.name_helper"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<input name="active" type="checkbox" {{if or $isNew .Webhook.IsActive}}checked{{end}}>
|
||||
|
||||
15
templates/swagger/v1_json.tmpl
generated
15
templates/swagger/v1_json.tmpl
generated
@@ -23463,6 +23463,11 @@
|
||||
},
|
||||
"x-go-name": "Events"
|
||||
},
|
||||
"name": {
|
||||
"description": "Optional human-readable name for the webhook",
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -24729,6 +24734,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "Events"
|
||||
},
|
||||
"name": {
|
||||
"description": "Optional human-readable name",
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
@@ -26149,6 +26159,11 @@
|
||||
"format": "int64",
|
||||
"x-go-name": "ID"
|
||||
},
|
||||
"name": {
|
||||
"description": "Optional human-readable name for the webhook",
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"type": {
|
||||
"description": "The type of the webhook (e.g., gitea, slack, discord)",
|
||||
"type": "string",
|
||||
|
||||
@@ -34,6 +34,7 @@ func TestAPICreateHook(t *testing.T) {
|
||||
"url": "http://example.com/",
|
||||
},
|
||||
AuthorizationHeader: "Bearer s3cr3t",
|
||||
Name: " CI notifications ",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
@@ -41,4 +42,54 @@ func TestAPICreateHook(t *testing.T) {
|
||||
DecodeJSON(t, resp, &apiHook)
|
||||
assert.Equal(t, "http://example.com/", apiHook.Config["url"])
|
||||
assert.Equal(t, "Bearer s3cr3t", apiHook.AuthorizationHeader)
|
||||
assert.Equal(t, "CI notifications", apiHook.Name)
|
||||
|
||||
newName := "Deploy hook"
|
||||
patchReq := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d", owner.Name, repo.Name, apiHook.ID), api.EditHookOption{
|
||||
Name: &newName,
|
||||
}).AddTokenAuth(token)
|
||||
patchResp := MakeRequest(t, patchReq, http.StatusOK)
|
||||
var patched *api.Hook
|
||||
DecodeJSON(t, patchResp, &patched)
|
||||
assert.Equal(t, newName, patched.Name)
|
||||
|
||||
hooksURL := fmt.Sprintf("/api/v1/repos/%s/%s/hooks", owner.Name, repo.Name)
|
||||
|
||||
// Create with Name field omitted: Name should be ""
|
||||
req2 := NewRequestWithJSON(t, "POST", hooksURL, api.CreateHookOption{
|
||||
Type: "gitea",
|
||||
Config: api.CreateHookOptionConfig{
|
||||
"content_type": "json",
|
||||
"url": "http://example.com/",
|
||||
},
|
||||
}).AddTokenAuth(token)
|
||||
resp2 := MakeRequest(t, req2, http.StatusCreated)
|
||||
var created *api.Hook
|
||||
DecodeJSON(t, resp2, &created)
|
||||
assert.Empty(t, created.Name)
|
||||
|
||||
hookURL := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d", owner.Name, repo.Name, created.ID)
|
||||
|
||||
// PATCH with Name set: existing Name must be updated
|
||||
setName := "original"
|
||||
setReq := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{
|
||||
Name: &setName,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, setReq, http.StatusOK)
|
||||
|
||||
// PATCH without Name field: name must remain "original"
|
||||
patchReq2 := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{}).AddTokenAuth(token)
|
||||
patchResp2 := MakeRequest(t, patchReq2, http.StatusOK)
|
||||
var notCleared *api.Hook
|
||||
DecodeJSON(t, patchResp2, ¬Cleared)
|
||||
assert.Equal(t, "original", notCleared.Name)
|
||||
|
||||
// PATCH with Name: "" explicitly: Name should be cleared to ""
|
||||
clearReq := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{
|
||||
Name: new(""),
|
||||
}).AddTokenAuth(token)
|
||||
clearResp := MakeRequest(t, clearReq, http.StatusOK)
|
||||
var cleared *api.Hook
|
||||
DecodeJSON(t, clearResp, &cleared)
|
||||
assert.Empty(t, cleared.Name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user