feat(webhook): fire repository event on repo rename (#38641)

Repository create and delete already fire `repository` webhooks, rename
did not. This adds the `renamed` action with `changes.name.from`
carrying the previous name, and renders it in the chat converters.

Actions workflows are unaffected, they still do not trigger on rename.

AI assistance was used for the implementation and tests.

Fixes https://github.com/go-gitea/gitea/issues/34891.
Co-authored-by: roman s <roman.sukach@dust-labs.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
roman.s
2026-07-31 14:31:17 +03:00
committed by GitHub
parent 7ca64566f5
commit 38b07d0c29
14 changed files with 70 additions and 1 deletions
+6
View File
@@ -424,6 +424,8 @@ type ChangesPayload struct {
Body *ChangesFromPayload `json:"body,omitempty"`
// Changes made to the reference
Ref *ChangesFromPayload `json:"ref,omitempty"`
// Changes made to the repository name
Name *ChangesFromPayload `json:"name,omitempty"`
// Changes made to the labels added
AddedLabels []*Label `json:"added_labels"`
// Changes made to the labels removed
@@ -508,6 +510,8 @@ const (
HookRepoCreated HookRepoAction = "created"
// HookRepoDeleted deleted
HookRepoDeleted HookRepoAction = "deleted"
// HookRepoRenamed renamed
HookRepoRenamed HookRepoAction = "renamed"
)
// RepositoryPayload payload for repository webhooks
@@ -520,6 +524,8 @@ type RepositoryPayload struct {
Organization *User `json:"organization"`
// The user who performed the action
Sender *User `json:"sender"`
// Changes made to the repository
Changes *ChangesPayload `json:"changes,omitempty"`
}
// JSONPayload JSON representation of the payload
+1 -1
View File
@@ -2298,7 +2298,7 @@
"repo.settings.event_force_push": "Force Push",
"repo.settings.event_push_desc": "Git push to a repository.",
"repo.settings.event_repository": "Repository",
"repo.settings.event_repository_desc": "Repository created or deleted.",
"repo.settings.event_repository_desc": "Repository created, deleted, or renamed.",
"repo.settings.event_header_issue": "Issue Events",
"repo.settings.event_issues": "Issues",
"repo.settings.event_issues_desc": "Issue opened, closed, reopened, edited or deleted.",
+3
View File
@@ -152,6 +152,9 @@ func (dc dingtalkConvertor) Repository(p *api.RepositoryPayload) (DingtalkPayloa
Content: title,
},
}, nil
case api.HookRepoRenamed:
title := fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
return createDingtalkPayload(title, title, "view repository", p.Repository.HTMLURL), nil
}
return DingtalkPayload{}, nil
+4
View File
@@ -241,6 +241,10 @@ func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload,
case api.HookRepoDeleted:
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
color = redColor
case api.HookRepoRenamed:
title = fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
url = p.Repository.HTMLURL
color = greenColor
}
return d.createPayload(p.Sender, title, "", url, color), nil
+3
View File
@@ -148,6 +148,9 @@ func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, e
case api.HookRepoDeleted:
text = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
return newFeishuTextPayload(text), nil
case api.HookRepoRenamed:
text = fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
return newFeishuTextPayload(text), nil
}
return FeishuPayload{}, nil
+7
View File
@@ -293,6 +293,13 @@ func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFo
return text, issueTitle, color
}
func getRepoRenamedFrom(p *api.RepositoryPayload) string {
if p.Changes == nil || p.Changes.Name == nil {
return ""
}
return p.Changes.Name.From
}
func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
refLink := linkFormatter(p.Package.HTMLURL, p.Package.Name+":"+p.Package.Version)
+2
View File
@@ -227,6 +227,8 @@ func (m matrixConvertor) Repository(p *api.RepositoryPayload) (MatrixPayload, er
text = fmt.Sprintf("[%s] Repository created by %s", repoLink, senderLink)
case api.HookRepoDeleted:
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
case api.HookRepoRenamed:
text = fmt.Sprintf("[%s] Repository renamed from %s by %s", repoLink, getRepoRenamedFrom(p), senderLink)
}
return m.newPayload(text)
}
+4
View File
@@ -259,6 +259,10 @@ func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload,
case api.HookRepoDeleted:
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
color = yellowColor
case api.HookRepoRenamed:
title = fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
url = p.Repository.HTMLURL
color = greenColor
}
return createMSTeamsPayload(
+12
View File
@@ -134,6 +134,18 @@ func (m *webhookNotifier) DeleteRepository(ctx context.Context, doer *user_model
}
}
func (m *webhookNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) {
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{
Action: api.HookRepoRenamed,
Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
Organization: convert.ToUser(ctx, repo.MustOwner(ctx), nil),
Sender: convert.ToUser(ctx, doer, nil),
Changes: &api.ChangesPayload{Name: &api.ChangesFromPayload{From: oldRepoName}},
}); err != nil {
log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err)
}
}
func (m *webhookNotifier) MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
// Add to hook queue for created repo after session commit.
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{
+2
View File
@@ -276,6 +276,8 @@ func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, erro
text = fmt.Sprintf("[%s] Repository created by %s", repoLink, senderLink)
case api.HookRepoDeleted:
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
case api.HookRepoRenamed:
text = fmt.Sprintf("[%s] Repository renamed from %s by %s", repoLink, getRepoRenamedFrom(p), senderLink)
}
return s.createPayload(text, nil), nil
+6
View File
@@ -114,6 +114,12 @@ func TestSlackPayload(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository created by <https://try.gitea.io/user1|user1>", pl.Text)
p.Action = api.HookRepoRenamed
p.Changes = &api.ChangesPayload{Name: &api.ChangesFromPayload{From: "old-repo"}}
pl, err = sc.Repository(p)
require.NoError(t, err)
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository renamed from old-repo by <https://try.gitea.io/user1|user1>", pl.Text)
})
t.Run("Package", func(t *testing.T) {
+3
View File
@@ -154,6 +154,9 @@ func (t telegramConvertor) Repository(p *api.RepositoryPayload) (TelegramPayload
case api.HookRepoDeleted:
title = fmt.Sprintf("[%s] Repository deleted", html.EscapeString(p.Repository.FullName))
return createTelegramPayloadHTML(title), nil
case api.HookRepoRenamed:
title = fmt.Sprintf("[%s] Repository renamed from %s", htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName), html.EscapeString(getRepoRenamedFrom(p)))
return createTelegramPayloadHTML(title), nil
}
return TelegramPayload{}, nil
}
+3
View File
@@ -150,6 +150,9 @@ func (wc wechatworkConvertor) Repository(p *api.RepositoryPayload) (WechatworkPa
case api.HookRepoDeleted:
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
return newWechatworkMarkdownPayload(title), nil
case api.HookRepoRenamed:
title = fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
return newWechatworkMarkdownPayload(title), nil
}
return WechatworkPayload{}, nil
+14
View File
@@ -859,6 +859,20 @@ func Test_WebhookRepository(t *testing.T) {
assert.Equal(t, "org3", payloads[0].Organization.UserName)
assert.Equal(t, "repo_new", payloads[0].Repository.Name)
assert.Equal(t, "org3/repo_new", payloads[0].Repository.FullName)
// 4. rename the repository and validate the webhook is triggered again
newName := "repo_renamed"
req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/org3/repo_new", &api.EditRepoOption{
Name: &newName,
}).AddTokenAuth(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository))
MakeRequest(t, req, http.StatusOK)
require.Len(t, payloads, 2)
assert.Equal(t, api.HookRepoRenamed, payloads[1].Action)
assert.Equal(t, newName, payloads[1].Repository.Name)
assert.Equal(t, "org3/"+newName, payloads[1].Repository.FullName)
require.NotNil(t, payloads[1].Changes.Name)
assert.Equal(t, "repo_new", payloads[1].Changes.Name.From)
})
}