From 38b07d0c295f2a2b834030a8b1fb70027ec4a289 Mon Sep 17 00:00:00 2001 From: "roman.s" <55788842+majorissuerep@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:31:17 +0300 Subject: [PATCH] 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 Co-authored-by: silverwind --- modules/structs/hook.go | 6 ++++++ options/locale/locale_en-US.json | 2 +- services/webhook/dingtalk.go | 3 +++ services/webhook/discord.go | 4 ++++ services/webhook/feishu.go | 3 +++ services/webhook/general.go | 7 +++++++ services/webhook/matrix.go | 2 ++ services/webhook/msteams.go | 4 ++++ services/webhook/notifier.go | 12 ++++++++++++ services/webhook/slack.go | 2 ++ services/webhook/slack_test.go | 6 ++++++ services/webhook/telegram.go | 3 +++ services/webhook/wechatwork.go | 3 +++ tests/integration/repo_webhook_test.go | 14 ++++++++++++++ 14 files changed, 70 insertions(+), 1 deletion(-) diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 0301b08e1d6..522c286571c 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -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 diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index ae14deee3ab..df4086bd5c5 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -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.", diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index 0403fdaa0f6..d9991a27361 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -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 diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 9bbeab3fadc..1b3911263ae 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -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 diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 18651ade4a2..f200e16bba2 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -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 diff --git a/services/webhook/general.go b/services/webhook/general.go index 327e36c120e..127df37bdd8 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -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) diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index e9dc17aeaf3..43f785326d0 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -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) } diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index c7881444dbe..84085cf25fe 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -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( diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 2586d09798e..44cbe7747da 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -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{ diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 23fd5a5b16d..293336059e7 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -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 diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go index f49e7203e09..99f8aa7ced8 100644 --- a/services/webhook/slack_test.go +++ b/services/webhook/slack_test.go @@ -114,6 +114,12 @@ func TestSlackPayload(t *testing.T) { require.NoError(t, err) assert.Equal(t, "[] Repository created by ", 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, "[] Repository renamed from old-repo by ", pl.Text) }) t.Run("Package", func(t *testing.T) { diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index cae9a883c79..4f1d98407da 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -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 } diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 938be3b499e..2bcfe1f3c8d 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -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 diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 0c58c0a4fc9..6263f3ea171 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -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) }) }