Skip to content

Commit 09a35a7

Browse files
author
Earl Warren
committed
Merge pull request '[v9.0/forgejo] Add label to Forgejo Actions PR labeled/unlabeled events and update the commit status' (go-gitea#5810) from bp-v9.0/forgejo-58e3c1f-66c85b7-f56fc51 into v9.0/forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5810 Reviewed-by: Otto <[email protected]>
2 parents a68a37f + 8a65c4d commit 09a35a7

File tree

6 files changed

+264
-7
lines changed

6 files changed

+264
-7
lines changed

models/actions/run.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) {
146146
}
147147

148148
func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) {
149-
if run.Event == webhook_module.HookEventPullRequest || run.Event == webhook_module.HookEventPullRequestSync {
149+
if run.Event == webhook_module.HookEventPullRequest ||
150+
run.Event == webhook_module.HookEventPullRequestSync ||
151+
run.Event == webhook_module.HookEventPullRequestAssign ||
152+
run.Event == webhook_module.HookEventPullRequestMilestone ||
153+
run.Event == webhook_module.HookEventPullRequestLabel {
150154
var payload api.PullRequestPayload
151155
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
152156
return nil, err

modules/structs/hook.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ type IssuePayload struct {
362362
Repository *Repository `json:"repository"`
363363
Sender *User `json:"sender"`
364364
CommitID string `json:"commit_id"`
365+
Label *Label `json:"label,omitempty"`
365366
}
366367

367368
// JSONPayload encodes the IssuePayload to JSON, with an indentation of two spaces.
@@ -399,6 +400,7 @@ type PullRequestPayload struct {
399400
Sender *User `json:"sender"`
400401
CommitID string `json:"commit_id"`
401402
Review *ReviewPayload `json:"review"`
403+
Label *Label `json:"label,omitempty"`
402404
}
403405

404406
// JSONPayload FIXME

release-notes/5778.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fix: In a Forgejo Actions workflow, the `unlabeled` event type for pull requests was incorrectly mapped to the labeled event type.
2+
fix: When a Forgejo Actions issue or pull request workflow is triggered by an `labeled` or `unlabeled` event type, it misses information about the label added or removed. It is now available in the `label` data member of the event payload.
3+
fix: The pull request workflow must always update the head SHA commit status. Not just when the PR is synchronized, opened or closed. Otherwise it makes it impossible to define a job to be a required check (for instance a job that is triggered when labels are modified and verifies that a given combination is present).

services/actions/commit_status.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
5353
return fmt.Errorf("head commit is missing in event payload")
5454
}
5555
sha = payload.HeadCommit.ID
56-
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync:
56+
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestLabel, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestMilestone:
5757
if run.TriggerEvent == actions_module.GithubEventPullRequestTarget {
5858
event = "pull_request_target"
5959
} else {

services/actions/notifier.go

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

171-
notifyIssueChange(ctx, doer, issue, hookEvent, action)
171+
notifyIssueChange(ctx, doer, issue, hookEvent, action, nil)
172172
}
173173

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

190-
notifyIssueChange(ctx, doer, issue, hookEvent, action)
190+
notifyIssueChange(ctx, doer, issue, hookEvent, action, nil)
191191
}
192192

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

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

203-
notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated)
203+
for _, added := range addedLabels {
204+
notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated, added)
205+
}
206+
for _, removed := range removedLabels {
207+
notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelCleared, removed)
208+
}
204209
}
205210

206-
func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) {
211+
func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction, label *issues_model.Label) {
207212
var err error
208213
if err = issue.LoadRepo(ctx); err != nil {
209214
log.Error("LoadRepo: %v", err)
@@ -215,6 +220,11 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues
215220
return
216221
}
217222

223+
var apiLabel *api.Label
224+
if action == api.HookIssueLabelUpdated || action == api.HookIssueLabelCleared {
225+
apiLabel = convert.ToLabel(label, issue.Repo, nil)
226+
}
227+
218228
if issue.IsPull {
219229
if err = issue.LoadPullRequest(ctx); err != nil {
220230
log.Error("loadPullRequest: %v", err)
@@ -228,6 +238,7 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues
228238
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
229239
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
230240
Sender: convert.ToUser(ctx, doer, nil),
241+
Label: apiLabel,
231242
}).
232243
WithPullRequest(issue.PullRequest).
233244
Notify(ctx)
@@ -242,6 +253,7 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues
242253
Issue: convert.ToAPIIssue(ctx, doer, issue),
243254
Repository: convert.ToRepo(ctx, issue.Repo, permission),
244255
Sender: convert.ToUser(ctx, doer, nil),
256+
Label: apiLabel,
245257
}).
246258
Notify(ctx)
247259
}

tests/integration/actions_trigger_test.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ package integration
55

