From 882e683e84fb78ee6bea7133c041ac60d52a0c06 Mon Sep 17 00:00:00 2001
From: "Alex Lau(AvengerMoJo)" <avengermojo@gmail.com>
Date: Sun, 25 Aug 2024 21:27:36 +0800
Subject: [PATCH] Combined all RequestActions implementation and merge upstream
 and remove WIP

Signed-off-by: Alex Lau(AvengerMoJo) <avengermojo@gmail.com>
---
 models/actions/require_action.go              |  80 +++++++++++
 models/repo/repo_unit.go                      |  19 ++-
 modules/actions/workflows.go                  |  34 +++++
 options/locale/locale_en-US.ini               |  28 ++++
 routers/web/org/setting/require_action.go     |  12 ++
 routers/web/repo/actions/actions.go           |  50 ++++++-
 routers/web/repo/actions/view.go              |  46 ++++++-
 routers/web/repo/setting/require_action.go    |  85 ++++++++++++
 routers/web/shared/actions/require_action.go  |  82 ++++++++++++
 routers/web/web.go                            |  12 ++
 services/actions/notifier_helper.go           |  43 +++++-
 services/actions/require_action.go            |  22 +++
 services/forms/user_form.go                   |   5 +
 templates/org/settings/actions.tmpl           |   4 +-
 templates/org/settings/navbar.tmpl            |   3 +
 templates/repo/actions/list.tmpl              |  19 ++-
 .../shared/actions/require_action_list.tmpl   | 126 ++++++++++++++++++
 web_src/js/features/require-actions-select.js |  19 +++
 web_src/js/index.ts                           |   2 +
 19 files changed, 676 insertions(+), 15 deletions(-)
 create mode 100644 models/actions/require_action.go
 create mode 100644 routers/web/org/setting/require_action.go
 create mode 100644 routers/web/repo/setting/require_action.go
 create mode 100644 routers/web/shared/actions/require_action.go
 create mode 100644 services/actions/require_action.go
 create mode 100644 templates/shared/actions/require_action_list.tmpl
 create mode 100644 web_src/js/features/require-actions-select.js

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 951994253ac..0d03011fba8 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3712,6 +3712,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
@@ -3723,6 +3744,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 f5fb056494d..45116299485 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="/<repoLink>/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 11199d69eb3..cda51de5f03 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -722,7 +722,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)
@@ -732,10 +740,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 {
@@ -743,10 +759,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),
@@ -914,3 +938,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 41b019e4b59..377a95e896e 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -458,6 +458,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.
 
