diff --git a/models/actions/require_action.go b/models/actions/require_action.go new file mode 100644 index 00000000000..7ebe77f4ce8 --- /dev/null +++ b/models/actions/require_action.go @@ -0,0 +1,80 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" +) + +type RequireAction struct { + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + RepoName string `xorm:"VARCHAR(255)"` + WorkflowName string `xorm:"VARCHAR(255) UNIQUE(require_action) NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +type GlobalWorkflow struct { + RepoName string + Filename string +} + +func init() { + db.RegisterModel(new(RequireAction)) +} + +type FindRequireActionOptions struct { + db.ListOptions + RequireActionID int64 + OrgID int64 + RepoName string +} + +func (opts FindRequireActionOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.OrgID > 0 { + cond = cond.And(builder.Eq{"org_id": opts.OrgID}) + } + if opts.RequireActionID > 0 { + cond = cond.And(builder.Eq{"id": opts.RequireActionID}) + } + if opts.RepoName != "" { + cond = cond.And(builder.Eq{"repo_name": opts.RepoName}) + } + return cond +} + +// LoadAttributes loads the attributes of the require action +func (r *RequireAction) LoadAttributes(ctx context.Context) error { + // place holder for now. + return nil +} + +// if the workflow is removable +func (r *RequireAction) Removable(orgID int64) bool { + // everyone can remove for now + return r.OrgID == orgID +} + +func AddRequireAction(ctx context.Context, orgID int64, repoName, workflowName string) (*RequireAction, error) { + ra := &RequireAction{ + OrgID: orgID, + RepoName: repoName, + WorkflowName: workflowName, + } + return ra, db.Insert(ctx, ra) +} + +func DeleteRequireAction(ctx context.Context, requireActionID int64) error { + if _, err := db.DeleteByID[RequireAction](ctx, requireActionID); err != nil { + return err + } + return nil +} diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index cb52c2c9e20..98ec0794b34 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -169,21 +169,34 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle { } type ActionsConfig struct { - DisabledWorkflows []string + DisabledWorkflows []string + EnabledGlobalWorkflows []string } func (cfg *ActionsConfig) EnableWorkflow(file string) { cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file) } +func (cfg *ActionsConfig) EnableGlobalWorkflow(file string) { + cfg.EnabledGlobalWorkflows = append(cfg.EnabledGlobalWorkflows, file) +} + func (cfg *ActionsConfig) ToString() string { return strings.Join(cfg.DisabledWorkflows, ",") } +func (cfg *ActionsConfig) GetGlobalWorkflow() []string { + return cfg.EnabledGlobalWorkflows +} + func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { return slices.Contains(cfg.DisabledWorkflows, file) } +func (cfg *ActionsConfig) IsGlobalWorkflowEnabled(file string) bool { + return slices.Contains(cfg.EnabledGlobalWorkflows, file) +} + func (cfg *ActionsConfig) DisableWorkflow(file string) { for _, workflow := range cfg.DisabledWorkflows { if file == workflow { @@ -194,6 +207,10 @@ func (cfg *ActionsConfig) DisableWorkflow(file string) { cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) } +func (cfg *ActionsConfig) DisableGlobalWorkflow(file string) { + cfg.EnabledGlobalWorkflows = util.SliceRemoveAll(cfg.EnabledGlobalWorkflows, file) +} + // FromDB fills up a ActionsConfig from serialized format. func (cfg *ActionsConfig) FromDB(bs []byte) error { return json.UnmarshalHandleDoubleEncode(bs, &cfg) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 0d2b0dd9194..83aaf5a1ea3 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -95,6 +95,17 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { return events, nil } +func DetectGlobalWorkflows( + gitRepo *git.Repository, + commit *git.Commit, + triggedEvent webhook_module.HookEventType, + payload api.Payloader, + detectSchedule bool, + entries git.Entries, +) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { + return _DetectWorkflows(gitRepo, commit, triggedEvent, payload, detectSchedule, entries) +} + func DetectWorkflows( gitRepo *git.Repository, commit *git.Commit, @@ -106,7 +117,17 @@ func DetectWorkflows( if err != nil { return nil, nil, err } + return _DetectWorkflows(gitRepo, commit, triggedEvent, payload, detectSchedule, entries) +} +func _DetectWorkflows( + gitRepo *git.Repository, + commit *git.Commit, + triggedEvent webhook_module.HookEventType, + payload api.Payloader, + detectSchedule bool, + entries git.Entries, +) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { workflows := make([]*DetectedWorkflow, 0, len(entries)) schedules := make([]*DetectedWorkflow, 0, len(entries)) for _, entry := range entries { @@ -146,12 +167,25 @@ func DetectWorkflows( return workflows, schedules, nil } +func DetectScheduledGlobalWorkflows(gitRepo *git.Repository, commit *git.Commit, entries git.Entries) ([]*DetectedWorkflow, error) { + return _DetectScheduledWorkflows(gitRepo, commit, entries) +} + func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) { entries, err := ListWorkflows(commit) if err != nil { return nil, err } + return _DetectScheduledWorkflows(gitRepo, commit, entries) +} +func _DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit, entries git.Entries) ([]*DetectedWorkflow, error) { + if gitRepo != nil { + log.Trace("detect scheduled workflow for gitRepo.Path: %q", gitRepo.Path) + } + if commit != nil { + log.Trace("detect scheduled commit for commit ID: %q", commit.ID) + } wfs := make([]*DetectedWorkflow, 0, len(entries)) for _, entry := range entries { content, err := GetContentFromEntry(entry) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f50ad1f2981..01d489b737b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3744,6 +3744,27 @@ runs.no_runs = The workflow has no runs yet. runs.empty_commit_message = (empty commit message) runs.expire_log_message = Logs have been purged because they were too old. +require_action = Require Action +require_action.require_action_manage_panel = Require Action Management Panel +require_action.enable_global_workflow = How to Enable Global Workflow +require_action.id = ID +require_action.add = Add Global Workflow +require_action.add_require_action = Enable selected Workflow +require_action.new = Create New +require_action.status = Status +require_action.search = Search... +require_action.version = Version +require_action.repo = Repo Name +require_action.workflow = Workflow Filename +require_action.link = Link +require_action.remove = Remove +require_action.none = No Require Action Available. +require_action.creation.failed = Create Global Require Action %s Failed. +require_action.creation.success = Create Global Require Action %s successfully. +require_action.deletion = Delete +require_action.deletion.description = Removing the Global Require Action is permanent and cannot be undone. Continue? +require_action.deletion.success = The Global Require Action has been removed. + workflow.disable = Disable Workflow workflow.disable_success = Workflow '%s' disabled successfully. workflow.enable = Enable Workflow @@ -3755,6 +3776,13 @@ workflow.run_success = Workflow '%s' run successfully. workflow.from_ref = Use workflow from workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger. +workflow.global = Global +workflow.global_disable = Disable Global Require +workflow.global_disable_success = Global Require '%s' disabled successfully. +workflow.global_enable = Enable Global Require +workflow.global_enable_success = Global Require '%s' enabled successfully. +workflow.global_enabled = Global Require is disabled. + need_approval_desc = Need approval to run workflows for fork pull request. variables = Variables diff --git a/routers/web/org/setting/require_action.go b/routers/web/org/setting/require_action.go new file mode 100644 index 00000000000..64f872ede30 --- /dev/null +++ b/routers/web/org/setting/require_action.go @@ -0,0 +1,12 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "code.gitea.io/gitea/services/context" +) + +func RedirectToRepoSetting(ctx *context.Context) { + ctx.Redirect(ctx.Org.OrgLink + "/settings/actions/require_action") +} diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index ad16b9fb4e4..ab37350e2e0 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" @@ -38,6 +39,7 @@ const ( type Workflow struct { Entry git.TreeEntry + Global bool ErrMsg string } @@ -71,9 +73,19 @@ func List(ctx *context.Context) { var workflows []Workflow var curWorkflow *model.Workflow + var globalEntries []*git.TreeEntry + globalWorkflow, err := db.Find[actions_model.RequireAction](ctx, actions_model.FindRequireActionOptions{ + OrgID: ctx.Repo.Repository.Owner.ID, + }) + if err != nil { + ctx.ServerError("Global Workflow DB find fail", err) + return + } if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil { ctx.ServerError("IsEmpty", err) - return + if len(globalWorkflow) < 1 { + return + } } else if !empty { commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) if err != nil { @@ -85,6 +97,23 @@ func List(ctx *context.Context) { ctx.ServerError("ListWorkflows", err) return } + for _, gEntry := range globalWorkflow { + if gEntry.RepoName == ctx.Repo.Repository.Name { + log.Trace("Same Repo conflict: %s\n", gEntry.RepoName) + continue + } + gRepo, _ := repo_model.GetRepositoryByName(ctx, gEntry.OrgID, gEntry.RepoName) + gGitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, gRepo) + // it may be a hack for now..... not sure any better way to do this + gCommit, _ := gGitRepo.GetBranchCommit(gRepo.DefaultBranch) + gEntries, _ := actions.ListWorkflows(gCommit) + for _, entry := range gEntries { + if gEntry.WorkflowName == entry.Name() { + globalEntries = append(globalEntries, entry) + entries = append(entries, entry) + } + } + } // Get all runner labels runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ @@ -103,7 +132,14 @@ func List(ctx *context.Context) { workflows = make([]Workflow, 0, len(entries)) for _, entry := range entries { - workflow := Workflow{Entry: *entry} + var workflowIsGlobal bool + workflowIsGlobal = false + for i := range globalEntries { + if globalEntries[i] == entry { + workflowIsGlobal = true + } + } + workflow := Workflow{Entry: *entry, Global: workflowIsGlobal} content, err := actions.GetContentFromEntry(entry) if err != nil { ctx.ServerError("GetContentFromEntry", err) @@ -165,6 +201,10 @@ func List(ctx *context.Context) { page = 1 } + workflow := ctx.FormString("workflow") + isGlobal := false + ctx.Data["CurWorkflow"] = workflow + actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() ctx.Data["ActionsConfig"] = actionsConfig @@ -205,6 +245,9 @@ func List(ctx *context.Context) { ctx.Data["Tags"] = tags } } + ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(workflow) + ctx.Data["CurGlobalWorkflowEnable"] = actionsConfig.IsGlobalWorkflowEnabled(workflow) + isGlobal = actionsConfig.IsGlobalWorkflowEnabled(workflow) } // if status or actor query param is not given to frontend href, (href="//actions") @@ -261,6 +304,9 @@ func List(ctx *context.Context) { pager.AddParamString("workflow", workflowID) pager.AddParamString("actor", fmt.Sprint(actorID)) pager.AddParamString("status", fmt.Sprint(status)) + if isGlobal { + pager.AddParamString("global", fmt.Sprint(isGlobal)) + } ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 73c6e54fbf5..49f1a15fe12 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -721,7 +721,15 @@ func EnableWorkflowFile(ctx *context_module.Context) { disableOrEnableWorkflowFile(ctx, true) } -func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) { +func EnableGlobalWorkflowFile(ctx *context_module.Context) { + disableOrEnableGlobalWorkflowFile(ctx, false) +} + +func DisableGlobalWorkflowFile(ctx *context_module.Context) { + disableOrEnableGlobalWorkflowFile(ctx, true) +} + +func disableOrEnable(ctx *context_module.Context, isEnable, isglobal bool) { workflow := ctx.FormString("workflow") if len(workflow) == 0 { ctx.ServerError("workflow", nil) @@ -731,10 +739,18 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) { cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() - if isEnable { - cfg.EnableWorkflow(workflow) + if isglobal { + if isEnable { + cfg.DisableGlobalWorkflow(workflow) + } else { + cfg.EnableGlobalWorkflow(workflow) + } } else { - cfg.DisableWorkflow(workflow) + if isEnable { + cfg.EnableWorkflow(workflow) + } else { + cfg.DisableWorkflow(workflow) + } } if err := repo_model.UpdateRepoUnit(ctx, cfgUnit); err != nil { @@ -742,10 +758,18 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) { return } - if isEnable { - ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow)) + if isglobal { + if isEnable { + ctx.Flash.Success(ctx.Tr("actions.workflow.global_disable_success", workflow)) + } else { + ctx.Flash.Success(ctx.Tr("actions.workflow.global_enable_success", workflow)) + } } else { - ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow)) + if isEnable { + ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow)) + } else { + ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow)) + } } redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(workflow), @@ -913,3 +937,11 @@ func Run(ctx *context_module.Context) { ctx.Flash.Success(ctx.Tr("actions.workflow.run_success", workflowID)) ctx.Redirect(redirectURL) } + +func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) { + disableOrEnable(ctx, isEnable, false) +} + +func disableOrEnableGlobalWorkflowFile(ctx *context_module.Context, isEnable bool) { + disableOrEnable(ctx, isEnable, true) +} diff --git a/routers/web/repo/setting/require_action.go b/routers/web/repo/setting/require_action.go new file mode 100644 index 00000000000..5d08b89522a --- /dev/null +++ b/routers/web/repo/setting/require_action.go @@ -0,0 +1,85 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/base" + shared "code.gitea.io/gitea/routers/web/shared/actions" + "code.gitea.io/gitea/services/context" +) + +const ( + tplOrgRequireAction base.TplName = "org/settings/actions" +) + +type requireActionsCtx struct { + OrgID int64 + IsOrg bool + RequireActionTemplate base.TplName + RedirectLink string +} + +func getRequireActionCtx(ctx *context.Context) (*requireActionsCtx, error) { + if ctx.Data["PageIsOrgSettings"] == true { + return &requireActionsCtx{ + OrgID: ctx.Org.Organization.ID, + IsOrg: true, + RequireActionTemplate: tplOrgRequireAction, + RedirectLink: ctx.Org.OrgLink + "/settings/actions/require_action", + }, nil + } + return nil, errors.New("unable to set Require Actions context") +} + +// Listing all RequireAction +func RequireAction(ctx *context.Context) { + ctx.Data["ActionsTitle"] = ctx.Tr("actions.requires") + ctx.Data["PageType"] = "require_action" + ctx.Data["PageIsSharedSettingsRequireAction"] = true + + vCtx, err := getRequireActionCtx(ctx) + if err != nil { + ctx.ServerError("getRequireActionCtx", err) + return + } + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + opts := actions_model.FindRequireActionOptions{ + OrgID: vCtx.OrgID, + ListOptions: db.ListOptions{ + Page: page, + PageSize: 10, + }, + } + shared.SetRequireActionContext(ctx, opts) + ctx.Data["Link"] = vCtx.RedirectLink + shared.GlobalEnableWorkflow(ctx, ctx.Org.Organization.ID) + ctx.HTML(http.StatusOK, vCtx.RequireActionTemplate) +} + +func RequireActionCreate(ctx *context.Context) { + vCtx, err := getRequireActionCtx(ctx) + if err != nil { + ctx.ServerError("getRequireActionCtx", err) + return + } + shared.CreateRequireAction(ctx, vCtx.OrgID, vCtx.RedirectLink) +} + +func RequireActionDelete(ctx *context.Context) { + vCtx, err := getRequireActionCtx(ctx) + if err != nil { + ctx.ServerError("getRequireActionCtx", err) + return + } + shared.DeleteRequireAction(ctx, vCtx.RedirectLink) +} diff --git a/routers/web/shared/actions/require_action.go b/routers/web/shared/actions/require_action.go new file mode 100644 index 00000000000..4bc3d3fa9b6 --- /dev/null +++ b/routers/web/shared/actions/require_action.go @@ -0,0 +1,82 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/web" + actions_service "code.gitea.io/gitea/services/actions" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" +) + +// SetRequireActionDeletePost response for deleting a require action workflow +func SetRequireActionContext(ctx *context.Context, opts actions_model.FindRequireActionOptions) { + requireActions, count, err := db.FindAndCount[actions_model.RequireAction](ctx, opts) + if err != nil { + ctx.ServerError("CountRequireActions", err) + return + } + ctx.Data["RequireActions"] = requireActions + ctx.Data["Total"] = count + ctx.Data["OrgID"] = ctx.Org.Organization.ID + ctx.Data["OrgName"] = ctx.Org.Organization.Name + pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + ctx.Data["Page"] = pager +} + +// get all the available enable global workflow in the org's repo +func GlobalEnableWorkflow(ctx *context.Context, orgID int64) { + var gwfList []actions_model.GlobalWorkflow + orgRepos, err := org_model.GetOrgRepositories(ctx, orgID) + if err != nil { + ctx.ServerError("GlobalEnableWorkflows get org repos: ", err) + return + } + for _, repo := range orgRepos { + err := repo.LoadUnits(ctx) + if err != nil { + ctx.ServerError("GlobalEnableWorkflows LoadUnits : ", err) + } + actionsConfig := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + enabledWorkflows := actionsConfig.GetGlobalWorkflow() + for _, workflow := range enabledWorkflows { + gwf := actions_model.GlobalWorkflow{ + RepoName: repo.Name, + Filename: workflow, + } + gwfList = append(gwfList, gwf) + } + } + ctx.Data["GlobalEnableWorkflows"] = gwfList +} + +func CreateRequireAction(ctx *context.Context, orgID int64, redirectURL string) { + ctx.Data["OrgID"] = ctx.Org.Organization.ID + form := web.GetForm(ctx).(*forms.RequireActionForm) + v, err := actions_service.CreateRequireAction(ctx, orgID, form.RepoName, form.WorkflowName) + if err != nil { + log.Error("CreateRequireAction: %v", err) + ctx.JSONError(ctx.Tr("actions.require_action.creation.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.require_action.creation.success", v.WorkflowName)) + ctx.JSONRedirect(redirectURL) +} + +func DeleteRequireAction(ctx *context.Context, redirectURL string) { + id := ctx.PathParamInt64(":require_action_id") + + if err := actions_service.DeleteRequireActionByID(ctx, id); err != nil { + log.Error("Delete RequireAction [%d] failed: %v", id, err) + ctx.JSONError(ctx.Tr("actions.require_action.deletion.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.require_action.deletion.success")) + ctx.JSONRedirect(redirectURL) +} diff --git a/routers/web/web.go b/routers/web/web.go index c87c01ea0f0..fa36b2648f0 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -467,6 +467,14 @@ func registerRoutes(m *web.Router) { }) } + addSettingsRequireActionRoutes := func() { + m.Group("/require_action", func() { + m.Get("", repo_setting.RequireAction) + m.Post("/add", web.Bind(forms.RequireActionForm{}), repo_setting.RequireActionCreate) + m.Post("/{require_action_id}/delete", repo_setting.RequireActionDelete) + }) + } + // FIXME: not all routes need go through same middleware. // Especially some AJAX requests, we can reduce middleware number to improve performance. @@ -651,6 +659,7 @@ func registerRoutes(m *web.Router) { m.Group("/actions", func() { m.Get("", user_setting.RedirectToDefaultSetting) + addSettingsRequireActionRoutes() addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() @@ -946,6 +955,7 @@ func registerRoutes(m *web.Router) { m.Group("/actions", func() { m.Get("", org_setting.RedirectToDefaultSetting) + addSettingsRequireActionRoutes() addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() @@ -1411,6 +1421,8 @@ func registerRoutes(m *web.Router) { m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile) m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile) m.Post("/run", reqRepoActionsWriter, actions.Run) + m.Post("/global_disable", reqRepoAdmin, actions.DisableGlobalWorkflowFile) + m.Post("/global_enable", reqRepoAdmin, actions.EnableGlobalWorkflowFile) m.Group("/runs/{run}", func() { m.Combo(""). diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 323c6a76e42..b6de65e54f1 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -191,14 +191,55 @@ func notify(ctx context.Context, input *notifyInput) error { if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } + var globalEntries []*git.TreeEntry + globalWorkflow, err := db.Find[actions_model.RequireAction](ctx, actions_model.FindRequireActionOptions{ + OrgID: input.Repo.OwnerID, + }) + if err != nil { + return fmt.Errorf("Global Entries DB find failed: %w", err) + } + for _, gEntry := range globalWorkflow { + if gEntry.RepoName == input.Repo.Name { + log.Trace("Same Repo conflict: %s\n", gEntry.RepoName) + continue + } + gRepo, _ := repo_model.GetRepositoryByName(ctx, gEntry.OrgID, gEntry.RepoName) + gGitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, gRepo) + gCommit, _ := gGitRepo.GetBranchCommit(gRepo.DefaultBranch) + gEntries, _ := actions_module.ListWorkflows(gCommit) + for _, entry := range gEntries { + if gEntry.WorkflowName == entry.Name() { + globalEntries = append(globalEntries, entry) + } + } + } + gWorkflows, gSchedules, err := actions_module.DetectGlobalWorkflows(gitRepo, commit, + input.Event, + input.Payload, + shouldDetectSchedules, + globalEntries, + ) + if err != nil { + return fmt.Errorf("Detect Global workflow failed: %w", err) + } - log.Trace("repo %s with commit %s event %s find %d workflows and %d schedules", + log.Trace("repo %s with commit %s event %s find %d workflows and %d schedules, %d global workflows and %d global schedules", input.Repo.RepoPath(), commit.ID, input.Event, len(workflows), len(schedules), + len(gWorkflows), + len(gSchedules), ) + for _, workflow := range gWorkflows { + workflows = append(workflows, workflow) + log.Trace("gWorkflows: %v\n", workflow) + } + for _, schedule := range gSchedules { + schedules = append(schedules, schedule) + log.Trace("gSchedules: %v\n", schedule) + } for _, wf := range workflows { if actionsConfig.IsWorkflowDisabled(wf.EntryName) { diff --git a/services/actions/require_action.go b/services/actions/require_action.go new file mode 100644 index 00000000000..8cd43ad2e91 --- /dev/null +++ b/services/actions/require_action.go @@ -0,0 +1,22 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + actions_model "code.gitea.io/gitea/models/actions" +) + +func CreateRequireAction(ctx context.Context, orgID int64, repoName, workflowName string) (*actions_model.RequireAction, error) { + v, err := actions_model.AddRequireAction(ctx, orgID, repoName, workflowName) + if err != nil { + return nil, err + } + return v, nil +} + +func DeleteRequireActionByID(ctx context.Context, requireActionID int64) error { + return actions_model.DeleteRequireAction(ctx, requireActionID) +} diff --git a/services/forms/user_form.go b/services/forms/user_form.go index ed79936add6..ea69d5935b5 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -340,6 +340,11 @@ type EditVariableForm struct { Data string `binding:"Required;MaxSize(65535)"` } +type RequireActionForm struct { + RepoName string `binding:"Required;MaxSize(255)"` + WorkflowName string `binding:"Required;MaxSize(255)"` +} + func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { ctx := context.GetValidateContext(req) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) diff --git a/templates/org/settings/actions.tmpl b/templates/org/settings/actions.tmpl index abb9c98435f..155cb078881 100644 --- a/templates/org/settings/actions.tmpl +++ b/templates/org/settings/actions.tmpl @@ -1,6 +1,8 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings actions")}}
- {{if eq .PageType "runners"}} + {{if eq .PageType "require_action"}} + {{template "shared/actions/require_action_list" .}} + {{else if eq .PageType "runners"}} {{template "shared/actions/runner_list" .}} {{else if eq .PageType "secrets"}} {{template "shared/secrets/add_list" .}} diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index ce792f667c4..0151af78992 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -29,6 +29,9 @@
{{ctx.Locale.Tr "actions.actions"}} @@ -64,7 +70,10 @@ {{end}}
- + + {{if .AllowDisableOrEnableWorkflow}} {{end}} diff --git a/templates/shared/actions/require_action_list.tmpl b/templates/shared/actions/require_action_list.tmpl new file mode 100644 index 00000000000..f9d30d0e946 --- /dev/null +++ b/templates/shared/actions/require_action_list.tmpl @@ -0,0 +1,126 @@ +
+

