From 6120f940903d7474279e423062e89d1820a22f0a Mon Sep 17 00:00:00 2001 From: Bence Santha Date: Tue, 17 Sep 2024 15:10:34 +0200 Subject: [PATCH 01/17] Feature: Support workflow event dispatch via API --- modules/structs/repo_actions.go | 33 +++ routers/api/v1/api.go | 19 ++ routers/api/v1/repo/action.go | 277 +++++++++++++++++++++++++ routers/api/v1/swagger/action.go | 14 ++ services/actions/workflow_interface.go | 16 ++ templates/swagger/v1_json.tmpl | 235 +++++++++++++++++++++ 6 files changed, 594 insertions(+) create mode 100644 services/actions/workflow_interface.go diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index b13f34473861f..af75bbd669fbb 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -32,3 +32,36 @@ type ActionTaskResponse struct { Entries []*ActionTask `json:"workflow_runs"` TotalCount int64 `json:"total_count"` } + +// CreateActionWorkflowDispatch represents the data structure for dispatching a workflow action. +// +// swagger:model CreateActionWorkflowDispatch +type CreateActionWorkflowDispatch struct { + // required: true + Ref string `json:"ref"` + Inputs map[string]interface{} `json:"inputs"` +} + +// ActionWorkflow represents a ActionWorkflow +type ActionWorkflow struct { + ID int64 `json:"id"` + NodeID string `json:"node_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"` +} + +// ActionWorkflowResponse returns a ActionWorkflow +type ActionWorkflowResponse struct { + Workflows []*ActionWorkflow `json:"workflows"` + TotalCount int64 `json:"total_count"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0aa38b8b6abbb..0cbaa89c00deb 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -915,6 +915,20 @@ func Routes() *web.Router { }) } + addActionsWorkflowRoutes := func( + m *web.Router, + reqChecker func(ctx *context.APIContext), + actw actions.WorkflowAPI, + ) { + m.Group("/actions", func() { + m.Group("/workflows", func() { + m.Get("", reqToken(), reqChecker, actw.ListRepositoryWorkflows) + m.Get("/{workflow_id}", reqToken(), reqChecker, actw.GetWorkflow) + m.Post("/{workflow_id}/dispatches", reqToken(), reqChecker, bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow) + }, context.ReferencesGitRepo(), reqRepoWriter(unit.TypeCode)) + }) + } + m.Group("", func() { // Miscellaneous (no scope required) if setting.API.EnableSwagger { @@ -1160,6 +1174,11 @@ func Routes() *web.Router { reqOwner(), repo.NewAction(), ) + addActionsWorkflowRoutes( + m, + reqRepoWriter(unit.TypeCode), + repo.NewActionWorkflow(), + ) m.Group("/hooks/git", func() { m.Combo("").Get(repo.ListGitHooks) m.Group("/{id}", func() { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d27e8d2427b39..905748bf8025f 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -4,8 +4,17 @@ package repo import ( + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/git" "errors" + "github.com/nektos/act/pkg/jobparser" + "github.com/nektos/act/pkg/model" "net/http" + "strconv" + "strings" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" @@ -581,3 +590,271 @@ 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" + // "409": + // "$ref": "#/responses/conflict" + // "422": + // "$ref": "#/responses/validationError" + panic("implement me") +} + +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" + // "409": + // "$ref": "#/responses/conflict" + // "422": + // "$ref": "#/responses/validationError" + panic("implement me") +} + +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" + // "409": + // "$ref": "#/responses/conflict" + // "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 + } + + // can not rerun job when workflow is disabled + cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) + cfg := cfgUnit.ActionsConfig() + if cfg.IsWorkflowDisabled(workflowID) { + ctx.Error(http.StatusInternalServerError, "WorkflowDisabled", ctx.Tr("actions.workflow.disabled")) + 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() { + // [E] PANIC: runtime error: invalid memory address or nil pointer dereference + runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) + } else { + ctx.Error(http.StatusInternalServerError, "WorkflowRefNameError", ctx.Tr("form.git_ref_name_error", ref)) + return + } + if err != nil { + ctx.Error(http.StatusNotFound, "WorkflowRefNotFound", ctx.Tr("form.target_ref_not_exist", ref)) + return + } + + // get workflow entry from default branch commit + defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowDefaultBranchError", err.Error()) + return + } + entries, err := actions.ListWorkflows(defaultBranchCommit) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowListError", err.Error()) + } + + // 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, "WorkflowGetContentError", err.Error()) + return + } + workflows, err = jobparser.Parse(content) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowParseError", err.Error()) + return + } + break + } + } + + if len(workflows) == 0 { + ctx.Error(http.StatusNotFound, "WorkflowNotFound", ctx.Tr("actions.workflow.not_found", workflowID)) + return + } + + 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, exists := opt.Inputs[name] + if !exists { + continue + } + if config.Type == "boolean" { + inputs[name] = strconv.FormatBool(value == "on") + } else if value != "" { + inputs[name] = value.(string) + } else { + inputs[name] = config.Default + } + } + } + + 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.Error(http.StatusInternalServerError, "WorkflowDispatchJSONParseError", err.Error()) + return + } + + run := &actions_model.ActionRun{ + Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0], + RepoID: ctx.Repo.Repository.ID, + OwnerID: ctx.Repo.Repository.Owner.ID, + 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 { + ctx.Error(http.StatusInternalServerError, "WorkflowCancelPreviousJobsError", err.Error()) + return + } + + if err := actions_model.InsertRun(ctx, run, workflows); err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowInsertRunError", err.Error()) + return + } + + alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowFindRunJobError", err.Error()) + return + } + actions_service.CreateCommitStatus(ctx, alljobs...) + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go index 665f4d0b85245..16a250184adb7 100644 --- a/routers/api/v1/swagger/action.go +++ b/routers/api/v1/swagger/action.go @@ -32,3 +32,17 @@ type swaggerResponseVariableList struct { // in:body Body []api.ActionVariable `json:"body"` } + +// ActionWorkflow +// swagger:response ActionWorkflow +type swaggerResponseActionWorkflow struct { + // in:body + Body api.ActionWorkflow `json:"body"` +} + +// ActionWorkflowList +// swagger:response ActionWorkflowList +type swaggerResponseActionWorkflowList struct { + // in:body + Body []api.ActionWorkflow `json:"body"` +} diff --git a/services/actions/workflow_interface.go b/services/actions/workflow_interface.go new file mode 100644 index 0000000000000..dfb108e02434b --- /dev/null +++ b/services/actions/workflow_interface.go @@ -0,0 +1,16 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import "code.gitea.io/gitea/services/context" + +// WorkflowAPI for action workflow of a repository +type WorkflowAPI interface { + // ListRepositoryWorkflows list repository workflows + ListRepositoryWorkflows(*context.APIContext) + // GetWorkflow get a workflow + GetWorkflow(*context.APIContext) + // DispatchWorkflow create a workflow dispatch event + DispatchWorkflow(*context.APIContext) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index d22e01c787619..33dffc63dd72e 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4421,6 +4421,171 @@ } } }, + "/repos/{owner}/{repo}/actions/workflows": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "List repository workflows", + "operationId": "ListRepositoryWorkflows", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionWorkflowList" + }, + "400": { + "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "409": { + "$ref": "#/responses/conflict" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, + "/repos/{owner}/{repo}/actions/workflows/{workflow_id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a workflow", + "operationId": "GetWorkflow", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "id of the workflow", + "name": "workflow_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionWorkflow" + }, + "400": { + "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "409": { + "$ref": "#/responses/conflict" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, + "/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Create a workflow dispatch event", + "operationId": "DispatchWorkflow", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "id of the workflow", + "name": "workflow_id", + "in": "path", + "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" + }, + "409": { + "$ref": "#/responses/conflict" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/repos/{owner}/{repo}/activities/feeds": { "get": { "produces": [ @@ -18680,6 +18845,61 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ActionWorkflow": { + "description": "ActionWorkflow represents a ActionWorkflow", + "type": "object", + "properties": { + "badge_url": { + "type": "string", + "x-go-name": "BadgeURL" + }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "CreatedAt" + }, + "deleted_at": { + "type": "string", + "format": "date-time", + "x-go-name": "DeletedAt" + }, + "html_url": { + "type": "string", + "x-go-name": "HTMLURL" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "node_id": { + "type": "string", + "x-go-name": "NodeID" + }, + "path": { + "type": "string", + "x-go-name": "Path" + }, + "state": { + "type": "string", + "x-go-name": "State" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "UpdatedAt" + }, + "url": { + "type": "string", + "x-go-name": "URL" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Activity": { "type": "object", "properties": { @@ -25687,6 +25907,21 @@ "$ref": "#/definitions/ActionVariable" } }, + "ActionWorkflow": { + "description": "ActionWorkflow", + "schema": { + "$ref": "#/definitions/ActionWorkflow" + } + }, + "ActionWorkflowList": { + "description": "ActionWorkflowList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/ActionWorkflow" + } + } + }, "ActivityFeedsList": { "description": "ActivityFeedsList", "schema": { From 367fd3ba1df1bf6ed29bd6138e84eb424a748374 Mon Sep 17 00:00:00 2001 From: Bence Santha Date: Thu, 19 Sep 2024 20:37:53 +0200 Subject: [PATCH 02/17] Feature: Support workflow event dispatch via API Signed-off-by: Bence Santha --- modules/structs/repo_actions.go | 13 +- routers/api/v1/api.go | 2 + routers/api/v1/repo/action.go | 260 ++++++++++++------------- routers/api/v1/swagger/options.go | 3 + services/actions/workflow.go | 245 +++++++++++++++++++++++ services/actions/workflow_interface.go | 4 + templates/swagger/v1_json.tmpl | 139 ++++++++++++- 7 files changed, 524 insertions(+), 142 deletions(-) create mode 100644 services/actions/workflow.go diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index af75bbd669fbb..1dea877f19622 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -33,18 +33,19 @@ type ActionTaskResponse struct { TotalCount int64 `json:"total_count"` } -// CreateActionWorkflowDispatch represents the data structure for dispatching a workflow action. -// -// swagger:model CreateActionWorkflowDispatch +// CreateActionWorkflowDispatch represents the payload for triggering a workflow dispatch event +// swagger:model type CreateActionWorkflowDispatch struct { // required: true - Ref string `json:"ref"` - Inputs map[string]interface{} `json:"inputs"` + // 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 int64 `json:"id"` + ID string `json:"id"` NodeID string `json:"node_id"` Name string `json:"name"` Path string `json:"path"` diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0cbaa89c00deb..813381d496028 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -924,7 +924,9 @@ func Routes() *web.Router { m.Group("/workflows", func() { m.Get("", reqToken(), reqChecker, actw.ListRepositoryWorkflows) m.Get("/{workflow_id}", reqToken(), reqChecker, actw.GetWorkflow) + m.Put("/{workflow_id}/disable", reqToken(), reqChecker, actw.DisableWorkflow) m.Post("/{workflow_id}/dispatches", reqToken(), reqChecker, bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow) + m.Put("/{workflow_id}/enable", reqToken(), reqChecker, actw.EnableWorkflow) }, context.ReferencesGitRepo(), reqRepoWriter(unit.TypeCode)) }) } diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 905748bf8025f..a113b778597eb 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -4,17 +4,8 @@ package repo import ( - "code.gitea.io/gitea/models/perm" - access_model "code.gitea.io/gitea/models/perm/access" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/actions" - "code.gitea.io/gitea/modules/git" "errors" - "github.com/nektos/act/pkg/jobparser" - "github.com/nektos/act/pkg/model" "net/http" - "strconv" - "strings" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" @@ -629,7 +620,20 @@ func (a ActionWorkflow) ListRepositoryWorkflows(ctx *context.APIContext) { // "$ref": "#/responses/conflict" // "422": // "$ref": "#/responses/validationError" - panic("implement me") + // "500": + // "$ref": "#/responses/error" + + workflows, err := actions_service.ListActionWorkflows(ctx) + if err != nil { + return + } + + if len(workflows) == 0 { + ctx.JSON(http.StatusNotFound, nil) + } + + ctx.SetTotalCountHeader(int64(len(workflows))) + ctx.JSON(http.StatusOK, workflows) } func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) { @@ -667,7 +671,75 @@ func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) { // "$ref": "#/responses/conflict" // "422": // "$ref": "#/responses/validationError" - panic("implement me") + // "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 { + return + } + + if workflow == nil { + ctx.JSON(http.StatusNotFound, nil) + } + + 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" + // "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.DisableActionWorkflow(ctx, workflowID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "DisableActionWorkflow", err) + } + + ctx.Status(http.StatusNoContent) } func (a ActionWorkflow) DispatchWorkflow(ctx *context.APIContext) { @@ -724,137 +796,57 @@ func (a ActionWorkflow) DispatchWorkflow(ctx *context.APIContext) { 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.Error(http.StatusInternalServerError, "WorkflowDisabled", ctx.Tr("actions.workflow.disabled")) - return - } + actions_service.DispatchActionWorkflow(ctx, workflowID, opt) - // 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() { - // [E] PANIC: runtime error: invalid memory address or nil pointer dereference - runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) - } else { - ctx.Error(http.StatusInternalServerError, "WorkflowRefNameError", ctx.Tr("form.git_ref_name_error", ref)) - return - } - if err != nil { - ctx.Error(http.StatusNotFound, "WorkflowRefNotFound", ctx.Tr("form.target_ref_not_exist", ref)) - return - } - - // get workflow entry from default branch commit - defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowDefaultBranchError", err.Error()) - return - } - entries, err := actions.ListWorkflows(defaultBranchCommit) - if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowListError", err.Error()) - } - - // 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, "WorkflowGetContentError", err.Error()) - return - } - workflows, err = jobparser.Parse(content) - if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowParseError", err.Error()) - return - } - break - } - } - - if len(workflows) == 0 { - ctx.Error(http.StatusNotFound, "WorkflowNotFound", ctx.Tr("actions.workflow.not_found", workflowID)) - return - } - - 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, exists := opt.Inputs[name] - if !exists { - continue - } - if config.Type == "boolean" { - inputs[name] = strconv.FormatBool(value == "on") - } else if value != "" { - inputs[name] = value.(string) - } else { - inputs[name] = config.Default - } - } - } - - 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.Error(http.StatusInternalServerError, "WorkflowDispatchJSONParseError", err.Error()) - return - } - - run := &actions_model.ActionRun{ - Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0], - RepoID: ctx.Repo.Repository.ID, - OwnerID: ctx.Repo.Repository.Owner.ID, - 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, - } + ctx.Status(http.StatusNoContent) +} - // cancel running jobs of the same workflow - if err := actions_model.CancelPreviousJobs( - ctx, - run.RepoID, - run.Ref, - run.WorkflowID, - run.Event, - ); err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowCancelPreviousJobsError", err.Error()) - return - } +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" - if err := actions_model.InsertRun(ctx, run, workflows); err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowInsertRunError", err.Error()) + workflowID := ctx.PathParam("workflow_id") + if len(workflowID) == 0 { + ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter")) return } - alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) + err := actions_service.EnableActionWorkflow(ctx, workflowID) if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowFindRunJobError", err.Error()) - return + ctx.Error(http.StatusInternalServerError, "EnableActionWorkflow", err) } - actions_service.CreateCommitStatus(ctx, alljobs...) ctx.Status(http.StatusNoContent) } diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 353d6de89b755..aa5990eb38452 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -211,6 +211,9 @@ type swaggerParameterBodies struct { // in:body RenameOrgOption api.RenameOrgOption + // in:body + CreateActionWorkflowDispatch api.CreateActionWorkflowDispatch + // in:body UpdateVariableOption api.UpdateVariableOption } diff --git a/services/actions/workflow.go b/services/actions/workflow.go new file mode 100644 index 0000000000000..eecfff008a3b9 --- /dev/null +++ b/services/actions/workflow.go @@ -0,0 +1,245 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "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" + "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + + "github.com/nektos/act/pkg/jobparser" + "github.com/nektos/act/pkg/model" +) + +func getActionWorkflowEntry(ctx *context.APIContext, entry *git.TreeEntry, commit *git.Commit) (*api.ActionWorkflow, error) { + cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) + cfg := cfgUnit.ActionsConfig() + + URL := fmt.Sprintf("%s/actions/workflows/%s", ctx.Repo.Repository.APIURL(), entry.Name()) + badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", ctx.Repo.Repository.HTMLURL(ctx), entry.Name(), ctx.Repo.Repository.DefaultBranch) + + // See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow + // State types: + // - active + // - deleted + // - disabled_fork + // - disabled_inactivity + // - disabled_manually + state := "active" + if cfg.IsWorkflowDisabled(entry.Name()) { + state = "disabled_manually" + } + + // TODO: NodeID + // TODO: CreatedAt + // TODO: UpdatedAt + // TODO: HTMLURL + // TODO: DeletedAt + + return &api.ActionWorkflow{ + ID: entry.Name(), + Name: entry.Name(), + Path: entry.Name(), + State: state, + URL: URL, + BadgeURL: badgeURL, + }, nil +} + +func disableOrEnableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error { + cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) + cfg := cfgUnit.ActionsConfig() + + if isEnable { + cfg.EnableWorkflow(workflowID) + } else { + cfg.DisableWorkflow(workflowID) + } + + return repo_model.UpdateRepoUnit(ctx, cfgUnit) +} + +func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) { + defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowDefaultBranchError", err.Error()) + return nil, err + } + + entries, err := actions.ListWorkflows(defaultBranchCommit) + if err != nil { + ctx.Error(http.StatusNotFound, "WorkflowListNotFound", err.Error()) + return nil, err + } + + workflows := make([]*api.ActionWorkflow, len(entries)) + for i, entry := range entries { + workflows[i], err = getActionWorkflowEntry(ctx, entry, defaultBranchCommit) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowGetError", err.Error()) + return nil, err + } + } + + return workflows, nil +} + +func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionWorkflow, error) { + entries, err := ListActionWorkflows(ctx) + if err != nil { + return nil, err + } + + workflows := make([]*api.ActionWorkflow, len(entries)) + for i, entry := range entries { + if entry.Name == workflowID { + workflows[i] = entry + break + } + } + + return workflows[len(workflows)-1], nil +} + +func DisableActionWorkflow(ctx *context.APIContext, workflowID string) error { + return disableOrEnableWorkflow(ctx, workflowID, false) +} + +func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api.CreateActionWorkflowDispatch) { + // can not run job when workflow is disabled + cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) + cfg := cfgUnit.ActionsConfig() + if cfg.IsWorkflowDisabled(workflowID) { + ctx.Error(http.StatusInternalServerError, "WorkflowDisabled", ctx.Tr("actions.workflow.disabled")) + return + } + + // get target commit of run from specified ref + refName := git.RefName(opt.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.Error(http.StatusInternalServerError, "WorkflowRefNameError", ctx.Tr("form.git_ref_name_error", opt.Ref)) + return + } + if err != nil { + ctx.Error(http.StatusNotFound, "WorkflowRefNotFound", ctx.Tr("form.target_ref_not_exist", opt.Ref)) + return + } + + // get workflow entry from default branch commit + defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowDefaultBranchError", err.Error()) + return + } + entries, err := actions.ListWorkflows(defaultBranchCommit) + if err != nil { + ctx.Error(http.StatusNotFound, "WorkflowListNotFound", err.Error()) + } + + // 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, "WorkflowGetContentError", err.Error()) + return + } + workflows, err = jobparser.Parse(content) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowParseError", err.Error()) + return + } + break + } + } + + if len(workflows) == 0 { + ctx.Error(http.StatusNotFound, "WorkflowNotFound", ctx.Tr("actions.workflow.not_found", workflowID)) + return + } + + 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, exists := opt.Inputs[name] + if !exists { + continue + } + if config.Type == "boolean" { + inputs[name] = strconv.FormatBool(value == "on") + } else if value != "" { + inputs[name] = value + } else { + inputs[name] = config.Default + } + } + } + + workflowDispatchPayload := &api.WorkflowDispatchPayload{ + Workflow: workflowID, + Ref: opt.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.Error(http.StatusInternalServerError, "WorkflowDispatchJSONParseError", err.Error()) + return + } + + run := &actions_model.ActionRun{ + Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0], + RepoID: ctx.Repo.Repository.ID, + OwnerID: ctx.Repo.Repository.Owner.ID, + WorkflowID: workflowID, + TriggerUserID: ctx.Doer.ID, + Ref: opt.Ref, + CommitSHA: runTargetCommit.ID.String(), + IsForkPullRequest: false, + Event: "workflow_dispatch", + TriggerEvent: "workflow_dispatch", + EventPayload: string(eventPayload), + Status: actions_model.StatusWaiting, + } + + if err := actions_model.InsertRun(ctx, run, workflows); err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowInsertRunError", err.Error()) + return + } + + alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowFindRunJobError", err.Error()) + return + } + CreateCommitStatus(ctx, alljobs...) +} + +func EnableActionWorkflow(ctx *context.APIContext, workflowID string) error { + return disableOrEnableWorkflow(ctx, workflowID, true) +} diff --git a/services/actions/workflow_interface.go b/services/actions/workflow_interface.go index dfb108e02434b..43fa92bdf8e16 100644 --- a/services/actions/workflow_interface.go +++ b/services/actions/workflow_interface.go @@ -11,6 +11,10 @@ type WorkflowAPI interface { ListRepositoryWorkflows(*context.APIContext) // GetWorkflow get a workflow GetWorkflow(*context.APIContext) + // DisableWorkflow disable a workflow + DisableWorkflow(*context.APIContext) // DispatchWorkflow create a workflow dispatch event DispatchWorkflow(*context.APIContext) + // EnableWorkflow enable a workflow + EnableWorkflow(*context.APIContext) } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 33dffc63dd72e..ba891555ea1db 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4465,6 +4465,9 @@ }, "422": { "$ref": "#/responses/validationError" + }, + "500": { + "$ref": "#/responses/error" } } } @@ -4518,6 +4521,64 @@ "409": { "$ref": "#/responses/conflict" }, + "422": { + "$ref": "#/responses/validationError" + }, + "500": { + "$ref": "#/responses/error" + } + } + } + }, + "/repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Disable a workflow", + "operationId": "DisableWorkflow", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "id of the workflow", + "name": "workflow_id", + "in": "path", + "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" } @@ -4586,6 +4647,61 @@ } } }, + "/repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Enable a workflow", + "operationId": "EnableWorkflow", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "id of the workflow", + "name": "workflow_id", + "in": "path", + "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" + } + } + } + }, "/repos/{owner}/{repo}/activities/feeds": { "get": { "produces": [ @@ -18868,8 +18984,7 @@ "x-go-name": "HTMLURL" }, "id": { - "type": "integer", - "format": "int64", + "type": "string", "x-go-name": "ID" }, "name": { @@ -19908,6 +20023,26 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CreateActionWorkflowDispatch": { + "description": "CreateActionWorkflowDispatch represents the payload for triggering a workflow dispatch event", + "type": "object", + "required": [ + "ref" + ], + "properties": { + "inputs": { + "type": "object", + "additionalProperties": {}, + "x-go-name": "Inputs" + }, + "ref": { + "type": "string", + "x-go-name": "Ref", + "example": "refs/heads/main" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CreateBranchProtectionOption": { "description": "CreateBranchProtectionOption options for creating a branch protection", "type": "object", From 5afb8aec5f2d82825a0112c873595c054218ea1f Mon Sep 17 00:00:00 2001 From: Bence Santha Date: Fri, 20 Sep 2024 14:10:50 +0200 Subject: [PATCH 03/17] adding more fields to the workflow list output Signed-off-by: Bence Santha --- services/actions/workflow.go | 57 ++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/services/actions/workflow.go b/services/actions/workflow.go index eecfff008a3b9..e192506d9f235 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -6,6 +6,7 @@ package actions import ( "fmt" "net/http" + "os" "strconv" "strings" @@ -25,11 +26,28 @@ import ( "github.com/nektos/act/pkg/model" ) -func getActionWorkflowEntry(ctx *context.APIContext, entry *git.TreeEntry, commit *git.Commit) (*api.ActionWorkflow, error) { +func getActionWorkflowPath(commit *git.Commit) string { + _, err := commit.SubTree(".gitea/workflows") + if err == nil { + return ".gitea/workflows" + } + + if _, ok := err.(git.ErrNotExist); ok { + _, err = commit.SubTree(".github/workflows") + return ".github/workflows" + } + + return "" +} + +func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, entry *git.TreeEntry) (*api.ActionWorkflow, error) { cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() + defaultBranch, _ := commit.GetBranchName() + URL := fmt.Sprintf("%s/actions/workflows/%s", ctx.Repo.Repository.APIURL(), entry.Name()) + HTMLURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", ctx.Repo.Repository.HTMLURL(ctx), defaultBranch, getActionWorkflowPath(commit), entry.Name()) badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", ctx.Repo.Repository.HTMLURL(ctx), entry.Name(), ctx.Repo.Repository.DefaultBranch) // See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow @@ -44,19 +62,32 @@ func getActionWorkflowEntry(ctx *context.APIContext, entry *git.TreeEntry, commi state = "disabled_manually" } - // TODO: NodeID - // TODO: CreatedAt - // TODO: UpdatedAt - // TODO: HTMLURL - // TODO: DeletedAt + // Currently, the NodeID returns the hostname of the server since, as far as I know, Gitea does not have a parameter + // similar to an instance ID. + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + // The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined + // by retrieving the first and last commits for the file history. The first commit would indicate the creation date, + // while the last commit would represent the modification date. The DeletedAt could be determined by identifying + // the last commit where the file existed. However, this implementation has not been done here yet, as it would likely + // cause a significant performance degradation. + createdAt := commit.Author.When + updatedAt := commit.Author.When return &api.ActionWorkflow{ - ID: entry.Name(), - Name: entry.Name(), - Path: entry.Name(), - State: state, - URL: URL, - BadgeURL: badgeURL, + ID: entry.Name(), + NodeID: hostname, + Name: entry.Name(), + Path: entry.Name(), + State: state, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + URL: URL, + HTMLURL: HTMLURL, + BadgeURL: badgeURL, }, nil } @@ -88,7 +119,7 @@ func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) workflows := make([]*api.ActionWorkflow, len(entries)) for i, entry := range entries { - workflows[i], err = getActionWorkflowEntry(ctx, entry, defaultBranchCommit) + workflows[i], err = getActionWorkflowEntry(ctx, defaultBranchCommit, entry) if err != nil { ctx.Error(http.StatusInternalServerError, "WorkflowGetError", err.Error()) return nil, err From 4fc2d9ccf2d300273ca708e1af1ac71cfe296603 Mon Sep 17 00:00:00 2001 From: Bence Santha Date: Tue, 24 Sep 2024 09:16:50 +0200 Subject: [PATCH 04/17] refactor Signed-off-by: Bence Santha --- routers/api/v1/repo/action.go | 12 ++-- services/actions/workflow.go | 119 ++++++++++++++++++---------------- 2 files changed, 67 insertions(+), 64 deletions(-) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index a113b778597eb..a026c66ee40ab 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -616,8 +616,6 @@ func (a ActionWorkflow) ListRepositoryWorkflows(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - // "409": - // "$ref": "#/responses/conflict" // "422": // "$ref": "#/responses/validationError" // "500": @@ -630,6 +628,7 @@ func (a ActionWorkflow) ListRepositoryWorkflows(ctx *context.APIContext) { if len(workflows) == 0 { ctx.JSON(http.StatusNotFound, nil) + return } ctx.SetTotalCountHeader(int64(len(workflows))) @@ -667,8 +666,6 @@ func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - // "409": - // "$ref": "#/responses/conflict" // "422": // "$ref": "#/responses/validationError" // "500": @@ -687,6 +684,7 @@ func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) { if workflow == nil { ctx.JSON(http.StatusNotFound, nil) + return } ctx.JSON(http.StatusOK, workflow) @@ -723,8 +721,6 @@ func (a ActionWorkflow) DisableWorkflow(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - // "409": - // "$ref": "#/responses/conflict" // "422": // "$ref": "#/responses/validationError" @@ -737,6 +733,7 @@ func (a ActionWorkflow) DisableWorkflow(ctx *context.APIContext) { err := actions_service.DisableActionWorkflow(ctx, workflowID) if err != nil { ctx.Error(http.StatusInternalServerError, "DisableActionWorkflow", err) + return } ctx.Status(http.StatusNoContent) @@ -777,8 +774,6 @@ func (a ActionWorkflow) DispatchWorkflow(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - // "409": - // "$ref": "#/responses/conflict" // "422": // "$ref": "#/responses/validationError" @@ -846,6 +841,7 @@ func (a ActionWorkflow) EnableWorkflow(ctx *context.APIContext) { err := actions_service.EnableActionWorkflow(ctx, workflowID) if err != nil { ctx.Error(http.StatusInternalServerError, "EnableActionWorkflow", err) + return } ctx.Status(http.StatusNoContent) diff --git a/services/actions/workflow.go b/services/actions/workflow.go index e192506d9f235..a7970a4dd1f0e 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -27,20 +27,16 @@ import ( ) func getActionWorkflowPath(commit *git.Commit) string { - _, err := commit.SubTree(".gitea/workflows") - if err == nil { - return ".gitea/workflows" - } - - if _, ok := err.(git.ErrNotExist); ok { - _, err = commit.SubTree(".github/workflows") - return ".github/workflows" + paths := []string{".gitea/workflows", ".github/workflows"} + for _, path := range paths { + if _, err := commit.SubTree(path); err == nil { + return path + } } - return "" } -func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, entry *git.TreeEntry) (*api.ActionWorkflow, error) { +func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, entry *git.TreeEntry) *api.ActionWorkflow { cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() @@ -88,17 +84,22 @@ func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, entry * URL: URL, HTMLURL: HTMLURL, BadgeURL: badgeURL, - }, nil + } } func disableOrEnableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error { + workflow, err := GetActionWorkflow(ctx, workflowID) + if err != nil { + return err + } + cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() if isEnable { - cfg.EnableWorkflow(workflowID) + cfg.EnableWorkflow(workflow.ID) } else { - cfg.DisableWorkflow(workflowID) + cfg.DisableWorkflow(workflow.ID) } return repo_model.UpdateRepoUnit(ctx, cfgUnit) @@ -119,11 +120,7 @@ func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) workflows := make([]*api.ActionWorkflow, len(entries)) for i, entry := range entries { - workflows[i], err = getActionWorkflowEntry(ctx, defaultBranchCommit, entry) - if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowGetError", err.Error()) - return nil, err - } + workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, entry) } return workflows, nil @@ -135,15 +132,13 @@ func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionW return nil, err } - workflows := make([]*api.ActionWorkflow, len(entries)) - for i, entry := range entries { + for _, entry := range entries { if entry.Name == workflowID { - workflows[i] = entry - break + return entry, nil } } - return workflows[len(workflows)-1], nil + return nil, fmt.Errorf("workflow not found") } func DisableActionWorkflow(ctx *context.APIContext, workflowID string) error { @@ -151,44 +146,46 @@ func DisableActionWorkflow(ctx *context.APIContext, workflowID string) error { } func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api.CreateActionWorkflowDispatch) { - // can not run job when workflow is disabled cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() + if cfg.IsWorkflowDisabled(workflowID) { ctx.Error(http.StatusInternalServerError, "WorkflowDisabled", ctx.Tr("actions.workflow.disabled")) return } - // get target commit of run from specified ref refName := git.RefName(opt.Ref) var runTargetCommit *git.Commit var err error - if refName.IsTag() { + + switch { + case refName.IsTag(): runTargetCommit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName()) - } else if refName.IsBranch() { + case refName.IsBranch(): runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) - } else { + default: ctx.Error(http.StatusInternalServerError, "WorkflowRefNameError", ctx.Tr("form.git_ref_name_error", opt.Ref)) return } + if err != nil { ctx.Error(http.StatusNotFound, "WorkflowRefNotFound", ctx.Tr("form.target_ref_not_exist", opt.Ref)) return } - // get workflow entry from default branch commit defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) if err != nil { ctx.Error(http.StatusInternalServerError, "WorkflowDefaultBranchError", err.Error()) return } + entries, err := actions.ListWorkflows(defaultBranchCommit) if err != nil { ctx.Error(http.StatusNotFound, "WorkflowListNotFound", err.Error()) + return } - // find workflow from commit - var workflows []*jobparser.SingleWorkflow + var workflow *jobparser.SingleWorkflow for _, entry := range entries { if entry.Name() == workflowID { content, err := actions.GetContentFromEntry(entry) @@ -196,39 +193,25 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api ctx.Error(http.StatusInternalServerError, "WorkflowGetContentError", err.Error()) return } - workflows, err = jobparser.Parse(content) - if err != nil { + workflows, err := jobparser.Parse(content) + if err != nil || len(workflows) == 0 { ctx.Error(http.StatusInternalServerError, "WorkflowParseError", err.Error()) return } + workflow = workflows[0] break } } - if len(workflows) == 0 { + if workflow == nil { ctx.Error(http.StatusNotFound, "WorkflowNotFound", ctx.Tr("actions.workflow.not_found", workflowID)) return } - 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, exists := opt.Inputs[name] - if !exists { - continue - } - if config.Type == "boolean" { - inputs[name] = strconv.FormatBool(value == "on") - } else if value != "" { - inputs[name] = value - } else { - inputs[name] = config.Default - } - } - } + // Process workflow inputs + inputs := processWorkflowInputs(opt, &model.Workflow{ + RawOn: workflow.RawOn, + }) workflowDispatchPayload := &api.WorkflowDispatchPayload{ Workflow: workflowID, @@ -237,8 +220,9 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api Inputs: inputs, Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone), } - var eventPayload []byte - if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil { + + eventPayload, err := workflowDispatchPayload.JSONPayload() + if err != nil { ctx.Error(http.StatusInternalServerError, "WorkflowDispatchJSONParseError", err.Error()) return } @@ -258,7 +242,7 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api Status: actions_model.StatusWaiting, } - if err := actions_model.InsertRun(ctx, run, workflows); err != nil { + if err := actions_model.InsertRun(ctx, run, []*jobparser.SingleWorkflow{workflow}); err != nil { ctx.Error(http.StatusInternalServerError, "WorkflowInsertRunError", err.Error()) return } @@ -268,9 +252,32 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api ctx.Error(http.StatusInternalServerError, "WorkflowFindRunJobError", err.Error()) return } + CreateCommitStatus(ctx, alljobs...) } +func processWorkflowInputs(opt *api.CreateActionWorkflowDispatch, workflow *model.Workflow) map[string]any { + inputs := make(map[string]any) + if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil { + for name, config := range workflowDispatch.Inputs { + value, exists := opt.Inputs[name] + if !exists { + continue + } + if value == "" { + value = config.Default + } + switch config.Type { + case "boolean": + inputs[name] = strconv.FormatBool(value == "on") + default: + inputs[name] = value + } + } + } + return inputs +} + func EnableActionWorkflow(ctx *context.APIContext, workflowID string) error { return disableOrEnableWorkflow(ctx, workflowID, true) } From 35afb368770b6c2a67a79718aedb2d1fd7974687 Mon Sep 17 00:00:00 2001 From: Bence Santha Date: Tue, 24 Sep 2024 09:34:17 +0200 Subject: [PATCH 05/17] updated swagger spec Signed-off-by: Bence Santha --- templates/swagger/v1_json.tmpl | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index ba891555ea1db..1ba23f02936a1 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4460,9 +4460,6 @@ "404": { "$ref": "#/responses/notFound" }, - "409": { - "$ref": "#/responses/conflict" - }, "422": { "$ref": "#/responses/validationError" }, @@ -4518,9 +4515,6 @@ "404": { "$ref": "#/responses/notFound" }, - "409": { - "$ref": "#/responses/conflict" - }, "422": { "$ref": "#/responses/validationError" }, @@ -4576,9 +4570,6 @@ "404": { "$ref": "#/responses/notFound" }, - "409": { - "$ref": "#/responses/conflict" - }, "422": { "$ref": "#/responses/validationError" } @@ -4638,9 +4629,6 @@ "404": { "$ref": "#/responses/notFound" }, - "409": { - "$ref": "#/responses/conflict" - }, "422": { "$ref": "#/responses/validationError" } From 48509706938687736b00a7c232e49129dfd23035 Mon Sep 17 00:00:00 2001 From: Bence Santha Date: Wed, 2 Oct 2024 10:55:31 +0200 Subject: [PATCH 06/17] refactor Signed-off-by: Bence Santha --- routers/api/v1/repo/action.go | 6 ++++-- services/actions/workflow.go | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index a026c66ee40ab..460f5d0d36111 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -623,11 +623,12 @@ func (a ActionWorkflow) ListRepositoryWorkflows(ctx *context.APIContext) { workflows, err := actions_service.ListActionWorkflows(ctx) if err != nil { + ctx.Error(http.StatusInternalServerError, "ListActionWorkflows", err) return } if len(workflows) == 0 { - ctx.JSON(http.StatusNotFound, nil) + ctx.Error(http.StatusNotFound, "ListActionWorkflows", err) return } @@ -679,11 +680,12 @@ func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) { workflow, err := actions_service.GetActionWorkflow(ctx, workflowID) if err != nil { + ctx.Error(http.StatusInternalServerError, "GetActionWorkflow", err) return } if workflow == nil { - ctx.JSON(http.StatusNotFound, nil) + ctx.Error(http.StatusNotFound, "GetActionWorkflow", err) return } diff --git a/services/actions/workflow.go b/services/actions/workflow.go index a7970a4dd1f0e..f136eb5f7d598 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -138,7 +138,7 @@ func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionW } } - return nil, fmt.Errorf("workflow not found") + return nil, fmt.Errorf("workflow '%s' not found", workflowID) } func DisableActionWorkflow(ctx *context.APIContext, workflowID string) error { @@ -150,7 +150,7 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api cfg := cfgUnit.ActionsConfig() if cfg.IsWorkflowDisabled(workflowID) { - ctx.Error(http.StatusInternalServerError, "WorkflowDisabled", ctx.Tr("actions.workflow.disabled")) + ctx.Error(http.StatusInternalServerError, "WorkflowDisabled", fmt.Sprintf("workflow '%s' is disabled", workflowID)) return } @@ -164,12 +164,12 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api case refName.IsBranch(): runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) default: - ctx.Error(http.StatusInternalServerError, "WorkflowRefNameError", ctx.Tr("form.git_ref_name_error", opt.Ref)) + ctx.Error(http.StatusInternalServerError, "WorkflowRefNameError", fmt.Sprintf("%s must be a well-formed Git reference name.", opt.Ref)) return } if err != nil { - ctx.Error(http.StatusNotFound, "WorkflowRefNotFound", ctx.Tr("form.target_ref_not_exist", opt.Ref)) + ctx.Error(http.StatusNotFound, "WorkflowRefNotFound", fmt.Sprintf("target ref does not exist %s", opt.Ref)) return } @@ -204,7 +204,7 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api } if workflow == nil { - ctx.Error(http.StatusNotFound, "WorkflowNotFound", ctx.Tr("actions.workflow.not_found", workflowID)) + ctx.Error(http.StatusNotFound, "WorkflowNotFound", fmt.Sprintf("workflow '%s' is not found", workflowID)) return } From a96eb2f4f2961c12993ffa9f9b0a5b82a1c7df46 Mon Sep 17 00:00:00 2001 From: Bence Santha Date: Thu, 3 Oct 2024 08:46:34 +0200 Subject: [PATCH 07/17] Updated permissions Signed-off-by: Bence Santha --- routers/api/v1/api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 813381d496028..15f181f157ba6 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -927,7 +927,7 @@ func Routes() *web.Router { m.Put("/{workflow_id}/disable", reqToken(), reqChecker, actw.DisableWorkflow) m.Post("/{workflow_id}/dispatches", reqToken(), reqChecker, bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow) m.Put("/{workflow_id}/enable", reqToken(), reqChecker, actw.EnableWorkflow) - }, context.ReferencesGitRepo(), reqRepoWriter(unit.TypeCode)) + }, context.ReferencesGitRepo(), reqRepoWriter(unit.TypeActions)) }) } @@ -1178,7 +1178,7 @@ func Routes() *web.Router { ) addActionsWorkflowRoutes( m, - reqRepoWriter(unit.TypeCode), + reqRepoWriter(unit.TypeActions), repo.NewActionWorkflow(), ) m.Group("/hooks/git", func() { From aede29b3b1a7f3ebd59748774ec8d9af952c4e5f Mon Sep 17 00:00:00 2001 From: Bence Santha Date: Sat, 12 Oct 2024 10:15:12 +0200 Subject: [PATCH 08/17] Refactor workflow dispatch Signed-off-by: Bence Santha --- services/actions/workflow.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/services/actions/workflow.go b/services/actions/workflow.go index f136eb5f7d598..f02bb207d2fc0 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -194,20 +194,19 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api return } workflows, err := jobparser.Parse(content) - if err != nil || len(workflows) == 0 { + if err != nil { ctx.Error(http.StatusInternalServerError, "WorkflowParseError", err.Error()) return } + if len(workflows) == 0 { + ctx.Error(http.StatusNotFound, "WorkflowNotFound", fmt.Sprintf("workflow '%s' is not found", workflowID)) + return + } workflow = workflows[0] break } } - if workflow == nil { - ctx.Error(http.StatusNotFound, "WorkflowNotFound", fmt.Sprintf("workflow '%s' is not found", workflowID)) - return - } - // Process workflow inputs inputs := processWorkflowInputs(opt, &model.Workflow{ RawOn: workflow.RawOn, From 51097fd7943d44319b1d14e7b3d019126f89681a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 4 Feb 2025 11:10:21 -0800 Subject: [PATCH 09/17] Some improvements --- routers/api/v1/api.go | 14 ++++++------- routers/web/repo/actions/view.go | 26 ++++++++++++----------- services/actions/workflow.go | 36 +++++++++++++++++--------------- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 15f181f157ba6..378d9c6b4167e 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -917,17 +917,16 @@ func Routes() *web.Router { addActionsWorkflowRoutes := func( m *web.Router, - reqChecker func(ctx *context.APIContext), actw actions.WorkflowAPI, ) { m.Group("/actions", func() { m.Group("/workflows", func() { - m.Get("", reqToken(), reqChecker, actw.ListRepositoryWorkflows) - m.Get("/{workflow_id}", reqToken(), reqChecker, actw.GetWorkflow) - m.Put("/{workflow_id}/disable", reqToken(), reqChecker, actw.DisableWorkflow) - m.Post("/{workflow_id}/dispatches", reqToken(), reqChecker, bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow) - m.Put("/{workflow_id}/enable", reqToken(), reqChecker, actw.EnableWorkflow) - }, context.ReferencesGitRepo(), reqRepoWriter(unit.TypeActions)) + 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)) }) } @@ -1178,7 +1177,6 @@ func Routes() *web.Router { ) addActionsWorkflowRoutes( m, - reqRepoWriter(unit.TypeActions), repo.NewActionWorkflow(), ) m.Group("/hooks/git", func() { diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index fc346b83b4736..6afe43ca02cc7 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -831,19 +831,21 @@ func Run(ctx *context_module.Context) { // 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 - } - break + if entry.Name() != workflowID { + continue + } + + 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 } + break } if len(workflows) == 0 { diff --git a/services/actions/workflow.go b/services/actions/workflow.go index f02bb207d2fc0..09910264ece17 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -187,24 +187,26 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api var workflow *jobparser.SingleWorkflow for _, entry := range entries { - if entry.Name() == workflowID { - content, err := actions.GetContentFromEntry(entry) - if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowGetContentError", err.Error()) - return - } - workflows, err := jobparser.Parse(content) - if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowParseError", err.Error()) - return - } - if len(workflows) == 0 { - ctx.Error(http.StatusNotFound, "WorkflowNotFound", fmt.Sprintf("workflow '%s' is not found", workflowID)) - return - } - workflow = workflows[0] - break + if entry.Name() != workflowID { + continue + } + + content, err := actions.GetContentFromEntry(entry) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowGetContentError", err.Error()) + return + } + workflows, err := jobparser.Parse(content) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WorkflowParseError", err.Error()) + return + } + if len(workflows) == 0 { + ctx.Error(http.StatusNotFound, "WorkflowNotFound", fmt.Sprintf("workflow '%s' is not found", workflowID)) + return } + workflow = workflows[0] + break } // Process workflow inputs From b6f7ce678d935d602c3d52c3a0a967549043f2c3 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Thu, 6 Feb 2025 21:19:54 +0100 Subject: [PATCH 10/17] add tests * all are failing --- tests/integration/actions_trigger_test.go | 321 ++++++++++++++++++++++ 1 file changed, 321 insertions(+) diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 8ea9b34efe54d..6c9103b567935 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -5,6 +5,7 @@ package integration import ( "fmt" + "net/http" "net/url" "strings" "testing" @@ -22,6 +23,7 @@ import ( actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" @@ -651,3 +653,322 @@ func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL, }) assert.NoError(t, err) } + +func TestWorkflowDispatchPublicApi(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // create the repo + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "workflow-dispatch-event", + Description: "test workflow-dispatch ci event", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + // add workflow file to the repo + addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/dispatch.yml", + ContentReader: strings.NewReader("name: test\non:\n workflow_dispatch\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflowToBaseResp) + + // Get the commit ID of the default branch + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) + assert.NoError(t, err) + defer gitRepo.Close() + branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + values := url.Values{} + values.Set("ref", "main") + req := NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), values). + AddTokenAuth(token) + _ = MakeRequest(t, req, http.StatusNoContent) + + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "workflow_dispatch", + Ref: "refs/heads/main", + WorkflowID: "dispatch.yml", + CommitSHA: branch.CommitID, + }) + assert.NotNil(t, run) + }) +} + +func TestWorkflowDispatchPublicApiWithInputs(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // create the repo + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "workflow-dispatch-event", + Description: "test workflow-dispatch ci event", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + // add workflow file to the repo + addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/dispatch.yml", + ContentReader: strings.NewReader("name: test\non:\n workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflowToBaseResp) + + // Get the commit ID of the default branch + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) + assert.NoError(t, err) + defer gitRepo.Close() + branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + values := url.Values{} + values.Set("ref", "main") + values.Set("inputs[myinput]", "val0") + values.Set("inputs[myinput3]", "true") + req := NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), values). + AddTokenAuth(token) + _ = MakeRequest(t, req, http.StatusNoContent) + + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "workflow_dispatch", + Ref: "refs/heads/main", + WorkflowID: "dispatch.yml", + CommitSHA: branch.CommitID, + }) + assert.NotNil(t, run) + dispatchPayload := &api.WorkflowDispatchPayload{} + err = json.Unmarshal([]byte(run.EventPayload), dispatchPayload) + assert.NoError(t, err) + assert.Contains(t, dispatchPayload.Inputs, "myinput") + assert.Contains(t, dispatchPayload.Inputs, "myinput2") + assert.Contains(t, dispatchPayload.Inputs, "myinput3") + assert.Equal(t, "val0", dispatchPayload.Inputs["myinput"]) + assert.Equal(t, "def2", dispatchPayload.Inputs["myinput2"]) + assert.Equal(t, "true", dispatchPayload.Inputs["myinput3"]) + }) +} + +func TestWorkflowDispatchPublicApiJSON(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // create the repo + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "workflow-dispatch-event", + Description: "test workflow-dispatch ci event", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + // add workflow file to the repo + addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/dispatch.yml", + ContentReader: strings.NewReader("name: test\non:\n workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflowToBaseResp) + + // Get the commit ID of the default branch + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) + assert.NoError(t, err) + defer gitRepo.Close() + branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + inputs := &api.CreateActionWorkflowDispatch{ + Ref: "main", + Inputs: map[string]any{ + "myinput": "val0", + "myinput3": "true", + }, + } + + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs). + AddTokenAuth(token) + _ = MakeRequest(t, req, http.StatusNoContent) + + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "workflow_dispatch", + Ref: "refs/heads/main", + WorkflowID: "dispatch.yml", + CommitSHA: branch.CommitID, + }) + assert.NotNil(t, run) + }) +} + +func TestWorkflowDispatchPublicApiWithInputsJSON(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // create the repo + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "workflow-dispatch-event", + Description: "test workflow-dispatch ci event", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + // add workflow file to the repo + addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/dispatch.yml", + ContentReader: strings.NewReader("name: test\non:\n workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflowToBaseResp) + + // Get the commit ID of the default branch + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) + assert.NoError(t, err) + defer gitRepo.Close() + branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + inputs := &api.CreateActionWorkflowDispatch{ + Ref: "main", + Inputs: map[string]any{ + "myinput": "val0", + "myinput3": "true", + }, + } + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs). + AddTokenAuth(token) + _ = MakeRequest(t, req, http.StatusNoContent) + + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "workflow_dispatch", + Ref: "refs/heads/main", + WorkflowID: "dispatch.yml", + CommitSHA: branch.CommitID, + }) + assert.NotNil(t, run) + dispatchPayload := &api.WorkflowDispatchPayload{} + err = json.Unmarshal([]byte(run.EventPayload), dispatchPayload) + assert.NoError(t, err) + assert.Contains(t, dispatchPayload.Inputs, "myinput") + assert.Contains(t, dispatchPayload.Inputs, "myinput2") + assert.Contains(t, dispatchPayload.Inputs, "myinput3") + assert.Equal(t, "val0", dispatchPayload.Inputs["myinput"]) + assert.Equal(t, "def2", dispatchPayload.Inputs["myinput2"]) + assert.Equal(t, "true", dispatchPayload.Inputs["myinput3"]) + }) +} From fab1826cde7a909a09f81bf81f0f0d4f7d05344b Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Thu, 6 Feb 2025 21:30:01 +0100 Subject: [PATCH 11/17] allow short branch name --- services/actions/workflow.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/actions/workflow.go b/services/actions/workflow.go index 09910264ece17..a06fdc46cc039 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -164,8 +164,8 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api case refName.IsBranch(): runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) default: - ctx.Error(http.StatusInternalServerError, "WorkflowRefNameError", fmt.Sprintf("%s must be a well-formed Git reference name.", opt.Ref)) - return + refName = git.RefNameFromBranch(opt.Ref) + runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.Ref) } if err != nil { @@ -234,7 +234,7 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api OwnerID: ctx.Repo.Repository.Owner.ID, WorkflowID: workflowID, TriggerUserID: ctx.Doer.ID, - Ref: opt.Ref, + Ref: string(refName), CommitSHA: runTargetCommit.ID.String(), IsForkPullRequest: false, Event: "workflow_dispatch", From 93668976b1c06f6cd11c12a6bbbd90ca7bff3c22 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Thu, 6 Feb 2025 21:48:41 +0100 Subject: [PATCH 12/17] migrate pending changes fixed tests --- routers/api/v1/repo/action.go | 38 +++++++- routers/web/repo/actions/view.go | 162 +++++-------------------------- services/actions/workflow.go | 159 ++++++++++++++++++------------ 3 files changed, 160 insertions(+), 199 deletions(-) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 460f5d0d36111..eddffb551a2ab 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -5,6 +5,7 @@ package repo import ( "errors" + "fmt" "net/http" actions_model "code.gitea.io/gitea/models/actions" @@ -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 @@ -793,7 +796,40 @@ func (a ActionWorkflow) DispatchWorkflow(ctx *context.APIContext) { return } - actions_service.DispatchActionWorkflow(ctx, workflowID, opt) + 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) } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 6afe43ca02cc7..6e09cd3de8065 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -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" @@ -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" ) @@ -792,145 +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 { - continue - } - - content, err := actions.GetContentFromEntry(entry) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return + err := actions_service.DispatchActionWorkflow(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 + } + } } - workflows, err = jobparser.Parse(content) - if err != nil { - ctx.ServerError("workflow", err) + 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 } - 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 - } - } - } - - // 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) } diff --git a/services/actions/workflow.go b/services/actions/workflow.go index a06fdc46cc039..6794d7a831d97 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -26,6 +27,23 @@ import ( "github.com/nektos/act/pkg/model" ) +type TranslateableError struct { + Translation string + Args []any + Code int +} + +func (t TranslateableError) Error() string { + return t.Translation +} + +func (t TranslateableError) GetCode() int { + if t.Code == 0 { + return http.StatusInternalServerError + } + return t.Code +} + func getActionWorkflowPath(commit *git.Commit) string { paths := []string{".gitea/workflows", ".github/workflows"} for _, path := range paths { @@ -145,93 +163,103 @@ func DisableActionWorkflow(ctx *context.APIContext, workflowID string) error { return disableOrEnableWorkflow(ctx, workflowID, false) } -func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api.CreateActionWorkflowDispatch) { +func DispatchActionWorkflow(ctx *context.Context, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs *map[string]any) error) error { + if len(workflowID) == 0 { + return fmt.Errorf("workflowID is empty") + } + + if len(ref) == 0 { + return fmt.Errorf("ref is empty") + } + + // can not rerun job when workflow is disabled cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() - if cfg.IsWorkflowDisabled(workflowID) { - ctx.Error(http.StatusInternalServerError, "WorkflowDisabled", fmt.Sprintf("workflow '%s' is disabled", workflowID)) - return + return &TranslateableError{ + Translation: "actions.workflow.disabled", + } } - refName := git.RefName(opt.Ref) + // get target commit of run from specified ref + refName := git.RefName(ref) var runTargetCommit *git.Commit var err error - - switch { - case refName.IsTag(): + if refName.IsTag() { runTargetCommit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName()) - case refName.IsBranch(): + } else if refName.IsBranch() { runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) - default: - refName = git.RefNameFromBranch(opt.Ref) - runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.Ref) - } - - if err != nil { - ctx.Error(http.StatusNotFound, "WorkflowRefNotFound", fmt.Sprintf("target ref does not exist %s", opt.Ref)) - return + } else { + refName = git.RefNameFromBranch(ref) + runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ref) } - - defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowDefaultBranchError", err.Error()) - return + return &TranslateableError{ + Code: http.StatusNotFound, + Translation: "form.target_ref_not_exist", + Args: []any{ref}, + } } - entries, err := actions.ListWorkflows(defaultBranchCommit) + // get workflow entry from runTargetCommit + entries, err := actions.ListWorkflows(runTargetCommit) if err != nil { - ctx.Error(http.StatusNotFound, "WorkflowListNotFound", err.Error()) - return + return err } - var workflow *jobparser.SingleWorkflow + // find workflow from commit + var workflows []*jobparser.SingleWorkflow for _, entry := range entries { - if entry.Name() != workflowID { - continue + if entry.Name() == workflowID { + content, err := actions.GetContentFromEntry(entry) + if err != nil { + return err + } + workflows, err = jobparser.Parse(content) + if err != nil { + return err + } + break } + } - content, err := actions.GetContentFromEntry(entry) - if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowGetContentError", err.Error()) - return - } - workflows, err := jobparser.Parse(content) - if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowParseError", err.Error()) - return - } - if len(workflows) == 0 { - ctx.Error(http.StatusNotFound, "WorkflowNotFound", fmt.Sprintf("workflow '%s' is not found", workflowID)) - return + if len(workflows) == 0 { + return &TranslateableError{ + Code: http.StatusNotFound, + Translation: "actions.workflow.not_found", + Args: []any{workflowID}, } - workflow = workflows[0] - break } - // Process workflow inputs - inputs := processWorkflowInputs(opt, &model.Workflow{ - RawOn: workflow.RawOn, - }) + // get inputs from post + workflow := &model.Workflow{ + RawOn: workflows[0].RawOn, + } + inputsWithDefaults := make(map[string]any) + workflowDispatch := workflow.WorkflowDispatchConfig() + if err := processInputs(workflowDispatch, &inputsWithDefaults); err != nil { + return err + } + // 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: opt.Ref, + Ref: ref, Repository: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}), - Inputs: inputs, + Inputs: inputsWithDefaults, Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone), } - - eventPayload, err := workflowDispatchPayload.JSONPayload() - if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowDispatchJSONParseError", err.Error()) - return + var eventPayload []byte + if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil { + return fmt.Errorf("JSONPayload: %w", err) } run := &actions_model.ActionRun{ Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0], RepoID: ctx.Repo.Repository.ID, - OwnerID: ctx.Repo.Repository.Owner.ID, + OwnerID: ctx.Repo.Repository.OwnerID, WorkflowID: workflowID, TriggerUserID: ctx.Doer.ID, Ref: string(refName), @@ -243,18 +271,29 @@ func DispatchActionWorkflow(ctx *context.APIContext, workflowID string, opt *api Status: actions_model.StatusWaiting, } - if err := actions_model.InsertRun(ctx, run, []*jobparser.SingleWorkflow{workflow}); err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowInsertRunError", err.Error()) - return + // 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 { + return fmt.Errorf("workflow: %w", err) } alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) if err != nil { - ctx.Error(http.StatusInternalServerError, "WorkflowFindRunJobError", err.Error()) - return + log.Error("FindRunJobs: %v", err) } - CreateCommitStatus(ctx, alljobs...) + + return nil } func processWorkflowInputs(opt *api.CreateActionWorkflowDispatch, workflow *model.Workflow) map[string]any { From 5f0b304f0f8fca3485848615ae3c8bb3e62fb2f3 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Thu, 6 Feb 2025 22:03:28 +0100 Subject: [PATCH 13/17] remove now dead code --- services/actions/workflow.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/services/actions/workflow.go b/services/actions/workflow.go index 6794d7a831d97..8bcb411f1dfd8 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "os" - "strconv" "strings" actions_model "code.gitea.io/gitea/models/actions" @@ -296,28 +295,6 @@ func DispatchActionWorkflow(ctx *context.Context, workflowID, ref string, proces return nil } -func processWorkflowInputs(opt *api.CreateActionWorkflowDispatch, workflow *model.Workflow) map[string]any { - inputs := make(map[string]any) - if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil { - for name, config := range workflowDispatch.Inputs { - value, exists := opt.Inputs[name] - if !exists { - continue - } - if value == "" { - value = config.Default - } - switch config.Type { - case "boolean": - inputs[name] = strconv.FormatBool(value == "on") - default: - inputs[name] = value - } - } - } - return inputs -} - func EnableActionWorkflow(ctx *context.APIContext, workflowID string) error { return disableOrEnableWorkflow(ctx, workflowID, true) } From 577ff3fa8eb4d165db9fd1b9eb65d17e53a1a269 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 7 Feb 2025 11:32:16 +0100 Subject: [PATCH 14/17] migrate some improvements discussed in review --- services/actions/workflow.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/services/actions/workflow.go b/services/actions/workflow.go index 8bcb411f1dfd8..ca3f43354eb5b 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -209,17 +209,19 @@ func DispatchActionWorkflow(ctx *context.Context, workflowID, ref string, proces // find workflow from commit var workflows []*jobparser.SingleWorkflow for _, entry := range entries { - if entry.Name() == workflowID { - content, err := actions.GetContentFromEntry(entry) - if err != nil { - return err - } - workflows, err = jobparser.Parse(content) - if err != nil { - return err - } - break + if entry.Name() != workflowID { + continue } + + content, err := actions.GetContentFromEntry(entry) + if err != nil { + return err + } + workflows, err = jobparser.Parse(content) + if err != nil { + return err + } + break } if len(workflows) == 0 { From cfdd251b8e5966ddbcc2abd3b1a4ae6b2b844e6e Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 8 Feb 2025 12:39:07 +0100 Subject: [PATCH 15/17] fix list endpoint response / add TestWorkflowApi * uses all endpoints added here * checks if disabled workflows cannot dispatch * remove node_id * fix path field * test that the URL field of the workflow result is usable * additional test for non default branch workflow * return 200 when no workflows found, with an empty list --- modules/structs/repo_actions.go | 11 +- routers/api/v1/repo/action.go | 8 +- services/actions/workflow.go | 20 +- tests/integration/actions_trigger_test.go | 303 ++++++++++++++++++++++ 4 files changed, 316 insertions(+), 26 deletions(-) diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 1dea877f19622..109cea85c4829 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -45,11 +45,10 @@ type CreateActionWorkflowDispatch struct { // ActionWorkflow represents a ActionWorkflow type ActionWorkflow struct { - ID string `json:"id"` - NodeID string `json:"node_id"` - Name string `json:"name"` - Path string `json:"path"` - State string `json:"state"` + 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 @@ -58,7 +57,7 @@ type ActionWorkflow struct { HTMLURL string `json:"html_url"` BadgeURL string `json:"badge_url"` // swagger:strfmt date-time - DeletedAt time.Time `json:"deleted_at"` + DeletedAt time.Time `json:"deleted_at,omitempty"` } // ActionWorkflowResponse returns a ActionWorkflow diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index eddffb551a2ab..8933a10b4b116 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -630,13 +630,7 @@ func (a ActionWorkflow) ListRepositoryWorkflows(ctx *context.APIContext) { return } - if len(workflows) == 0 { - ctx.Error(http.StatusNotFound, "ListActionWorkflows", err) - return - } - - ctx.SetTotalCountHeader(int64(len(workflows))) - ctx.JSON(http.StatusOK, workflows) + ctx.JSON(http.StatusOK, &api.ActionWorkflowResponse{Workflows: workflows, TotalCount: int64(len(workflows))}) } func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) { diff --git a/services/actions/workflow.go b/services/actions/workflow.go index ca3f43354eb5b..0877e62ea1873 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -6,7 +6,7 @@ package actions import ( "fmt" "net/http" - "os" + "path" "strings" actions_model "code.gitea.io/gitea/models/actions" @@ -53,14 +53,14 @@ func getActionWorkflowPath(commit *git.Commit) string { return "" } -func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, entry *git.TreeEntry) *api.ActionWorkflow { +func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow { cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() defaultBranch, _ := commit.GetBranchName() URL := fmt.Sprintf("%s/actions/workflows/%s", ctx.Repo.Repository.APIURL(), entry.Name()) - HTMLURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", ctx.Repo.Repository.HTMLURL(ctx), defaultBranch, getActionWorkflowPath(commit), entry.Name()) + HTMLURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", ctx.Repo.Repository.HTMLURL(ctx), defaultBranch, folder, entry.Name()) badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", ctx.Repo.Repository.HTMLURL(ctx), entry.Name(), ctx.Repo.Repository.DefaultBranch) // See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow @@ -75,13 +75,6 @@ func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, entry * state = "disabled_manually" } - // Currently, the NodeID returns the hostname of the server since, as far as I know, Gitea does not have a parameter - // similar to an instance ID. - hostname, err := os.Hostname() - if err != nil { - hostname = "unknown" - } - // The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined // by retrieving the first and last commits for the file history. The first commit would indicate the creation date, // while the last commit would represent the modification date. The DeletedAt could be determined by identifying @@ -92,9 +85,8 @@ func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, entry * return &api.ActionWorkflow{ ID: entry.Name(), - NodeID: hostname, Name: entry.Name(), - Path: entry.Name(), + Path: path.Join(folder, entry.Name()), State: state, CreatedAt: createdAt, UpdatedAt: updatedAt, @@ -135,9 +127,11 @@ func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) return nil, err } + folder := getActionWorkflowPath(defaultBranchCommit) + workflows := make([]*api.ActionWorkflow, len(entries)) for i, entry := range entries { - workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, entry) + workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, folder, entry) } return workflows, nil diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 6c9103b567935..00b668ce6ae84 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -972,3 +972,306 @@ func TestWorkflowDispatchPublicApiWithInputsJSON(t *testing.T) { assert.Equal(t, "true", dispatchPayload.Inputs["myinput3"]) }) } + +func TestWorkflowDispatchPublicApiWithInputsNonDefaultBranchJSON(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // create the repo + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "workflow-dispatch-event", + Description: "test workflow-dispatch ci event", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + // add workflow file to the repo + addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/dispatch.yml", + ContentReader: strings.NewReader("name: test\non:\n workflow_dispatch\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflowToBaseResp) + + // add workflow file to the repo + addWorkflowToBaseResp, err = files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: ".gitea/workflows/dispatch.yml", + ContentReader: strings.NewReader("name: test\non:\n workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "dispatch", + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflowToBaseResp) + + // Get the commit ID of the dispatch branch + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) + assert.NoError(t, err) + defer gitRepo.Close() + commit, err := gitRepo.GetBranchCommit("dispatch") + assert.NoError(t, err) + inputs := &api.CreateActionWorkflowDispatch{ + Ref: "refs/heads/dispatch", + Inputs: map[string]any{ + "myinput": "val0", + "myinput3": "true", + }, + } + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs). + AddTokenAuth(token) + _ = MakeRequest(t, req, http.StatusNoContent) + + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "workflow_dispatch", + Ref: "refs/heads/dispatch", + WorkflowID: "dispatch.yml", + CommitSHA: commit.ID.String(), + }) + assert.NotNil(t, run) + dispatchPayload := &api.WorkflowDispatchPayload{} + err = json.Unmarshal([]byte(run.EventPayload), dispatchPayload) + assert.NoError(t, err) + assert.Contains(t, dispatchPayload.Inputs, "myinput") + assert.Contains(t, dispatchPayload.Inputs, "myinput2") + assert.Contains(t, dispatchPayload.Inputs, "myinput3") + assert.Equal(t, "val0", dispatchPayload.Inputs["myinput"]) + assert.Equal(t, "def2", dispatchPayload.Inputs["myinput2"]) + assert.Equal(t, "true", dispatchPayload.Inputs["myinput3"]) + }) +} + +func TestWorkflowApi(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // create the repo + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "workflow-api", + Description: "test workflow apis", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/workflows", repo.FullName())). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + workflows := &api.ActionWorkflowResponse{} + json.NewDecoder(resp.Body).Decode(workflows) + assert.Len(t, workflows.Workflows, 0) + + // add workflow file to the repo + addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/dispatch.yml", + ContentReader: strings.NewReader("name: test\non:\n workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflowToBaseResp) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/workflows", repo.FullName())). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + json.NewDecoder(resp.Body).Decode(workflows) + assert.Len(t, workflows.Workflows, 1) + assert.Equal(t, "dispatch.yml", workflows.Workflows[0].Name) + assert.Equal(t, ".gitea/workflows/dispatch.yml", workflows.Workflows[0].Path) + assert.Equal(t, ".gitea/workflows/dispatch.yml", workflows.Workflows[0].Path) + assert.Equal(t, "active", workflows.Workflows[0].State) + + // Use a hardcoded api path + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/%s", repo.FullName(), workflows.Workflows[0].ID)). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + workflow := &api.ActionWorkflow{} + json.NewDecoder(resp.Body).Decode(workflow) + assert.Equal(t, workflows.Workflows[0].ID, workflow.ID) + assert.Equal(t, workflows.Workflows[0].Path, workflow.Path) + assert.Equal(t, workflows.Workflows[0].URL, workflow.URL) + assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL) + assert.Equal(t, workflows.Workflows[0].Name, workflow.Name) + assert.Equal(t, workflows.Workflows[0].State, workflow.State) + + // Use the provided url instead of the hardcoded one + req = NewRequest(t, "GET", workflows.Workflows[0].URL). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + workflow = &api.ActionWorkflow{} + json.NewDecoder(resp.Body).Decode(workflow) + assert.Equal(t, workflows.Workflows[0].ID, workflow.ID) + assert.Equal(t, workflows.Workflows[0].Path, workflow.Path) + assert.Equal(t, workflows.Workflows[0].URL, workflow.URL) + assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL) + assert.Equal(t, workflows.Workflows[0].Name, workflow.Name) + assert.Equal(t, workflows.Workflows[0].State, workflow.State) + + // Disable the workflow + req = NewRequest(t, "PUT", workflows.Workflows[0].URL+"/disable"). + AddTokenAuth(token) + _ = MakeRequest(t, req, http.StatusNoContent) + + // Use the provided url instead of the hardcoded one + req = NewRequest(t, "GET", workflows.Workflows[0].URL). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + workflow = &api.ActionWorkflow{} + json.NewDecoder(resp.Body).Decode(workflow) + assert.Equal(t, workflows.Workflows[0].ID, workflow.ID) + assert.Equal(t, workflows.Workflows[0].Path, workflow.Path) + assert.Equal(t, workflows.Workflows[0].URL, workflow.URL) + assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL) + assert.Equal(t, workflows.Workflows[0].Name, workflow.Name) + assert.Equal(t, "disabled_manually", workflow.State) + + inputs := &api.CreateActionWorkflowDispatch{ + Ref: "main", + Inputs: map[string]any{ + "myinput": "val0", + "myinput3": "true", + }, + } + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs). + AddTokenAuth(token) + // TODO which http code is expected here? + _ = MakeRequest(t, req, http.StatusInternalServerError) + + // Enable the workflow again + req = NewRequest(t, "PUT", workflows.Workflows[0].URL+"/enable"). + AddTokenAuth(token) + _ = MakeRequest(t, req, http.StatusNoContent) + + // Use the provided url instead of the hardcoded one + req = NewRequest(t, "GET", workflows.Workflows[0].URL). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + workflow = &api.ActionWorkflow{} + json.NewDecoder(resp.Body).Decode(workflow) + assert.Equal(t, workflows.Workflows[0].ID, workflow.ID) + assert.Equal(t, workflows.Workflows[0].Path, workflow.Path) + assert.Equal(t, workflows.Workflows[0].URL, workflow.URL) + assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL) + assert.Equal(t, workflows.Workflows[0].Name, workflow.Name) + assert.Equal(t, workflows.Workflows[0].State, workflow.State) + + req = NewRequest(t, "GET", workflows.Workflows[0].URL). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + workflow = &api.ActionWorkflow{} + json.NewDecoder(resp.Body).Decode(workflow) + assert.Equal(t, workflows.Workflows[0].ID, workflow.ID) + assert.Equal(t, workflows.Workflows[0].Path, workflow.Path) + assert.Equal(t, workflows.Workflows[0].URL, workflow.URL) + assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL) + assert.Equal(t, workflows.Workflows[0].Name, workflow.Name) + assert.Equal(t, workflows.Workflows[0].State, workflow.State) + + // Get the commit ID of the default branch + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) + assert.NoError(t, err) + defer gitRepo.Close() + branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + inputs = &api.CreateActionWorkflowDispatch{ + Ref: "main", + Inputs: map[string]any{ + "myinput": "val0", + "myinput3": "true", + }, + } + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs). + AddTokenAuth(token) + _ = MakeRequest(t, req, http.StatusNoContent) + + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "workflow_dispatch", + Ref: "refs/heads/main", + WorkflowID: "dispatch.yml", + CommitSHA: branch.CommitID, + }) + assert.NotNil(t, run) + dispatchPayload := &api.WorkflowDispatchPayload{} + err = json.Unmarshal([]byte(run.EventPayload), dispatchPayload) + assert.NoError(t, err) + assert.Contains(t, dispatchPayload.Inputs, "myinput") + assert.Contains(t, dispatchPayload.Inputs, "myinput2") + assert.Contains(t, dispatchPayload.Inputs, "myinput3") + assert.Equal(t, "val0", dispatchPayload.Inputs["myinput"]) + assert.Equal(t, "def2", dispatchPayload.Inputs["myinput2"]) + assert.Equal(t, "true", dispatchPayload.Inputs["myinput3"]) + }) +} From 0ce82653e247ad9893eabbfba1f8e1b2cd8b7f21 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 8 Feb 2025 12:43:50 +0100 Subject: [PATCH 16/17] update swagger --- templates/swagger/v1_json.tmpl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 1ba23f02936a1..3f80d3fd9eee5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -18979,10 +18979,6 @@ "type": "string", "x-go-name": "Name" }, - "node_id": { - "type": "string", - "x-go-name": "NodeID" - }, "path": { "type": "string", "x-go-name": "Path" From fb747b10a21e118a0aded3def754cc5508af0ce2 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 8 Feb 2025 12:53:34 +0100 Subject: [PATCH 17/17] use assert empty --- tests/integration/actions_trigger_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 00b668ce6ae84..e2c97662f22ec 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -1112,7 +1112,7 @@ func TestWorkflowApi(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) workflows := &api.ActionWorkflowResponse{} json.NewDecoder(resp.Body).Decode(workflows) - assert.Len(t, workflows.Workflows, 0) + assert.Empty(t, workflows.Workflows) // add workflow file to the repo addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{