Skip to content

Commit f09bea7

Browse files
Sumit189lunny
andauthored
[Fix] Trigger 'unlabeled' event when label is Deleted from PR (#34316)
This pull request updates the handling of issue label events in workflows to distinguish between label additions and deletions, introduces corresponding test cases, and extends the `IssuePayload` structure to support this functionality. ### Enhancements to issue label event handling: * Updated `matchIssuesEvent` in `modules/actions/workflows.go` to differentiate between "labeled" and "unlabeled" events based on whether labels were added or removed. * Added a new field, `RemovedLabels`, to the `IssuePayload` struct in `modules/structs/hook.go` to track labels that were removed during an issue event. ### Testing improvements: * Added `TestMatchIssuesEvent` in `modules/actions/workflows_test.go` to cover scenarios such as label addition, label deletion, and label clearing, ensuring the correct event type is triggered. --------- Co-authored-by: Lunny Xiao <[email protected]>
1 parent 0b706b0 commit f09bea7

File tree

4 files changed

+249
-25
lines changed

4 files changed

+249
-25
lines changed

modules/actions/workflows.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -377,20 +377,28 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
377377
// Actions with the same name:
378378
// opened, edited, closed, reopened, assigned, unassigned, milestoned, demilestoned
379379
// Actions need to be converted:
380-
// label_updated -> labeled
380+
// label_updated -> labeled (when adding) or unlabeled (when removing)
381381
// label_cleared -> unlabeled
382382
// Unsupported activity types:
383383
// deleted, transferred, pinned, unpinned, locked, unlocked
384384

385-
action := issuePayload.Action
386-
switch action {
385+
actions := []string{}
386+
switch issuePayload.Action {
387387
case api.HookIssueLabelUpdated:
388-
action = "labeled"
388+
if len(issuePayload.Changes.AddedLabels) > 0 {
389+
actions = append(actions, "labeled")
390+
}
391+
if len(issuePayload.Changes.RemovedLabels) > 0 {
392+
actions = append(actions, "unlabeled")
393+
}
389394
case api.HookIssueLabelCleared:
390-
action = "unlabeled"
395+
actions = append(actions, "unlabeled")
396+
default:
397+
actions = append(actions, string(issuePayload.Action))
391398
}
399+
392400
for _, val := range vals {
393-
if glob.MustCompile(val, '/').Match(string(action)) {
401+
if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) {
394402
matchTimes++
395403
break
396404
}

modules/actions/workflows_test.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,184 @@ func TestDetectMatched(t *testing.T) {
154154
})
155155
}
156156
}
157+
158+
func TestMatchIssuesEvent(t *testing.T) {
159+
testCases := []struct {
160+
desc string
161+
payload *api.IssuePayload
162+
yamlOn string
163+
expected bool
164+
eventType string
165+
}{
166+
{
167+
desc: "Label deletion should trigger unlabeled event",
168+
payload: &api.IssuePayload{
169+
Action: api.HookIssueLabelUpdated,
170+
Issue: &api.Issue{
171+
Labels: []*api.Label{},
172+
},
173+
Changes: &api.ChangesPayload{
174+
RemovedLabels: []*api.Label{
175+
{ID: 123, Name: "deleted-label"},
176+
},
177+
},
178+
},
179+
yamlOn: "on:\n issues:\n types: [unlabeled]",
180+
expected: true,
181+
eventType: "unlabeled",
182+
},
183+
{
184+
desc: "Label deletion with existing labels should trigger unlabeled event",
185+
payload: &api.IssuePayload{
186+
Action: api.HookIssueLabelUpdated,
187+
Issue: &api.Issue{
188+
Labels: []*api.Label{
189+
{ID: 456, Name: "existing-label"},
190+
},
191+
},
192+
Changes: &api.ChangesPayload{
193+
AddedLabels: nil,
194+
RemovedLabels: []*api.Label{
195+
{ID: 123, Name: "deleted-label"},
196+
},
197+
},
198+
},
199+
yamlOn: "on:\n issues:\n types: [unlabeled]",
200+
expected: true,
201+
eventType: "unlabeled",
202+
},
203+
{
204+
desc: "Label addition should trigger labeled event",
205+
payload: &api.IssuePayload{
206+
Action: api.HookIssueLabelUpdated,
207+
Issue: &api.Issue{
208+
Labels: []*api.Label{
209+
{ID: 123, Name: "new-label"},
210+
},
211+
},
212+
Changes: &api.ChangesPayload{
213+
AddedLabels: []*api.Label{
214+
{ID: 123, Name: "new-label"},
215+
},
216+
RemovedLabels: []*api.Label{}, // Empty array, no labels removed
217+
},
218+
},
219+
yamlOn: "on:\n issues:\n types: [labeled]",
220+
expected: true,
221+
eventType: "labeled",
222+
},
223+
{
224+
desc: "Label clear should trigger unlabeled event",
225+
payload: &api.IssuePayload{
226+
Action: api.HookIssueLabelCleared,
227+
Issue: &api.Issue{
228+
Labels: []*api.Label{},
229+
},
230+
},
231+
yamlOn: "on:\n issues:\n types: [unlabeled]",
232+
expected: true,
233+
eventType: "unlabeled",
234+
},
235+
{
236+
desc: "Both adding and removing labels should trigger labeled event",
237+
payload: &api.IssuePayload{
238+
Action: api.HookIssueLabelUpdated,
239+
Issue: &api.Issue{
240+
Labels: []*api.Label{
241+
{ID: 789, Name: "new-label"},
242+
},
243+
},
244+
Changes: &api.ChangesPayload{
245+
AddedLabels: []*api.Label{
246+
{ID: 789, Name: "new-label"},
247+
},
248+
RemovedLabels: []*api.Label{
249+
{ID: 123, Name: "deleted-label"},
250+
},
251+
},
252+
},
253+
yamlOn: "on:\n issues:\n types: [labeled]",
254+
expected: true,
255+
eventType: "labeled",
256+
},
257+
{
258+
desc: "Both adding and removing labels should trigger unlabeled event",
259+
payload: &api.IssuePayload{
260+
Action: api.HookIssueLabelUpdated,
261+
Issue: &api.Issue{
262+
Labels: []*api.Label{
263+
{ID: 789, Name: "new-label"},
264+
},
265+
},
266+
Changes: &api.ChangesPayload{
267+
AddedLabels: []*api.Label{
268+
{ID: 789, Name: "new-label"},
269+
},
270+
RemovedLabels: []*api.Label{
271+
{ID: 123, Name: "deleted-label"},
272+
},
273+
},
274+
},
275+
yamlOn: "on:\n issues:\n types: [unlabeled]",
276+
expected: true,
277+
eventType: "unlabeled",
278+
},
279+
{
280+
desc: "Both adding and removing labels should trigger both events",
281+
payload: &api.IssuePayload{
282+
Action: api.HookIssueLabelUpdated,
283+
Issue: &api.Issue{
284+
Labels: []*api.Label{
285+
{ID: 789, Name: "new-label"},
286+
},
287+
},
288+
Changes: &api.ChangesPayload{
289+
AddedLabels: []*api.Label{
290+
{ID: 789, Name: "new-label"},
291+
},
292+
RemovedLabels: []*api.Label{
293+
{ID: 123, Name: "deleted-label"},
294+
},
295+
},
296+
},
297+
yamlOn: "on:\n issues:\n types: [labeled, unlabeled]",
298+
expected: true,
299+
eventType: "multiple",
300+
},
301+
}
302+
303+
for _, tc := range testCases {
304+
t.Run(tc.desc, func(t *testing.T) {
305+
evts, err := GetEventsFromContent([]byte(tc.yamlOn))
306+
assert.NoError(t, err)
307+
assert.Len(t, evts, 1)
308+
309+
// Test if the event matches as expected
310+
assert.Equal(t, tc.expected, matchIssuesEvent(tc.payload, evts[0]))
311+
312+
// For extra validation, check that action mapping works correctly
313+
if tc.eventType == "multiple" {
314+
// Skip direct action mapping validation for multiple events case
315+
// as one action can map to multiple event types
316+
return
317+
}
318+
319+
// Determine expected action for single event case
320+
var expectedAction string
321+
switch tc.payload.Action {
322+
case api.HookIssueLabelUpdated:
323+
if tc.eventType == "labeled" {
324+
expectedAction = "labeled"
325+
} else if tc.eventType == "unlabeled" {
326+
expectedAction = "unlabeled"
327+
}
328+
case api.HookIssueLabelCleared:
329+
expectedAction = "unlabeled"
330+
default:
331+
expectedAction = string(tc.payload.Action)
332+
}
333+
334+
assert.Equal(t, expectedAction, tc.eventType, "Event type should match expected")
335+
})
336+
}
337+
}

