Skip to content

Add webhook payload size optimization options #35129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,9 @@ func prepareMigrationTasks() []*migration {
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),

// 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, "Add webhook payload optimization JSON field", v1_25.AddWebhookPayloadOptimizationColumns),
}
return preparedMigrations
}
Expand Down
22 changes: 22 additions & 0 deletions models/migrations/v1_25/v322.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 {
PayloadOptimization string `xorm:"payload_optimization TEXT"`
}
_, err := x.SyncWithOptions(
xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
},
new(Webhook),
)
return err
}
95 changes: 95 additions & 0 deletions models/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ import (
"xorm.io/builder"
)

// PayloadOptimizationConfig represents the configuration for webhook payload optimization
type PayloadOptimizationConfig struct {
Files *PayloadOptimizationItem `json:"files,omitempty"` // Files optimization config
Commits *PayloadOptimizationItem `json:"commits,omitempty"` // Commits optimization config
}

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

// DefaultPayloadOptimizationConfig returns the default payload optimization configuration
func DefaultPayloadOptimizationConfig() *PayloadOptimizationConfig {
return &PayloadOptimizationConfig{
Files: &PayloadOptimizationItem{Enable: false, Limit: 0},
Commits: &PayloadOptimizationItem{Enable: false, Limit: 0},
}
}

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

// Payload size optimization options
PayloadOptimization string `xorm:"payload_optimization TEXT"` // JSON: {"enable": bool, "limit": int}

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

// GetPayloadOptimizationConfig returns the payload optimization configuration
func (w *Webhook) GetPayloadOptimizationConfig() *PayloadOptimizationConfig {
if w.PayloadOptimization == "" {
return DefaultPayloadOptimizationConfig()
}

var config PayloadOptimizationConfig
if err := json.Unmarshal([]byte(w.PayloadOptimization), &config); err != nil {
log.Error("Failed to unmarshal payload optimization config: %v", err)
return DefaultPayloadOptimizationConfig()
}

return &config
}

// SetPayloadOptimizationConfig sets the payload optimization configuration
func (w *Webhook) SetPayloadOptimizationConfig(config *PayloadOptimizationConfig) error {
if config == nil {
config = DefaultPayloadOptimizationConfig()
}

data, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal payload optimization config: %w", err)
}

w.PayloadOptimization = string(data)
return nil
}

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

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

// IsFilesOptimizationEnabled returns whether files optimization is enabled
func (w *Webhook) IsFilesOptimizationEnabled() bool {
config := w.GetPayloadOptimizationConfig()
return config.Files.Enable
}

// GetFilesOptimizationLimit returns the files optimization limit
func (w *Webhook) GetFilesOptimizationLimit() int {
config := w.GetPayloadOptimizationConfig()
return config.Files.Limit
}

// IsCommitsOptimizationEnabled returns whether commits optimization is enabled
func (w *Webhook) IsCommitsOptimizationEnabled() bool {
config := w.GetPayloadOptimizationConfig()
return config.Commits.Enable
}

// GetCommitsOptimizationLimit returns the commits optimization limit
func (w *Webhook) GetCommitsOptimizationLimit() int {
config := w.GetPayloadOptimizationConfig()
return config.Commits.Limit
}
38 changes: 38 additions & 0 deletions models/webhook/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,41 @@ 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.GetPayloadOptimizationConfig()
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
newConfig := &PayloadOptimizationConfig{
Files: &PayloadOptimizationItem{
Enable: true,
Limit: 5,
},
Commits: &PayloadOptimizationItem{
Enable: true,
Limit: -3,
},
}
webhook.SetPayloadOptimizationConfig(newConfig)

