Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1915361
Add webhook payload size optimization options
kerwin612 Jul 19, 2025
5472d66
refactor webhook payload optimization: switch from bool to limit number
kerwin612 Jul 23, 2025
39718b2
Merge branch 'main' into feature/webhook-payload-optimization
kerwin612 Jul 24, 2025
9682610
fix
kerwin612 Jul 24, 2025
544e445
refactor: swap webhook payload optimization logic (-1/0 values)
kerwin612 Jul 30, 2025
42a880f
Merge branch 'main' into feature/webhook-payload-optimization
kerwin612 Jul 30, 2025
6e07c6f
add webhook payload optimization API support
kerwin612 Jul 30, 2025
11cdba1
Merge branch 'main' into feature/webhook-payload-optimization
kerwin612 Aug 4, 2025
b718c54
refactor: replace webhook payload optimization with JSON-based config…
kerwin612 Aug 4, 2025
9767573
Merge branch 'main' into feature/webhook-payload-optimization
kerwin612 Aug 9, 2025
655bfcd
clean code
kerwin612 Aug 11, 2025
6b635dc
Merge branch 'main' into feature/webhook-payload-optimization
kerwin612 Aug 15, 2025
6d932c8
clean code
kerwin612 Aug 15, 2025
eec07ee
clean code
kerwin612 Aug 15, 2025
80f8d2c
Merge branch 'main' into feature/webhook-payload-optimization
kerwin612 Aug 24, 2025
a3581be
fix
kerwin612 Aug 24, 2025
10d0450
clean code
kerwin612 Aug 24, 2025
f4ea1a1
clean code
kerwin612 Aug 24, 2025
98d4da1
clean code
kerwin612 Aug 24, 2025
94225ed
clean code
kerwin612 Aug 24, 2025
46c0824
Update models/webhook/webhook.go
kerwin612 Aug 28, 2025
7f9debb
Merge branch 'feature/webhook-payload-optimization' of github.com:ker…
lunny Sep 18, 2025
ef6aeda
revert unnecessary change
lunny Sep 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ func prepareMigrationTasks() []*migration {
// Gitea 1.24.0 ends at database version 321
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength),
newMigration(323, "Add webhook payload optimization JSON field", v1_25.AddWebhookPayloadOptimizationColumns),
}
return preparedMigrations
}
Expand Down
22 changes: 22 additions & 0 deletions models/migrations/v1_25/v323.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_25

import (
"xorm.io/xorm"
)

func AddWebhookPayloadOptimizationColumns(x *xorm.Engine) error {
type Webhook struct {
MetaSettings string `xorm:"meta_settings TEXT"`
}
_, err := x.SyncWithOptions(
xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
},
new(Webhook),
)
return err
}
112 changes: 112 additions & 0 deletions models/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,35 @@ import (
"xorm.io/builder"
)

// MetaSettings represents the metadata settings for webhook
type MetaSettings struct {
PayloadConfig PayloadConfig `json:"payload_config"` // Payload configuration
}

// PayloadConfig represents the configuration for webhook payload
type PayloadConfig struct {
Files PayloadConfigItem `json:"files"` // Files configuration
Commits PayloadConfigItem `json:"commits"` // Commits configuration
}

// PayloadConfigItem represents a single payload configuration item
type PayloadConfigItem struct {
Enable bool `json:"enable"` // Whether to enable this configuration
Limit int `json:"limit"` // 0: trim all (none kept), >0: keep N items (forward order), <0: keep N items (reverse order)
}

// DefaultMetaSettings returns the default webhook meta settings
func DefaultMetaSettings() MetaSettings {
return MetaSettings{
PayloadConfig: DefaultPayloadConfig(),
}
}

// DefaultPayloadConfig returns the default payload configuration
func DefaultPayloadConfig() PayloadConfig {
return PayloadConfig{}
}

