Skip to content

Gitea Actions Workflow dispatch rest api #33525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions modules/structs/repo_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ type ActionTaskResponse struct {
Entries []*ActionTask `json:"workflow_runs"`
TotalCount int64 `json:"total_count"`
}

// ActionWorkflowDispatchOption represents a WorkflowDispatch invocation
type ActionWorkflowDispatchOption struct {
Ref string `json:"ref"`
Inputs map[string]string `json:"inputs"`
}
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,7 @@ func Routes() *web.Router {
}, reqToken(), reqAdmin())
m.Group("/actions", func() {
m.Get("/tasks", repo.ListActionTasks)
m.Post("/workflows/{workflow_id}/dispatches", bind(api.ActionWorkflowDispatchOption{}), reqRepoWriter(unit.TypeActions), repo.DispatchWorkflow)
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
m.Group("/keys", func() {
m.Combo("").Get(repo.ListDeployKeys).
Expand Down
86 changes: 86 additions & 0 deletions routers/api/v1/repo/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package repo

import (
"errors"
"fmt"
"net/http"

actions_model "code.gitea.io/gitea/models/actions"
Expand All @@ -19,6 +20,8 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
secret_service "code.gitea.io/gitea/services/secrets"

"github.com/nektos/act/pkg/model"
)

// ListActionsSecrets list an repo's actions secrets
Expand Down Expand Up @@ -581,3 +584,86 @@ func ListActionTasks(ctx *context.APIContext) {

ctx.JSON(http.StatusOK, &res)
}

// DispatchWorkflow Dispatches a specific workflow as a workflow run.
func DispatchWorkflow(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository dispatchWorkflow
// ---
// summary: Dispatches a specific workflow as a workflow run
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: workflow_id
// in: path
// description: name of the workflow yaml
// type: string
// required: true
// responses:
// "204":
// description: "No Content"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"

opt := web.GetForm(ctx).(*api.ActionWorkflowDispatchOption)

workflowID := ctx.PathParam("workflow_id")
if len(workflowID) == 0 {
ctx.ServerError("workflow", nil)
return
}

ref := opt.Ref
if len(ref) == 0 {
ctx.ServerError("ref", nil)
return
}

err := actions_service.DispatchWorkflow(&context.Context{
Base: ctx.Base,
Doer: ctx.Doer,
Repo: ctx.Repo,
}, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs *map[string]any) error {
if workflowDispatch != nil {
// TODO figure out why the inputs map is empty for url form encoding workaround
if opt.Inputs == nil {
for name, config := range workflowDispatch.Inputs {
value := ctx.FormString("inputs["+name+"]", config.Default)
(*inputs)[name] = value
}
} else {
for name, config := range workflowDispatch.Inputs {
value, ok := opt.Inputs[name]
if ok {
(*inputs)[name] = value
} else {
(*inputs)[name] = config.Default
}
}
}
}
return nil
})
if err != nil {
if terr, ok := err.(*actions_service.TranslateableError); ok {
msg := ctx.Locale.TrString(terr.Translation, terr.Args...)
ctx.Error(terr.GetCode(), msg, fmt.Errorf("%s", msg))
return
}
ctx.Error(http.StatusInternalServerError, err.Error(), err)
return
}

ctx.Status(http.StatusNoContent)
}
3 changes: 3 additions & 0 deletions routers/api/v1/swagger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,7 @@ type swaggerParameterBodies struct {

// in:body
UpdateVariableOption api.UpdateVariableOption

// in:body
ActionWorkflowDispatchOption api.ActionWorkflowDispatchOption
}
160 changes: 24 additions & 136 deletions routers/web/repo/actions/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/actions"
Expand All @@ -30,16 +28,13 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
context_module "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"

"github.com/nektos/act/pkg/jobparser"
"github.com/nektos/act/pkg/model"
"xorm.io/builder"
)
Expand Down Expand Up @@ -792,143 +787,36 @@ func Run(ctx *context_module.Context) {
ctx.ServerError("ref", nil)
return
}

// can not rerun job when workflow is disabled
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
if cfg.IsWorkflowDisabled(workflowID) {
ctx.Flash.Error(ctx.Tr("actions.workflow.disabled"))
ctx.Redirect(redirectURL)
return
}

// get target commit of run from specified ref
refName := git.RefName(ref)
var runTargetCommit *git.Commit
var err error
if refName.IsTag() {
runTargetCommit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName())
} else if refName.IsBranch() {
runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName())
} else {
ctx.Flash.Error(ctx.Tr("form.git_ref_name_error", ref))
ctx.Redirect(redirectURL)
return
}
if err != nil {
ctx.Flash.Error(ctx.Tr("form.target_ref_not_exist", ref))
ctx.Redirect(redirectURL)
return
}

