From d0f24ff4cad05c1145afeca791e7d02fe146d46a Mon Sep 17 00:00:00 2001
From: Jean-Baptiste Gomond <dev@jbgomond.com>
Date: Mon, 25 Dec 2023 08:28:59 +0100
Subject: [PATCH] Added instance-level variables (#28115)

This PR adds instance-level variables, and so closes #27726



![gitea_instance_variables_1](https://github.com/go-gitea/gitea/assets/8344487/ad409cd4-ce36-4c84-a764-34451b0fb63a)

![gitea_instance_variables_2](https://github.com/go-gitea/gitea/assets/8344487/426f0965-dec6-4560-948c-067cdeddd720)

![gitea_instance_variables_3](https://github.com/go-gitea/gitea/assets/8344487/cf1d7776-4938-4825-922e-cbbbf28a5f33)
---
 models/actions/variable.go            | 12 ++++--------
 routers/api/actions/runner/utils.go   | 10 ++++++++--
 routers/web/repo/setting/variables.go | 21 ++++++++++++++++++---
 routers/web/web.go                    |  9 +++++----
 templates/admin/actions.tmpl          |  3 +++
 templates/admin/navbar.tmpl           |  5 ++++-
 6 files changed, 42 insertions(+), 18 deletions(-)

diff --git a/models/actions/variable.go b/models/actions/variable.go
index 030b7bae92ae..12717e0ae461 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -31,8 +31,8 @@ func init() {
 }
 
 func (v *ActionVariable) Validate() error {
-	if v.OwnerID == 0 && v.RepoID == 0 {
-		return errors.New("the variable is not bound to any scope")
+	if v.OwnerID != 0 && v.RepoID != 0 {
+		return errors.New("a variable should not be bound to an owner and a repository at the same time")
 	}
 	return nil
 }
@@ -58,12 +58,8 @@ type FindVariablesOpts struct {
 
 func (opts FindVariablesOpts) ToConds() builder.Cond {
 	cond := builder.NewCond()
-	if opts.OwnerID > 0 {
-		cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
-	}
-	if opts.RepoID > 0 {
-		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
-	}
+	cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
+	cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 	return cond
 }
 
diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go
index bf913f2c0578..2555f86c80d8 100644
--- a/routers/api/actions/runner/utils.go
+++ b/routers/api/actions/runner/utils.go
@@ -94,6 +94,12 @@ func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[s
 func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
 	variables := map[string]string{}
 
+	// Global
+	globalVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{})
+	if err != nil {
+		log.Error("find global variables: %v", err)
+	}
+
 	// Org / User level
 	ownerVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID})
 	if err != nil {
@@ -106,8 +112,8 @@ func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map
 		log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err)
 	}
 
-	// Level precedence: Repo > Org / User
-	for _, v := range append(ownerVariables, repoVariables...) {
+	// Level precedence: Repo > Org / User > Global
+	for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) {
 		variables[v.Name] = v.Data
 	}
 
diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go
index a697a5d8d857..428aa0bd5c4f 100644
--- a/routers/web/repo/setting/variables.go
+++ b/routers/web/repo/setting/variables.go
@@ -15,9 +15,10 @@ import (
 )
 
 const (
-	tplRepoVariables base.TplName = "repo/settings/actions"
-	tplOrgVariables  base.TplName = "org/settings/actions"
-	tplUserVariables base.TplName = "user/settings/actions"
+	tplRepoVariables  base.TplName = "repo/settings/actions"
+	tplOrgVariables   base.TplName = "org/settings/actions"
+	tplUserVariables  base.TplName = "user/settings/actions"
+	tplAdminVariables base.TplName = "admin/actions"
 )
 
 type variablesCtx struct {
@@ -26,6 +27,7 @@ type variablesCtx struct {
 	IsRepo            bool
 	IsOrg             bool
 	IsUser            bool
+	IsGlobal          bool
 	VariablesTemplate base.TplName
 	RedirectLink      string
 }
@@ -33,6 +35,7 @@ type variablesCtx struct {
 func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
 	if ctx.Data["PageIsRepoSettings"] == true {
 		return &variablesCtx{
+			OwnerID:           0,
 			RepoID:            ctx.Repo.Repository.ID,
 			IsRepo:            true,
 			VariablesTemplate: tplRepoVariables,
@@ -48,6 +51,7 @@ func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
 		}
 		return &variablesCtx{
 			OwnerID:           ctx.ContextUser.ID,
+			RepoID:            0,
 			IsOrg:             true,
 			VariablesTemplate: tplOrgVariables,
 			RedirectLink:      ctx.Org.OrgLink + "/settings/actions/variables",
@@ -57,12 +61,23 @@ func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
 	if ctx.Data["PageIsUserSettings"] == true {
 		return &variablesCtx{
 			OwnerID:           ctx.Doer.ID,
+			RepoID:            0,
 			IsUser:            true,
 			VariablesTemplate: tplUserVariables,
 			RedirectLink:      setting.AppSubURL + "/user/settings/actions/variables",
 		}, nil
 	}
 
+	if ctx.Data["PageIsAdmin"] == true {
+		return &variablesCtx{
+			OwnerID:           0,
+			RepoID:            0,
+			IsGlobal:          true,
+			VariablesTemplate: tplAdminVariables,
+			RedirectLink:      setting.AppSubURL + "/admin/actions/variables",
+		}, nil
+	}
+
 	return nil, errors.New("unable to set Variables context")
 }
 
diff --git a/routers/web/web.go b/routers/web/web.go
index 359b608c71e7..02fb11b1f516 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -417,7 +417,7 @@ func registerRoutes(m *web.Route) {
 		m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksEditPost)
 	}
 
-	addSettingVariablesRoutes := func() {
+	addSettingsVariablesRoutes := func() {
 		m.Group("/variables", func() {
 			m.Get("", repo_setting.Variables)
 			m.Post("/new", web.Bind(forms.EditVariableForm{}), repo_setting.VariableCreate)
@@ -618,7 +618,7 @@ func registerRoutes(m *web.Route) {
 			m.Get("", user_setting.RedirectToDefaultSetting)
 			addSettingsRunnersRoutes()
 			addSettingsSecretsRoutes()
-			addSettingVariablesRoutes()
+			addSettingsVariablesRoutes()
 		}, actions.MustEnableActions)
 
 		m.Get("/organization", user_setting.Organization)
@@ -763,6 +763,7 @@ func registerRoutes(m *web.Route) {
 		m.Group("/actions", func() {
 			m.Get("", admin.RedirectToDefaultSetting)
 			addSettingsRunnersRoutes()
+			addSettingsVariablesRoutes()
 		})
 	}, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enable, "EnablePackages", setting.Packages.Enabled))
 	// ***** END: Admin *****
@@ -905,7 +906,7 @@ func registerRoutes(m *web.Route) {
 					m.Get("", org_setting.RedirectToDefaultSetting)
 					addSettingsRunnersRoutes()
 					addSettingsSecretsRoutes()
-					addSettingVariablesRoutes()
+					addSettingsVariablesRoutes()
 				}, actions.MustEnableActions)
 
 				m.Methods("GET,POST", "/delete", org.SettingsDelete)
@@ -1084,7 +1085,7 @@ func registerRoutes(m *web.Route) {
 				m.Get("", repo_setting.RedirectToDefaultSetting)
 				addSettingsRunnersRoutes()
 				addSettingsSecretsRoutes()
-				addSettingVariablesRoutes()
+				addSettingsVariablesRoutes()
 			}, actions.MustEnableActions)
 			// the follow handler must be under "settings", otherwise this incomplete repo can't be accessed
 			m.Group("/migrate", func() {
diff --git a/templates/admin/actions.tmpl b/templates/admin/actions.tmpl
index 9640e0fd1f4c..597863d73b15 100644
--- a/templates/admin/actions.tmpl
+++ b/templates/admin/actions.tmpl
@@ -3,5 +3,8 @@
 	{{if eq .PageType "runners"}}
 		{{template "shared/actions/runner_list" .}}
 	{{end}}
+	{{if eq .PageType "variables"}}
+		{{template "shared/variables/variable_list" .}}
+	{{end}}
 	</div>
 {{template "admin/layout_footer" .}}
diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl
index 8ece95239c1f..b22db1d1fc8f 100644
--- a/templates/admin/navbar.tmpl
+++ b/templates/admin/navbar.tmpl
@@ -60,12 +60,15 @@
 			{{end}}
 		{{end}}
 		{{if .EnableActions}}
-		<details class="item toggleable-item" {{if .PageIsSharedSettingsRunners}}open{{end}}>
+		<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsVariables}}open{{end}}>
 			<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
 			<div class="menu">
 				<a class="{{if .PageIsSharedSettingsRunners}}active {{end}}item" href="{{AppSubUrl}}/admin/actions/runners">
 					{{ctx.Locale.Tr "actions.runners"}}
 				</a>
+				<a class="{{if .PageIsSharedSettingsVariables}}active {{end}}item" href="{{AppSubUrl}}/admin/actions/variables">
+					{{ctx.Locale.Tr "actions.variables"}}
+				</a>
 			</div>
 		</details>
 		{{end}}