diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 109cea85c4..b13f344738 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -32,36 +32,3 @@ type ActionTaskResponse struct { Entries []*ActionTask `json:"workflow_runs"` TotalCount int64 `json:"total_count"` } - -// CreateActionWorkflowDispatch represents the payload for triggering a workflow dispatch event -// swagger:model -type CreateActionWorkflowDispatch struct { - // required: true - // example: refs/heads/main - Ref string `json:"ref" binding:"Required"` - // required: false - Inputs map[string]any `json:"inputs,omitempty"` -} - -// ActionWorkflow represents a ActionWorkflow -type ActionWorkflow struct { - ID string `json:"id"` - Name string `json:"name"` - Path string `json:"path"` - State string `json:"state"` - // swagger:strfmt date-time - CreatedAt time.Time `json:"created_at"` - // swagger:strfmt date-time - UpdatedAt time.Time `json:"updated_at"` - URL string `json:"url"` - HTMLURL string `json:"html_url"` - BadgeURL string `json:"badge_url"` - // swagger:strfmt date-time - DeletedAt time.Time `json:"deleted_at,omitempty"` -} - -// ActionWorkflowResponse returns a ActionWorkflow -type ActionWorkflowResponse struct { - Workflows []*ActionWorkflow `json:"workflows"` - TotalCount int64 `json:"total_count"` -} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 6d6e09bb8e..2ffd6b129b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -915,21 +915,6 @@ func Routes() *web.Router { }) } - addActionsWorkflowRoutes := func( - m *web.Router, - actw actions.WorkflowAPI, - ) { - m.Group("/actions", func() { - m.Group("/workflows", func() { - m.Get("", reqToken(), actw.ListRepositoryWorkflows) - m.Get("/{workflow_id}", reqToken(), actw.GetWorkflow) - m.Put("/{workflow_id}/disable", reqToken(), reqRepoWriter(unit.TypeActions), actw.DisableWorkflow) - m.Post("/{workflow_id}/dispatches", reqToken(), reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow) - m.Put("/{workflow_id}/enable", reqToken(), reqRepoWriter(unit.TypeActions), actw.EnableWorkflow) - }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeActions)) - }) - } - m.Group("", func() { // Miscellaneous (no scope required) if setting.API.EnableSwagger { @@ -1175,10 +1160,6 @@ func Routes() *web.Router { reqOwner(), repo.NewAction(), ) - addActionsWorkflowRoutes( - m, - repo.NewActionWorkflow(), - ) m.Group("/hooks/git", func() { m.Combo("").Get(repo.ListGitHooks) m.Group("/{id}", func() { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 8933a10b4b..d27e8d2427 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -5,7 +5,6 @@ package repo import ( "errors" - "fmt" "net/http" actions_model "code.gitea.io/gitea/models/actions" @@ -20,8 +19,6 @@ 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 @@ -584,297 +581,3 @@ func ListActionTasks(ctx *context.APIContext) { ctx.JSON(http.StatusOK, &res) } - -// ActionWorkflow implements actions_service.WorkflowAPI -type ActionWorkflow struct{} - -// NewActionWorkflow creates a new ActionWorkflow service -func NewActionWorkflow() actions_service.WorkflowAPI { - return ActionWorkflow{} -} - -func (a ActionWorkflow) ListRepositoryWorkflows(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/actions/workflows repository ListRepositoryWorkflows - // --- - // summary: List repository workflows - // produces: - // - application/json - // parameters: - // - name: owner - // in: path - // description: owner of the repo - // type: string - // required: true - // - name: repo - // in: path - // description: name of the repo - // type: string - // required: true - // responses: - // "200": - // "$ref": "#/responses/ActionWorkflowList" - // "400": - // "$ref": "#/responses/error" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" - // "422": - // "$ref": "#/responses/validationError" - // "500": - // "$ref": "#/responses/error" - - workflows, err := actions_service.ListActionWorkflows(ctx) - if err != nil { - ctx.Error(http.StatusInternalServerError, "ListActionWorkflows", err) - return - } - - ctx.JSON(http.StatusOK, &api.ActionWorkflowResponse{Workflows: workflows, TotalCount: int64(len(workflows))}) -} - -func (a ActionWorkflow) GetWorkflow(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id} repository GetWorkflow - // --- - // summary: Get a workflow - // produces: - // - application/json - // parameters: - // - name: owner - // in: path - // description: owner of the repo - // type: string - // required: true - // - name: repo - // in: path - // description: name of the repo - // type: string - // required: true - // - name: workflow_id - // in: path - // description: id of the workflow - // type: string - // required: true - // responses: - // "200": - // "$ref": "#/responses/ActionWorkflow" - // "400": - // "$ref": "#/responses/error" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" - // "422": - // "$ref": "#/responses/validationError" - // "500": - // "$ref": "#/responses/error" - - workflowID := ctx.PathParam("workflow_id") - if len(workflowID) == 0 { - ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter")) - return - } - - workflow, err := actions_service.GetActionWorkflow(ctx, workflowID) - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetActionWorkflow", err) - return - } - - if workflow == nil { - ctx.Error(http.StatusNotFound, "GetActionWorkflow", err) - return - } - - ctx.JSON(http.StatusOK, workflow) -} - -func (a ActionWorkflow) DisableWorkflow(ctx *context.APIContext) { - // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository DisableWorkflow - // --- - // summary: Disable a workflow - // produces: - // - application/json - // parameters: - // - name: owner - // in: path - // description: owner of the repo - // type: string - // required: true - // - name: repo - // in: path - // description: name of the repo - // type: string - // required: true - // - name: workflow_id - // in: path - // description: id of the workflow - // type: string - // required: true - // responses: - // "204": - // description: No Content - // "400": - // "$ref": "#/responses/error" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" - // "422": - // "$ref": "#/responses/validationError" - - workflowID := ctx.PathParam("workflow_id") - if len(workflowID) == 0 { - ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter")) - return - } - - err := actions_service.DisableActionWorkflow(ctx, workflowID) - if err != nil { - ctx.Error(http.StatusInternalServerError, "DisableActionWorkflow", err) - return - } - - ctx.Status(http.StatusNoContent) -} - -func (a ActionWorkflow) DispatchWorkflow(ctx *context.APIContext) { - // swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository DispatchWorkflow - // --- - // summary: Create a workflow dispatch event - // produces: - // - application/json - // parameters: - // - name: owner - // in: path - // description: owner of the repo - // type: string - // required: true - // - name: repo - // in: path - // description: name of the repo - // type: string - // required: true - // - name: workflow_id - // in: path - // description: id of the workflow - // type: string - // required: true - // - name: body - // in: body - // schema: - // "$ref": "#/definitions/CreateActionWorkflowDispatch" - // responses: - // "204": - // description: No Content - // "400": - // "$ref": "#/responses/error" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" - // "422": - // "$ref": "#/responses/validationError" - - opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch) - - workflowID := ctx.PathParam("workflow_id") - if len(workflowID) == 0 { - ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter")) - return - } - - ref := opt.Ref - if len(ref) == 0 { - ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("ref is required parameter")) - return - } - - err := actions_service.DispatchActionWorkflow(&context.Context{ - Base: ctx.Base, - Doer: ctx.Doer, - Repo: ctx.Repo, - }, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs *map[string]any) error { - if workflowDispatch != nil { - // TODO figure out why the inputs map is empty for url form encoding workaround - if opt.Inputs == nil { - for name, config := range workflowDispatch.Inputs { - value := ctx.FormString("inputs["+name+"]", config.Default) - (*inputs)[name] = value - } - } else { - for name, config := range workflowDispatch.Inputs { - value, ok := opt.Inputs[name] - if ok { - (*inputs)[name] = value - } else { - (*inputs)[name] = config.Default - } - } - } - } - return nil - }) - if err != nil { - if terr, ok := err.(*actions_service.TranslateableError); ok { - msg := ctx.Locale.TrString(terr.Translation, terr.Args...) - ctx.Error(terr.GetCode(), msg, fmt.Errorf("%s", msg)) - return - } - ctx.Error(http.StatusInternalServerError, err.Error(), err) - return - } - - ctx.Status(http.StatusNoContent) -} - -func (a ActionWorkflow) EnableWorkflow(ctx *context.APIContext) { - // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable repository EnableWorkflow - // --- - // summary: Enable a workflow - // produces: - // - application/json - // parameters: - // - name: owner - // in: path - // description: owner of the repo - // type: string - // required: true - // - name: repo - // in: path - // description: name of the repo - // type: string - // required: true - // - name: workflow_id - // in: path - // description: id of the workflow - // type: string - // required: true - // responses: - // "204": - // description: No Content - // "400": - // "$ref": "#/responses/error" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" - // "409": - // "$ref": "#/responses/conflict" - // "422": - // "$ref": "#/responses/validationError" - - workflowID := ctx.PathParam("workflow_id") - if len(workflowID) == 0 { - ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("workflow_id is required parameter")) - return - } - - err := actions_service.EnableActionWorkflow(ctx, workflowID) - if err != nil { - ctx.Error(http.StatusInternalServerError, "EnableActionWorkflow", err) - return - } - - ctx.Status(http.StatusNoContent) -} diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go index 16a250184a..665f4d0b85 100644 --- a/routers/api/v1/swagger/action.go +++ b/routers/api/v1/swagger/action.go @@ -32,17 +32,3 @@ 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/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index aa5990eb38..353d6de89b 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -211,9 +211,6 @@ type swaggerParameterBodies struct { // in:body RenameOrgOption api.RenameOrgOption - // in:body - CreateActionWorkflowDispatch api.CreateActionWorkflowDispatch - // in:body UpdateVariableOption api.UpdateVariableOption } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 6e09cd3de8..fc346b83b4 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -20,6 +20,8 @@ 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" @@ -28,13 +30,16 @@ 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" ) @@ -787,36 +792,143 @@ func Run(ctx *context_module.Context) { ctx.ServerError("ref", nil) 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 - } - } - } - return nil - }) - if err != nil { - if terr, ok := err.(*actions_service.TranslateableError); ok { - ctx.Flash.Error(ctx.Tr(terr.Translation, terr.Args...)) - ctx.Redirect(redirectURL) - return - } - ctx.ServerError(err.Error(), err) + + // can not rerun job when workflow is disabled + cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) + cfg := cfgUnit.ActionsConfig() + if cfg.IsWorkflowDisabled(workflowID) { + ctx.Flash.Error(ctx.Tr("actions.workflow.disabled")) + ctx.Redirect(redirectURL) return } + // get target commit of run from specified ref + refName := git.RefName(ref) + var runTargetCommit *git.Commit + var err error + if refName.IsTag() { + runTargetCommit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName()) + } else if refName.IsBranch() { + runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) + } else { + ctx.Flash.Error(ctx.Tr("form.git_ref_name_error", ref)) + ctx.Redirect(redirectURL) + return + } + if err != nil { + ctx.Flash.Error(ctx.Tr("form.target_ref_not_exist", ref)) + ctx.Redirect(redirectURL) + return + } + + // get workflow entry from runTargetCommit + entries, err := actions.ListWorkflows(runTargetCommit) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + // find workflow from commit + var workflows []*jobparser.SingleWorkflow + for _, entry := range entries { + if entry.Name() == workflowID { + content, err := actions.GetContentFromEntry(entry) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + workflows, err = jobparser.Parse(content) + if err != nil { + ctx.ServerError("workflow", err) + return + } + 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) + 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 deleted file mode 100644 index 0877e62ea1..0000000000 --- a/services/actions/workflow.go +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package actions - -import ( - "fmt" - "net/http" - "path" - "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" - "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" - - "github.com/nektos/act/pkg/jobparser" - "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 { - if _, err := commit.SubTree(path); err == nil { - return path - } - } - return "" -} - -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, 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 - // State types: - // - active - // - deleted - // - disabled_fork - // - disabled_inactivity - // - disabled_manually - state := "active" - if cfg.IsWorkflowDisabled(entry.Name()) { - state = "disabled_manually" - } - - // 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: path.Join(folder, entry.Name()), - State: state, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - URL: URL, - HTMLURL: HTMLURL, - BadgeURL: badgeURL, - } -} - -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(workflow.ID) - } else { - cfg.DisableWorkflow(workflow.ID) - } - - 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 - } - - folder := getActionWorkflowPath(defaultBranchCommit) - - workflows := make([]*api.ActionWorkflow, len(entries)) - for i, entry := range entries { - workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, folder, entry) - } - - return workflows, nil -} - -func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionWorkflow, error) { - entries, err := ListActionWorkflows(ctx) - if err != nil { - return nil, err - } - - for _, entry := range entries { - if entry.Name == workflowID { - return entry, nil - } - } - - return nil, fmt.Errorf("workflow '%s' not found", workflowID) -} - -func DisableActionWorkflow(ctx *context.APIContext, workflowID string) error { - return disableOrEnableWorkflow(ctx, workflowID, false) -} - -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) { - return &TranslateableError{ - Translation: "actions.workflow.disabled", - } - } - - // 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 { - refName = git.RefNameFromBranch(ref) - runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ref) - } - if err != nil { - return &TranslateableError{ - Code: http.StatusNotFound, - Translation: "form.target_ref_not_exist", - Args: []any{ref}, - } - } - - // get workflow entry from runTargetCommit - entries, err := actions.ListWorkflows(runTargetCommit) - if err != nil { - return err - } - - // 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 { - return err - } - workflows, err = jobparser.Parse(content) - if err != nil { - return err - } - break - } - - if len(workflows) == 0 { - return &TranslateableError{ - Code: http.StatusNotFound, - Translation: "actions.workflow.not_found", - Args: []any{workflowID}, - } - } - - // 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: ref, - Repository: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}), - Inputs: inputsWithDefaults, - Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone), - } - 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.OwnerID, - WorkflowID: workflowID, - TriggerUserID: ctx.Doer.ID, - Ref: string(refName), - 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 { - return fmt.Errorf("workflow: %w", err) - } - - alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) - if err != nil { - log.Error("FindRunJobs: %v", err) - } - CreateCommitStatus(ctx, alljobs...) - - return nil -} - -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 deleted file mode 100644 index 43fa92bdf8..0000000000 --- a/services/actions/workflow_interface.go +++ /dev/null @@ -1,20 +0,0 @@ -// 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) - // 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 3f80d3fd9e..d22e01c787 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4421,275 +4421,6 @@ } } }, - "/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" - }, - "422": { - "$ref": "#/responses/validationError" - }, - "500": { - "$ref": "#/responses/error" - } - } - } - }, - "/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" - }, - "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" - }, - "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" - }, - "422": { - "$ref": "#/responses/validationError" - } - } - } - }, - "/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": [ @@ -18949,56 +18680,6 @@ }, "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": "string", - "x-go-name": "ID" - }, - "name": { - "type": "string", - "x-go-name": "Name" - }, - "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": { @@ -20007,26 +19688,6 @@ }, "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", @@ -26026,21 +25687,6 @@ "$ref": "#/definitions/ActionVariable" } }, - "ActionWorkflow": { - "description": "ActionWorkflow", - "schema": { - "$ref": "#/definitions/ActionWorkflow" - } - }, - "ActionWorkflowList": { - "description": "ActionWorkflowList", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/ActionWorkflow" - } - } - }, "ActivityFeedsList": { "description": "ActivityFeedsList", "schema": { diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index e2c97662f2..8ea9b34efe 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -5,7 +5,6 @@ package integration import ( "fmt" - "net/http" "net/url" "strings" "testing" @@ -23,7 +22,6 @@ 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" @@ -653,625 +651,3 @@ 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"]) - }) -} - -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.Empty(t, workflows.Workflows) - - // 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"]) - }) -}