Skip to content

Commit abbc3bc

Browse files
committed
experimental-workflow-dispatch-api
fixup remove comment fix allow branchname cleanup
1 parent 40426ad commit abbc3bc

File tree

6 files changed

+286
-136
lines changed

6 files changed

+286
-136
lines changed

modules/structs/repo_actions.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,9 @@ type ActionTaskResponse struct {
3232
Entries []*ActionTask `json:"workflow_runs"`
3333
TotalCount int64 `json:"total_count"`
3434
}
35+
36+
// ActionWorkflowDispatchOption represents a WorkflowDispatch invocation
37+
type ActionWorkflowDispatchOption struct {
38+
Ref string `json:"ref"`
39+
Inputs map[string]string `json:"inputs"`
40+
}

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,7 @@ func Routes() *web.Router {
12351235
}, reqToken(), reqAdmin())
12361236
m.Group("/actions", func() {
12371237
m.Get("/tasks", repo.ListActionTasks)
1238+
m.Post("/workflows/{workflow_id}/dispatches", bind(api.ActionWorkflowDispatchOption{}), reqRepoWriter(unit.TypeActions), repo.DispatchWorkflow)
12381239
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
12391240
m.Group("/keys", func() {
12401241
m.Combo("").Get(repo.ListDeployKeys).

routers/api/v1/repo/action.go

Lines changed: 78 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,78 @@ func ListActionTasks(ctx *context.APIContext) {
581584

582585
ctx.JSON(http.StatusOK, &res)
583586
}
587+
588+
// DispatchWorkflow Dispatches a specific workflow as a workflow run.
589+
func DispatchWorkflow(ctx *context.APIContext) {
590+
// swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository dispatchWorkflow
591+
// ---
592+
// summary: Dispatches a specific workflow as a workflow run
593+
// produces:
594+
// - application/json
595+
// parameters:
596+
// - name: owner
597+
// in: path
598+
// description: name of the owner
599+
// type: string
600+
// required: true
601+
// - name: repo
602+
// in: path
603+
// description: name of the repository
604+
// type: string
605+
// required: true
606+
// - name: workflow_id
607+
// in: path
608+
// description: name of the workflow yaml
609+
// type: string
610+
// required: true
611+
// responses:
612+
// "204":
613+
// description: "No Content"
614+
// "400":
615+
// "$ref": "#/responses/error"
616+
// "404":
617+
// "$ref": "#/responses/notFound"
618+
619+
opt := web.GetForm(ctx).(*api.ActionWorkflowDispatchOption)
620+
621+
workflowID := ctx.PathParam("workflow_id")
622+
if len(workflowID) == 0 {
623+
ctx.ServerError("workflow", nil)
624+
return
625+
}
626+
627+
ref := opt.Ref
628+
if len(ref) == 0 {
629+
ctx.ServerError("ref", nil)
630+
return
631+
}
632+
633+
err := actions_service.DispatchWorkflow(&context.Context{
634+
Base: ctx.Base,
635+
Doer: ctx.Doer,
636+
Repo: ctx.Repo,
637+
}, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs *map[string]any) error {
638+
if workflowDispatch != nil {
639+
for name, config := range workflowDispatch.Inputs {
640+
value, ok := opt.Inputs[name]
641+
if ok {
642+
(*inputs)[name] = value
643+
} else {
644+
(*inputs)[name] = config.Default
645+
}
646+
}
647+
}
648+
return nil
649+
})
650+
if err != nil {
651+
if terr, ok := err.(*actions_service.TranslateableError); ok {
652+
msg := ctx.Locale.TrString(terr.Translation, terr.Args...)
653+
ctx.Error(terr.GetCode(), msg, fmt.Errorf("%s", msg))
654+
return
655+
}
656+
ctx.Error(http.StatusInternalServerError, err.Error(), err)
657+
return
658+
}
659+
660+
ctx.Status(http.StatusNoContent)
661+
}

routers/api/v1/swagger/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,7 @@ type swaggerParameterBodies struct {
213213

214214
// in:body
215215
UpdateVariableOption api.UpdateVariableOption
216+
217+
// in:body
218+
ActionWorkflowDispatchOption api.ActionWorkflowDispatchOption
216219
}

routers/web/repo/actions/view.go

Lines changed: 24 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import (
2020
actions_model "code.gitea.io/gitea/models/actions"
2121
"code.gitea.io/gitea/models/db"
2222
git_model "code.gitea.io/gitea/models/git"
23-
"code.gitea.io/gitea/models/perm"
24-
access_model "code.gitea.io/gitea/models/perm/access"
2523
repo_model "code.gitea.io/gitea/models/repo"
2624
"code.gitea.io/gitea/models/unit"
2725
"code.gitea.io/gitea/modules/actions"
@@ -30,16 +28,13 @@ import (
3028
"code.gitea.io/gitea/modules/log"
3129
"code.gitea.io/gitea/modules/setting"
3230
"code.gitea.io/gitea/modules/storage"
33-
api "code.gitea.io/gitea/modules/structs"
3431
"code.gitea.io/gitea/modules/templates"
3532
"code.gitea.io/gitea/modules/timeutil"
3633
"code.gitea.io/gitea/modules/util"
3734
"code.gitea.io/gitea/modules/web"
3835
actions_service "code.gitea.io/gitea/services/actions"
3936
context_module "code.gitea.io/gitea/services/context"
40-
"code.gitea.io/gitea/services/convert"
4137

42-
"github.com/nektos/act/pkg/jobparser"
4338
"github.com/nektos/act/pkg/model"
4439
"xorm.io/builder"
4540
)
@@ -792,143 +787,36 @@ func Run(ctx *context_module.Context) {
792787
ctx.ServerError("ref", nil)
793788
return
794789
}
795-
796-
// can not rerun job when workflow is disabled
797-
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
798-
cfg := cfgUnit.ActionsConfig()
799-
if cfg.IsWorkflowDisabled(workflowID) {
800-
ctx.Flash.Error(ctx.Tr("actions.workflow.disabled"))
801-
ctx.Redirect(redirectURL)
802-
return
803-
}
804-
805-
// get target commit of run from specified ref
806-
refName := git.RefName(ref)
807-
var runTargetCommit *git.Commit
808-
var err error
809-
if refName.IsTag() {
810-
runTargetCommit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName())
811-
} else if refName.IsBranch() {
812-
runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName())
813-
} else {
814-
ctx.Flash.Error(ctx.Tr("form.git_ref_name_error", ref))
815-
ctx.Redirect(redirectURL)
816-
return
817-
}
818-
if err != nil {
819-
ctx.Flash.Error(ctx.Tr("form.target_ref_not_exist", ref))
820-
ctx.Redirect(redirectURL)
821-
return
822-
}
823-
824-
// get workflow entry from runTargetCommit
825-
entries, err := actions.ListWorkflows(runTargetCommit)
826-
if err != nil {
827-
ctx.Error(http.StatusInternalServerError, err.Error())
828-
return
829-
}
830-
831-
// find workflow from commit
832-
var workflows []*jobparser.SingleWorkflow
833-
for _, entry := range entries {
834-
if entry.Name() == workflowID {
835-
content, err := actions.GetContentFromEntry(entry)
836-
if err != nil {
837-
ctx.Error(http.StatusInternalServerError, err.Error())
838-
return
839-
}
840-
workflows, err = jobparser.Parse(content)
841-
if err != nil {
842-
ctx.ServerError("workflow", err)
843-
return
790+
err := actions_service.DispatchWorkflow(ctx, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs *map[string]any) error {
791+
if workflowDispatch != nil {
792+
for name, config := range workflowDispatch.Inputs {
793+
value := ctx.Req.PostFormValue(name)
794+
if config.Type == "boolean" {
795+
// https://www.w3.org/TR/html401/interact/forms.html
796+
// https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked
797+
// Checkboxes (and radio buttons) are on/off switches that may be toggled by the user.
798+
// A switch is "on" when the control element's checked attribute is set.
799+
// When a form is submitted, only "on" checkbox controls can become successful.
800+
(*inputs)[name] = strconv.FormatBool(value == "on")
801+
} else if value != "" {
802+
(*inputs)[name] = value
803+
} else {
804+
(*inputs)[name] = config.Default
805+
}
844806
}
845-
break
846807
}
847-
}
848-
849-
if len(workflows) == 0 {
850-
ctx.Flash.Error(ctx.Tr("actions.workflow.not_found", workflowID))
851-
ctx.Redirect(redirectURL)
852-
return
853-
}
854-
855-
// get inputs from post
856-
workflow := &model.Workflow{
857-
RawOn: workflows[0].RawOn,
858-
}
859-
inputs := make(map[string]any)
860-
if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
861-
for name, config := range workflowDispatch.Inputs {
862-
value := ctx.Req.PostFormValue(name)
863-
if config.Type == "boolean" {
864-
// https://www.w3.org/TR/html401/interact/forms.html
865-
// https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked
866-
// Checkboxes (and radio buttons) are on/off switches that may be toggled by the user.
867-
// A switch is "on" when the control element's checked attribute is set.
868-
// When a form is submitted, only "on" checkbox controls can become successful.
869-
inputs[name] = strconv.FormatBool(value == "on")
870-
} else if value != "" {
871-
inputs[name] = value
872-
} else {
873-
inputs[name] = config.Default
874-
}
808+
return nil
809+
})
810+
if err != nil {
811+
if terr, ok := err.(*actions_service.TranslateableError); ok {
812+
ctx.Flash.Error(ctx.Tr(terr.Translation, terr.Args...))
813+
ctx.Redirect(redirectURL)
814+
return
875815
}
876-
}
877-
878-
// ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event
879-
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
880-
// https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch
881-
workflowDispatchPayload := &api.WorkflowDispatchPayload{
882-
Workflow: workflowID,
883-
Ref: ref,
884-
Repository: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}),
885-
Inputs: inputs,
886-
Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
887-
}
888-
var eventPayload []byte
889-
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
890-
ctx.ServerError("JSONPayload", err)
816+
ctx.ServerError(err.Error(), err)
891817
return
892818
}
893819

894-
run := &actions_model.ActionRun{
895-
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
896-
RepoID: ctx.Repo.Repository.ID,
897-
OwnerID: ctx.Repo.Repository.OwnerID,
898-
WorkflowID: workflowID,
899-
TriggerUserID: ctx.Doer.ID,
900-
Ref: ref,
901-
CommitSHA: runTargetCommit.ID.String(),
902-
IsForkPullRequest: false,
903-
Event: "workflow_dispatch",
904-
TriggerEvent: "workflow_dispatch",
905-
EventPayload: string(eventPayload),
906-
Status: actions_model.StatusWaiting,
907-
}
908-
909-
// cancel running jobs of the same workflow
910-
if err := actions_model.CancelPreviousJobs(
911-
ctx,
912-
run.RepoID,
913-
run.Ref,
914-
run.WorkflowID,
915-
run.Event,
916-
); err != nil {
917-
log.Error("CancelRunningJobs: %v", err)
918-
}
919-
920-
// Insert the action run and its associated jobs into the database
921-
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
922-
ctx.ServerError("workflow", err)
923-
return
924-
}
925-
926-
alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
927-
if err != nil {
928-
log.Error("FindRunJobs: %v", err)
929-
}
930-
actions_service.CreateCommitStatus(ctx, alljobs...)
931-
932820
ctx.Flash.Success(ctx.Tr("actions.workflow.run_success", workflowID))
933821
ctx.Redirect(redirectURL)
934822
}

0 commit comments

Comments
 (0)