Skip to content

Commit a4dfa0b

Browse files
committed
Feature: Support workflow event dispatch via API
Signed-off-by: Bence Santha <[email protected]>
1 parent 8814e9f commit a4dfa0b

File tree

7 files changed

+524
-142
lines changed

7 files changed

+524
-142
lines changed

modules/structs/repo_actions.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,19 @@ type ActionTaskResponse struct {
3333
TotalCount int64 `json:"total_count"`
3434
}
3535

36-
// CreateActionWorkflowDispatch represents the data structure for dispatching a workflow action.
37-
//
38-
// swagger:model CreateActionWorkflowDispatch
36+
// CreateActionWorkflowDispatch represents the payload for triggering a workflow dispatch event
37+
// swagger:model
3938
type CreateActionWorkflowDispatch struct {
4039
// required: true
41-
Ref string `json:"ref"`
42-
Inputs map[string]interface{} `json:"inputs"`
40+
// example: refs/heads/main
41+
Ref string `json:"ref" binding:"Required"`
42+
// required: false
43+
Inputs map[string]any `json:"inputs,omitempty"`
4344
}
4445

4546
// ActionWorkflow represents a ActionWorkflow
4647
type ActionWorkflow struct {
47-
ID int64 `json:"id"`
48+
ID string `json:"id"`
4849
NodeID string `json:"node_id"`
4950
Name string `json:"name"`
5051
Path string `json:"path"`

routers/api/v1/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,9 @@ func Routes() *web.Router {
873873
m.Group("/workflows", func() {
874874
m.Get("", reqToken(), reqChecker, actw.ListRepositoryWorkflows)
875875
m.Get("/{workflow_id}", reqToken(), reqChecker, actw.GetWorkflow)
876+
m.Put("/{workflow_id}/disable", reqToken(), reqChecker, actw.DisableWorkflow)
876877
m.Post("/{workflow_id}/dispatches", reqToken(), reqChecker, bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow)
878+
m.Put("/{workflow_id}/enable", reqToken(), reqChecker, actw.EnableWorkflow)
877879
}, context.ReferencesGitRepo(), reqRepoWriter(unit.TypeCode))
878880
})
879881
}

routers/api/v1/repo/action.go

Lines changed: 126 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,8 @@
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"
127
"errors"
13-
"github.com/nektos/act/pkg/jobparser"
14-
"github.com/nektos/act/pkg/model"
158
"net/http"
16-
"strconv"
17-
"strings"
189

1910
actions_model "code.gitea.io/gitea/models/actions"
2011
"code.gitea.io/gitea/models/db"
@@ -629,7 +620,20 @@ func (a ActionWorkflow) ListRepositoryWorkflows(ctx *context.APIContext) {
629620
// "$ref": "#/responses/conflict"
630621
// "422":
631622
// "$ref": "#/responses/validationError"
632-
panic("implement me")
623+
// "500":
624+
// "$ref": "#/responses/error"
625+
626+
workflows, err := actions_service.ListActionWorkflows(ctx)
627+
if err != nil {
628+
return
629+
}
630+
631+
if len(workflows) == 0 {
632+
ctx.JSON(http.StatusNotFound, nil)
633+
}
634+
635+
ctx.SetTotalCountHeader(int64(len(workflows)))
636+
ctx.JSON(http.StatusOK, workflows)
633637
}
634638