// ErrWebhookNotExist represents a "WebhookNotExist" kind of error.
type ErrWebhookNotExist struct {
ID int64
Expand Down Expand Up @@ -139,6 +168,9 @@ type Webhook struct {
// HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization()
HeaderAuthorizationEncrypted string `xorm:"TEXT"`

// Webhook metadata settings (JSON format)
MetaSettings string `xorm:"meta_settings TEXT"` // JSON: webhook metadata configuration

CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
Expand Down Expand Up @@ -346,3 +378,83 @@ func DeleteWebhookByOwnerID(ctx context.Context, ownerID, id int64) error {
}
return DeleteWebhookByID(ctx, id)
}

// GetMetaSettings returns the webhook meta settings
func (w *Webhook) GetMetaSettings() MetaSettings {
if w.MetaSettings == "" {
return DefaultMetaSettings()
}

var settings MetaSettings
if err := json.Unmarshal([]byte(w.MetaSettings), &settings); err != nil {
log.Error("Failed to unmarshal webhook meta settings: %v", err)
return DefaultMetaSettings()
}

return settings
}

// GetPayloadConfig returns the payload configuration
func (w *Webhook) GetPayloadConfig() PayloadConfig {
return w.GetMetaSettings().PayloadConfig
}

// SetMetaSettings sets the webhook meta settings
func (w *Webhook) SetMetaSettings(settings MetaSettings) error {
data, err := json.Marshal(settings)
if err != nil {
return fmt.Errorf("failed to marshal webhook meta settings: %w", err)
}

w.MetaSettings = string(data)
return nil
}

// SetPayloadConfig sets the payload configuration
func (w *Webhook) SetPayloadConfig(config PayloadConfig) error {
settings := w.GetMetaSettings()
settings.PayloadConfig = config
return w.SetMetaSettings(settings)
}

// IsPayloadConfigEnabled returns whether payload configuration is enabled
func (w *Webhook) IsPayloadConfigEnabled() bool {
config := w.GetPayloadConfig()
return config.Files.Enable || config.Commits.Enable
}

// GetPayloadConfigLimit returns the payload configuration limit
func (w *Webhook) GetPayloadConfigLimit() int {
config := w.GetPayloadConfig()
if config.Files.Enable {
return config.Files.Limit
}
if config.Commits.Enable {
return config.Commits.Limit
}
return 0
}

// IsFilesConfigEnabled returns whether files configuration is enabled
func (w *Webhook) IsFilesConfigEnabled() bool {
config := w.GetPayloadConfig()
return config.Files.Enable
}

// GetFilesConfigLimit returns the files configuration limit
func (w *Webhook) GetFilesConfigLimit() int {
config := w.GetPayloadConfig()
return config.Files.Limit
}

// IsCommitsConfigEnabled returns whether commits configuration is enabled
func (w *Webhook) IsCommitsConfigEnabled() bool {
config := w.GetPayloadConfig()
return config.Commits.Enable
}

// GetCommitsConfigLimit returns the commits configuration limit
func (w *Webhook) GetCommitsConfigLimit() int {
config := w.GetPayloadConfig()
return config.Commits.Limit
}
60 changes: 60 additions & 0 deletions models/webhook/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,63 @@ func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *test
assert.NoError(t, CleanupHookTaskTable(t.Context(), OlderThan, 168*time.Hour, 0))
unittest.AssertExistsAndLoadBean(t, hookTask)
}