// get workflow entry from runTargetCommit
entries, err := actions.ListWorkflows(runTargetCommit)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}

// find workflow from commit
var workflows []*jobparser.SingleWorkflow
for _, entry := range entries {
if entry.Name() == workflowID {
content, err := actions.GetContentFromEntry(entry)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
workflows, err = jobparser.Parse(content)
if err != nil {
ctx.ServerError("workflow", err)
return
err := actions_service.DispatchWorkflow(ctx, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs *map[string]any) error {
if workflowDispatch != nil {
for name, config := range workflowDispatch.Inputs {
value := ctx.Req.PostFormValue(name)
if config.Type == "boolean" {
// https://www.w3.org/TR/html401/interact/forms.html
// https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked
// Checkboxes (and radio buttons) are on/off switches that may be toggled by the user.
// A switch is "on" when the control element's checked attribute is set.
// When a form is submitted, only "on" checkbox controls can become successful.
(*inputs)[name] = strconv.FormatBool(value == "on")
} else if value != "" {
(*inputs)[name] = value
} else {
(*inputs)[name] = config.Default
}
}
break
}
}

if len(workflows) == 0 {
ctx.Flash.Error(ctx.Tr("actions.workflow.not_found", workflowID))
ctx.Redirect(redirectURL)
return
}

// get inputs from post
workflow := &model.Workflow{
RawOn: workflows[0].RawOn,
}
inputs := make(map[string]any)
if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
for name, config := range workflowDispatch.Inputs {
value := ctx.Req.PostFormValue(name)
if config.Type == "boolean" {
// https://www.w3.org/TR/html401/interact/forms.html
// https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked
// Checkboxes (and radio buttons) are on/off switches that may be toggled by the user.
// A switch is "on" when the control element's checked attribute is set.
// When a form is submitted, only "on" checkbox controls can become successful.
inputs[name] = strconv.FormatBool(value == "on")
} else if value != "" {
inputs[name] = value
} else {
inputs[name] = config.Default
}
return nil
})
if err != nil {
if terr, ok := err.(*actions_service.TranslateableError); ok {
ctx.Flash.Error(ctx.Tr(terr.Translation, terr.Args...))
ctx.Redirect(redirectURL)
return
}
}

// ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
// https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch
workflowDispatchPayload := &api.WorkflowDispatchPayload{
Workflow: workflowID,
Ref: ref,
Repository: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}),
Inputs: inputs,
Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
}
var eventPayload []byte
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
ctx.ServerError("JSONPayload", err)
ctx.ServerError(err.Error(), err)
return
}

run := &actions_model.ActionRun{
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
RepoID: ctx.Repo.Repository.ID,
OwnerID: ctx.Repo.Repository.OwnerID,
WorkflowID: workflowID,
TriggerUserID: ctx.Doer.ID,
Ref: ref,
CommitSHA: runTargetCommit.ID.String(),
IsForkPullRequest: false,
Event: "workflow_dispatch",
TriggerEvent: "workflow_dispatch",
EventPayload: string(eventPayload),
Status: actions_model.StatusWaiting,
}

// cancel running jobs of the same workflow
if err := actions_model.CancelPreviousJobs(
ctx,
run.RepoID,
run.Ref,
run.WorkflowID,
run.Event,
); err != nil {
log.Error("CancelRunningJobs: %v", err)
}

// Insert the action run and its associated jobs into the database
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
ctx.ServerError("workflow", err)
return
}

alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
if err != nil {
log.Error("FindRunJobs: %v", err)
}
actions_service.CreateCommitStatus(ctx, alljobs...)

ctx.Flash.Success(ctx.Tr("actions.workflow.run_success", workflowID))
ctx.Redirect(redirectURL)
}
Loading