+ {{ctx.Locale.Tr "actions.require_action.require_action_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}}) +
+
+ +
+
+

+
+
+ + {{template "shared/search/combo" dict "Value" .Keyword}} + +
+
+
+ + + + + + + + + + + + {{if .RequireActions}} + {{range .RequireActions}} + + + + + + + + {{end}} + {{else}} + + + + {{end}} + +
+ {{ctx.Locale.Tr "actions.require_action.id"}} + + {{ctx.Locale.Tr "actions.require_action.workflow"}} + {{ctx.Locale.Tr "actions.require_action.repo"}}{{ctx.Locale.Tr "actions.require_action.link"}}{{ctx.Locale.Tr "actions.require_action.remove"}}
{{.ID}}

{{.WorkflowName}}

{{.RepoName}}Workflow Link + {{if .Removable $.OrgID}} + + + {{end}} +
{{ctx.Locale.Tr "actions.require_action.none"}}
+
+ {{template "base/paginate"}} +
+ + + +{{/* Add RequireAction dialog */}} + diff --git a/web_src/js/features/require-actions-select.js b/web_src/js/features/require-actions-select.js new file mode 100644 index 00000000000..3fc4ee72c71 --- /dev/null +++ b/web_src/js/features/require-actions-select.js @@ -0,0 +1,19 @@ +export function initRequireActionsSelect() { + const raselect = document.querySelector('add-require-actions-modal'); + if (!raselect) return; + const checkboxes = document.querySelectorAll('.ui.radio.checkbox'); + for (const box of checkboxes) { + box.addEventListener('change', function() { + const hiddenInput = this.nextElementSibling; + const isChecked = this.querySelector('input[type="radio"]').checked; + hiddenInput.disabled = !isChecked; + // Disable other hidden inputs + for (const otherbox of checkboxes) { + const otherHiddenInput = otherbox.nextElementSibling; + if (otherbox !== box) { + otherHiddenInput.disabled = isChecked; + } + } + }); + } +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 51d8c96fbdf..0f994302873 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -40,6 +40,7 @@ import {initSshKeyFormParser} from './features/sshkey-helper.ts'; import {initUserSettings} from './features/user-settings.ts'; import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts'; import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts'; +import {initRequireActionsSelect} from './features/require-actions-select.js'; import {initRepoDiffView} from './features/repo-diff.ts'; import {initOrgTeamSearchRepoBox, initOrgTeamSettings} from './features/org-team.ts'; import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.ts'; @@ -172,6 +173,7 @@ onDomReady(() => { initRepoActivityTopAuthorsChart, initRepoArchiveLinks, + initRequireActionsSelect, initRepoBranchButton, initRepoCodeView, initBranchSelectorTabs,