func TestWebhookPayloadOptimization(t *testing.T) {
webhook := &Webhook{}

// Test default configuration
config := webhook.GetPayloadConfig()
assert.False(t, config.Files.Enable)
assert.Equal(t, 0, config.Files.Limit)
assert.False(t, config.Commits.Enable)
assert.Equal(t, 0, config.Commits.Limit)

// Test setting configuration via meta settings
metaSettings := MetaSettings{
PayloadConfig: PayloadConfig{
Files: PayloadConfigItem{
Enable: true,
Limit: 5,
},
Commits: PayloadConfigItem{
Enable: true,
Limit: -3,
},
},
}
webhook.SetMetaSettings(metaSettings)

// Test getting configuration
config = webhook.GetPayloadConfig()
assert.True(t, config.Files.Enable)
assert.Equal(t, 5, config.Files.Limit)
assert.True(t, config.Commits.Enable)
assert.Equal(t, -3, config.Commits.Limit)

// Test individual methods
assert.True(t, webhook.IsFilesConfigEnabled())
assert.Equal(t, 5, webhook.GetFilesConfigLimit())
assert.True(t, webhook.IsCommitsConfigEnabled())
assert.Equal(t, -3, webhook.GetCommitsConfigLimit())
assert.True(t, webhook.IsPayloadConfigEnabled())

// Test backward compatibility with direct payload config setting
newConfig := PayloadConfig{
Files: PayloadConfigItem{
Enable: false,
Limit: 10,
},
Commits: PayloadConfigItem{
Enable: false,
Limit: 20,
},
}
webhook.SetPayloadConfig(newConfig)

// Verify the config is properly set through meta settings
config = webhook.GetPayloadConfig()
assert.False(t, config.Files.Enable)
assert.Equal(t, 10, config.Files.Limit)
assert.False(t, config.Commits.Enable)
assert.Equal(t, 20, config.Commits.Limit)
}
6 changes: 6 additions & 0 deletions modules/structs/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type Hook struct {
AuthorizationHeader string `json:"authorization_header"`
// Whether the webhook is active and will be triggered
Active bool `json:"active"`
// MetaSettings webhook metadata settings including payload optimization
MetaSettings map[string]any `json:"meta_settings"`
// swagger:strfmt date-time
// The date and time when the webhook was last updated
Updated time.Time `json:"updated_at"`
Expand Down Expand Up @@ -63,6 +65,8 @@ type CreateHookOption struct {
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
// Authorization header to include in webhook requests
AuthorizationHeader string `json:"authorization_header"`
// Webhook metadata settings including payload optimization
MetaSettings map[string]any `json:"meta_settings"` // {"payload_config": {"files": {"enable": bool, "limit": int}, "commits": {"enable": bool, "limit": int}}}
// default: false
// Whether the webhook should be active upon creation
Active bool `json:"active"`
Expand All @@ -78,6 +82,8 @@ type EditHookOption struct {
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
// Authorization header to include in webhook requests
AuthorizationHeader string `json:"authorization_header"`
// Webhook metadata settings including payload optimization
MetaSettings *map[string]any `json:"meta_settings"` // {"payload_config": {"files": {"enable": bool, "limit": int}, "commits": {"enable": bool, "limit": int}}}
// Whether the webhook is active and will be triggered
Active *bool `json:"active"`
}
Expand Down
9 changes: 8 additions & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2434,6 +2434,13 @@ settings.event_package = Package
settings.event_package_desc = Package created or deleted in a repository.
settings.branch_filter = Branch filter
settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or <code>*</code>, events for all branches are reported. See <a href="%[1]s">%[2]s</a> documentation for syntax. Examples: <code>master</code>, <code>{master,release*}</code>.
settings.payload_optimization = Payload Size Optimization
settings.payload_optimization_files = Files
settings.payload_optimization_commits = Commits
settings.payload_optimization_enable = Enable optimization
settings.payload_optimization_enable_desc = Enable payload size optimization for this item
settings.payload_optimization_limit = Limit
settings.payload_optimization_limit_desc = 0: trim all (none kept), >0: keep N items (forward order), <0: keep N items (reverse order)
settings.authorization_header = Authorization Header
settings.authorization_header_desc = Will be included as authorization header for requests when present. Examples: %s.
settings.active = Active
Expand Down Expand Up @@ -3297,7 +3304,7 @@ auths.tip.github = Register a new OAuth application on %s
auths.tip.gitlab_new = Register a new application on %s
auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console at %s
auths.tip.openid_connect = Use the OpenID Connect Discovery URL "https://{server}/.well-known/openid-configuration" to specify the endpoints
auths.tip.twitter = Go to %s, create an application and ensure that the Allow this application to be used to Sign in with Twitter option is enabled
auths.tip.twitter = Go to %s, create an application and ensure that the "Allow this application to be used to Sign in with Twitter" option is enabled
auths.tip.discord = Register a new application on %s
auths.tip.gitea = Register a new OAuth2 application. Guide can be found at %s
auths.tip.yandex = Create a new application at %s. Select following permissions from the "Yandex.Passport API" section: "Access to email address", "Access to user avatar" and "Access to username, first name and surname, gender"
Expand Down
102 changes: 102 additions & 0 deletions routers/api/v1/utils/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,33 @@ import (
webhook_service "code.gitea.io/gitea/services/webhook"
)

// getPayloadConfigEnable extracts the "enable" boolean value from a payload config map
func getPayloadConfigEnable(m map[string]any) bool {
if val, ok := m["enable"]; ok {
if boolVal, ok := val.(bool); ok {
return boolVal
}
}
return false
}

// getPayloadConfigLimit extracts the "limit" integer value from a payload config map
func getPayloadConfigLimit(m map[string]any) int {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems fragile, I don't think we need these tricks to parse form input

if val, ok := m["limit"]; ok {
switch v := val.(type) {
case int:
return v
case float64:
return int(v)
case string:
if intVal, err := strconv.Atoi(v); err == nil {
return intVal
}
}
}
return 0
}

// ListOwnerHooks lists the webhooks of the provided owner
func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) {
opts := &webhook.ListWebhookOptions{
Expand Down Expand Up @@ -227,6 +254,44 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
IsActive: form.Active,
Type: form.Type,
}

// Set webhook meta settings
if form.MetaSettings != nil {
metaSettings := webhook.MetaSettings{}

// Parse payload config
if payloadOptMap, ok := form.MetaSettings["payload_config"].(map[string]any); ok {
payloadOptConfig := webhook.PayloadConfig{}

// Parse files config
if filesConfig, ok := payloadOptMap["files"].(map[string]any); ok {
payloadOptConfig.Files = webhook.PayloadConfigItem{
Enable: getPayloadConfigEnable(filesConfig),
Limit: getPayloadConfigLimit(filesConfig),
}
} else {
payloadOptConfig.Files = webhook.PayloadConfigItem{Enable: false, Limit: 0}
}

// Parse commits config
if commitsConfig, ok := payloadOptMap["commits"].(map[string]any); ok {
payloadOptConfig.Commits = webhook.PayloadConfigItem{
Enable: getPayloadConfigEnable(commitsConfig),
Limit: getPayloadConfigLimit(commitsConfig),
}
} else {
payloadOptConfig.Commits = webhook.PayloadConfigItem{Enable: false, Limit: 0}
}

metaSettings.PayloadConfig = payloadOptConfig
}

if err := w.SetMetaSettings(metaSettings); err != nil {
ctx.APIErrorInternal(err)
return nil, false
}
}

err := w.SetHeaderAuthorization(form.AuthorizationHeader)
if err != nil {
ctx.APIErrorInternal(err)
Expand Down Expand Up @@ -391,6 +456,43 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.IsActive = *form.Active
}

// Update webhook meta settings
if form.MetaSettings != nil {
metaSettings := webhook.MetaSettings{}

// Parse payload config
if payloadOptMap, ok := (*form.MetaSettings)["payload_config"].(map[string]any); ok {
payloadOptConfig := webhook.PayloadConfig{}

// Parse files config
if filesConfig, ok := payloadOptMap["files"].(map[string]any); ok {
payloadOptConfig.Files = webhook.PayloadConfigItem{
Enable: getPayloadConfigEnable(filesConfig),
Limit: getPayloadConfigLimit(filesConfig),
}
} else {
payloadOptConfig.Files = webhook.PayloadConfigItem{Enable: false, Limit: 0}
}

// Parse commits config
if commitsConfig, ok := payloadOptMap["commits"].(map[string]any); ok {
payloadOptConfig.Commits = webhook.PayloadConfigItem{
Enable: getPayloadConfigEnable(commitsConfig),
Limit: getPayloadConfigLimit(commitsConfig),
}
} else {
payloadOptConfig.Commits = webhook.PayloadConfigItem{Enable: false, Limit: 0}
}

metaSettings.PayloadConfig = payloadOptConfig
}

if err := w.SetMetaSettings(metaSettings); err != nil {
ctx.APIErrorInternal(err)
return false
}
}

if err := webhook.UpdateWebhook(ctx, w); err != nil {
ctx.APIErrorInternal(err)
return false
Expand Down
Loading