// Test getting configuration
config = webhook.GetPayloadOptimizationConfig()
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.IsFilesOptimizationEnabled())
assert.Equal(t, 5, webhook.GetFilesOptimizationLimit())
assert.True(t, webhook.IsCommitsOptimizationEnabled())
assert.Equal(t, -3, webhook.GetCommitsOptimizationLimit())
assert.True(t, webhook.IsPayloadOptimizationEnabled())
}
8 changes: 7 additions & 1 deletion modules/structs/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type Hook struct {
Events []string `json:"events"`
AuthorizationHeader string `json:"authorization_header"`
Active bool `json:"active"`
// PayloadOptimization configuration for webhook payload optimization
PayloadOptimization map[string]any `json:"payload_optimization"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
// swagger:strfmt date-time
Expand All @@ -48,6 +50,8 @@ type CreateHookOption struct {
Events []string `json:"events"`
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
AuthorizationHeader string `json:"authorization_header"`
// Payload size optimization options
PayloadOptimization map[string]any `json:"payload_optimization"` // {"enable": bool, "limit": int}
// default: false
Active bool `json:"active"`
}
Expand All @@ -58,7 +62,9 @@ type EditHookOption struct {
Events []string `json:"events"`
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
AuthorizationHeader string `json:"authorization_header"`
Active *bool `json:"active"`
// Payload size optimization options
PayloadOptimization *map[string]any `json:"payload_optimization"` // {"enable": bool, "limit": int}
Active *bool `json:"active"`
}

// Payloader payload is some part of one hook
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 @@ -2424,6 +2424,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 @@ -3282,7 +3289,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
92 changes: 92 additions & 0 deletions routers/api/v1/utils/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,37 @@ import (
webhook_service "code.gitea.io/gitea/services/webhook"
)

// getBoolFromMap extracts a boolean value from a map with a default fallback
//
//nolint:unparam // defaultValue is needed for generic helper function
func getBoolFromMap(m map[string]any, defaultValue bool) bool {
if val, ok := m["enable"]; ok {
if boolVal, ok := val.(bool); ok {
return boolVal
}
}
return defaultValue
}

// getIntFromMap extracts an integer value from a map with a default fallback
//
//nolint:unparam // defaultValue is needed for generic helper function
func getIntFromMap(m map[string]any, defaultValue int) int {
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 defaultValue
}

// 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 +258,37 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
IsActive: form.Active,
Type: form.Type,
}

// Set payload optimization config
if form.PayloadOptimization != nil {
payloadOptConfig := &webhook.PayloadOptimizationConfig{}

// Parse files config
if filesConfig, ok := form.PayloadOptimization["files"].(map[string]any); ok {
payloadOptConfig.Files = &webhook.PayloadOptimizationItem{
Enable: getBoolFromMap(filesConfig, false),
Limit: getIntFromMap(filesConfig, 0),
}
} else {
payloadOptConfig.Files = &webhook.PayloadOptimizationItem{Enable: false, Limit: 0}
}

// Parse commits config
if commitsConfig, ok := form.PayloadOptimization["commits"].(map[string]any); ok {
payloadOptConfig.Commits = &webhook.PayloadOptimizationItem{
Enable: getBoolFromMap(commitsConfig, false),
Limit: getIntFromMap(commitsConfig, 0),
}
} else {
payloadOptConfig.Commits = &webhook.PayloadOptimizationItem{Enable: false, Limit: 0}
}

if err := w.SetPayloadOptimizationConfig(payloadOptConfig); 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 +453,36 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.IsActive = *form.Active
}

// Update payload optimization config
if form.PayloadOptimization != nil {
payloadOptConfig := &webhook.PayloadOptimizationConfig{}

// Parse files config
if filesConfig, ok := (*form.PayloadOptimization)["files"].(map[string]any); ok {
payloadOptConfig.Files = &webhook.PayloadOptimizationItem{
Enable: getBoolFromMap(filesConfig, false),
Limit: getIntFromMap(filesConfig, 0),
}
} else {
payloadOptConfig.Files = &webhook.PayloadOptimizationItem{Enable: false, Limit: 0}
}

// Parse commits config
if commitsConfig, ok := (*form.PayloadOptimization)["commits"].(map[string]any); ok {
payloadOptConfig.Commits = &webhook.PayloadOptimizationItem{
Enable: getBoolFromMap(commitsConfig, false),
Limit: getIntFromMap(commitsConfig, 0),
}
} else {
payloadOptConfig.Commits = &webhook.PayloadOptimizationItem{Enable: false, Limit: 0}
}

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

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