Skip to content

Commit 30993e9

Browse files
wxiaoguangbencurioChristopherHX
authored
Feature: Support workflow event dispatch via API (#33545)
Fix: #31765 (Re-open #32059) --------- Co-authored-by: Bence Santha <[email protected]> Co-authored-by: Bence Sántha <[email protected]> Co-authored-by: Christopher Homberger <[email protected]>
1 parent 085f273 commit 30993e9

File tree

13 files changed

+1748
-148
lines changed

13 files changed

+1748
-148
lines changed

modules/structs/repo_actions.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,36 @@ type ActionTaskResponse struct {
3232
Entries []*ActionTask `json:"workflow_runs"`
3333
TotalCount int64 `json:"total_count"`
3434
}
35+
36+
// CreateActionWorkflowDispatch represents the payload for triggering a workflow dispatch event
37+
// swagger:model
38+
type CreateActionWorkflowDispatch struct {
39+
// required: true
40+
// example: refs/heads/main
41+
Ref string `json:"ref" binding:"Required"`
42+
// required: false
43+
Inputs map[string]string `json:"inputs,omitempty"`
44+
}
45+
46+
// ActionWorkflow represents a ActionWorkflow
47+
type ActionWorkflow struct {
48+
ID string `json:"id"`
49+
Name string `json:"name"`
50+
Path string `json:"path"`
51+
State string `json:"state"`
52+
// swagger:strfmt date-time
53+
CreatedAt time.Time `json:"created_at"`
54+
// swagger:strfmt date-time
55+
UpdatedAt time.Time `json:"updated_at"`
56+
URL string `json:"url"`
57+
HTMLURL string `json:"html_url"`
58+
BadgeURL string `json:"badge_url"`
59+
// swagger:strfmt date-time
60+
DeletedAt time.Time `json:"deleted_at,omitempty"`
61+
}
62+
63+
// ActionWorkflowResponse returns a ActionWorkflow
64+
type ActionWorkflowResponse struct {
65+
Workflows []*ActionWorkflow `json:"workflows"`
66+
TotalCount int64 `json:"total_count"`
67+
}

modules/util/error.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ func (w SilentWrap) Unwrap() error {
3636
return w.Err
3737
}
3838

39+
type LocaleWrap struct {
40+
err error
41+
TrKey string
42+
TrArgs []any
43+
}
44+
45+
// Error returns the message
46+
func (w LocaleWrap) Error() string {
47+
return w.err.Error()
48+
}
49+
50+
// Unwrap returns the underlying error
51+
func (w LocaleWrap) Unwrap() error {
52+
return w.err
53+
}
54+
3955
// NewSilentWrapErrorf returns an error that formats as the given text but unwraps as the provided error
4056
func NewSilentWrapErrorf(unwrap error, message string, args ...any) error {
4157
if len(args) == 0 {
@@ -63,3 +79,16 @@ func NewAlreadyExistErrorf(message string, args ...any) error {
6379
func NewNotExistErrorf(message string, args ...any) error {
6480
return NewSilentWrapErrorf(ErrNotExist, message, args...)
6581
}
82+
83+
// ErrWrapLocale wraps an err with a translation key and arguments
84+
func ErrWrapLocale(err error, trKey string, trArgs ...any) error {
85+
return LocaleWrap{err: err, TrKey: trKey, TrArgs: trArgs}
86+
}
87+
88+
func ErrAsLocale(err error) *LocaleWrap {
89+
var e LocaleWrap
90+
if errors.As(err, &e) {
91+
return &e
92+
}
93+
return nil
94+
}

routers/api/v1/api.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,11 +1155,17 @@ func Routes() *web.Router {
11551155
m.Post("/accept", repo.AcceptTransfer)
11561156
m.Post("/reject", repo.RejectTransfer)
11571157
}, reqToken())
1158-
addActionsRoutes(
1159-
m,
1160-
reqOwner(),
1161-
repo.NewAction(),
1162-
)
1158+
1159+
addActionsRoutes(m, reqOwner(), repo.NewAction()) // it adds the routes for secrets/variables and runner management
1160+
1161+
m.Group("/actions/workflows", func() {
1162+
m.Get("", repo.ActionsListRepositoryWorkflows)
1163+
m.Get("/{workflow_id}", repo.ActionsGetWorkflow)
1164+
m.Put("/{workflow_id}/disable", reqRepoWriter(unit.TypeActions), repo.ActionsDisableWorkflow)
1165+
m.Put("/{workflow_id}/enable", reqRepoWriter(unit.TypeActions), repo.ActionsEnableWorkflow)
1166+
m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow)
1167+
}, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions))
1168+
11631169
m.Group("/hooks/git", func() {
11641170
m.Combo("").Get(repo.ListGitHooks)
11651171
m.Group("/{id}", func() {

routers/api/v1/repo/action.go

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package repo
66
import (
77
"errors"
88
"net/http"
9+
"strings"
910

1011
actions_model "code.gitea.io/gitea/models/actions"
1112
"code.gitea.io/gitea/models/db"
@@ -19,6 +20,8 @@ import (
1920
"code.gitea.io/gitea/services/context"
2021
"code.gitea.io/gitea/services/convert"
2122
secret_service "code.gitea.io/gitea/services/secrets"
23+
24+
"github.com/nektos/act/pkg/model"
2225
)
2326

2427
// ListActionsSecrets list an repo's actions secrets
@@ -581,3 +584,270 @@ func ListActionTasks(ctx *context.APIContext) {
581584

582585
ctx.JSON(http.StatusOK, &res)
583586
}
587+
588+
func ActionsListRepositoryWorkflows(ctx *context.APIContext) {
589+
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows repository ActionsListRepositoryWorkflows
590+
// ---
591+
// summary: List repository workflows
592+
// produces:
593+
// - application/json
594+
// parameters:
595+
// - name: owner
596+
// in: path
597+
// description: owner of the repo
598+
// type: string
599+
// required: true
600+
// - name: repo
601+
// in: path
602+
// description: name of the repo
603+
// type: string
604+
// required: true
605+
// responses:
606+
// "200":
607+
// "$ref": "#/responses/ActionWorkflowList"
608+
// "400":
609+
// "$ref": "#/responses/error"
610+
// "403":
611+
// "$ref": "#/responses/forbidden"
612+
// "404":
613+
// "$ref": "#/responses/notFound"
614+
// "422":
615+
// "$ref": "#/responses/validationError"
616+
// "500":
617+
// "$ref": "#/responses/error"
618+
619+
workflows, err := actions_service.ListActionWorkflows(ctx)
620+
if err != nil {
621+
ctx.Error(http.StatusInternalServerError, "ListActionWorkflows", err)
622+
return
623+
}
624+
625+
ctx.JSON(http.StatusOK, &api.ActionWorkflowResponse{Workflows: workflows, TotalCount: int64(len(workflows))})
626+
}
627+
628+
func ActionsGetWorkflow(ctx *context.APIContext) {
629+
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id} repository ActionsGetWorkflow
630+
// ---
631+
// summary: Get a workflow
632+
// produces:
633+
// - application/json
634+
// parameters:
635+
// - name: owner
636+
// in: path
637+
// description: owner of the repo
638+
// type: string
639+
// required: true
640+
// - name: repo
641+
// in: path
642+
// description: name of the repo
643+
// type: string
644+
// required: true
645+
// - name: workflow_id
646+
// in: path
647+
// description: id of the workflow
648+
// type: string
649+
// required: true
650+
// responses:
651+
// "200":
652+
// "$ref": "#/responses/ActionWorkflow"
653+
// "400":
654+
// "$ref": "#/responses/error"
655+
// "403":
656+
// "$ref": "#/responses/forbidden"
657+
// "404":
658+
// "$ref": "#/responses/notFound"
659+
// "422":
660+
// "$ref": "#/responses/validationError"
661+
// "500":
662+
// "$ref": "#/responses/error"
663+
664+
workflowID := ctx.PathParam("workflow_id")
665+
workflow, err := actions_service.GetActionWorkflow(ctx, workflowID)
666+
if err != nil {
667+
if errors.Is(err, util.ErrNotExist) {
668+
ctx.Error(http.StatusNotFound, "GetActionWorkflow", err)
669+
} else {
670+
ctx.Error(http.StatusInternalServerError, "GetActionWorkflow", err)
671+
}
672+
return
673+
}
674+
675+
ctx.JSON(http.StatusOK, workflow)
676+
}
677+
678+
func ActionsDisableWorkflow(ctx *context.APIContext) {
679+
// swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository ActionsDisableWorkflow
680+
// ---
681+
// summary: Disable a workflow
682+
// produces:
683+
// - application/json
684+
// parameters:
685+
// - name: owner
686+
// in: path
687+
// description: owner of the repo
688+
// type: string
689+
// required: true
690+
// - name: repo
691+
// in: path
692+
// description: name of the repo
693+
// type: string
694+
// required: true
695+
// - name: workflow_id
696+
// in: path
697+
// description: id of the workflow
698+
// type: string
699+
// required: true
700+
// responses:
701+
// "204":
702+
// description: No Content
703+
// "400":
704+
// "$ref": "#/responses/error"
705+
// "403":
706+
// "$ref": "#/responses/forbidden"
707+
// "404":
708+
// "$ref": "#/responses/notFound"
709+
// "422":
710+
// "$ref": "#/responses/validationError"
711+
712+
workflowID := ctx.PathParam("workflow_id")
713+
err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, false)
714+
if err != nil {
715+
if errors.Is(err, util.ErrNotExist) {
716+
ctx.Error(http.StatusNotFound, "DisableActionWorkflow", err)
717+
} else {
718+
ctx.Error(http.StatusInternalServerError, "DisableActionWorkflow", err)
719+
}
720+
return
721+
}
722+
723+
ctx.Status(http.StatusNoContent)
724+
}
725+
726+
func ActionsDispatchWorkflow(ctx *context.APIContext) {
727+
// swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository ActionsDispatchWorkflow
728+
// ---
729+
// summary: Create a workflow dispatch event
730+
// produces:
731+
// - application/json
732+
// parameters:
733+
// - name: owner
734+
// in: path
735+
// description: owner of the repo
736+
// type: string
737+
// required: true
738+
// - name: repo
739+
// in: path
740+
// description: name of the repo
741+
// type: string
742+
// required: true
743+
// - name: workflow_id
744+
// in: path
745+
// description: id of the workflow
746+
// type: string
747+
// required: true
748+
// - name: body
749+
// in: body
750+
// schema:
751+
// "$ref": "#/definitions/CreateActionWorkflowDispatch"
752+
// responses:
753+
// "204":
754+
// description: No Content
755+
// "400":
756+
// "$ref": "#/responses/error"
757+
// "403":
758+
// "$ref": "#/responses/forbidden"
759+
// "404":
760+
// "$ref": "#/responses/notFound"
761+
// "422":
762+
// "$ref": "#/responses/validationError"
763+
764+
workflowID := ctx.PathParam("workflow_id")
765+
opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch)
766+
if opt.Ref == "" {
767+
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("ref is required parameter"))
768+
return
769+
}
770+
771+
err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
772+
if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
773+
// The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
774+
// So we have to manually read the `inputs[key]` from the form
775+
for name, config := range workflowDispatch.Inputs {
776+
value := ctx.FormString("inputs["+name+"]", config.Default)
777+
inputs[name] = value
778+
}
779+
} else {
780+
for name, config := range workflowDispatch.Inputs {
781+
value, ok := opt.Inputs[name]
782+
if ok {
783+
inputs[name] = value
784+
} else {
785+
inputs[name] = config.Default
786+
}
787+
}
788+
}
789+
return nil
790+
})
791+
if err != nil {
792+
if errors.Is(err, util.ErrNotExist) {
793+
ctx.Error(http.StatusNotFound, "DispatchActionWorkflow", err)
794+
} else if errors.Is(err, util.ErrPermissionDenied) {
795+
ctx.Error(http.StatusForbidden, "DispatchActionWorkflow", err)
796+
} else {
797+
ctx.Error(http.StatusInternalServerError, "DispatchActionWorkflow", err)
798+
}
799+
return
800+
}
801+
802+
ctx.Status(http.StatusNoContent)
803+
}
804+
805+
func ActionsEnableWorkflow(ctx *context.APIContext) {
806+
// swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable repository ActionsEnableWorkflow
807+
// ---
808+
// summary: Enable a workflow
809+
// produces:
810+
// - application/json
811+
// parameters:
812+
// - name: owner
813+
// in: path
814+
// description: owner of the repo
815+
// type: string
816+
// required: true
817+
// - name: repo
818+
// in: path
819+
// description: name of the repo
820+
// type: string
821+
// required: true
822+
// - name: workflow_id
823+
// in: path
824+
// description: id of the workflow
825+
// type: string
826+
// required: true
827+
// responses:
828+
// "204":
829+
// description: No Content
830+
// "400":
831+
// "$ref": "#/responses/error"
832+
// "403":
833+
// "$ref": "#/responses/forbidden"
834+
// "404":
835+
// "$ref": "#/responses/notFound"
836+
// "409":
837+
// "$ref": "#/responses/conflict"
838+
// "422":
839+
// "$ref": "#/responses/validationError"
840+
841+
workflowID := ctx.PathParam("workflow_id")
842+
err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, true)
843+
if err != nil {
844+
if errors.Is(err, util.ErrNotExist) {
845+
ctx.Error(http.StatusNotFound, "EnableActionWorkflow", err)
846+
} else {
847+
ctx.Error(http.StatusInternalServerError, "EnableActionWorkflow", err)
848+
}
849+
return
850+
}
851+
852+
ctx.Status(http.StatusNoContent)
853+
}

0 commit comments

Comments
 (0)