66
import (
77
"fmt"
8+
"net/http"
89
"net/url"
910
"strings"
1011
"testing"
1112
"time"
1213

1314
actions_model "code.gitea.io/gitea/models/actions"
15+
auth_model "code.gitea.io/gitea/models/auth"
1416
"code.gitea.io/gitea/models/db"
1517
git_model "code.gitea.io/gitea/models/git"
1618
issues_model "code.gitea.io/gitea/models/issues"
@@ -22,9 +24,11 @@ import (
2224
"code.gitea.io/gitea/modules/git"
2325
"code.gitea.io/gitea/modules/gitrepo"
2426
"code.gitea.io/gitea/modules/setting"
27+
api "code.gitea.io/gitea/modules/structs"
2528
"code.gitea.io/gitea/modules/test"
2629
webhook_module "code.gitea.io/gitea/modules/webhook"
2730
actions_service "code.gitea.io/gitea/services/actions"
31+
issue_service "code.gitea.io/gitea/services/issue"
2832
pull_service "code.gitea.io/gitea/services/pull"
2933
release_service "code.gitea.io/gitea/services/release"
3034
repo_service "code.gitea.io/gitea/services/repository"
@@ -35,6 +39,238 @@ import (
3539
"github.com/stretchr/testify/require"
3640
)
3741

42+
func TestPullRequestCommitStatus(t *testing.T) {
43+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
44+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo
45+
session := loginUser(t, "user2")
46+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
47+
48+
// prepare the repository
49+
files := make([]*files_service.ChangeRepoFile, 0, 10)
50+
for _, onType := range []string{
51+
"opened",
52+
"synchronize",
53+
"labeled",
54+
"unlabeled",
55+
"assigned",
56+
"unassigned",
57+
"milestoned",
58+
"demilestoned",
59+
"closed",
60+
"reopened",
61+
} {
62+
files = append(files, &files_service.ChangeRepoFile{
63+
Operation: "create",
64+
TreePath: fmt.Sprintf(".forgejo/workflows/%s.yml", onType),
65+
ContentReader: strings.NewReader(fmt.Sprintf(`name: %[1]s
66+
on:
67+
pull_request:
68+
types:
69+
- %[1]s
70+
jobs:
71+
%[1]s:
72+
runs-on: docker
73+
steps:
74+
- run: true
75+
`, onType)),
76+
})
77+
}
78+
baseRepo, _, f := tests.CreateDeclarativeRepo(t, user2, "repo-pull-request",
79+
[]unit_model.Type{unit_model.TypeActions}, nil, files)
80+
defer f()
81+
baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
82+
require.NoError(t, err)
83+
defer func() {
84+
baseGitRepo.Close()
85+
}()
86+
87+
// prepare the pull request
88+
testEditFileToNewBranch(t, session, "user2", "repo-pull-request", "main", "wip-something", "README.md", "Hello, world 1")
89+
testPullCreate(t, session, "user2", "repo-pull-request", true, "main", "wip-something", "Commit status PR")
90+
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: baseRepo.ID})
91+
require.NoError(t, pr.LoadIssue(db.DefaultContext))
92+
93+
// prepare the assignees
94+
issueURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%s", "user2", "repo-pull-request", fmt.Sprintf("%d", pr.Issue.Index))
95+
96+
// prepare the labels
97+
labelStr := "/api/v1/repos/user2/repo-pull-request/labels"
98+
req := NewRequestWithJSON(t, "POST", labelStr, &api.CreateLabelOption{
99+
Name: "mylabel",
100+
Color: "abcdef",
101+
Description: "description mylabel",
102+
}).AddTokenAuth(token)
103+
resp := MakeRequest(t, req, http.StatusCreated)
104+
label := new(api.Label)
105+
DecodeJSON(t, resp, &label)
106+
labelURL := fmt.Sprintf("%s/labels", issueURL)
107+
108+
// prepare the milestone
109+
milestoneStr := "/api/v1/repos/user2/repo-pull-request/milestones"
110+
req = NewRequestWithJSON(t, "POST", milestoneStr, &api.CreateMilestoneOption{
111+
Title: "mymilestone",
112+
State: "open",
113+
}).AddTokenAuth(token)
114+
resp = MakeRequest(t, req, http.StatusCreated)
115+
milestone := new(api.Milestone)
116+
DecodeJSON(t, resp, &milestone)
117+
118+
// check that one of the status associated with the commit sha matches both
119+
// context & state
120+
checkCommitStatus := func(sha, context string, state api.CommitStatusState) bool {
121+
commitStatuses, _, err := git_model.GetLatestCommitStatus(db.DefaultContext, pr.BaseRepoID, sha, db.ListOptionsAll)
122+
require.NoError(t, err)
123+
for _, commitStatus := range commitStatuses {
124+
if state == commitStatus.State && context == commitStatus.Context {
125+
return true
126+
}
127+
}
128+
return false
129+
}
130+
131+
count := 0
132+
for _, testCase := range []struct {
133+
onType string
134+
jobID string
135+
doSomething func()
136+
action api.HookIssueAction
137+
hasLabel bool
138+
}{
139+
{
140+
onType: "opened",
141+
doSomething: func() {},
142+
action: api.HookIssueOpened,
143+
},
144+
{
145+
onType: "synchronize",
146+
doSomething: func() {
147+
testEditFile(t, session, "user2", "repo-pull-request", "wip-something", "README.md", "Hello, world 2")
148+
},
149+
action: api.HookIssueSynchronized,
150+
},
151+
{
152+
onType: "labeled",
153+
doSomething: func() {
154+
req := NewRequestWithJSON(t, "POST", labelURL, &api.IssueLabelsOption{
155+
Labels: []any{label.ID},
156+
}).AddTokenAuth(token)
157+
MakeRequest(t, req, http.StatusOK)
158+
},
159+
action: api.HookIssueLabelUpdated,
160+
hasLabel: true,
161+
},
162+
{
163+
onType: "unlabeled",
164+
doSomething: func() {
165+
req := NewRequestWithJSON(t, "PUT", labelURL, &api.IssueLabelsOption{
166+
Labels: []any{},
167+
}).AddTokenAuth(token)
168+
MakeRequest(t, req, http.StatusOK)
169+
},
170+
action: api.HookIssueLabelCleared,
171+
hasLabel: true,
172+
},
173+
{
174+
onType: "assigned",
175+
doSomething: func() {
176+
req := NewRequestWithJSON(t, "PATCH", issueURL, &api.EditIssueOption{
177+
Assignees: []string{"user2"},
178+
}).AddTokenAuth(token)
179+
MakeRequest(t, req, http.StatusCreated)
180+
},
181+
action: api.HookIssueAssigned,
182+
},
183+
{
184+
onType: "unassigned",
185+
doSomething: func() {
186+
req := NewRequestWithJSON(t, "PATCH", issueURL, &api.EditIssueOption{
187+
Assignees: []string{},
188+
}).AddTokenAuth(token)
189+
MakeRequest(t, req, http.StatusCreated)
190+
},
191+
action: api.HookIssueUnassigned,
192+
},
193+
{
194+
onType: "milestoned",
195+
doSomething: func() {
196+
req := NewRequestWithJSON(t, "PATCH", issueURL, &api.EditIssueOption{
197+
Milestone: &milestone.ID,
198+
}).AddTokenAuth(token)
199+
MakeRequest(t, req, http.StatusCreated)
200+
},
201+
action: api.HookIssueMilestoned,
202+
},
203+
{
204+
onType: "demilestoned",
205+
doSomething: func() {
206+
var zero int64
207+
req := NewRequestWithJSON(t, "PATCH", issueURL, &api.EditIssueOption{
208+
Milestone: &zero,
209+
}).AddTokenAuth(token)
210+
MakeRequest(t, req, http.StatusCreated)
211+
},
212+
action: api.HookIssueDemilestoned,
213+
},
214+
{
215+
onType: "closed",
216+
doSomething: func() {
217+
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
218+
require.NoError(t, err)
219+
err = issue_service.ChangeStatus(db.DefaultContext, pr.Issue, user2, sha, true)
220+
require.NoError(t, err)
221+
},
222+
action: api.HookIssueClosed,
223+
},
224+
{
225+
onType: "reopened",
226+
doSomething: func() {
227+
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
228+
require.NoError(t, err)
229+
err = issue_service.ChangeStatus(db.DefaultContext, pr.Issue, user2, sha, false)
230+
require.NoError(t, err)
231+
},
232+
action: api.HookIssueReOpened,
233+
},
234+
} {
235+
t.Run(testCase.onType, func(t *testing.T) {
236+
// trigger the onType event
237+
testCase.doSomething()
238+
count++
239+
context := fmt.Sprintf("%[1]s / %[1]s (pull_request)", testCase.onType)
240+
241+
// wait for a new ActionRun to be created
242+
assert.Eventually(t, func() bool {
243+
return count == unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
244+
}, 30*time.Second, 1*time.Second)
245+
246+
// verify the expected ActionRun was created
247+
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
248+
require.NoError(t, err)
249+
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, WorkflowID: fmt.Sprintf("%s.yml", testCase.onType)})
250+
251+
assert.Equal(t, sha, actionRun.CommitSHA)
252+
assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
253+
event, err := actionRun.GetPullRequestEventPayload()
254+
if testCase.hasLabel {
255+
assert.NotNil(t, event.Label)
256+
}
257+
require.NoError(t, err)
258+
assert.Equal(t, testCase.action, event.Action)
259+
260+
// verify the expected ActionRunJob was created and is StatusWaiting
261+
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{JobID: testCase.onType, CommitSHA: sha})
262+
assert.Equal(t, actions_model.StatusWaiting, job.Status)
263+
264+
// verify the commit status changes to CommitStatusSuccess when the job changes to StatusSuccess
265+
assert.True(t, checkCommitStatus(sha, context, api.CommitStatusPending))
266+
job.Status = actions_model.StatusSuccess
267+
actions_service.CreateCommitStatus(db.DefaultContext, job)
268+
assert.True(t, checkCommitStatus(sha, context, api.CommitStatusSuccess))
269+
})
270+
}
271+
})
272+
}
273+
38274
func TestPullRequestTargetEvent(t *testing.T) {
39275
onGiteaRun(t, func(t *testing.T, u *url.URL) {
40276
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo

0 commit comments

Comments
 (0)