Skip to content

Commit

Permalink
Feature: Support workflow event dispatch via API (#32059)
Browse files Browse the repository at this point in the history
ref: #31765

---------

Signed-off-by: Bence Santha <[email protected]>
Co-authored-by: Lunny Xiao <[email protected]>
Co-authored-by: Christopher Homberger <[email protected]>
  • Loading branch information
3 people authored Feb 9, 2025
1 parent 06088ec commit 523751d
Show file tree
Hide file tree
Showing 10 changed files with 1,684 additions and 136 deletions.
33 changes: 33 additions & 0 deletions modules/structs/repo_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,36 @@ type ActionTaskResponse struct {
Entries []*ActionTask `json:"workflow_runs"`
TotalCount int64 `json:"total_count"`
}

// CreateActionWorkflowDispatch represents the payload for triggering a workflow dispatch event
// swagger:model
type CreateActionWorkflowDispatch struct {
// required: true
// example: refs/heads/main
Ref string `json:"ref" binding:"Required"`
// required: false
Inputs map[string]any `json:"inputs,omitempty"`
}

// ActionWorkflow represents a ActionWorkflow
type ActionWorkflow struct {
ID string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
State string `json:"state"`
// swagger:strfmt date-time
CreatedAt time.Time `json:"created_at"`
// swagger:strfmt date-time
UpdatedAt time.Time `json:"updated_at"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
BadgeURL string `json:"badge_url"`
// swagger:strfmt date-time
DeletedAt time.Time `json:"deleted_at,omitempty"`
}

// ActionWorkflowResponse returns a ActionWorkflow
type ActionWorkflowResponse struct {
Workflows []*ActionWorkflow `json:"workflows"`
TotalCount int64 `json:"total_count"`
}
19 changes: 19 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,21 @@ func Routes() *web.Router {
})
}

addActionsWorkflowRoutes := func(
m *web.Router,
actw actions.WorkflowAPI,
) {
m.Group("/actions", func() {
m.Group("/workflows", func() {
m.Get("", reqToken(), actw.ListRepositoryWorkflows)
m.Get("/{workflow_id}", reqToken(), actw.GetWorkflow)
m.Put("/{workflow_id}/disable", reqToken(), reqRepoWriter(unit.TypeActions), actw.DisableWorkflow)
m.Post("/{workflow_id}/dispatches", reqToken(), reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow)
m.Put("/{workflow_id}/enable", reqToken(), reqRepoWriter(unit.TypeActions), actw.EnableWorkflow)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeActions))
})
}

m.Group("", func() {
// Miscellaneous (no scope required)
if setting.API.EnableSwagger {
Expand Down Expand Up @@ -1160,6 +1175,10 @@ func Routes() *web.Router {
reqOwner(),
repo.NewAction(),
)
addActionsWorkflowRoutes(
m,
repo.NewActionWorkflow(),
)
m.Group("/hooks/git", func() {
m.Combo("").Get(repo.ListGitHooks)
m.Group("/{id}", func() {
Expand Down
297 changes: 297 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,297 @@ func ListActionTasks(ctx *context.APIContext) {

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

// ActionWorkflow implements actions_service.WorkflowAPI
type ActionWorkflow struct{}

// NewActionWorkflow creates a new ActionWorkflow service
func NewActionWorkflow() actions_service.WorkflowAPI {
return ActionWorkflow{}
}

func (a ActionWorkflow) ListRepositoryWorkflows(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows repository ListRepositoryWorkflows
// ---
// summary: List repository workflows
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionWorkflowList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "500":
// "$ref": "#/responses/error"

workflows, err := actions_service.ListActionWorkflows(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListActionWorkflows", err)
return
}

ctx.JSON(http.StatusOK, &api.ActionWorkflowResponse{Workflows: workflows, TotalCount: int64(len(workflows))})
}

func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id} repository GetWorkflow
// ---
// summary: Get a workflow
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: workflow_id
// in: path
// description: id of the workflow
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionWorkflow"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "500":
// "$ref": "#/responses/error"

workflowID := ctx.PathParam("workflow_id")
if len(workflowID) == 0 {
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter"))
return
}

workflow, err := actions_service.GetActionWorkflow(ctx, workflowID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetActionWorkflow", err)
return
}

if workflow == nil {
ctx.Error(http.StatusNotFound, "GetActionWorkflow", err)
return
}

ctx.JSON(http.StatusOK, workflow)
}

func (a ActionWorkflow) DisableWorkflow(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository DisableWorkflow
// ---
// summary: Disable a workflow
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: workflow_id
// in: path
// description: id of the workflow
// type: string
// required: true
// responses:
// "204":
// description: No Content
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"

workflowID := ctx.PathParam("workflow_id")
if len(workflowID) == 0 {
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter"))
return
}

err := actions_service.DisableActionWorkflow(ctx, workflowID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "DisableActionWorkflow", err)
return
}

ctx.Status(http.StatusNoContent)
}

func (a ActionWorkflow) DispatchWorkflow(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository DispatchWorkflow
// ---
// summary: Create a workflow dispatch event
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: workflow_id
// in: path
// description: id of the workflow
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateActionWorkflowDispatch"
// responses:
// "204":
// description: No Content
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"

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

workflowID := ctx.PathParam("workflow_id")
if len(workflowID) == 0 {
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter"))
return
}

ref := opt.Ref
if len(ref) == 0 {
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("ref is required parameter"))
return
}

err := actions_service.DispatchActionWorkflow(&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)
}

func (a ActionWorkflow) EnableWorkflow(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable repository EnableWorkflow
// ---
// summary: Enable a workflow
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: workflow_id
// in: path
// description: id of the workflow
// type: string
// required: true
// responses:
// "204":
// description: No Content
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"

workflowID := ctx.PathParam("workflow_id")
if len(workflowID) == 0 {
ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter"))
return
}

err := actions_service.EnableActionWorkflow(ctx, workflowID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "EnableActionWorkflow", err)
return
}

ctx.Status(http.StatusNoContent)
}
Loading

0 comments on commit 523751d

Please sign in to comment.