modules/structs/hook.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,10 @@ type ChangesPayload struct {
418418
Body *ChangesFromPayload `json:"body,omitempty"`
419419
// Changes made to the reference
420420
Ref *ChangesFromPayload `json:"ref,omitempty"`
421+
// Changes made to the labels added
422+
AddedLabels []*Label `json:"added_labels"`
423+
// Changes made to the labels removed
424+
RemovedLabels []*Label `json:"removed_labels"`
421425
}
422426

423427
// PullRequestPayload represents a payload information of pull request event.

services/actions/notifier.go

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ func (n *actionsNotifier) IssueChangeAssignee(ctx context.Context, doer *user_mo
169169
hookEvent = webhook_module.HookEventPullRequestAssign
170170
}
171171

172-
notifyIssueChange(ctx, doer, issue, hookEvent, action)
172+
notifyIssueChange(ctx, doer, issue, hookEvent, action, nil, nil)
173173
}
174174

175175
// IssueChangeMilestone notifies assignee to notifiers
@@ -188,11 +188,11 @@ func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
188188
hookEvent = webhook_module.HookEventPullRequestMilestone
189189
}
190190

191-
notifyIssueChange(ctx, doer, issue, hookEvent, action)
191+
notifyIssueChange(ctx, doer, issue, hookEvent, action, nil, nil)
192192
}
193193

