aboutsummaryrefslogtreecommitdiffstats
path: root/routers
diff options
context:
space:
mode:
authorsillyguodong <33891828+sillyguodong@users.noreply.github.com>2023-06-21 06:54:15 +0800
committerGitHub <noreply@github.com>2023-06-20 22:54:15 +0000
commit35a653d7edbe0d693649604b8309bfc578dd988b (patch)
treed804f5341067234c2d286b5f07b5ad839f4ead52 /routers
parent8220e50b56cf7bf9cdfff29a287c5721c3949464 (diff)
downloadgitea-35a653d7edbe0d693649604b8309bfc578dd988b.tar.gz
gitea-35a653d7edbe0d693649604b8309bfc578dd988b.zip
Support configuration variables on Gitea Actions (#24724)
Co-Author: @silverwind @wxiaoguang Replace: #24404 See: - [defining configuration variables for multiple workflows](https://docs.github.com/en/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows) - [vars context](https://docs.github.com/en/actions/learn-github-actions/contexts#vars-context) Related to: - [x] protocol: https://gitea.com/gitea/actions-proto-def/pulls/7 - [x] act_runner: https://gitea.com/gitea/act_runner/pulls/157 - [x] act: https://gitea.com/gitea/act/pulls/43 #### Screenshoot Create Variable: ![image](https://user-images.githubusercontent.com/33891828/236758288-032b7f64-44e7-48ea-b07d-de8b8b0e3729.png) ![image](https://user-images.githubusercontent.com/33891828/236758174-5203f64c-1d0e-4737-a5b0-62061dee86f8.png) Workflow: ```yaml test_vars: runs-on: ubuntu-latest steps: - name: Print Custom Variables run: echo "${{ vars.test_key }}" - name: Try to print a non-exist var run: echo "${{ vars.NON_EXIST_VAR }}" ``` Actions Log: ![image](https://user-images.githubusercontent.com/33891828/236759075-af0c5950-368d-4758-a8ac-47a96e43b6e2.png) --- This PR just implement the org / user (depends on the owner of the current repository) and repo level variables, The Environment level variables have not been implemented. Because [Environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#about-environments) is a module separate from `Actions`. Maybe it would be better to create a new PR to do it. --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Giteabot <teabot@gitea.io>
Diffstat (limited to 'routers')
-rw-r--r--routers/api/actions/runner/utils.go24
-rw-r--r--routers/web/repo/setting/secrets.go6
-rw-r--r--routers/web/repo/setting/variables.go119
-rw-r--r--routers/web/shared/actions/variables.go128
-rw-r--r--routers/web/shared/secrets/secrets.go36
-rw-r--r--routers/web/web.go12
6 files changed, 303 insertions, 22 deletions
diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go
index 9af51f2d7e..cc9c06ab45 100644
--- a/routers/api/actions/runner/utils.go
+++ b/routers/api/actions/runner/utils.go
@@ -36,6 +36,7 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
WorkflowPayload: t.Job.WorkflowPayload,
Context: generateTaskContext(t),
Secrets: getSecretsOfTask(ctx, t),
+ Vars: getVariablesOfTask(ctx, t),
}
if needs, err := findTaskNeeds(ctx, t); err != nil {
@@ -88,6 +89,29 @@ func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[s
return secrets
}
+func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
+ variables := map[string]string{}
+
+ // Org / User level
+ ownerVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID})
+ if err != nil {
+ log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err)
+ }
+
+ // Repo level
+ repoVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID})
+ if err != nil {
+ 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...) {
+ variables[v.Name] = v.Data
+ }
+
+ return variables
+}
+
func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
event := map[string]interface{}{}
_ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event)
diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go
index 444f16f86c..3d7a057602 100644
--- a/routers/web/repo/setting/secrets.go
+++ b/routers/web/repo/setting/secrets.go
@@ -92,6 +92,12 @@ func SecretsPost(ctx *context.Context) {
ctx.ServerError("getSecretsCtx", err)
return
}
+
+ if ctx.HasError() {
+ ctx.JSONError(ctx.GetErrMsg())
+ return
+ }
+
shared.PerformSecretsPost(
ctx,
sCtx.OwnerID,
diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go
new file mode 100644
index 0000000000..1005d1d9c6
--- /dev/null
+++ b/routers/web/repo/setting/variables.go
@@ -0,0 +1,119 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "errors"
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+ shared "code.gitea.io/gitea/routers/web/shared/actions"
+)
+
+const (
+ tplRepoVariables base.TplName = "repo/settings/actions"
+ tplOrgVariables base.TplName = "org/settings/actions"
+ tplUserVariables base.TplName = "user/settings/actions"
+)
+
+type variablesCtx struct {
+ OwnerID int64
+ RepoID int64
+ IsRepo bool
+ IsOrg bool
+ IsUser bool
+ VariablesTemplate base.TplName
+ RedirectLink string
+}
+
+func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
+ if ctx.Data["PageIsRepoSettings"] == true {
+ return &variablesCtx{
+ RepoID: ctx.Repo.Repository.ID,
+ IsRepo: true,
+ VariablesTemplate: tplRepoVariables,
+ RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables",
+ }, nil
+ }
+
+ if ctx.Data["PageIsOrgSettings"] == true {
+ return &variablesCtx{
+ OwnerID: ctx.ContextUser.ID,
+ IsOrg: true,
+ VariablesTemplate: tplOrgVariables,
+ RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables",
+ }, nil
+ }
+
+ if ctx.Data["PageIsUserSettings"] == true {
+ return &variablesCtx{
+ OwnerID: ctx.Doer.ID,
+ IsUser: true,
+ VariablesTemplate: tplUserVariables,
+ RedirectLink: setting.AppSubURL + "/user/settings/actions/variables",
+ }, nil
+ }
+
+ return nil, errors.New("unable to set Variables context")
+}
+
+func Variables(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("actions.variables")
+ ctx.Data["PageType"] = "variables"
+ ctx.Data["PageIsSharedSettingsVariables"] = true
+
+ vCtx, err := getVariablesCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getVariablesCtx", err)
+ return
+ }
+
+ shared.SetVariablesContext(ctx, vCtx.OwnerID, vCtx.RepoID)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
+}
+
+func VariableCreate(ctx *context.Context) {
+ vCtx, err := getVariablesCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getVariablesCtx", err)
+ return
+ }
+
+ if ctx.HasError() { // form binding validation error
+ ctx.JSONError(ctx.GetErrMsg())
+ return
+ }
+
+ shared.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, vCtx.RedirectLink)
+}
+
+func VariableUpdate(ctx *context.Context) {
+ vCtx, err := getVariablesCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getVariablesCtx", err)
+ return
+ }
+
+ if ctx.HasError() { // form binding validation error
+ ctx.JSONError(ctx.GetErrMsg())
+ return
+ }
+
+ shared.UpdateVariable(ctx, vCtx.RedirectLink)
+}
+
+func VariableDelete(ctx *context.Context) {
+ vCtx, err := getVariablesCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getVariablesCtx", err)
+ return
+ }
+ shared.DeleteVariable(ctx, vCtx.RedirectLink)
+}
diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go
new file mode 100644
index 0000000000..8d1516c91c
--- /dev/null
+++ b/routers/web/shared/actions/variables.go
@@ -0,0 +1,128 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "errors"
+ "regexp"
+ "strings"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/forms"
+)
+
+func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
+ variables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ownerID,
+ RepoID: repoID,
+ })
+ if err != nil {
+ ctx.ServerError("FindVariables", err)
+ return
+ }
+ ctx.Data["Variables"] = variables
+}
+
+// some regular expression of `variables` and `secrets`
+// reference to:
+// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
+// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
+var (
+ nameRx = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$")
+ forbiddenPrefixRx = regexp.MustCompile("(?i)^GIT(EA|HUB)_")
+
+ forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
+)
+
+func NameRegexMatch(name string) error {
+ if !nameRx.MatchString(name) || forbiddenPrefixRx.MatchString(name) {
+ log.Error("Name %s, regex match error", name)
+ return errors.New("name has invalid character")
+ }
+ return nil
+}
+
+func envNameCIRegexMatch(name string) error {
+ if forbiddenEnvNameCIRx.MatchString(name) {
+ log.Error("Env Name cannot be ci")
+ return errors.New("env name cannot be ci")
+ }
+ return nil
+}
+
+func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
+ form := web.GetForm(ctx).(*forms.EditVariableForm)
+
+ if err := NameRegexMatch(form.Name); err != nil {
+ ctx.JSONError(err.Error())
+ return
+ }
+
+ if err := envNameCIRegexMatch(form.Name); err != nil {
+ ctx.JSONError(err.Error())
+ return
+ }
+
+ v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data))
+ if err != nil {
+ log.Error("InsertVariable error: %v", err)
+ ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
+ ctx.JSONRedirect(redirectURL)
+}
+
+func UpdateVariable(ctx *context.Context, redirectURL string) {
+ id := ctx.ParamsInt64(":variable_id")
+ form := web.GetForm(ctx).(*forms.EditVariableForm)
+
+ if err := NameRegexMatch(form.Name); err != nil {
+ ctx.JSONError(err.Error())
+ return
+ }
+
+ if err := envNameCIRegexMatch(form.Name); err != nil {
+ ctx.JSONError(err.Error())
+ return
+ }
+
+ ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
+ ID: id,
+ Name: strings.ToUpper(form.Name),
+ Data: ReserveLineBreakForTextarea(form.Data),
+ })
+ if err != nil || !ok {
+ log.Error("UpdateVariable error: %v", err)
+ ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("actions.variables.update.success"))
+ ctx.JSONRedirect(redirectURL)
+}
+
+func DeleteVariable(ctx *context.Context, redirectURL string) {
+ id := ctx.ParamsInt64(":variable_id")
+
+ if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil {
+ log.Error("Delete variable [%d] failed: %v", id, err)
+ ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
+ ctx.JSONRedirect(redirectURL)
+}
+
+func ReserveLineBreakForTextarea(input string) string {
+ // Since the content is from a form which is a textarea, the line endings are \r\n.
+ // It's a standard behavior of HTML.
+ // But we want to store them as \n like what GitHub does.
+ // And users are unlikely to really need to keep the \r.
+ // Other than this, we should respect the original content, even leading or trailing spaces.
+ return strings.ReplaceAll(input, "\r\n", "\n")
+}
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index a0d648f908..c09ce51499 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -4,14 +4,12 @@
package secrets
import (
- "net/http"
- "strings"
-
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/web/shared/actions"
"code.gitea.io/gitea/services/forms"
)
@@ -28,23 +26,20 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
- content := form.Content
- // Since the content is from a form which is a textarea, the line endings are \r\n.
- // It's a standard behavior of HTML.
- // But we want to store them as \n like what GitHub does.
- // And users are unlikely to really need to keep the \r.
- // Other than this, we should respect the original content, even leading or trailing spaces.
- content = strings.ReplaceAll(content, "\r\n", "\n")
+ if err := actions.NameRegexMatch(form.Name); err != nil {
+ ctx.JSONError(ctx.Tr("secrets.creation.failed"))
+ return
+ }
- s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Title, content)
+ s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data))
if err != nil {
log.Error("InsertEncryptedSecret: %v", err)
- ctx.Flash.Error(ctx.Tr("secrets.creation.failed"))
- } else {
- ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name))
+ ctx.JSONError(ctx.Tr("secrets.creation.failed"))
+ return
}
- ctx.Redirect(redirectURL)
+ ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name))
+ ctx.JSONRedirect(redirectURL)
}
func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
@@ -52,12 +47,9 @@ func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectU
if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id, OwnerID: ownerID, RepoID: repoID}); err != nil {
log.Error("Delete secret %d failed: %v", id, err)
- ctx.Flash.Error(ctx.Tr("secrets.deletion.failed"))
- } else {
- ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
+ ctx.JSONError(ctx.Tr("secrets.deletion.failed"))
+ return
}
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": redirectURL,
- })
+ ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
+ ctx.JSONRedirect(redirectURL)
}
diff --git a/routers/web/web.go b/routers/web/web.go
index 8ac01f1742..a7573b38f5 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -307,6 +307,15 @@ func registerRoutes(m *web.Route) {
m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost)
}
+ addSettingVariablesRoutes := func() {
+ m.Group("/variables", func() {
+ m.Get("", repo_setting.Variables)
+ m.Post("/new", web.Bind(forms.EditVariableForm{}), repo_setting.VariableCreate)
+ m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), repo_setting.VariableUpdate)
+ m.Post("/{variable_id}/delete", repo_setting.VariableDelete)
+ })
+ }
+
addSettingsSecretsRoutes := func() {
m.Group("/secrets", func() {
m.Get("", repo_setting.Secrets)
@@ -494,6 +503,7 @@ func registerRoutes(m *web.Route) {
m.Get("", user_setting.RedirectToDefaultSetting)
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
+ addSettingVariablesRoutes()
}, actions.MustEnableActions)
m.Get("/organization", user_setting.Organization)
@@ -760,6 +770,7 @@ func registerRoutes(m *web.Route) {
m.Get("", org_setting.RedirectToDefaultSetting)
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
+ addSettingVariablesRoutes()
}, actions.MustEnableActions)
m.RouteMethods("/delete", "GET,POST", org.SettingsDelete)
@@ -941,6 +952,7 @@ func registerRoutes(m *web.Route) {
m.Get("", repo_setting.RedirectToDefaultSetting)
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
+ addSettingVariablesRoutes()
}, actions.MustEnableActions)
m.Post("/migrate/cancel", repo.MigrateCancelPost) // this handler must be under "settings", otherwise this incomplete repo can't be accessed
}, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer))