mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-19 07:30:50 +00:00
Actions Failure, Succes, Recover Webhooks (#7508)
Implement Actions Success, Failure and Recover webhooks for Forgejo, Gitea, Gogs, Slack, Discord, DingTalk, Telegram, Microsoft Teams, Feishu / Lark Suite, Matrix, WeCom (Wechat Work), Packagist. Some of these webhooks have not been manually tested. Implement settings for these new webhooks. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [x] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/7508): <!--number 7508 --><!--line 0 --><!--description QWN0aW9ucyBGYWlsdXJlLCBTdWNjZXMsIFJlY292ZXIgV2ViaG9va3M=-->Actions Failure, Succes, Recover Webhooks<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7508 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: christopher-besch <mail@chris-besch.com> Co-committed-by: christopher-besch <mail@chris-besch.com>
This commit is contained in:
parent
6ce9d764bc
commit
d17aa98262
27 changed files with 683 additions and 12 deletions
|
@ -299,6 +299,24 @@ func (w *Webhook) HasPackageEvent() bool {
|
||||||
(w.ChooseEvents && w.Package)
|
(w.ChooseEvents && w.Package)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasActionRunFailureEvent returns if hook enabled action failure event.
|
||||||
|
func (w *Webhook) HasActionRunFailureEvent() bool {
|
||||||
|
return w.SendEverything ||
|
||||||
|
(w.ChooseEvents && w.ActionRunFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasActionRunRecoverEvent returns if hook enabled action recover event.
|
||||||
|
func (w *Webhook) HasActionRunRecoverEvent() bool {
|
||||||
|
return w.SendEverything ||
|
||||||
|
(w.ChooseEvents && w.ActionRunRecover)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasActionRunSuccessEvent returns if hook enabled action success event.
|
||||||
|
func (w *Webhook) HasActionRunSuccessEvent() bool {
|
||||||
|
return w.SendEverything ||
|
||||||
|
(w.ChooseEvents && w.ActionRunSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
|
// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
|
||||||
func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
|
func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
|
||||||
return w.SendEverything ||
|
return w.SendEverything ||
|
||||||
|
@ -337,6 +355,9 @@ func (w *Webhook) EventCheckers() []struct {
|
||||||
{w.HasReleaseEvent, webhook_module.HookEventRelease},
|
{w.HasReleaseEvent, webhook_module.HookEventRelease},
|
||||||
{w.HasPackageEvent, webhook_module.HookEventPackage},
|
{w.HasPackageEvent, webhook_module.HookEventPackage},
|
||||||
{w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
|
{w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
|
||||||
|
{w.HasActionRunFailureEvent, webhook_module.HookEventActionRunFailure},
|
||||||
|
{w.HasActionRunRecoverEvent, webhook_module.HookEventActionRunRecover},
|
||||||
|
{w.HasActionRunSuccessEvent, webhook_module.HookEventActionRunSuccess},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,8 @@ func TestWebhook_EventsArray(t *testing.T) {
|
||||||
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
|
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
|
||||||
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
|
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
|
||||||
"pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release",
|
"pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release",
|
||||||
"package", "pull_request_review_request",
|
"package", "pull_request_review_request", "action_run_failure",
|
||||||
|
"action_run_recover", "action_run_success",
|
||||||
},
|
},
|
||||||
(&Webhook{
|
(&Webhook{
|
||||||
HookEvent: &webhook_module.HookEvent{SendEverything: true},
|
HookEvent: &webhook_module.HookEvent{SendEverything: true},
|
||||||
|
@ -89,15 +90,78 @@ func TestWebhook_EventsArray(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateWebhook(t *testing.T) {
|
func TestCreateWebhook(t *testing.T) {
|
||||||
|
t.Run("Some chosen events 1", func(t *testing.T) {
|
||||||
hook := &Webhook{
|
hook := &Webhook{
|
||||||
RepoID: 3,
|
RepoID: 3,
|
||||||
URL: "https://www.example.com/unit_test",
|
URL: "https://www.example.com/unit_test",
|
||||||
ContentType: ContentTypeJSON,
|
ContentType: ContentTypeJSON,
|
||||||
Events: `{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}`,
|
Events: `{"push_only":false,"send_everything":false,"choose_events":true,"events":{"create":false,"push":true,"pull_request":true}}`,
|
||||||
}
|
}
|
||||||
unittest.AssertNotExistsBean(t, hook)
|
unittest.AssertNotExistsBean(t, hook)
|
||||||
require.NoError(t, CreateWebhook(db.DefaultContext, hook))
|
require.NoError(t, CreateWebhook(db.DefaultContext, hook))
|
||||||
unittest.AssertExistsAndLoadBean(t, hook)
|
hookFromDb := unittest.AssertExistsAndLoadBean(t, hook)
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
string(webhook_module.HookEventPush),
|
||||||
|
string(webhook_module.HookEventPullRequest),
|
||||||
|
}, hookFromDb.EventsArray())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Some chosen events 2", func(t *testing.T) {
|
||||||
|
hook := &Webhook{
|
||||||
|
RepoID: 3,
|
||||||
|
URL: "https://www.example.com/unit_test",
|
||||||
|
ContentType: ContentTypeJSON,
|
||||||
|
Events: `{"push_only":false,"send_everything":false,"choose_events":true,"events":{"action_run_recover":false,"action_run_success":true}}`,
|
||||||
|
}
|
||||||
|
unittest.AssertNotExistsBean(t, hook)
|
||||||
|
require.NoError(t, CreateWebhook(db.DefaultContext, hook))
|
||||||
|
hookFromDb := unittest.AssertExistsAndLoadBean(t, hook)
|
||||||
|
assert.Equal(t, []string{string(webhook_module.HookEventActionRunSuccess)}, hookFromDb.EventsArray())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("All events", func(t *testing.T) {
|
||||||
|
hook := &Webhook{
|
||||||
|
RepoID: 3,
|
||||||
|
URL: "https://www.example.com/unit_test",
|
||||||
|
ContentType: ContentTypeJSON,
|
||||||
|
Events: `{"push_only":false,"send_everything":false,"choose_events":true,"events":{"create":true,"delete":true,"fork":true,"issues":true,"issue_assign":true,"issue_label":true,"issue_milestone":true,"issue_comment":true,"push":true,"pull_request":true,"pull_request_assign":true,"pull_request_label":true,"pull_request_milestone":true,"pull_request_comment":true,"pull_request_review":true,"pull_request_sync":true,"pull_request_review_request":true,"wiki":true,"repository":true,"release":true,"package":true,"action_run_failure":true,"action_run_recover":true,"action_run_success":true}}`,
|
||||||
|
}
|
||||||
|
unittest.AssertNotExistsBean(t, hook)
|
||||||
|
require.NoError(t, CreateWebhook(db.DefaultContext, hook))
|
||||||
|
hookFromDb := unittest.AssertExistsAndLoadBean(t, hook)
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
string(webhook_module.HookEventCreate),
|
||||||
|
string(webhook_module.HookEventDelete),
|
||||||
|
string(webhook_module.HookEventFork),
|
||||||
|
string(webhook_module.HookEventPush),
|
||||||
|
string(webhook_module.HookEventIssues),
|
||||||
|
string(webhook_module.HookEventIssueAssign),
|
||||||
|
string(webhook_module.HookEventIssueLabel),
|
||||||
|
string(webhook_module.HookEventIssueMilestone),
|
||||||
|
string(webhook_module.HookEventIssueComment),
|
||||||
|
string(webhook_module.HookEventPullRequest),
|
||||||
|
string(webhook_module.HookEventPullRequestAssign),
|
||||||
|
string(webhook_module.HookEventPullRequestLabel),
|
||||||
|
string(webhook_module.HookEventPullRequestMilestone),
|
||||||
|
string(webhook_module.HookEventPullRequestComment),
|
||||||
|
string(webhook_module.HookEventPullRequestReviewApproved),
|
||||||
|
string(webhook_module.HookEventPullRequestReviewRejected),
|
||||||
|
string(webhook_module.HookEventPullRequestReviewComment),
|
||||||
|
string(webhook_module.HookEventPullRequestSync),
|
||||||
|
string(webhook_module.HookEventWiki),
|
||||||
|
string(webhook_module.HookEventRepository),
|
||||||
|
string(webhook_module.HookEventRelease),
|
||||||
|
string(webhook_module.HookEventPackage),
|
||||||
|
string(webhook_module.HookEventPullRequestReviewRequest),
|
||||||
|
// these aren't webhook event types
|
||||||
|
// string(webhook_module.HookEventSchedule),
|
||||||
|
// string(webhook_module.HookEventWorkflowDispatch),
|
||||||
|
string(webhook_module.HookEventActionRunFailure),
|
||||||
|
string(webhook_module.HookEventActionRunRecover),
|
||||||
|
string(webhook_module.HookEventActionRunSuccess),
|
||||||
|
},
|
||||||
|
hookFromDb.EventsArray())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetWebhookByRepoID(t *testing.T) {
|
func TestGetWebhookByRepoID(t *testing.T) {
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
|
|
||||||
package structs
|
package structs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// ActionRunJob represents a job of a run
|
// ActionRunJob represents a job of a run
|
||||||
// swagger:model
|
// swagger:model
|
||||||
type ActionRunJob struct {
|
type ActionRunJob struct {
|
||||||
|
@ -23,3 +27,54 @@ type ActionRunJob struct {
|
||||||
// the action run job status
|
// the action run job status
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActionRun represents an action run
|
||||||
|
// swagger:model
|
||||||
|
type ActionRun struct {
|
||||||
|
// the action run id
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
// the action run's title
|
||||||
|
Title string `json:"title"`
|
||||||
|
// the repo this action is part of
|
||||||
|
Repo *Repository `json:"repository"`
|
||||||
|
// the name of workflow file
|
||||||
|
WorkflowID string `json:"workflow_id"`
|
||||||
|
// a unique number for each run of a repository
|
||||||
|
Index int64 `json:"index_in_repo"`
|
||||||
|
// the user that triggered this action run
|
||||||
|
TriggerUser *User `json:"trigger_user"`
|
||||||
|
// the cron id for the schedule trigger
|
||||||
|
ScheduleID int64
|
||||||
|
// the commit/tag/… the action run ran on
|
||||||
|
PrettyRef string `json:"prettyref"`
|
||||||
|
// has the commit/tag/… the action run ran on been deleted
|
||||||
|
IsRefDeleted bool `json:"is_ref_deleted"`
|
||||||
|
// the commit sha the action run ran on
|
||||||
|
CommitSHA string `json:"commit_sha"`
|
||||||
|
// If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
|
||||||
|
IsForkPullRequest bool `json:"is_fork_pull_request"`
|
||||||
|
// may need approval if it's a fork pull request
|
||||||
|
NeedApproval bool `json:"need_approval"`
|
||||||
|
// who approved this action run
|
||||||
|
ApprovedBy int64 `json:"approved_by"`
|
||||||
|
// the webhook event that causes the workflow to run
|
||||||
|
Event string `json:"event"`
|
||||||
|
// the payload of the webhook event that causes the workflow to run
|
||||||
|
EventPayload string `json:"event_payload"`
|
||||||
|
// the trigger event defined in the `on` configuration of the triggered workflow
|
||||||
|
TriggerEvent string `json:"trigger_event"`
|
||||||
|
// the current status of this run
|
||||||
|
Status string `json:"status"`
|
||||||
|
// when the action run was started
|
||||||
|
Started time.Time `json:"started,omitempty"`
|
||||||
|
// when the action run was stopped
|
||||||
|
Stopped time.Time `json:"stopped,omitempty"`
|
||||||
|
// when the action run was created
|
||||||
|
Created time.Time `json:"created,omitempty"`
|
||||||
|
// when the action run was last updated
|
||||||
|
Updated time.Time `json:"updated,omitempty"`
|
||||||
|
// how long the action run ran for
|
||||||
|
Duration time.Duration `json:"duration,omitempty"`
|
||||||
|
// the url of this action run
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
}
|
||||||
|
|
|
@ -119,6 +119,7 @@ var (
|
||||||
_ Payloader = &RepositoryPayload{}
|
_ Payloader = &RepositoryPayload{}
|
||||||
_ Payloader = &ReleasePayload{}
|
_ Payloader = &ReleasePayload{}
|
||||||
_ Payloader = &PackagePayload{}
|
_ Payloader = &PackagePayload{}
|
||||||
|
_ Payloader = &ActionPayload{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// _________ __
|
// _________ __
|
||||||
|
@ -484,3 +485,36 @@ type PackagePayload struct {
|
||||||
func (p *PackagePayload) JSONPayload() ([]byte, error) {
|
func (p *PackagePayload) JSONPayload() ([]byte, error) {
|
||||||
return json.MarshalIndent(p, "", " ")
|
return json.MarshalIndent(p, "", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _ _ _
|
||||||
|
// / \ ___| |_(_) ___ _ __
|
||||||
|
// / _ \ / __| __| |/ _ \| '_ \
|
||||||
|
// / ___ \ (__| |_| | (_) | | | |
|
||||||
|
// /_/ \_\___|\__|_|\___/|_| |_|
|
||||||
|
|
||||||
|
// this name is ridiculous, yes
|
||||||
|
// it's the sub-type of hook that has something to do with Forgejo Actions
|
||||||
|
type HookActionAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
HookActionFailure HookActionAction = "failure"
|
||||||
|
HookActionRecover HookActionAction = "recover"
|
||||||
|
HookActionSuccess HookActionAction = "success"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ActionPayload payload for action webhooks
|
||||||
|
type ActionPayload struct {
|
||||||
|
Action HookActionAction `json:"action"`
|
||||||
|
Run *ActionRun `json:"run"`
|
||||||
|
// the status of this run before it completed
|
||||||
|
// this must be a not done status
|
||||||
|
PriorStatus string `json:"prior_status"`
|
||||||
|
// the last run for the same workflow
|
||||||
|
// could be nil when Run is the first for it's workflow
|
||||||
|
LastRun *ActionRun `json:"last_run,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONPayload return payload information
|
||||||
|
func (p *ActionPayload) JSONPayload() ([]byte, error) {
|
||||||
|
return json.MarshalIndent(p, "", " ")
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package webhook
|
package webhook
|
||||||
|
|
||||||
// HookEvents is a set of web hook events
|
// HookEvents is a set of web hook events
|
||||||
|
// update TestCreateWebhook in models/webhook/webhook_test.go when adding or changing values here
|
||||||
type HookEvents struct {
|
type HookEvents struct {
|
||||||
Create bool `json:"create"`
|
Create bool `json:"create"`
|
||||||
Delete bool `json:"delete"`
|
Delete bool `json:"delete"`
|
||||||
|
@ -26,9 +27,12 @@ type HookEvents struct {
|
||||||
Repository bool `json:"repository"`
|
Repository bool `json:"repository"`
|
||||||
Release bool `json:"release"`
|
Release bool `json:"release"`
|
||||||
Package bool `json:"package"`
|
Package bool `json:"package"`
|
||||||
|
ActionRunFailure bool `json:"action_run_failure"`
|
||||||
|
ActionRunRecover bool `json:"action_run_recover"`
|
||||||
|
ActionRunSuccess bool `json:"action_run_success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HookEvent represents events that will delivery hook.
|
// HookEvent represents events that will deliver a hook.
|
||||||
type HookEvent struct {
|
type HookEvent struct {
|
||||||
PushOnly bool `json:"push_only"`
|
PushOnly bool `json:"push_only"`
|
||||||
SendEverything bool `json:"send_everything"`
|
SendEverything bool `json:"send_everything"`
|
||||||
|
|
|
@ -7,6 +7,7 @@ package webhook
|
||||||
type HookEventType string
|
type HookEventType string
|
||||||
|
|
||||||
// Types of hook events
|
// Types of hook events
|
||||||
|
// update TestCreateWebhook in models/webhook/webhook_test.go when adding or changing values here
|
||||||
const (
|
const (
|
||||||
HookEventCreate HookEventType = "create"
|
HookEventCreate HookEventType = "create"
|
||||||
HookEventDelete HookEventType = "delete"
|
HookEventDelete HookEventType = "delete"
|
||||||
|
@ -33,6 +34,9 @@ const (
|
||||||
HookEventPackage HookEventType = "package"
|
HookEventPackage HookEventType = "package"
|
||||||
HookEventSchedule HookEventType = "schedule"
|
HookEventSchedule HookEventType = "schedule"
|
||||||
HookEventWorkflowDispatch HookEventType = "workflow_dispatch"
|
HookEventWorkflowDispatch HookEventType = "workflow_dispatch"
|
||||||
|
HookEventActionRunFailure HookEventType = "action_run_failure"
|
||||||
|
HookEventActionRunRecover HookEventType = "action_run_recover"
|
||||||
|
HookEventActionRunSuccess HookEventType = "action_run_success"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event returns the HookEventType as an event string
|
// Event returns the HookEventType as an event string
|
||||||
|
@ -65,6 +69,12 @@ func (h HookEventType) Event() string {
|
||||||
return "repository"
|
return "repository"
|
||||||
case HookEventRelease:
|
case HookEventRelease:
|
||||||
return "release"
|
return "release"
|
||||||
|
case HookEventActionRunFailure:
|
||||||
|
return "action_run_failure"
|
||||||
|
case HookEventActionRunRecover:
|
||||||
|
return "action_run_recover"
|
||||||
|
case HookEventActionRunSuccess:
|
||||||
|
return "action_run_success"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -2486,6 +2486,13 @@ settings.event_pull_request_review_request_desc = Pull request review requested
|
||||||
settings.event_pull_request_approvals = Pull request approvals
|
settings.event_pull_request_approvals = Pull request approvals
|
||||||
settings.event_pull_request_merge = Pull request merge
|
settings.event_pull_request_merge = Pull request merge
|
||||||
settings.event_pull_request_enforcement = Enforcement
|
settings.event_pull_request_enforcement = Enforcement
|
||||||
|
settings.event_header_action = Action Run events
|
||||||
|
settings.event_action_failure = Failure
|
||||||
|
settings.event_action_failure_desc = Action Run ended as failure.
|
||||||
|
settings.event_action_recover = Recover
|
||||||
|
settings.event_action_recover_desc = Action Run succeeded after last Action Run in the same workflow failed.
|
||||||
|
settings.event_action_success = Success
|
||||||
|
settings.event_action_success_desc = Action Run succeeded.
|
||||||
settings.event_package = Package
|
settings.event_package = Package
|
||||||
settings.event_package_desc = Package created or deleted in a repository.
|
settings.event_package_desc = Package created or deleted in a repository.
|
||||||
settings.branch_filter = Branch filter
|
settings.branch_filter = Branch filter
|
||||||
|
|
|
@ -175,6 +175,9 @@ func ParseHookEvent(form forms.WebhookCoreForm) *webhook_module.HookEvent {
|
||||||
Wiki: form.Wiki,
|
Wiki: form.Wiki,
|
||||||
Repository: form.Repository,
|
Repository: form.Repository,
|
||||||
Package: form.Package,
|
Package: form.Package,
|
||||||
|
ActionRunFailure: form.ActionFailure,
|
||||||
|
ActionRunRecover: form.ActionRecover,
|
||||||
|
ActionRunSuccess: form.ActionSuccess,
|
||||||
},
|
},
|
||||||
BranchFilter: form.BranchFilter,
|
BranchFilter: form.BranchFilter,
|
||||||
}
|
}
|
||||||
|
|
54
services/convert/action.go
Normal file
54
services/convert/action.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
actions_model "forgejo.org/models/actions"
|
||||||
|
access_model "forgejo.org/models/perm/access"
|
||||||
|
api "forgejo.org/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToActionRun convert actions_model.User to api.ActionRun
|
||||||
|
// the run needs all attributes loaded
|
||||||
|
func ToActionRun(ctx context.Context, run *actions_model.ActionRun) *api.ActionRun {
|
||||||
|
if run == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The doer is the one whose perspective is used to view this ActionRun.
|
||||||
|
// In the best case we use the user that created the webhook.
|
||||||
|
// Unfortunately we don't know who that was.
|
||||||
|
// So instead we use the repo owner, who is able to create webhooks and allow others to do so by making them repo admins.
|
||||||
|
// This is pretty close to perfect.
|
||||||
|
doer := run.Repo.Owner
|
||||||
|
permissionInRepo, _ := access_model.GetUserRepoPermission(ctx, run.Repo, doer)
|
||||||
|
|
||||||
|
return &api.ActionRun{
|
||||||
|
ID: run.ID,
|
||||||
|
Title: run.Title,
|
||||||
|
Repo: ToRepo(ctx, run.Repo, permissionInRepo),
|
||||||
|
WorkflowID: run.WorkflowID,
|
||||||
|
Index: run.Index,
|
||||||
|
TriggerUser: ToUser(ctx, run.TriggerUser, doer),
|
||||||
|
ScheduleID: run.ScheduleID,
|
||||||
|
PrettyRef: run.PrettyRef(),
|
||||||
|
IsRefDeleted: run.IsRefDeleted,
|
||||||
|
CommitSHA: run.CommitSHA,
|
||||||
|
IsForkPullRequest: run.IsForkPullRequest,
|
||||||
|
NeedApproval: run.NeedApproval,
|
||||||
|
ApprovedBy: run.ApprovedBy,
|
||||||
|
Event: run.Event.Event(),
|
||||||
|
EventPayload: run.EventPayload,
|
||||||
|
TriggerEvent: run.TriggerEvent,
|
||||||
|
Status: run.Status.String(),
|
||||||
|
Started: run.Started.AsTime(),
|
||||||
|
Stopped: run.Stopped.AsTime(),
|
||||||
|
Created: run.Created.AsTime(),
|
||||||
|
Updated: run.Updated.AsTime(),
|
||||||
|
Duration: run.Duration(),
|
||||||
|
HTMLURL: run.HTMLURL(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -277,6 +277,9 @@ type WebhookCoreForm struct {
|
||||||
Wiki bool
|
Wiki bool
|
||||||
Repository bool
|
Repository bool
|
||||||
Package bool
|
Package bool
|
||||||
|
ActionFailure bool
|
||||||
|
ActionRecover bool
|
||||||
|
ActionSuccess bool
|
||||||
Active bool
|
Active bool
|
||||||
BranchFilter string `binding:"GlobPattern"`
|
BranchFilter string `binding:"GlobPattern"`
|
||||||
AuthorizationHeader string
|
AuthorizationHeader string
|
||||||
|
|
|
@ -207,6 +207,12 @@ func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, err
|
||||||
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
|
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dc dingtalkConvertor) Action(p *api.ActionPayload) (DingtalkPayload, error) {
|
||||||
|
text, _ := getActionPayloadInfo(p, noneLinkFormatter)
|
||||||
|
|
||||||
|
return createDingtalkPayload(text, text, "view action", p.Run.HTMLURL), nil
|
||||||
|
}
|
||||||
|
|
||||||
func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
|
func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
|
||||||
return DingtalkPayload{
|
return DingtalkPayload{
|
||||||
MsgType: "actionCard",
|
MsgType: "actionCard",
|
||||||
|
|
|
@ -325,6 +325,12 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error)
|
||||||
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
|
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d discordConvertor) Action(p *api.ActionPayload) (DiscordPayload, error) {
|
||||||
|
text, color := getActionPayloadInfo(p, noneLinkFormatter)
|
||||||
|
|
||||||
|
return d.createPayload(p.Run.TriggerUser, text, "", p.Run.HTMLURL, color), nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ shared.PayloadConvertor[DiscordPayload] = discordConvertor{}
|
var _ shared.PayloadConvertor[DiscordPayload] = discordConvertor{}
|
||||||
|
|
||||||
func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
|
|
|
@ -191,6 +191,12 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error)
|
||||||
return newFeishuTextPayload(text), nil
|
return newFeishuTextPayload(text), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fc feishuConvertor) Action(p *api.ActionPayload) (FeishuPayload, error) {
|
||||||
|
text, _ := getActionPayloadInfo(p, noneLinkFormatter)
|
||||||
|
|
||||||
|
return newFeishuTextPayload(text), nil
|
||||||
|
}
|
||||||
|
|
||||||
type feishuConvertor struct{}
|
type feishuConvertor struct{}
|
||||||
|
|
||||||
var _ shared.PayloadConvertor[FeishuPayload] = feishuConvertor{}
|
var _ shared.PayloadConvertor[FeishuPayload] = feishuConvertor{}
|
||||||
|
|
|
@ -304,6 +304,25 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
|
||||||
return text, color
|
return text, color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getActionPayloadInfo(p *api.ActionPayload, linkFormatter linkFormatter) (text string, color int) {
|
||||||
|
runLink := linkFormatter(p.Run.HTMLURL, p.Run.Title)
|
||||||
|
repoLink := linkFormatter(p.Run.Repo.HTMLURL, p.Run.Repo.FullName)
|
||||||
|
|
||||||
|
switch p.Action {
|
||||||
|
case api.HookActionFailure:
|
||||||
|
text = fmt.Sprintf("%s Action Failed in %s %s", runLink, repoLink, p.Run.PrettyRef)
|
||||||
|
color = redColor
|
||||||
|
case api.HookActionRecover:
|
||||||
|
text = fmt.Sprintf("%s Action Recovered in %s %s", runLink, repoLink, p.Run.PrettyRef)
|
||||||
|
color = greenColor
|
||||||
|
case api.HookActionSuccess:
|
||||||
|
text = fmt.Sprintf("%s Action Succeeded in %s %s", runLink, repoLink, p.Run.PrettyRef)
|
||||||
|
color = greenColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return text, color
|
||||||
|
}
|
||||||
|
|
||||||
// ToHook convert models.Webhook to api.Hook
|
// ToHook convert models.Webhook to api.Hook
|
||||||
// This function is not part of the convert package to prevent an import cycle
|
// This function is not part of the convert package to prevent an import cycle
|
||||||
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
||||||
|
|
|
@ -270,6 +270,22 @@ func pullReleaseTestPayload() *api.ReleasePayload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ActionTestPayload() *api.ActionPayload {
|
||||||
|
// this is not a complete action payload but enough for testing purposes
|
||||||
|
return &api.ActionPayload{
|
||||||
|
Run: &api.ActionRun{
|
||||||
|
Repo: &api.Repository{
|
||||||
|
HTMLURL: "http://localhost:3000/test/repo",
|
||||||
|
Name: "repo",
|
||||||
|
FullName: "test/repo",
|
||||||
|
},
|
||||||
|
PrettyRef: "main",
|
||||||
|
HTMLURL: "http://localhost:3000/test/repo/actions/runs/69",
|
||||||
|
Title: "Build release",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func pullRequestTestPayload() *api.PullRequestPayload {
|
func pullRequestTestPayload() *api.PullRequestPayload {
|
||||||
return &api.PullRequestPayload{
|
return &api.PullRequestPayload{
|
||||||
Action: api.HookIssueOpened,
|
Action: api.HookIssueOpened,
|
||||||
|
@ -675,3 +691,36 @@ func TestGetIssueCommentPayloadInfo(t *testing.T) {
|
||||||
assert.Equal(t, c.color, color, "case %d", i)
|
assert.Equal(t, c.color, color, "case %d", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetActionPayloadInfo(t *testing.T) {
|
||||||
|
p := ActionTestPayload()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
action api.HookActionAction
|
||||||
|
text string
|
||||||
|
color int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
api.HookActionFailure,
|
||||||
|
"Build release Action Failed in test/repo main",
|
||||||
|
redColor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api.HookActionSuccess,
|
||||||
|
"Build release Action Succeeded in test/repo main",
|
||||||
|
greenColor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api.HookActionRecover,
|
||||||
|
"Build release Action Recovered in test/repo main",
|
||||||
|
greenColor,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, c := range cases {
|
||||||
|
p.Action = c.action
|
||||||
|
text, color := getActionPayloadInfo(p, noneLinkFormatter)
|
||||||
|
assert.Equal(t, c.text, text, "case %d", i)
|
||||||
|
assert.Equal(t, c.color, color, "case %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -273,6 +273,12 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
|
||||||
return m.newPayload(text)
|
return m.newPayload(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m matrixConvertor) Action(p *api.ActionPayload) (MatrixPayload, error) {
|
||||||
|
text, _ := getActionPayloadInfo(p, htmlLinkFormatter)
|
||||||
|
|
||||||
|
return m.newPayload(text)
|
||||||
|
}
|
||||||
|
|
||||||
var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
|
var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
|
||||||
|
|
||||||
func getMessageBody(htmlText string) string {
|
func getMessageBody(htmlText string) string {
|
||||||
|
|
|
@ -326,6 +326,23 @@ func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error)
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m msteamsConvertor) Action(p *api.ActionPayload) (MSTeamsPayload, error) {
|
||||||
|
title, color := getActionPayloadInfo(p, noneLinkFormatter)
|
||||||
|
|
||||||
|
// TODO: is TriggerUser correct here?
|
||||||
|
// if you'd like to test these proprietary services, see the discussion on: https://codeberg.org/forgejo/forgejo/pulls/7508
|
||||||
|
return createMSTeamsPayload(
|
||||||
|
p.Run.Repo,
|
||||||
|
p.Run.TriggerUser,
|
||||||
|
title,
|
||||||
|
"",
|
||||||
|
p.Run.HTMLURL,
|
||||||
|
color,
|
||||||
|
// TODO: does this make any sense?
|
||||||
|
&MSTeamsFact{"Action:", p.Run.Title},
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
|
func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
|
||||||
facts := make([]MSTeamsFact, 0, 2)
|
facts := make([]MSTeamsFact, 0, 2)
|
||||||
if r != nil {
|
if r != nil {
|
||||||
|
|
|
@ -6,6 +6,7 @@ package webhook
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
actions_model "forgejo.org/models/actions"
|
||||||
issues_model "forgejo.org/models/issues"
|
issues_model "forgejo.org/models/issues"
|
||||||
packages_model "forgejo.org/models/packages"
|
packages_model "forgejo.org/models/packages"
|
||||||
"forgejo.org/models/perm"
|
"forgejo.org/models/perm"
|
||||||
|
@ -887,6 +888,38 @@ func (m *webhookNotifier) PackageDelete(ctx context.Context, doer *user_model.Us
|
||||||
notifyPackage(ctx, doer, pd, api.HookPackageDeleted)
|
notifyPackage(ctx, doer, pd, api.HookPackageDeleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *webhookNotifier) ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) {
|
||||||
|
source := EventSource{
|
||||||
|
Repository: run.Repo,
|
||||||
|
Owner: run.TriggerUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := &api.ActionPayload{
|
||||||
|
Run: convert.ToActionRun(ctx, run),
|
||||||
|
LastRun: convert.ToActionRun(ctx, lastRun),
|
||||||
|
PriorStatus: priorStatus.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if run.Status.IsSuccess() {
|
||||||
|
payload.Action = api.HookActionSuccess
|
||||||
|
if err := PrepareWebhooks(ctx, source, webhook_module.HookEventActionRunSuccess, payload); err != nil {
|
||||||
|
log.Error("PrepareWebhooks: %v", err)
|
||||||
|
}
|
||||||
|
// send another event when this is a recover
|
||||||
|
if lastRun != nil && !lastRun.Status.IsSuccess() {
|
||||||
|
payload.Action = api.HookActionRecover
|
||||||
|
if err := PrepareWebhooks(ctx, source, webhook_module.HookEventActionRunRecover, payload); err != nil {
|
||||||
|
log.Error("PrepareWebhooks: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payload.Action = api.HookActionFailure
|
||||||
|
if err := PrepareWebhooks(ctx, source, webhook_module.HookEventActionRunFailure, payload); err != nil {
|
||||||
|
log.Error("PrepareWebhooks: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) {
|
func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) {
|
||||||
source := EventSource{
|
source := EventSource{
|
||||||
Repository: pd.Repository,
|
Repository: pd.Repository,
|
||||||
|
|
|
@ -6,6 +6,7 @@ package webhook
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
actions_model "forgejo.org/models/actions"
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
repo_model "forgejo.org/models/repo"
|
repo_model "forgejo.org/models/repo"
|
||||||
"forgejo.org/models/unittest"
|
"forgejo.org/models/unittest"
|
||||||
|
@ -13,10 +14,12 @@ import (
|
||||||
webhook_model "forgejo.org/models/webhook"
|
webhook_model "forgejo.org/models/webhook"
|
||||||
"forgejo.org/modules/git"
|
"forgejo.org/modules/git"
|
||||||
"forgejo.org/modules/json"
|
"forgejo.org/modules/json"
|
||||||
|
"forgejo.org/modules/log"
|
||||||
"forgejo.org/modules/repository"
|
"forgejo.org/modules/repository"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
"forgejo.org/modules/structs"
|
"forgejo.org/modules/structs"
|
||||||
"forgejo.org/modules/test"
|
"forgejo.org/modules/test"
|
||||||
|
webhook_module "forgejo.org/modules/webhook"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -119,3 +122,190 @@ func TestPushCommits(t *testing.T) {
|
||||||
assert.Equal(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID)
|
assert.Equal(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertActionEqual(t *testing.T, expectedRun *actions_model.ActionRun, actualRun *structs.ActionRun) {
|
||||||
|
assert.NotNil(t, expectedRun)
|
||||||
|
assert.NotNil(t, actualRun)
|
||||||
|
// only test a few things
|
||||||
|
assert.Equal(t, expectedRun.ID, actualRun.ID)
|
||||||
|
assert.Equal(t, expectedRun.Status.String(), actualRun.Status)
|
||||||
|
assert.Equal(t, expectedRun.Index, actualRun.Index)
|
||||||
|
assert.Equal(t, expectedRun.RepoID, actualRun.Repo.ID)
|
||||||
|
// convert to unix because of time zones
|
||||||
|
assert.Equal(t, expectedRun.Stopped.AsTime().Unix(), actualRun.Stopped.Unix())
|
||||||
|
assert.Equal(t, expectedRun.Title, actualRun.Title)
|
||||||
|
assert.Equal(t, expectedRun.WorkflowID, actualRun.WorkflowID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAction(t *testing.T) {
|
||||||
|
defer unittest.OverrideFixtures("services/webhook/TestPushCommits")()
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
triggerUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: triggerUser.ID})
|
||||||
|
|
||||||
|
oldSuccessRun := &actions_model.ActionRun{
|
||||||
|
ID: 1,
|
||||||
|
Status: actions_model.StatusSuccess,
|
||||||
|
Index: 1,
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Stopped: 1693648027,
|
||||||
|
WorkflowID: "some_workflow",
|
||||||
|
Title: "oldSuccessRun",
|
||||||
|
TriggerUser: triggerUser,
|
||||||
|
TriggerUserID: triggerUser.ID,
|
||||||
|
TriggerEvent: "push",
|
||||||
|
}
|
||||||
|
oldSuccessRun.LoadAttributes(db.DefaultContext)
|
||||||
|
oldFailureRun := &actions_model.ActionRun{
|
||||||
|
ID: 1,
|
||||||
|
Status: actions_model.StatusFailure,
|
||||||
|
Index: 1,
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Stopped: 1693648027,
|
||||||
|
WorkflowID: "some_workflow",
|
||||||
|
Title: "oldFailureRun",
|
||||||
|
TriggerUser: triggerUser,
|
||||||
|
TriggerUserID: triggerUser.ID,
|
||||||
|
TriggerEvent: "push",
|
||||||
|
}
|
||||||
|
oldFailureRun.LoadAttributes(db.DefaultContext)
|
||||||
|
newSuccessRun := &actions_model.ActionRun{
|
||||||
|
ID: 1,
|
||||||
|
Status: actions_model.StatusSuccess,
|
||||||
|
Index: 1,
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Stopped: 1693648327,
|
||||||
|
WorkflowID: "some_workflow",
|
||||||
|
Title: "newSuccessRun",
|
||||||
|
TriggerUser: triggerUser,
|
||||||
|
TriggerUserID: triggerUser.ID,
|
||||||
|
TriggerEvent: "push",
|
||||||
|
}
|
||||||
|
newSuccessRun.LoadAttributes(db.DefaultContext)
|
||||||
|
newFailureRun := &actions_model.ActionRun{
|
||||||
|
ID: 1,
|
||||||
|
Status: actions_model.StatusFailure,
|
||||||
|
Index: 1,
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Stopped: 1693648327,
|
||||||
|
WorkflowID: "some_workflow",
|
||||||
|
Title: "newFailureRun",
|
||||||
|
TriggerUser: triggerUser,
|
||||||
|
TriggerUserID: triggerUser.ID,
|
||||||
|
TriggerEvent: "push",
|
||||||
|
}
|
||||||
|
newFailureRun.LoadAttributes(db.DefaultContext)
|
||||||
|
|
||||||
|
t.Run("Successful Run after Nothing", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
|
||||||
|
|
||||||
|
NewNotifier().ActionRunNowDone(db.DefaultContext, newSuccessRun, actions_model.StatusWaiting, nil)
|
||||||
|
|
||||||
|
// there's only one of these at the time
|
||||||
|
hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_success' AND payload_content LIKE '%success%newSuccessRun%'"))
|
||||||
|
assert.Equal(t, webhook_module.HookEventActionRunSuccess, hookTask.EventType)
|
||||||
|
|
||||||
|
var payloadContent structs.ActionPayload
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
|
||||||
|
assert.Equal(t, structs.HookActionSuccess, payloadContent.Action)
|
||||||
|
assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
|
||||||
|
assertActionEqual(t, newSuccessRun, payloadContent.Run)
|
||||||
|
assert.Nil(t, payloadContent.LastRun)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Successful Run after Failure", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
|
||||||
|
|
||||||
|
NewNotifier().ActionRunNowDone(db.DefaultContext, newSuccessRun, actions_model.StatusWaiting, oldFailureRun)
|
||||||
|
|
||||||
|
{
|
||||||
|
hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_success' AND payload_content LIKE '%success%newSuccessRun%oldFailureRun%'"))
|
||||||
|
assert.Equal(t, webhook_module.HookEventActionRunSuccess, hookTask.EventType)
|
||||||
|
|
||||||
|
var payloadContent structs.ActionPayload
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
|
||||||
|
assert.Equal(t, structs.HookActionSuccess, payloadContent.Action)
|
||||||
|
assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
|
||||||
|
assertActionEqual(t, newSuccessRun, payloadContent.Run)
|
||||||
|
assertActionEqual(t, oldFailureRun, payloadContent.LastRun)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_recover' AND payload_content LIKE '%recover%newSuccessRun%oldFailureRun%'"))
|
||||||
|
assert.Equal(t, webhook_module.HookEventActionRunRecover, hookTask.EventType)
|
||||||
|
|
||||||
|
log.Error("something: %s", hookTask.PayloadContent)
|
||||||
|
var payloadContent structs.ActionPayload
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
|
||||||
|
assert.Equal(t, structs.HookActionRecover, payloadContent.Action)
|
||||||
|
assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
|
||||||
|
assertActionEqual(t, newSuccessRun, payloadContent.Run)
|
||||||
|
assertActionEqual(t, oldFailureRun, payloadContent.LastRun)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Successful Run after Success", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
|
||||||
|
|
||||||
|
NewNotifier().ActionRunNowDone(db.DefaultContext, newSuccessRun, actions_model.StatusWaiting, oldSuccessRun)
|
||||||
|
|
||||||
|
hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_success' AND payload_content LIKE '%success%newSuccessRun%oldSuccessRun%'"))
|
||||||
|
assert.Equal(t, webhook_module.HookEventActionRunSuccess, hookTask.EventType)
|
||||||
|
|
||||||
|
var payloadContent structs.ActionPayload
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
|
||||||
|
assert.Equal(t, structs.HookActionSuccess, payloadContent.Action)
|
||||||
|
assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
|
||||||
|
assertActionEqual(t, newSuccessRun, payloadContent.Run)
|
||||||
|
assertActionEqual(t, oldSuccessRun, payloadContent.LastRun)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Failed Run after Nothing", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
|
||||||
|
|
||||||
|
NewNotifier().ActionRunNowDone(db.DefaultContext, newFailureRun, actions_model.StatusWaiting, nil)
|
||||||
|
|
||||||
|
// there should only be this one at the time
|
||||||
|
hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_failure' AND payload_content LIKE '%failure%newFailureRun%'"))
|
||||||
|
assert.Equal(t, webhook_module.HookEventActionRunFailure, hookTask.EventType)
|
||||||
|
|
||||||
|
var payloadContent structs.ActionPayload
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
|
||||||
|
assert.Equal(t, structs.HookActionFailure, payloadContent.Action)
|
||||||
|
assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
|
||||||
|
assertActionEqual(t, newFailureRun, payloadContent.Run)
|
||||||
|
assert.Nil(t, payloadContent.LastRun)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Failed Run after Failure", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
|
||||||
|
|
||||||
|
NewNotifier().ActionRunNowDone(db.DefaultContext, newFailureRun, actions_model.StatusWaiting, oldFailureRun)
|
||||||
|
|
||||||
|
hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_failure' AND payload_content LIKE '%failure%newFailureRun%oldFailureRun%'"))
|
||||||
|
assert.Equal(t, webhook_module.HookEventActionRunFailure, hookTask.EventType)
|
||||||
|
|
||||||
|
var payloadContent structs.ActionPayload
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
|
||||||
|
assert.Equal(t, structs.HookActionFailure, payloadContent.Action)
|
||||||
|
assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
|
||||||
|
assertActionEqual(t, newFailureRun, payloadContent.Run)
|
||||||
|
assertActionEqual(t, oldFailureRun, payloadContent.LastRun)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Failed Run after Success", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
|
||||||
|
|
||||||
|
NewNotifier().ActionRunNowDone(db.DefaultContext, newFailureRun, actions_model.StatusWaiting, oldSuccessRun)
|
||||||
|
|
||||||
|
hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_failure' AND payload_content LIKE '%failure%newFailureRun%oldSuccessRun%'"))
|
||||||
|
assert.Equal(t, webhook_module.HookEventActionRunFailure, hookTask.EventType)
|
||||||
|
|
||||||
|
var payloadContent structs.ActionPayload
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
|
||||||
|
assert.Equal(t, structs.HookActionFailure, payloadContent.Action)
|
||||||
|
assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
|
||||||
|
assertActionEqual(t, newFailureRun, payloadContent.Run)
|
||||||
|
assertActionEqual(t, oldSuccessRun, payloadContent.LastRun)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ type PayloadConvertor[T any] interface {
|
||||||
Release(*api.ReleasePayload) (T, error)
|
Release(*api.ReleasePayload) (T, error)
|
||||||
Wiki(*api.WikiPayload) (T, error)
|
Wiki(*api.WikiPayload) (T, error)
|
||||||
Package(*api.PackagePayload) (T, error)
|
Package(*api.PackagePayload) (T, error)
|
||||||
|
Action(*api.ActionPayload) (T, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) {
|
func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) {
|
||||||
|
@ -86,6 +87,8 @@ func NewPayload[T any](rc PayloadConvertor[T], data []byte, event webhook_module
|
||||||
return convertUnmarshalledJSON(rc.Wiki, data)
|
return convertUnmarshalledJSON(rc.Wiki, data)
|
||||||
case webhook_module.HookEventPackage:
|
case webhook_module.HookEventPackage:
|
||||||
return convertUnmarshalledJSON(rc.Package, data)
|
return convertUnmarshalledJSON(rc.Package, data)
|
||||||
|
case webhook_module.HookEventActionRunFailure, webhook_module.HookEventActionRunRecover, webhook_module.HookEventActionRunSuccess:
|
||||||
|
return convertUnmarshalledJSON(rc.Action, data)
|
||||||
}
|
}
|
||||||
var t T
|
var t T
|
||||||
return t, fmt.Errorf("newPayload unsupported event: %s", event)
|
return t, fmt.Errorf("newPayload unsupported event: %s", event)
|
||||||
|
|
|
@ -142,6 +142,7 @@ func SlackLinkToRef(repoURL, ref string) string {
|
||||||
return SlackLinkFormatter(url, refName)
|
return SlackLinkFormatter(url, refName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: fix spelling to Converter
|
||||||
// Create implements payloadConvertor Create method
|
// Create implements payloadConvertor Create method
|
||||||
func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) {
|
func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) {
|
||||||
refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
|
refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
|
||||||
|
@ -311,6 +312,12 @@ func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, erro
|
||||||
return s.createPayload(text, nil), nil
|
return s.createPayload(text, nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s slackConvertor) Action(p *api.ActionPayload) (SlackPayload, error) {
|
||||||
|
text, _ := getActionPayloadInfo(p, SlackLinkFormatter)
|
||||||
|
|
||||||
|
return s.createPayload(text, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload {
|
func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload {
|
||||||
return SlackPayload{
|
return SlackPayload{
|
||||||
Channel: s.Channel,
|
Channel: s.Channel,
|
||||||
|
|
|
@ -190,6 +190,10 @@ func (pc sourcehutConvertor) Package(_ *api.PackagePayload) (graphqlPayload[buil
|
||||||
return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported
|
return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pc sourcehutConvertor) Action(_ *api.ActionPayload) (graphqlPayload[buildsVariables], error) {
|
||||||
|
return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
// newPayload opens and adjusts the manifest to submit to the builds service
|
// newPayload opens and adjusts the manifest to submit to the builds service
|
||||||
//
|
//
|
||||||
// in case of an error the Error field will be set, to be visible by the end-user under recent deliveries
|
// in case of an error the Error field will be set, to be visible by the end-user under recent deliveries
|
||||||
|
|
|
@ -205,6 +205,12 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro
|
||||||
return createTelegramPayload(text), nil
|
return createTelegramPayload(text), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (telegramConvertor) Action(p *api.ActionPayload) (TelegramPayload, error) {
|
||||||
|
text, _ := getActionPayloadInfo(p, htmlLinkFormatter)
|
||||||
|
|
||||||
|
return createTelegramPayload(text), nil
|
||||||
|
}
|
||||||
|
|
||||||
func createTelegramPayload(message string) TelegramPayload {
|
func createTelegramPayload(message string) TelegramPayload {
|
||||||
return TelegramPayload{
|
return TelegramPayload{
|
||||||
Message: markup.Sanitize(strings.TrimSpace(message)),
|
Message: markup.Sanitize(strings.TrimSpace(message)),
|
||||||
|
|
|
@ -103,7 +103,7 @@ type EventSource struct {
|
||||||
Owner *user_model.User
|
Owner *user_model.User
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle delivers hook tasks
|
// handler delivers hook tasks
|
||||||
func handler(items ...int64) []int64 {
|
func handler(items ...int64) []int64 {
|
||||||
ctx := graceful.GetManager().HammerContext()
|
ctx := graceful.GetManager().HammerContext()
|
||||||
|
|
||||||
|
|
|
@ -201,6 +201,12 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload,
|
||||||
return newWechatworkMarkdownPayload(text), nil
|
return newWechatworkMarkdownPayload(text), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wc wechatworkConvertor) Action(p *api.ActionPayload) (WechatworkPayload, error) {
|
||||||
|
text, _ := getActionPayloadInfo(p, noneLinkFormatter)
|
||||||
|
|
||||||
|
return newWechatworkMarkdownPayload(text), nil
|
||||||
|
}
|
||||||
|
|
||||||
type wechatworkConvertor struct{}
|
type wechatworkConvertor struct{}
|
||||||
|
|
||||||
var _ shared.PayloadConvertor[WechatworkPayload] = wechatworkConvertor{}
|
var _ shared.PayloadConvertor[WechatworkPayload] = wechatworkConvertor{}
|
||||||
|
|
|
@ -153,6 +153,28 @@
|
||||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_review_request_desc"}}</span>
|
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_review_request_desc"}}</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<!-- Action Run Events -->
|
||||||
|
<fieldset class="simple-grid grid-2">
|
||||||
|
<legend>{{ctx.Locale.Tr "repo.settings.event_header_action"}}</legend>
|
||||||
|
<!-- Action Run Failure -->
|
||||||
|
<label>
|
||||||
|
<input name="action_failure" type="checkbox" {{if .Webhook.ActionRunFailure}}checked{{end}}>
|
||||||
|
{{ctx.Locale.Tr "repo.settings.event_action_failure"}}
|
||||||
|
<span class="help">{{ctx.Locale.Tr "repo.settings.event_action_failure_desc"}}</span>
|
||||||
|
</label>
|
||||||
|
<!-- Action Run Recover -->
|
||||||
|
<label>
|
||||||
|
<input name="action_recover" type="checkbox" {{if .Webhook.ActionRunRecover}}checked{{end}}>
|
||||||
|
{{ctx.Locale.Tr "repo.settings.event_action_recover"}}
|
||||||
|
<span class="help">{{ctx.Locale.Tr "repo.settings.event_action_recover_desc"}}</span>
|
||||||
|
</label>
|
||||||
|
<!-- Action Run Success -->
|
||||||
|
<label>
|
||||||
|
<input name="action_success" type="checkbox" {{if .Webhook.ActionRunSuccess}}checked{{end}}>
|
||||||
|
{{ctx.Locale.Tr "repo.settings.event_action_success"}}
|
||||||
|
<span class="help">{{ctx.Locale.Tr "repo.settings.event_action_success_desc"}}</span>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -337,6 +337,8 @@ func testWebhookFormsShared(t *testing.T, endpoint, name string, session *TestSe
|
||||||
resp := session.MakeRequest(t, NewRequest(t, "GET", endpoint+"/"+name+"/new"), http.StatusOK)
|
resp := session.MakeRequest(t, NewRequest(t, "GET", endpoint+"/"+name+"/new"), http.StatusOK)
|
||||||
htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`)
|
htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`)
|
||||||
|
|
||||||
|
testWebhookFormsSharedChooseEvents(t, htmlForm)
|
||||||
|
|
||||||
// fill the form
|
// fill the form
|
||||||
payload := map[string]string{
|
payload := map[string]string{
|
||||||
"_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
|
"_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
|
||||||
|
@ -472,3 +474,37 @@ func assertHasFlashMessages(t *testing.T, resp *httptest.ResponseRecorder, expec
|
||||||
t.Errorf("unexpected flash message %q: %q", k, v)
|
t.Errorf("unexpected flash message %q: %q", k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testWebhookFormsSharedChooseEvents(t *testing.T, htmlForm *goquery.Selection) {
|
||||||
|
webhookTypes := []string{
|
||||||
|
"create",
|
||||||
|
"delete",
|
||||||
|
"fork",
|
||||||
|
"push",
|
||||||
|
"repository",
|
||||||
|
"release",
|
||||||
|
"package",
|
||||||
|
"wiki",
|
||||||
|
"issues",
|
||||||
|
"issue_assign",
|
||||||
|
"issue_label",
|
||||||
|
"issue_milestone",
|
||||||
|
"issue_comment",
|
||||||
|
"pull_request",
|
||||||
|
"pull_request_assign",
|
||||||
|
"pull_request_label",
|
||||||
|
"pull_request_milestone",
|
||||||
|
"pull_request_comment",
|
||||||
|
"pull_request_review",
|
||||||
|
"pull_request_sync",
|
||||||
|
"pull_request_review_request",
|
||||||
|
"action_failure",
|
||||||
|
"action_recover",
|
||||||
|
"action_success",
|
||||||
|
}
|
||||||
|
|
||||||
|
// check all types of webhooks are present in the form
|
||||||
|
for _, webhookType := range webhookTypes {
|
||||||
|
assertInput(t, htmlForm, webhookType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue