Skip to content

Commit 523751d

Browse files
bencuriolunnyChristopherHX
authored
Feature: Support workflow event dispatch via API (#32059)
ref: #31765 --------- Signed-off-by: Bence Santha <git@santha.eu> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
1 parent 06088ec commit 523751d

File tree

10 files changed

+1684
-136
lines changed

10 files changed

+1684
-136
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]any `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+
}

routers/api/v1/api.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,21 @@ func Routes() *web.Router {
915915
})
916916
}
917917

918+
addActionsWorkflowRoutes := func(
919+
m *web.Router,
920+
actw actions.WorkflowAPI,
921+
) {
922+
m.Group("/actions", func() {
923+
m.Group("/workflows", func() {
924+
m.Get("", reqToken(), actw.ListRepositoryWorkflows)
925+
m.Get("/{workflow_id}", reqToken(), actw.GetWorkflow)
926+
m.Put("/{workflow_id}/disable", reqToken(), reqRepoWriter(unit.TypeActions), actw.DisableWorkflow)
927+
m.Post("/{workflow_id}/dispatches", reqToken(), reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow)
928+
m.Put("/{workflow_id}/enable", reqToken(), reqRepoWriter(unit.TypeActions), actw.EnableWorkflow)
929+
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeActions))
930+
})
931+
}
932+
918933
m.Group("", func() {
919934
// Miscellaneous (no scope required)
920935
if setting.API.EnableSwagger {
@@ -1160,6 +1175,10 @@ func Routes() *web.Router {
11601175
reqOwner(),
11611176
repo.NewAction(),
11621177
)
1178+
addActionsWorkflowRoutes(
1179+
m,
1180+
repo.NewActionWorkflow(),
1181+
)
11631182
m.Group("/hooks/git", func() {
11641183
m.Combo("").Get(repo.ListGitHooks)
11651184
m.Group("/{id}", func() {

routers/api/v1/repo/action.go

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package repo
55

66
import (
77
"errors"
8+
"fmt"
89
"net/http"
910

1011
actions_model "code.gitea.io/gitea/models/actions"
@@ -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,297 @@ func ListActionTasks(ctx *context.APIContext) {
581584

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

0 commit comments

Comments
 (0)