Skip to content

Commit 06fa8dc

Browse files
Merge branch 'main' into lunny/move_diverging
2 parents 5b5a5d5 + 8ad2a53 commit 06fa8dc

File tree

11 files changed

+371
-62
lines changed

11 files changed

+371
-62
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.

0 commit comments

Comments
 (0)