194194
func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
195-
_, _ []*issues_model.Label,
195+
addedLabels, removedLabels []*issues_model.Label,
196196
) {
197197
ctx = withMethod(ctx, "IssueChangeLabels")
198198

@@ -201,10 +201,10 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
201201
hookEvent = webhook_module.HookEventPullRequestLabel
202202
}
203203

204-
notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated)
204+
notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated, addedLabels, removedLabels)
205205
}
206206

207-
func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) {
207+
func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction, addedLabels, removedLabels []*issues_model.Label) {
208208
var err error
209209
if err = issue.LoadRepo(ctx); err != nil {
210210
log.Error("LoadRepo: %v", err)
@@ -216,34 +216,65 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues
216216
return
217217
}
218218

219+
var addedAPILabels []*api.Label
220+
if addedLabels != nil {
221+
addedAPILabels = make([]*api.Label, 0, len(addedLabels))
222+
for _, label := range addedLabels {
223+
addedAPILabels = append(addedAPILabels, convert.ToLabel(label, issue.Repo, doer))
224+
}
225+
}
226+
227+
// Get removed labels from context if present
228+
var removedAPILabels []*api.Label
229+
if removedLabels != nil {
230+
removedAPILabels = make([]*api.Label, 0, len(removedLabels))
231+
for _, label := range removedLabels {
232+
removedAPILabels = append(removedAPILabels, convert.ToLabel(label, issue.Repo, doer))
233+
}
234+
}
235+
219236
if issue.IsPull {
220237
if err = issue.LoadPullRequest(ctx); err != nil {
221238
log.Error("loadPullRequest: %v", err)
222239
return
223240
}
241+
242+
payload := &api.PullRequestPayload{
243+
Action: action,
244+
Index: issue.Index,
245+
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
246+
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
247+
Sender: convert.ToUser(ctx, doer, nil),
248+
Changes: &api.ChangesPayload{
249+
AddedLabels: addedAPILabels,
250+
RemovedLabels: removedAPILabels,
251+
},
252+
}
253+
224254
newNotifyInputFromIssue(issue, event).
225255
WithDoer(doer).
226-
WithPayload(&api.PullRequestPayload{
227-
Action: action,
228-
Index: issue.Index,
229-
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
230-
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
231-
Sender: convert.ToUser(ctx, doer, nil),
232-
}).
256+
WithPayload(payload).
233257
WithPullRequest(issue.PullRequest).
234258
Notify(ctx)
235259
return
236260
}
261+
237262
permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
263+
payload := &api.IssuePayload{
264+
Action: action,
265+
Index: issue.Index,
266+
Issue: convert.ToAPIIssue(ctx, doer, issue),
267+
Repository: convert.ToRepo(ctx, issue.Repo, permission),
268+
Sender: convert.ToUser(ctx, doer, nil),
269+
Changes: &api.ChangesPayload{
270+
AddedLabels: addedAPILabels,
271+
RemovedLabels: removedAPILabels,
272+
},
273+
}
274+
238275
newNotifyInputFromIssue(issue, event).
239276
WithDoer(doer).
240-
WithPayload(&api.IssuePayload{
241-
Action: action,
242-
Index: issue.Index,
243-
Issue: convert.ToAPIIssue(ctx, doer, issue),
244-
Repository: convert.ToRepo(ctx, issue.Repo, permission),
245-
Sender: convert.ToUser(ctx, doer, nil),
246-
}).
277+
WithPayload(payload).
247278
Notify(ctx)
248279
}
249280

0 commit comments

Comments
 (0)