635639
func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) {
@@ -667,7 +671,75 @@ func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) {
667671
// "$ref": "#/responses/conflict"
668672
// "422":
669673
// "$ref": "#/responses/validationError"
670-
panic("implement me")
674+
// "500":
675+
// "$ref": "#/responses/error"
676+
677+
workflowID := ctx.PathParam("workflow_id")
678+
if len(workflowID) == 0 {
679+
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter"))
680+
return
681+
}
682+
683+
workflow, err := actions_service.GetActionWorkflow(ctx, workflowID)
684+
if err != nil {
685+
return
686+
}
687+
688+
if workflow == nil {
689+
ctx.JSON(http.StatusNotFound, nil)
690+
}
691+
692+
ctx.JSON(http.StatusOK, workflow)
693+
}
694+
695+
func (a ActionWorkflow) DisableWorkflow(ctx *context.APIContext) {
696+
// swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository DisableWorkflow
697+
// ---
698+
// summary: Disable a workflow
699+
// produces:
700+
// - application/json
701+
// parameters:
702+
// - name: owner
703+
// in: path
704+
// description: owner of the repo
705+
// type: string
706+
// required: true
707+
// - name: repo
708+
// in: path
709+
// description: name of the repo
710+
// type: string
711+
// required: true
712+
// - name: workflow_id
713+
// in: path
714+
// description: id of the workflow
715+
// type: string
716+
// required: true
717+
// responses:
718+
// "204":
719+
// description: No Content
720+
// "400":
721+
// "$ref": "#/responses/error"
722+
// "403":
723+
// "$ref": "#/responses/forbidden"
724+
// "404":
725+
// "$ref": "#/responses/notFound"
726+
// "409":
727+
// "$ref": "#/responses/conflict"
728+
// "422":
729+
// "$ref": "#/responses/validationError"
730+
731+
workflowID := ctx.PathParam("workflow_id")
732+
if len(workflowID) == 0 {
733+
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter"))
734+
return
735+
}
736+
737+
err := actions_service.DisableActionWorkflow(ctx, workflowID)
738+
if err != nil {
739+
ctx.Error(http.StatusInternalServerError, "DisableActionWorkflow", err)
740+
}
741+
742+
ctx.Status(http.StatusNoContent)
671743
}
672744

673745
func (a ActionWorkflow) DispatchWorkflow(ctx *context.APIContext) {
@@ -724,137 +796,57 @@ func (a ActionWorkflow) DispatchWorkflow(ctx *context.APIContext) {
724796
return
725797
}
726798

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-
}
799+
actions_service.DispatchActionWorkflow(ctx, workflowID, opt)
734800

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-
}
801+
ctx.Status(http.StatusNoContent)
802+
}
834803

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-
}
804+
func (a ActionWorkflow) EnableWorkflow(ctx *context.APIContext) {
805+
// swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable repository EnableWorkflow
806+
// ---
807+
// summary: Enable a workflow
808+
// produces:
809+
// - application/json
810+
// parameters:
811+
// - name: owner
812+
// in: path
813+
// description: owner of the repo
814+
// type: string
815+
// required: true
816+
// - name: repo
817+
// in: path
818+
// description: name of the repo
819+
// type: string
820+
// required: true
821+
// - name: workflow_id
822+
// in: path
823+
// description: id of the workflow
824+
// type: string
825+
// required: true
826+
// responses:
827+
// "204":
828+
// description: No Content
829+
// "400":
830+
// "$ref": "#/responses/error"
831+
// "403":
832+
// "$ref": "#/responses/forbidden"
833+
// "404":
834+
// "$ref": "#/responses/notFound"
835+
// "409":
836+
// "$ref": "#/responses/conflict"
837+
// "422":
838+
// "$ref": "#/responses/validationError"
846839

847-
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
848-
ctx.Error(http.StatusInternalServerError, "WorkflowInsertRunError", err.Error())
840+
workflowID := ctx.PathParam("workflow_id")
841+
if len(workflowID) == 0 {
842+
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter"))
849843
return
850844
}
851845

852-
alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
846+
err := actions_service.EnableActionWorkflow(ctx, workflowID)
853847
if err != nil {
854-
ctx.Error(http.StatusInternalServerError, "WorkflowFindRunJobError", err.Error())
855-
return
848+
ctx.Error(http.StatusInternalServerError, "EnableActionWorkflow", err)
856849
}
857-
actions_service.CreateCommitStatus(ctx, alljobs...)
858850

859851
ctx.Status(http.StatusNoContent)
860852
}

routers/api/v1/swagger/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ type swaggerParameterBodies struct {
203203
// in:body
204204
CreateVariableOption api.CreateVariableOption
205205

206+
// in:body
207+
CreateActionWorkflowDispatch api.CreateActionWorkflowDispatch
208+
206209
// in:body
207210
UpdateVariableOption api.UpdateVariableOption
208211
}

0 commit comments

Comments
 (0)