diff --git a/models/actions/require_action.go b/models/actions/require_action.go new file mode 100644 index 000000000000..ce0ba98a1270 --- /dev/null +++ b/models/actions/require_action.go @@ -0,0 +1,82 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// WIP RequireAction + +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/migrations/v1_23/v295.go b/models/migrations/v1_23/v295.go new file mode 100644 index 000000000000..9f26673c487b --- /dev/null +++ b/models/migrations/v1_23/v295.go @@ -0,0 +1,22 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func AddRequireActionTable(x *xorm.Engine) error { + 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"` + } + return x.Sync(new(RequireAction)) +} diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index fd5baa948861..45ac293e7e25 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -169,13 +169,30 @@ 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) DisableGlobalWorkflow(file string) { + cfg.EnabledGlobalWorkflows = util.SliceRemoveAll(cfg.EnabledGlobalWorkflows, file) +} + +func (cfg *ActionsConfig) IsGlobalWorkflowEnabled(file string) bool { + return slices.Contains(cfg.EnabledGlobalWorkflows, file) +} + +func (cfg *ActionsConfig) EnableGlobalWorkflow(file string) { + cfg.EnabledGlobalWorkflows = append(cfg.EnabledGlobalWorkflows, file) +} + +func (cfg *ActionsConfig) GetGlobalWorkflow() []string { + return cfg.EnabledGlobalWorkflows +} + func (cfg *ActionsConfig) ToString() string { return strings.Join(cfg.DisabledWorkflows, ",") } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index eef4f5696a7f..042183a81787 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3647,11 +3647,38 @@ runs.no_workflows.documentation = For more information on Gitea Actions, see 0 && ctx.Repo.IsAdmin() { ctx.Data["AllowDisableOrEnableWorkflow"] = true 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") @@ -209,6 +212,9 @@ func List(ctx *context.Context) { pager.AddParamString("workflow", workflow) 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 12909bddd55b..862db4b49dc3 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -685,33 +685,60 @@ func EnableWorkflowFile(ctx *context_module.Context) { } func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) { + disableOrEnable(ctx, isEnable, false) +} + +func disableOrEnable(ctx *context_module.Context, isEnable, isglobal bool) { workflow := ctx.FormString("workflow") if len(workflow) == 0 { ctx.ServerError("workflow", nil) return } - 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 { ctx.ServerError("UpdateRepoUnit", err) 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), url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status"))) ctx.JSONRedirect(redirectURL) } + +func DisableGlobalWorkflowFile(ctx *context_module.Context) { + disableOrEnableGlobalWorkflowFile(ctx, true) +} + +func EnableGlobalWorkflowFile(ctx *context_module.Context) { + disableOrEnableGlobalWorkflowFile(ctx, 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 000000000000..cc9395db7822 --- /dev/null +++ b/routers/web/repo/setting/require_action.go @@ -0,0 +1,87 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// WIP RequireAction + +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 ( + // let start with org WIP + 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{ + 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 000000000000..d51aa59e5369 --- /dev/null +++ b/routers/web/shared/actions/require_action.go @@ -0,0 +1,84 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// WIP RequireAction + +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.ParamsInt64(":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 91ab378d97c5..d2262015f78b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -457,6 +457,15 @@ func registerRoutes(m *web.Route) { }) } + // WIP RequireAction + 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. @@ -628,6 +637,7 @@ func registerRoutes(m *web.Route) { m.Group("/actions", func() { m.Get("", user_setting.RedirectToDefaultSetting) + addSettingsRequireActionRoutes() addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() @@ -926,6 +936,7 @@ func registerRoutes(m *web.Route) { m.Group("/actions", func() { m.Get("", org_setting.RedirectToDefaultSetting) + addSettingsRequireActionRoutes() addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() @@ -1371,10 +1382,12 @@ func registerRoutes(m *web.Route) { }, ignSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) // end "/{username}/{reponame}/projects" - m.Group("/{username}/{reponame}/actions", func() { + m.Group("/actions", func() { m.Get("", actions.List) m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile) m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile) + 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/require_action.go b/services/actions/require_action.go new file mode 100644 index 000000000000..8cd43ad2e914 --- /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 418a87b863d9..348c2e826b5c 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -344,6 +344,12 @@ func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) bind return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +// WIP RequireAction create form +type RequireActionForm struct { + RepoName string `binding:"Required;MaxSize(255)"` + WorkflowName string `binding:"Required;MaxSize(255)"` +} + // NewAccessTokenForm form for creating access token type NewAccessTokenForm struct { Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"` diff --git a/templates/org/settings/actions.tmpl b/templates/org/settings/actions.tmpl index abb9c98435f4..155cb0788816 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 ce792f667c4f..0151af78992f 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -29,6 +29,9 @@
{{ctx.Locale.Tr "actions.actions"}} @@ -65,6 +68,10 @@
+ + {{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 000000000000..e04e551dfcd1 --- /dev/null +++ b/templates/shared/actions/require_action_list.tmpl @@ -0,0 +1,147 @@ +
+

+ {{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 000000000000..fa8c7f967b81 --- /dev/null +++ b/web_src/js/features/require-actions-select.js @@ -0,0 +1,19 @@ +export function initRequireActionsSelect() { + const raselect = document.getElementById('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.js b/web_src/js/index.js index fc2f6b9b0b91..05bfb46258ad 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -54,6 +54,7 @@ import {initRepoCodeView} from './features/repo-code.js'; import {initSshKeyFormParser} from './features/sshkey-helper.js'; import {initUserSettings} from './features/user-settings.js'; import {initRepoArchiveLinks} from './features/repo-common.js'; +import {initRequireActionsSelect} from './features/require-actions-select.js'; import {initRepoMigrationStatusChecker} from './features/repo-migrate.js'; import { initRepoSettingGitHook, @@ -143,6 +144,7 @@ onDomReady(() => { initRepoActivityTopAuthorsChart(); initRepoArchiveLinks(); + initRequireActionsSelect(); initRepoBranchButton(); initRepoCodeView(); initRepoCommentForm();