@@ -631,6 +639,7 @@ func registerRoutes(m *web.Router) {
 
 		m.Group("/actions", func() {
 			m.Get("", user_setting.RedirectToDefaultSetting)
+			addSettingsRequireActionRoutes()
 			addSettingsRunnersRoutes()
 			addSettingsSecretsRoutes()
 			addSettingsVariablesRoutes()
@@ -937,6 +946,7 @@ func registerRoutes(m *web.Router) {
 
 				m.Group("/actions", func() {
 					m.Get("", org_setting.RedirectToDefaultSetting)
+					addSettingsRequireActionRoutes()
 					addSettingsRunnersRoutes()
 					addSettingsSecretsRoutes()
 					addSettingsVariablesRoutes()
@@ -1392,6 +1402,8 @@ func registerRoutes(m *web.Router) {
 		m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
 		m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
 		m.Post("/run", reqRepoAdmin, 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 b21d889d036..e4de6dc08c1 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -183,14 +183,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 5b7a43642ab..d8fd3389202 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")}}
 	<div class="org-setting-content">
-	{{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 @@
 		<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
 			<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
 			<div class="menu">
+				<a class="{{if .PageIsSharedSettingsRequireAction}}active {{end}}item" href="{{.OrgLink}}/settings/actions/require_action">
+					{{ctx.Locale.Tr "actions.require_action"}}
+				</a>
 				<a class="{{if .PageIsSharedSettingsRunners}}active {{end}}item" href="{{.OrgLink}}/settings/actions/runners">
 					{{ctx.Locale.Tr "actions.runners"}}
 				</a>
diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl
index 7d782c0adeb..3ad80fc0371 100644
--- a/templates/repo/actions/list.tmpl
+++ b/templates/repo/actions/list.tmpl
@@ -10,7 +10,11 @@
 				<div class="ui fluid vertical menu">
 					<a class="item{{if not $.CurWorkflow}} active{{end}}" href="?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
 					{{range .workflows}}
-						<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}}
+						{{if .Global}}
+						<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="?workflow={{.Entry.Name}}&isGlobal=true&actor={{$.CurActor}}&status={{$.CurStatus}}">(global) {{.Entry.Name}}</a>
+						{{else}}
+						<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}}</a>
+{{end}}
 							{{if .ErrMsg}}
 								<span data-tooltip-content="{{.ErrMsg}}">
 									{{svg "octicon-alert" 16 "text red"}}
@@ -20,7 +24,9 @@
 							{{if $.ActionsConfig.IsWorkflowDisabled .Entry.Name}}
 								<div class="ui red label">{{ctx.Locale.Tr "disabled"}}</div>
 							{{end}}
-						</a>
+							{{if $.ActionsConfig.IsGlobalWorkflowEnabled .Entry.Name}}
+								<div class="ui red label">{{ctx.Locale.Tr "Global Enabled"}}</div>
+							{{end}}
 					{{end}}
 				</div>
 			</div>
@@ -64,7 +70,10 @@
 							{{end}}
 						</div>
 					</div>
-
+					<!-- IsGlobalWorkflowEnabled -->
+					<div class="ui dropdown jump item">
+						<span class="text">{{ctx.Locale.Tr "actions.workflow.global"}}</span>
+					</div>
 					{{if .AllowDisableOrEnableWorkflow}}
 						<button class="ui jump dropdown btn interact-bg tw-p-2">
 							{{svg "octicon-kebab-horizontal"}}
@@ -72,6 +81,10 @@
 								<a class="item link-action" data-url="{{$.Link}}/{{if .CurWorkflowDisabled}}enable{{else}}disable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}">
 									{{if .CurWorkflowDisabled}}{{ctx.Locale.Tr "actions.workflow.enable"}}{{else}}{{ctx.Locale.Tr "actions.workflow.disable"}}{{end}}
 								</a>
+								<a class="item link-action" data-url="{{$.Link}}/{{if .CurGlobalWorkflowEnable}}global_disable{{else}}global_enable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}">
+									{{if .CurGlobalWorkflowEnable}}{{ctx.Locale.Tr "actions.workflow.global_disable"}}{{else}}{{ctx.Locale.Tr "actions.workflow.global_enable"}}{{end}}
+								</a>
+
 							</div>
 						</button>
 					{{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 @@
+<div class="require-actions-container">
+	<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "actions.require_action.require_action_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
+	<div class="ui right">
+		<div class="ui top right">
+			<button class="ui primary tiny button show-modal"
+					data-modal="#add-require-actions-modal"
+					data-modal-form.action="{{.Link}}/add"
+					data-modal-header="{{ctx.Locale.Tr "actions.require_action.add"}}">
+					{{ctx.Locale.Tr "actions.require_action.add"}}
+			</button>
+		</div>
+	</div>
+	</h4>
+	<div class="ui attached segment">
+		<form class="ui form ignore-dirty" id="require-action-list-search-form" action="{{$.Link}}">
+			<!-- Search Text -->
+				{{template "shared/search/combo" dict "Value" .Keyword}}
+				<button class="ui primary button">{{ctx.Locale.Tr "actions.require_action.search"}}</button>
+		</form>
+	</div>
+	<div class="ui attached table segment">
+		<table class="ui very basic striped table unstackable">
+			<thead>
+				<tr>
+					<th data-sortt-asc="newest" data-sortt-desc="oldest">
+						{{ctx.Locale.Tr "actions.require_action.id"}}
+					</th>
+					<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
+						{{ctx.Locale.Tr "actions.require_action.workflow"}}
+					</th>
+					<th>{{ctx.Locale.Tr "actions.require_action.repo"}}</th>
+					<th>{{ctx.Locale.Tr "actions.require_action.link"}}</th>
+					<th>{{ctx.Locale.Tr "actions.require_action.remove"}}</th>
+				</tr>
+			</thead>
+			<tbody>
+				{{if .RequireActions}}
+					{{range .RequireActions}}
+					<tr>
+						<td>{{.ID}}</td>
+						<td><p data-tooltip-content="{{.RepoName}}">{{.WorkflowName}}</p></td>
+						<td>{{.RepoName}}</td>
+						<td><a href="/{{$.OrgName}}/{{.RepoName}}">Workflow Link</a></td>
+						<td class="require_action-ops">
+							{{if .Removable $.OrgID}}
+							<button class="btn interact-bg tw-p-2 link-action"
+								data-tooltip-content="{{ctx.Locale.Tr "actions.require_action.deletion"}}"
+								data-url="{{$.Link}}/{{.ID}}/delete"
+								data-modal-confirm="{{ctx.Locale.Tr "actions.require_action.deletion.description"}}"
+							>
+								{{svg "octicon-trash"}}
+							</button>
+							<!-- <a href="{{$.Link}}/{{.ID}}/delete">{{svg "octicon-x-circle-fill"}}</a>-->
+							{{end}}
+						</td>
+					</tr>
+					{{end}}
+				{{else}}
+					<tr>
+						<td class="center aligned" colspan="8">{{ctx.Locale.Tr "actions.require_action.none"}}</td>
+					</tr>
+				{{end}}
+			</tbody>
+		</table>
+	</div>
+	{{template "base/paginate"}}
+</div>
+
+
+
+{{/* Add RequireAction dialog */}}
+<div class="ui small modal" id="add-require-actions-modal">
+	<div class="header">
+		<span id="actions-modal-header">Enable Workflows</span>
+	</div>
+	<form class="ui form form-fetch-action" method="post">
+		<div class="content">
+			<div class="item">
+				<a href="https://docs.gitea.com/usage/actions/require-action">{{ctx.Locale.Tr "actions.require_action.enable_global_workflow"}}</a>
+			</div>
+			<div class="divider"></div>
+			<table class="ui very basic striped table unstackable">
+			<thead>
+				<tr>
+					<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
+						{{ctx.Locale.Tr "actions.require_action.workflow"}}
+					</th>
+					<th>
+						{{ctx.Locale.Tr "actions.require_action.repo"}}
+					</th>
+				</tr>
+			</thead>
+			<tbody>
+			{{if .GlobalEnableWorkflows}}
+				{{range .GlobalEnableWorkflows}}
+				<tr>
+				<td><div class="field">
+					<div class="ui radio checkbox">
+						<input class="select-org-radio" name="workflow_name" type="radio" value="{{.Filename}}">
+						<label>{{.Filename}}</label>
+					</div>
+					<input name="repo_name" type="hidden" value="{{.RepoName}}">
+				</div></td>
+				<td><div class="field">
+					<a href="/{{$.OrgName}}/{{.RepoName}}">
+						<label>{{.RepoName}}</label>
+					</a>
+				</div></td>
+				</tr>
+				{{end}}
+			{{else}}
+					<tr>
+						<td class="center aligned" colspan="8">{{ctx.Locale.Tr "actions.require_action.none"}}</td>
+					</tr>
+			{{end}}
+			</tbody>
+			</table>
+			<div class="divider"></div>
+			<div class="item">
+				<a href="{{$.Link}}/add">{{ctx.Locale.Tr "actions.require_action.add_require_action"}}</a>
+			</div>
+		</div>
+		{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
+	</form>
+</div>
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 db678a25ba3..e533a261b1d 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -44,6 +44,7 @@ import {initSshKeyFormParser} from './features/sshkey-helper.ts';
 import {initUserSettings} from './features/user-settings.ts';
 import {initRepoArchiveLinks} from './features/repo-common.ts';
 import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts';
+import {initRequireActionsSelect} from './features/require-actions-select.js';
 import {
   initRepoSettingGitHook,
   initRepoSettingsCollaboration,
@@ -179,6 +180,7 @@ onDomReady(() => {
 
     initRepoActivityTopAuthorsChart,
     initRepoArchiveLinks,
+    initRequireActionsSelect,
     initRepoBranchButton,
     initRepoCodeView,
     initRepoCommentForm,