Skip to content

Commit 727cd1f

Browse files
committed
Feature: Support workflow event dispatch via API
1 parent f528df9 commit 727cd1f

File tree

6 files changed

+592
-0
lines changed

6 files changed

+592
-0
lines changed

modules/structs/repo_actions.go

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

routers/api/v1/api.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,20 @@ func Routes() *web.Router {
864864
})
865865
}
866866

867+
addActionsWorkflowRoutes := func(
868+
m *web.Router,
869+
reqChecker func(ctx *context.APIContext),
870+
actw actions.WorkflowAPI,
871+
) {
872+
m.Group("/actions", func() {
873+
m.Group("/workflows", func() {
874+
m.Get("", reqToken(), reqChecker, actw.ListRepositoryWorkflows)
875+
m.Get("/{workflow_id}", reqToken(), reqChecker, actw.GetWorkflow)
876+
m.Post("/{workflow_id}/dispatches", reqToken(), reqChecker, bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow)
877+
})
878+
})
879+
}
880+
867881
m.Group("", func() {
868882
// Miscellaneous (no scope required)
869883
if setting.API.EnableSwagger {
@@ -1107,6 +1121,11 @@ func Routes() *web.Router {
11071121
reqOwner(),
11081122
repo.NewAction(),
11091123
)
1124+
addActionsWorkflowRoutes(
1125+
m,
1126+
reqOwner(),
1127+
repo.NewActionWorkflow(),
1128+
)
11101129
m.Group("/hooks/git", func() {
11111130
m.Combo("").Get(repo.ListGitHooks)
11121131
m.Group("/{id}", func() {

routers/api/v1/repo/action.go

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,17 @@
44
package repo
55

66
import (
7+
"code.gitea.io/gitea/models/perm"
8+
access_model "code.gitea.io/gitea/models/perm/access"
9+
"code.gitea.io/gitea/models/unit"
10+
"code.gitea.io/gitea/modules/actions"
11+
"code.gitea.io/gitea/modules/git"
712
"errors"
13+
"github.com/nektos/act/pkg/jobparser"
14+
"github.com/nektos/act/pkg/model"
815
"net/http"
16+
"strconv"
17+
"strings"
918

1019
actions_model "code.gitea.io/gitea/models/actions"
1120
"code.gitea.io/gitea/models/db"
@@ -581,3 +590,271 @@ func ListActionTasks(ctx *context.APIContext) {
581590

582591
ctx.JSON(http.StatusOK, &res)
583592
}
593+
594+
// ActionWorkflow implements actions_service.WorkflowAPI
595+
type ActionWorkflow struct{}
596+
597+
// NewActionWorkflow creates a new ActionWorkflow service
598+
func NewActionWorkflow() actions_service.WorkflowAPI {
599+
return ActionWorkflow{}
600+
}
601+
602+
func (a ActionWorkflow) ListRepositoryWorkflows(ctx *context.APIContext) {
603+
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows repository ListRepositoryWorkflows
604+
// ---
605+
// summary: List repository workflows
606+
// produces:
607+
// - application/json
608+
// parameters:
609+
// - name: owner
610+
// in: path
611+
// description: owner of the repo
612+
// type: string
613+
// required: true
614+
// - name: repo
615+
// in: path
616+
// description: name of the repo
617+
// type: string
618+
// required: true
619+
// responses:
620+
// "200":
621+
// "$ref": "#/responses/ActionWorkflowList"
622+
// "400":
623+
// "$ref": "#/responses/error"
624+
// "403":
625+
// "$ref": "#/responses/forbidden"
626+
// "404":
627+
// "$ref": "#/responses/notFound"
628+
// "409":
629+
// "$ref": "#/responses/conflict"
630+
// "422":
631+
// "$ref": "#/responses/validationError"
632+
panic("implement me")
633+
}
634+
635+
func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) {
636+
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id} repository GetWorkflow
637+
// ---
638+
// summary: Get a workflow
639+
// produces:
640+
// - application/json
641+
// parameters:
642+
// - name: owner
643+
// in: path
644+
// description: owner of the repo
645+
// type: string
646+
// required: true
647+
// - name: repo
648+
// in: path
649+
// description: name of the repo
650+
// type: string
651+
// required: true
652+
// - name: workflow_id
653+
// in: path
654+
// description: id of the workflow
655+
// type: string
656+
// required: true
657+
// responses:
658+
// "200":
659+
// "$ref": "#/responses/ActionWorkflow"
660+
// "400":
661+
// "$ref": "#/responses/error"
662+
// "403":
663+
// "$ref": "#/responses/forbidden"
664+
// "404":
665+
// "$ref": "#/responses/notFound"
666+
// "409":
667+
// "$ref": "#/responses/conflict"
668+
// "422":
669+
// "$ref": "#/responses/validationError"
670+
panic("implement me")
671+
}
672+
673+
func (a ActionWorkflow) DispatchWorkflow(ctx *context.APIContext) {
674+
// swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository DispatchWorkflow
675+
// ---
676+
// summary: Create a workflow dispatch event
677+
// produces:
678+
// - application/json
679+
// parameters:
680+
// - name: owner
681+
// in: path
682+
// description: owner of the repo
683+
// type: string
684+
// required: true
685+
// - name: repo
686+
// in: path
687+
// description: name of the repo
688+
// type: string
689+
// required: true
690+
// - name: workflow_id
691+
// in: path
692+
// description: id of the workflow
693+
// type: string
694+
// required: true
695+
// - name: body
696+
// in: body
697+
// schema:
698+
// "$ref": "#/definitions/CreateActionWorkflowDispatch"
699+
// responses:
700+
// "204":
701+
// description: No Content
702+
// "400":
703+
// "$ref": "#/responses/error"
704+
// "403":
705+
// "$ref": "#/responses/forbidden"
706+
// "404":
707+
// "$ref": "#/responses/notFound"
708+
// "409":
709+
// "$ref": "#/responses/conflict"
710+
// "422":
711+
// "$ref": "#/responses/validationError"
712+
713+
opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch)
714+
715+
workflowID := ctx.PathParam("workflow_id")
716+
if len(workflowID) == 0 {
717+
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter"))
718+
return
719+
}
720+
721+
ref := opt.Ref
722+
if len(ref) == 0 {
723+
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("req is requried parameter"))
724+
return
725+
}
726+
727+
// can not rerun job when workflow is disabled
728+
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
729+
cfg := cfgUnit.ActionsConfig()
730+
if cfg.IsWorkflowDisabled(workflowID) {
731+
ctx.Error(http.StatusInternalServerError, "WorkflowDisabled", ctx.Tr("actions.workflow.disabled"))
732+
return
733+
}
734+
735+
// get target commit of run from specified ref
736+
refName := git.RefName(ref)
737+
var runTargetCommit *git.Commit
738+
var err error
739+
if refName.IsTag() {
740+
runTargetCommit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName())
741+
} else if refName.IsBranch() {
742+
// [E] PANIC: runtime error: invalid memory address or nil pointer dereference
743+
runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName())
744+
} else {
745+
ctx.Error(http.StatusInternalServerError, "WorkflowRefNameError", ctx.Tr("form.git_ref_name_error", ref))
746+
return
747+
}
748+
if err != nil {
749+
ctx.Error(http.StatusNotFound, "WorkflowRefNotFound", ctx.Tr("form.target_ref_not_exist", ref))
750+
return
751+
}
752+
753+
// get workflow entry from default branch commit
754+
defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
755+
if err != nil {
756+
ctx.Error(http.StatusInternalServerError, "WorkflowDefaultBranchError", err.Error())
757+
return
758+
}
759+
entries, err := actions.ListWorkflows(defaultBranchCommit)
760+
if err != nil {
761+
ctx.Error(http.StatusInternalServerError, "WorkflowListError", err.Error())
762+
}
763+
764+
// find workflow from commit
765+
var workflows []*jobparser.SingleWorkflow
766+
for _, entry := range entries {
767+
if entry.Name() == workflowID {
768+
content, err := actions.GetContentFromEntry(entry)
769+
if err != nil {
770+
ctx.Error(http.StatusInternalServerError, "WorkflowGetContentError", err.Error())
771+
return
772+
}
773+
workflows, err = jobparser.Parse(content)
774+
if err != nil {
775+
ctx.Error(http.StatusInternalServerError, "WorkflowParseError", err.Error())
776+
return
777+
}
778+
break
779+
}
780+
}
781+
782+
if len(workflows) == 0 {
783+
ctx.Error(http.StatusNotFound, "WorkflowNotFound", ctx.Tr("actions.workflow.not_found", workflowID))
784+
return
785+
}
786+
787+
workflow := &model.Workflow{
788+
RawOn: workflows[0].RawOn,
789+
}
790+
inputs := make(map[string]any)
791+
if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
792+
for name, config := range workflowDispatch.Inputs {
793+
value, exists := opt.Inputs[name]
794+
if !exists {
795+
continue
796+
}
797+
if config.Type == "boolean" {
798+
inputs[name] = strconv.FormatBool(value == "on")
799+
} else if value != "" {
800+
inputs[name] = value.(string)
801+
} else {
802+
inputs[name] = config.Default
803+
}
804+
}
805+
}
806+
807+
workflowDispatchPayload := &api.WorkflowDispatchPayload{
808+
Workflow: workflowID,
809+
Ref: ref,
810+
Repository: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}),
811+
Inputs: inputs,
812+
Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
813+
}
814+
var eventPayload []byte
815+
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
816+
ctx.Error(http.StatusInternalServerError, "WorkflowDispatchJSONParseError", err.Error())
817+
return
818+
}
819+
820+
run := &actions_model.ActionRun{
821+
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
822+
RepoID: ctx.Repo.Repository.ID,
823+
OwnerID: ctx.Repo.Repository.Owner.ID,
824+
WorkflowID: workflowID,
825+
TriggerUserID: ctx.Doer.ID,
826+
Ref: ref,
827+
CommitSHA: runTargetCommit.ID.String(),
828+
IsForkPullRequest: false,
829+
Event: "workflow_dispatch",
830+
TriggerEvent: "workflow_dispatch",
831+
EventPayload: string(eventPayload),
832+
Status: actions_model.StatusWaiting,
833+
}
834+
835+
// cancel running jobs of the same workflow
836+
if err := actions_model.CancelPreviousJobs(
837+
ctx,
838+
run.RepoID,
839+
run.Ref,
840+
run.WorkflowID,
841+
run.Event,
842+
); err != nil {
843+
ctx.Error(http.StatusInternalServerError, "WorkflowCancelPreviousJobsError", err.Error())
844+
return
845+
}
846+
847+
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
848+
ctx.Error(http.StatusInternalServerError, "WorkflowInsertRunError", err.Error())
849+
return
850+
}
851+
852+
alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
853+
if err != nil {
854+
ctx.Error(http.StatusInternalServerError, "WorkflowFindRunJobError", err.Error())
855+
return
856+
}
857+
actions_service.CreateCommitStatus(ctx, alljobs...)
858+
859+
ctx.Status(http.StatusNoContent)
860+
}

routers/api/v1/swagger/action.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,17 @@ type swaggerResponseVariableList struct {
3232
// in:body
3333
Body []api.ActionVariable `json:"body"`
3434
}
35+
36+
// ActionWorkflow
37+
// swagger:response ActionWorkflow
38+
type swaggerResponseActionWorkflow struct {
39+
// in:body
40+
Body api.ActionWorkflow `json:"body"`
41+
}
42+
43+
// ActionWorkflowList
44+
// swagger:response ActionWorkflowList
45+
type swaggerResponseActionWorkflowList struct {
46+
// in:body
47+
Body []api.ActionWorkflow `json:"body"`
48+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import "code.gitea.io/gitea/services/context"
7+
8+
// WorkflowAPI for action workflow of a repository
9+
type WorkflowAPI interface {
10+
// ListRepositoryWorkflows list repository workflows
11+
ListRepositoryWorkflows(*context.APIContext)
12+
// GetWorkflow get a workflow
13+
GetWorkflow(*context.APIContext)
14+
// DispatchWorkflow create a workflow dispatch event
15+
DispatchWorkflow(*context.APIContext)
16+
}

0 commit comments

Comments
 (0)