mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-02-23 03:45:47 -05:00
chore(refactor): partial port of Add API for Variables
(#29520)
The commit has, in addition to the implementation of the API, a few
function refactor that are useful in backports.
---
close #27801
---------
Co-authored-by: silverwind <me@silverwind.io>
(cherry picked from commit 62b073e6f31645e446c7e8d6b5a506f61b47924e)
Conflicts:
- modules/util/util.go
Trivial resolution, only picking the newly introduced function
- routers/api/v1/swagger/options.go
Trivial resolution. We don't have UserBadges, don't pick that part.
- templates/swagger/v1_json.tmpl
Regenerated.
(cherry picked from commit 16696a42f5
)
This commit is contained in:
parent
5b30b7dc6f
commit
0e82cf121d
6 changed files with 139 additions and 73 deletions
|
@ -6,13 +6,11 @@ package actions
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
@ -55,24 +53,24 @@ type FindVariablesOpts struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
OwnerID int64
|
OwnerID int64
|
||||||
RepoID int64
|
RepoID int64
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindVariablesOpts) ToConds() builder.Cond {
|
func (opts FindVariablesOpts) ToConds() builder.Cond {
|
||||||
cond := builder.NewCond()
|
cond := builder.NewCond()
|
||||||
|
// Since we now support instance-level variables,
|
||||||
|
// there is no need to check for null values for `owner_id` and `repo_id`
|
||||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||||
|
|
||||||
|
if opts.Name != "" {
|
||||||
|
cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
|
||||||
|
}
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) {
|
func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) {
|
||||||
var variable ActionVariable
|
return db.Find[ActionVariable](ctx, opts)
|
||||||
has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !has {
|
|
||||||
return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist)
|
|
||||||
}
|
|
||||||
return &variable, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
|
func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
|
||||||
|
@ -84,6 +82,13 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error)
|
||||||
return count != 0, err
|
return count != 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteVariable(ctx context.Context, id int64) error {
|
||||||
|
if _, err := db.DeleteByID[ActionVariable](ctx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) {
|
func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) {
|
||||||
variables := map[string]string{}
|
variables := map[string]string{}
|
||||||
|
|
||||||
|
|
|
@ -220,3 +220,12 @@ func Iif[T any](condition bool, trueVal, falseVal T) T {
|
||||||
}
|
}
|
||||||
return falseVal
|
return falseVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -236,3 +236,8 @@ func TestToPointer(t *testing.T) {
|
||||||
val123 := 123
|
val123 := 123
|
||||||
assert.NotSame(t, &val123, ToPointer(val123))
|
assert.NotSame(t, &val123, ToPointer(val123))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReserveLineBreakForTextarea(t *testing.T) {
|
||||||
|
assert.Equal(t, "test\ndata", ReserveLineBreakForTextarea("test\r\ndata"))
|
||||||
|
assert.Equal(t, "test\ndata\n", ReserveLineBreakForTextarea("test\r\ndata\r\n"))
|
||||||
|
}
|
||||||
|
|
|
@ -4,17 +4,13 @@
|
||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"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/context"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
secret_service "code.gitea.io/gitea/services/secrets"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
|
func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
|
||||||
|
@ -29,41 +25,16 @@ func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
|
||||||
ctx.Data["Variables"] = variables
|
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 (
|
|
||||||
forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
|
|
||||||
)
|
|
||||||
|
|
||||||
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) {
|
func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||||
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
||||||
|
|
||||||
if err := secret_service.ValidateName(form.Name); err != nil {
|
v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
|
||||||
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 {
|
if err != nil {
|
||||||
log.Error("InsertVariable error: %v", err)
|
log.Error("CreateVariable: %v", err)
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
|
ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
|
||||||
ctx.JSONRedirect(redirectURL)
|
ctx.JSONRedirect(redirectURL)
|
||||||
}
|
}
|
||||||
|
@ -72,23 +43,8 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
|
||||||
id := ctx.ParamsInt64(":variable_id")
|
id := ctx.ParamsInt64(":variable_id")
|
||||||
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
||||||
|
|
||||||
if err := secret_service.ValidateName(form.Name); err != nil {
|
if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
|
||||||
ctx.JSONError(err.Error())
|
log.Error("UpdateVariable: %v", err)
|
||||||
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"))
|
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -99,7 +55,7 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
|
||||||
func DeleteVariable(ctx *context.Context, redirectURL string) {
|
func DeleteVariable(ctx *context.Context, redirectURL string) {
|
||||||
id := ctx.ParamsInt64(":variable_id")
|
id := ctx.ParamsInt64(":variable_id")
|
||||||
|
|
||||||
if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil {
|
if err := actions_service.DeleteVariableByID(ctx, id); err != nil {
|
||||||
log.Error("Delete variable [%d] failed: %v", id, err)
|
log.Error("Delete variable [%d] failed: %v", id, err)
|
||||||
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
|
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
|
||||||
return
|
return
|
||||||
|
@ -107,12 +63,3 @@ func DeleteVariable(ctx *context.Context, redirectURL string) {
|
||||||
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
|
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
|
||||||
ctx.JSONRedirect(redirectURL)
|
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")
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
secret_model "code.gitea.io/gitea/models/secret"
|
secret_model "code.gitea.io/gitea/models/secret"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/web/shared/actions"
|
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
secret_service "code.gitea.io/gitea/services/secrets"
|
secret_service "code.gitea.io/gitea/services/secrets"
|
||||||
|
@ -27,7 +27,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
|
||||||
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||||
form := web.GetForm(ctx).(*forms.AddSecretForm)
|
form := web.GetForm(ctx).(*forms.AddSecretForm)
|
||||||
|
|
||||||
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data))
|
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("CreateOrUpdateSecret failed: %v", err)
|
log.Error("CreateOrUpdateSecret failed: %v", err)
|
||||||
ctx.JSONError(ctx.Tr("secrets.creation.failed"))
|
ctx.JSONError(ctx.Tr("secrets.creation.failed"))
|
||||||
|
|
100
services/actions/variables.go
Normal file
100
services/actions/variables.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
secret_service "code.gitea.io/gitea/services/secrets"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
|
||||||
|
if err := secret_service.ValidateName(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := envNameCIRegexMatch(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
|
||||||
|
if err := secret_service.ValidateName(name); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := envNameCIRegexMatch(name); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
|
||||||
|
ID: variableID,
|
||||||
|
Name: strings.ToUpper(name),
|
||||||
|
Data: util.ReserveLineBreakForTextarea(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteVariableByID(ctx context.Context, variableID int64) error {
|
||||||
|
return actions_model.DeleteVariable(ctx, variableID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error {
|
||||||
|
if err := secret_service.ValidateName(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := envNameCIRegexMatch(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := GetVariable(ctx, actions_model.FindVariablesOpts{
|
||||||
|
OwnerID: ownerID,
|
||||||
|
RepoID: repoID,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions_model.DeleteVariable(ctx, v.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*actions_model.ActionVariable, error) {
|
||||||
|
vars, err := actions_model.FindVariables(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(vars) != 1 {
|
||||||
|
return nil, util.NewNotExistErrorf("variable not found")
|
||||||
|
}
|
||||||
|
return vars[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
|
||||||
|
)
|
||||||
|
|
||||||
|
func envNameCIRegexMatch(name string) error {
|
||||||
|
if forbiddenEnvNameCIRx.MatchString(name) {
|
||||||
|
log.Error("Env Name cannot be ci")
|
||||||
|
return util.NewInvalidArgumentErrorf("env name cannot be ci")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue